From 6cd64500ce4fa18edfc4d99d86fd747129dd1e52 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sat, 22 Feb 2025 21:24:17 -0600 Subject: [PATCH] Fix stats test --- packages/db/adapters/DittoPglite.ts | 1 - packages/ditto/storages/DittoPgStore.ts | 6 +- packages/ditto/utils/stats.test.ts | 154 ++++++++++++++---------- packages/ditto/utils/stats.ts | 34 +++--- 4 files changed, 113 insertions(+), 82 deletions(-) diff --git a/packages/db/adapters/DittoPglite.ts b/packages/db/adapters/DittoPglite.ts index 33516ee2..7fcd5bab 100644 --- a/packages/db/adapters/DittoPglite.ts +++ b/packages/db/adapters/DittoPglite.ts @@ -47,7 +47,6 @@ export class DittoPglite implements DittoDB { } async [Symbol.asyncDispose](): Promise { - await this.pglite.close(); await this.kysely.destroy(); } } diff --git a/packages/ditto/storages/DittoPgStore.ts b/packages/ditto/storages/DittoPgStore.ts index 619495c0..f473a791 100644 --- a/packages/ditto/storages/DittoPgStore.ts +++ b/packages/ditto/storages/DittoPgStore.ts @@ -164,9 +164,9 @@ export class DittoPgStore extends NPostgres { opts: { signal?: AbortSignal; timeout?: number } = {}, ): Promise { try { - await super.transaction(async (store, kysely) => { - await updateStats({ event, store, kysely: kysely as unknown as Kysely }); - await store.event(event, opts); + await super.transaction(async (relay, kysely) => { + await updateStats({ event, relay, kysely: kysely as unknown as Kysely }); + await relay.event(event, opts); }); } catch (e) { // If the failure is only because of updateStats (which runs first), insert the event anyway. diff --git a/packages/ditto/utils/stats.test.ts b/packages/ditto/utils/stats.test.ts index 762db37c..043e6f13 100644 --- a/packages/ditto/utils/stats.test.ts +++ b/packages/ditto/utils/stats.test.ts @@ -1,43 +1,48 @@ +import { DittoConf } from '@ditto/conf'; +import { DittoPolyPg } from '@ditto/db'; +import { NPostgres } from '@nostrify/db'; import { genEvent } from '@nostrify/nostrify/test'; import { assertEquals } from '@std/assert'; +import { sql } from 'kysely'; import { generateSecretKey, getPublicKey } from 'nostr-tools'; -import { createTestDB } from '@/test.ts'; import { countAuthorStats, getAuthorStats, getEventStats, getFollowDiff, updateStats } from '@/utils/stats.ts'; Deno.test('updateStats with kind 1 increments notes count', async () => { - await using db = await createTestDB(); + await using test = await setupTest(); const sk = generateSecretKey(); const pubkey = getPublicKey(sk); - await updateStats({ ...db, event: genEvent({ kind: 1 }, sk) }); + await updateStats({ ...test, event: genEvent({ kind: 1 }, sk) }); - const stats = await getAuthorStats(db.kysely, pubkey); + const stats = await getAuthorStats(test.kysely, pubkey); assertEquals(stats!.notes_count, 1); }); Deno.test('updateStats with kind 1 increments replies count', async () => { - await using db = await createTestDB(); + await using test = await setupTest(); + const { relay, kysely } = test; const sk = generateSecretKey(); const note = genEvent({ kind: 1 }, sk); - await updateStats({ ...db, event: note }); - await db.store.event(note); + await updateStats({ ...test, event: note }); + await relay.event(note); const reply = genEvent({ kind: 1, tags: [['e', note.id]] }, sk); - await updateStats({ ...db, event: reply }); - await db.store.event(reply); + await updateStats({ ...test, event: reply }); + await relay.event(reply); - const stats = await getEventStats(db.kysely, note.id); + const stats = await getEventStats(kysely, note.id); assertEquals(stats!.replies_count, 1); }); Deno.test('updateStats with kind 5 decrements notes count', async () => { - await using db = await createTestDB(); + await using test = await setupTest(); + const { relay, kysely } = test; const sk = generateSecretKey(); const pubkey = getPublicKey(sk); @@ -45,41 +50,43 @@ Deno.test('updateStats with kind 5 decrements notes count', async () => { const create = genEvent({ kind: 1 }, sk); const remove = genEvent({ kind: 5, tags: [['e', create.id]] }, sk); - await updateStats({ ...db, event: create }); - assertEquals((await getAuthorStats(db.kysely, pubkey))!.notes_count, 1); - await db.store.event(create); + await updateStats({ ...test, event: create }); + assertEquals((await getAuthorStats(kysely, pubkey))!.notes_count, 1); + await relay.event(create); - await updateStats({ ...db, event: remove }); - assertEquals((await getAuthorStats(db.kysely, pubkey))!.notes_count, 0); - await db.store.event(remove); + await updateStats({ ...test, event: remove }); + assertEquals((await getAuthorStats(kysely, pubkey))!.notes_count, 0); + await relay.event(remove); }); Deno.test('updateStats with kind 3 increments followers count', async () => { - await using db = await createTestDB(); + await using test = await setupTest(); + const { kysely } = test; - await updateStats({ ...db, event: genEvent({ kind: 3, tags: [['p', 'alex']] }) }); - await updateStats({ ...db, event: genEvent({ kind: 3, tags: [['p', 'alex']] }) }); - await updateStats({ ...db, event: genEvent({ kind: 3, tags: [['p', 'alex']] }) }); + await updateStats({ ...test, event: genEvent({ kind: 3, tags: [['p', 'alex']] }) }); + await updateStats({ ...test, event: genEvent({ kind: 3, tags: [['p', 'alex']] }) }); + await updateStats({ ...test, event: genEvent({ kind: 3, tags: [['p', 'alex']] }) }); - const stats = await getAuthorStats(db.kysely, 'alex'); + const stats = await getAuthorStats(kysely, 'alex'); assertEquals(stats!.followers_count, 3); }); Deno.test('updateStats with kind 3 decrements followers count', async () => { - await using db = await createTestDB(); + await using test = await setupTest(); + const { relay, kysely } = test; const sk = generateSecretKey(); const follow = genEvent({ kind: 3, tags: [['p', 'alex']], created_at: 0 }, sk); const remove = genEvent({ kind: 3, tags: [], created_at: 1 }, sk); - await updateStats({ ...db, event: follow }); - assertEquals((await getAuthorStats(db.kysely, 'alex'))!.followers_count, 1); - await db.store.event(follow); + await updateStats({ ...test, event: follow }); + assertEquals((await getAuthorStats(kysely, 'alex'))!.followers_count, 1); + await relay.event(follow); - await updateStats({ ...db, event: remove }); - assertEquals((await getAuthorStats(db.kysely, 'alex'))!.followers_count, 0); - await db.store.event(remove); + await updateStats({ ...test, event: remove }); + assertEquals((await getAuthorStats(kysely, 'alex'))!.followers_count, 0); + await relay.event(remove); }); Deno.test('getFollowDiff returns added and removed followers', () => { @@ -93,86 +100,91 @@ Deno.test('getFollowDiff returns added and removed followers', () => { }); Deno.test('updateStats with kind 6 increments reposts count', async () => { - await using db = await createTestDB(); + await using test = await setupTest(); + const { relay, kysely } = test; const note = genEvent({ kind: 1 }); - await updateStats({ ...db, event: note }); - await db.store.event(note); + await updateStats({ ...test, event: note }); + await relay.event(note); const repost = genEvent({ kind: 6, tags: [['e', note.id]] }); - await updateStats({ ...db, event: repost }); - await db.store.event(repost); + await updateStats({ ...test, event: repost }); + await relay.event(repost); - const stats = await getEventStats(db.kysely, note.id); + const stats = await getEventStats(kysely, note.id); assertEquals(stats!.reposts_count, 1); }); Deno.test('updateStats with kind 5 decrements reposts count', async () => { - await using db = await createTestDB(); + await using test = await setupTest(); + const { relay, kysely } = test; const note = genEvent({ kind: 1 }); - await updateStats({ ...db, event: note }); - await db.store.event(note); + await updateStats({ ...test, event: note }); + await relay.event(note); const sk = generateSecretKey(); const repost = genEvent({ kind: 6, tags: [['e', note.id]] }, sk); - await updateStats({ ...db, event: repost }); - await db.store.event(repost); + await updateStats({ ...test, event: repost }); + await relay.event(repost); - await updateStats({ ...db, event: genEvent({ kind: 5, tags: [['e', repost.id]] }, sk) }); + await updateStats({ ...test, event: genEvent({ kind: 5, tags: [['e', repost.id]] }, sk) }); - const stats = await getEventStats(db.kysely, note.id); + const stats = await getEventStats(kysely, note.id); assertEquals(stats!.reposts_count, 0); }); Deno.test('updateStats with kind 7 increments reactions count', async () => { - await using db = await createTestDB(); + await using test = await setupTest(); + const { relay, kysely } = test; const note = genEvent({ kind: 1 }); - await updateStats({ ...db, event: note }); - await db.store.event(note); + await updateStats({ ...test, event: note }); + await relay.event(note); - await updateStats({ ...db, event: genEvent({ kind: 7, content: '+', tags: [['e', note.id]] }) }); - await updateStats({ ...db, event: genEvent({ kind: 7, content: '😂', tags: [['e', note.id]] }) }); + await updateStats({ ...test, event: genEvent({ kind: 7, content: '+', tags: [['e', note.id]] }) }); + await updateStats({ ...test, event: genEvent({ kind: 7, content: '😂', tags: [['e', note.id]] }) }); - const stats = await getEventStats(db.kysely, note.id); + const stats = await getEventStats(kysely, note.id); assertEquals(stats!.reactions, JSON.stringify({ '+': 1, '😂': 1 })); assertEquals(stats!.reactions_count, 2); }); Deno.test('updateStats with kind 5 decrements reactions count', async () => { - await using db = await createTestDB(); + await using test = await setupTest(); + const { relay, kysely } = test; const note = genEvent({ kind: 1 }); - await updateStats({ ...db, event: note }); - await db.store.event(note); + await updateStats({ ...test, event: note }); + await relay.event(note); const sk = generateSecretKey(); const reaction = genEvent({ kind: 7, content: '+', tags: [['e', note.id]] }, sk); - await updateStats({ ...db, event: reaction }); - await db.store.event(reaction); + await updateStats({ ...test, event: reaction }); + await relay.event(reaction); - await updateStats({ ...db, event: genEvent({ kind: 5, tags: [['e', reaction.id]] }, sk) }); + await updateStats({ ...test, event: genEvent({ kind: 5, tags: [['e', reaction.id]] }, sk) }); - const stats = await getEventStats(db.kysely, note.id); + const stats = await getEventStats(kysely, note.id); assertEquals(stats!.reactions, JSON.stringify({})); }); Deno.test('countAuthorStats counts author stats from the database', async () => { - await using db = await createTestDB(); + await using test = await setupTest(); + const { relay } = test; const sk = generateSecretKey(); const pubkey = getPublicKey(sk); - await db.store.event(genEvent({ kind: 1, content: 'hello' }, sk)); - await db.store.event(genEvent({ kind: 1, content: 'yolo' }, sk)); - await db.store.event(genEvent({ kind: 3, tags: [['p', pubkey]] })); + await relay.event(genEvent({ kind: 1, content: 'hello' }, sk)); + await relay.event(genEvent({ kind: 1, content: 'yolo' }, sk)); + await relay.event(genEvent({ kind: 3, tags: [['p', pubkey]] })); - await db.kysely.insertInto('author_stats').values({ + await test.kysely.insertInto('author_stats').values({ pubkey, search: 'Yolo Lolo', notes_count: 0, @@ -181,8 +193,28 @@ Deno.test('countAuthorStats counts author stats from the database', async () => }).onConflict((oc) => oc.column('pubkey').doUpdateSet({ 'search': 'baka' })) .execute(); - const stats = await countAuthorStats({ store: db.store, pubkey, kysely: db.kysely }); + const stats = await countAuthorStats({ ...test, pubkey }); assertEquals(stats!.notes_count, 2); assertEquals(stats!.followers_count, 1); }); + +async function setupTest() { + const conf = new DittoConf(Deno.env); + + const db = new DittoPolyPg(conf.databaseUrl); + await db.migrate(); + + const { kysely } = db; + const relay = new NPostgres(kysely); + + return { + relay, + kysely, + [Symbol.asyncDispose]: async () => { + await sql`truncate table event_stats cascade`.execute(kysely); + await sql`truncate table author_stats cascade`.execute(kysely); + await db[Symbol.asyncDispose](); + }, + }; +} diff --git a/packages/ditto/utils/stats.ts b/packages/ditto/utils/stats.ts index 01ec80d9..448ba241 100644 --- a/packages/ditto/utils/stats.ts +++ b/packages/ditto/utils/stats.ts @@ -9,14 +9,14 @@ import { findQuoteTag, findReplyTag, getTagSet } from '@/utils/tags.ts'; interface UpdateStatsOpts { kysely: Kysely; - store: NStore; + relay: NStore; event: NostrEvent; x?: 1 | -1; } /** Handle one event at a time and update relevant stats for it. */ // deno-lint-ignore require-await -export async function updateStats({ event, kysely, store, x = 1 }: UpdateStatsOpts): Promise { +export async function updateStats({ event, kysely, relay, x = 1 }: UpdateStatsOpts): Promise { switch (event.kind) { case 1: case 20: @@ -24,9 +24,9 @@ export async function updateStats({ event, kysely, store, x = 1 }: UpdateStatsOp case 30023: return handleEvent1(kysely, event, x); case 3: - return handleEvent3(kysely, event, x, store); + return handleEvent3(kysely, event, x, relay); case 5: - return handleEvent5(kysely, event, -1, store); + return handleEvent5(kysely, event, -1, relay); case 6: return handleEvent6(kysely, event, x); case 7: @@ -88,12 +88,12 @@ async function handleEvent1(kysely: Kysely, event: NostrEvent, x: n } /** Update stats for kind 3 event. */ -async function handleEvent3(kysely: Kysely, event: NostrEvent, x: number, store: NStore): Promise { +async function handleEvent3(kysely: Kysely, event: NostrEvent, x: number, relay: NStore): Promise { const following = getTagSet(event.tags, 'p'); await updateAuthorStats(kysely, event.pubkey, () => ({ following_count: following.size })); - const [prev] = await store.query([ + const [prev] = await relay.query([ { kinds: [3], authors: [event.pubkey], limit: 1 }, ]); @@ -117,12 +117,12 @@ async function handleEvent3(kysely: Kysely, event: NostrEvent, x: n } /** Update stats for kind 5 event. */ -async function handleEvent5(kysely: Kysely, event: NostrEvent, x: -1, store: NStore): Promise { +async function handleEvent5(kysely: Kysely, event: NostrEvent, x: -1, relay: NStore): Promise { const id = event.tags.find(([name]) => name === 'e')?.[1]; if (id) { - const [target] = await store.query([{ ids: [id], authors: [event.pubkey], limit: 1 }]); + const [target] = await relay.query([{ ids: [id], authors: [event.pubkey], limit: 1 }]); if (target) { - await updateStats({ event: target, kysely, store, x }); + await updateStats({ event: target, kysely, relay, x }); } } } @@ -300,13 +300,13 @@ export async function updateEventStats( /** Calculate author stats from the database. */ export async function countAuthorStats( - { pubkey, store }: RefreshAuthorStatsOpts, + { pubkey, relay }: RefreshAuthorStatsOpts, ): Promise { const [{ count: followers_count }, { count: notes_count }, [followList], [kind0]] = await Promise.all([ - store.count([{ kinds: [3], '#p': [pubkey] }]), - store.count([{ kinds: [1, 20], authors: [pubkey] }]), - store.query([{ kinds: [3], authors: [pubkey], limit: 1 }]), - store.query([{ kinds: [0], authors: [pubkey], limit: 1 }]), + relay.count([{ kinds: [3], '#p': [pubkey] }]), + relay.count([{ kinds: [1, 20], authors: [pubkey] }]), + relay.query([{ kinds: [3], authors: [pubkey], limit: 1 }]), + relay.query([{ kinds: [0], authors: [pubkey], limit: 1 }]), ]); let search: string = ''; const metadata = n.json().pipe(n.metadata()).catch({}).safeParse(kind0?.content); @@ -333,14 +333,14 @@ export async function countAuthorStats( export interface RefreshAuthorStatsOpts { pubkey: string; kysely: Kysely; - store: SetRequired; + relay: SetRequired; } /** Refresh the author's stats in the database. */ export async function refreshAuthorStats( - { pubkey, kysely, store }: RefreshAuthorStatsOpts, + { pubkey, kysely, relay }: RefreshAuthorStatsOpts, ): Promise { - const stats = await countAuthorStats({ store, pubkey, kysely }); + const stats = await countAuthorStats({ relay, pubkey, kysely }); await kysely.insertInto('author_stats') .values(stats)