Fix stats test

This commit is contained in:
Alex Gleason 2025-02-22 21:24:17 -06:00
parent 4f46a69131
commit 6cd64500ce
No known key found for this signature in database
GPG key ID: 7211D1F99744FBB7
4 changed files with 113 additions and 82 deletions

View file

@ -47,7 +47,6 @@ export class DittoPglite implements DittoDB {
} }
async [Symbol.asyncDispose](): Promise<void> { async [Symbol.asyncDispose](): Promise<void> {
await this.pglite.close();
await this.kysely.destroy(); await this.kysely.destroy();
} }
} }

View file

@ -164,9 +164,9 @@ export class DittoPgStore extends NPostgres {
opts: { signal?: AbortSignal; timeout?: number } = {}, opts: { signal?: AbortSignal; timeout?: number } = {},
): Promise<undefined> { ): Promise<undefined> {
try { try {
await super.transaction(async (store, kysely) => { await super.transaction(async (relay, kysely) => {
await updateStats({ event, store, kysely: kysely as unknown as Kysely<DittoTables> }); await updateStats({ event, relay, kysely: kysely as unknown as Kysely<DittoTables> });
await store.event(event, opts); await relay.event(event, opts);
}); });
} catch (e) { } catch (e) {
// If the failure is only because of updateStats (which runs first), insert the event anyway. // If the failure is only because of updateStats (which runs first), insert the event anyway.

View file

@ -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 { genEvent } from '@nostrify/nostrify/test';
import { assertEquals } from '@std/assert'; import { assertEquals } from '@std/assert';
import { sql } from 'kysely';
import { generateSecretKey, getPublicKey } from 'nostr-tools'; import { generateSecretKey, getPublicKey } from 'nostr-tools';
import { createTestDB } from '@/test.ts';
import { countAuthorStats, getAuthorStats, getEventStats, getFollowDiff, updateStats } from '@/utils/stats.ts'; import { countAuthorStats, getAuthorStats, getEventStats, getFollowDiff, updateStats } from '@/utils/stats.ts';
Deno.test('updateStats with kind 1 increments notes count', async () => { Deno.test('updateStats with kind 1 increments notes count', async () => {
await using db = await createTestDB(); await using test = await setupTest();
const sk = generateSecretKey(); const sk = generateSecretKey();
const pubkey = getPublicKey(sk); 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); assertEquals(stats!.notes_count, 1);
}); });
Deno.test('updateStats with kind 1 increments replies count', async () => { 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 sk = generateSecretKey();
const note = genEvent({ kind: 1 }, sk); const note = genEvent({ kind: 1 }, sk);
await updateStats({ ...db, event: note }); await updateStats({ ...test, event: note });
await db.store.event(note); await relay.event(note);
const reply = genEvent({ kind: 1, tags: [['e', note.id]] }, sk); const reply = genEvent({ kind: 1, tags: [['e', note.id]] }, sk);
await updateStats({ ...db, event: reply }); await updateStats({ ...test, event: reply });
await db.store.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); assertEquals(stats!.replies_count, 1);
}); });
Deno.test('updateStats with kind 5 decrements notes count', async () => { 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 sk = generateSecretKey();
const pubkey = getPublicKey(sk); 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 create = genEvent({ kind: 1 }, sk);
const remove = genEvent({ kind: 5, tags: [['e', create.id]] }, sk); const remove = genEvent({ kind: 5, tags: [['e', create.id]] }, sk);
await updateStats({ ...db, event: create }); await updateStats({ ...test, event: create });
assertEquals((await getAuthorStats(db.kysely, pubkey))!.notes_count, 1); assertEquals((await getAuthorStats(kysely, pubkey))!.notes_count, 1);
await db.store.event(create); await relay.event(create);
await updateStats({ ...db, event: remove }); await updateStats({ ...test, event: remove });
assertEquals((await getAuthorStats(db.kysely, pubkey))!.notes_count, 0); assertEquals((await getAuthorStats(kysely, pubkey))!.notes_count, 0);
await db.store.event(remove); await relay.event(remove);
}); });
Deno.test('updateStats with kind 3 increments followers count', async () => { 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({ ...test, 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({ ...db, 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); assertEquals(stats!.followers_count, 3);
}); });
Deno.test('updateStats with kind 3 decrements followers count', async () => { 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 sk = generateSecretKey();
const follow = genEvent({ kind: 3, tags: [['p', 'alex']], created_at: 0 }, sk); const follow = genEvent({ kind: 3, tags: [['p', 'alex']], created_at: 0 }, sk);
const remove = genEvent({ kind: 3, tags: [], created_at: 1 }, sk); const remove = genEvent({ kind: 3, tags: [], created_at: 1 }, sk);
await updateStats({ ...db, event: follow }); await updateStats({ ...test, event: follow });
assertEquals((await getAuthorStats(db.kysely, 'alex'))!.followers_count, 1); assertEquals((await getAuthorStats(kysely, 'alex'))!.followers_count, 1);
await db.store.event(follow); await relay.event(follow);
await updateStats({ ...db, event: remove }); await updateStats({ ...test, event: remove });
assertEquals((await getAuthorStats(db.kysely, 'alex'))!.followers_count, 0); assertEquals((await getAuthorStats(kysely, 'alex'))!.followers_count, 0);
await db.store.event(remove); await relay.event(remove);
}); });
Deno.test('getFollowDiff returns added and removed followers', () => { 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 () => { 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 }); const note = genEvent({ kind: 1 });
await updateStats({ ...db, event: note }); await updateStats({ ...test, event: note });
await db.store.event(note); await relay.event(note);
const repost = genEvent({ kind: 6, tags: [['e', note.id]] }); const repost = genEvent({ kind: 6, tags: [['e', note.id]] });
await updateStats({ ...db, event: repost }); await updateStats({ ...test, event: repost });
await db.store.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); assertEquals(stats!.reposts_count, 1);
}); });
Deno.test('updateStats with kind 5 decrements reposts count', async () => { 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 }); const note = genEvent({ kind: 1 });
await updateStats({ ...db, event: note }); await updateStats({ ...test, event: note });
await db.store.event(note); await relay.event(note);
const sk = generateSecretKey(); const sk = generateSecretKey();
const repost = genEvent({ kind: 6, tags: [['e', note.id]] }, sk); const repost = genEvent({ kind: 6, tags: [['e', note.id]] }, sk);
await updateStats({ ...db, event: repost }); await updateStats({ ...test, event: repost });
await db.store.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); assertEquals(stats!.reposts_count, 0);
}); });
Deno.test('updateStats with kind 7 increments reactions count', async () => { 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 }); const note = genEvent({ kind: 1 });
await updateStats({ ...db, event: note }); await updateStats({ ...test, event: note });
await db.store.event(note); await relay.event(note);
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({ ...db, 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, JSON.stringify({ '+': 1, '😂': 1 }));
assertEquals(stats!.reactions_count, 2); assertEquals(stats!.reactions_count, 2);
}); });
Deno.test('updateStats with kind 5 decrements reactions count', async () => { 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 }); const note = genEvent({ kind: 1 });
await updateStats({ ...db, event: note }); await updateStats({ ...test, event: note });
await db.store.event(note); await relay.event(note);
const sk = generateSecretKey(); const sk = generateSecretKey();
const reaction = genEvent({ kind: 7, content: '+', tags: [['e', note.id]] }, sk); const reaction = genEvent({ kind: 7, content: '+', tags: [['e', note.id]] }, sk);
await updateStats({ ...db, event: reaction }); await updateStats({ ...test, event: reaction });
await db.store.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({})); assertEquals(stats!.reactions, JSON.stringify({}));
}); });
Deno.test('countAuthorStats counts author stats from the database', async () => { 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 sk = generateSecretKey();
const pubkey = getPublicKey(sk); const pubkey = getPublicKey(sk);
await db.store.event(genEvent({ kind: 1, content: 'hello' }, sk)); await relay.event(genEvent({ kind: 1, content: 'hello' }, sk));
await db.store.event(genEvent({ kind: 1, content: 'yolo' }, sk)); await relay.event(genEvent({ kind: 1, content: 'yolo' }, sk));
await db.store.event(genEvent({ kind: 3, tags: [['p', pubkey]] })); await relay.event(genEvent({ kind: 3, tags: [['p', pubkey]] }));
await db.kysely.insertInto('author_stats').values({ await test.kysely.insertInto('author_stats').values({
pubkey, pubkey,
search: 'Yolo Lolo', search: 'Yolo Lolo',
notes_count: 0, 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' })) }).onConflict((oc) => oc.column('pubkey').doUpdateSet({ 'search': 'baka' }))
.execute(); .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!.notes_count, 2);
assertEquals(stats!.followers_count, 1); 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]();
},
};
}

View file

@ -9,14 +9,14 @@ import { findQuoteTag, findReplyTag, getTagSet } from '@/utils/tags.ts';
interface UpdateStatsOpts { interface UpdateStatsOpts {
kysely: Kysely<DittoTables>; kysely: Kysely<DittoTables>;
store: NStore; relay: NStore;
event: NostrEvent; event: NostrEvent;
x?: 1 | -1; x?: 1 | -1;
} }
/** Handle one event at a time and update relevant stats for it. */ /** Handle one event at a time and update relevant stats for it. */
// deno-lint-ignore require-await // deno-lint-ignore require-await
export async function updateStats({ event, kysely, store, x = 1 }: UpdateStatsOpts): Promise<void> { export async function updateStats({ event, kysely, relay, x = 1 }: UpdateStatsOpts): Promise<void> {
switch (event.kind) { switch (event.kind) {
case 1: case 1:
case 20: case 20:
@ -24,9 +24,9 @@ export async function updateStats({ event, kysely, store, x = 1 }: UpdateStatsOp
case 30023: case 30023:
return handleEvent1(kysely, event, x); return handleEvent1(kysely, event, x);
case 3: case 3:
return handleEvent3(kysely, event, x, store); return handleEvent3(kysely, event, x, relay);
case 5: case 5:
return handleEvent5(kysely, event, -1, store); return handleEvent5(kysely, event, -1, relay);
case 6: case 6:
return handleEvent6(kysely, event, x); return handleEvent6(kysely, event, x);
case 7: case 7:
@ -88,12 +88,12 @@ async function handleEvent1(kysely: Kysely<DittoTables>, event: NostrEvent, x: n
} }
/** Update stats for kind 3 event. */ /** Update stats for kind 3 event. */
async function handleEvent3(kysely: Kysely<DittoTables>, event: NostrEvent, x: number, store: NStore): Promise<void> { async function handleEvent3(kysely: Kysely<DittoTables>, event: NostrEvent, x: number, relay: NStore): Promise<void> {
const following = getTagSet(event.tags, 'p'); const following = getTagSet(event.tags, 'p');
await updateAuthorStats(kysely, event.pubkey, () => ({ following_count: following.size })); 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 }, { kinds: [3], authors: [event.pubkey], limit: 1 },
]); ]);
@ -117,12 +117,12 @@ async function handleEvent3(kysely: Kysely<DittoTables>, event: NostrEvent, x: n
} }
/** Update stats for kind 5 event. */ /** Update stats for kind 5 event. */
async function handleEvent5(kysely: Kysely<DittoTables>, event: NostrEvent, x: -1, store: NStore): Promise<void> { async function handleEvent5(kysely: Kysely<DittoTables>, event: NostrEvent, x: -1, relay: NStore): Promise<void> {
const id = event.tags.find(([name]) => name === 'e')?.[1]; const id = event.tags.find(([name]) => name === 'e')?.[1];
if (id) { 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) { 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. */ /** Calculate author stats from the database. */
export async function countAuthorStats( export async function countAuthorStats(
{ pubkey, store }: RefreshAuthorStatsOpts, { pubkey, relay }: RefreshAuthorStatsOpts,
): Promise<DittoTables['author_stats']> { ): Promise<DittoTables['author_stats']> {
const [{ count: followers_count }, { count: notes_count }, [followList], [kind0]] = await Promise.all([ const [{ count: followers_count }, { count: notes_count }, [followList], [kind0]] = await Promise.all([
store.count([{ kinds: [3], '#p': [pubkey] }]), relay.count([{ kinds: [3], '#p': [pubkey] }]),
store.count([{ kinds: [1, 20], authors: [pubkey] }]), relay.count([{ kinds: [1, 20], authors: [pubkey] }]),
store.query([{ kinds: [3], authors: [pubkey], limit: 1 }]), relay.query([{ kinds: [3], authors: [pubkey], limit: 1 }]),
store.query([{ kinds: [0], authors: [pubkey], limit: 1 }]), relay.query([{ kinds: [0], authors: [pubkey], limit: 1 }]),
]); ]);
let search: string = ''; let search: string = '';
const metadata = n.json().pipe(n.metadata()).catch({}).safeParse(kind0?.content); const metadata = n.json().pipe(n.metadata()).catch({}).safeParse(kind0?.content);
@ -333,14 +333,14 @@ export async function countAuthorStats(
export interface RefreshAuthorStatsOpts { export interface RefreshAuthorStatsOpts {
pubkey: string; pubkey: string;
kysely: Kysely<DittoTables>; kysely: Kysely<DittoTables>;
store: SetRequired<NStore, 'count'>; relay: SetRequired<NStore, 'count'>;
} }
/** Refresh the author's stats in the database. */ /** Refresh the author's stats in the database. */
export async function refreshAuthorStats( export async function refreshAuthorStats(
{ pubkey, kysely, store }: RefreshAuthorStatsOpts, { pubkey, kysely, relay }: RefreshAuthorStatsOpts,
): Promise<DittoTables['author_stats']> { ): Promise<DittoTables['author_stats']> {
const stats = await countAuthorStats({ store, pubkey, kysely }); const stats = await countAuthorStats({ relay, pubkey, kysely });
await kysely.insertInto('author_stats') await kysely.insertInto('author_stats')
.values(stats) .values(stats)