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> {
await this.pglite.close();
await this.kysely.destroy();
}
}

View file

@ -164,9 +164,9 @@ export class DittoPgStore extends NPostgres {
opts: { signal?: AbortSignal; timeout?: number } = {},
): Promise<undefined> {
try {
await super.transaction(async (store, kysely) => {
await updateStats({ event, store, kysely: kysely as unknown as Kysely<DittoTables> });
await store.event(event, opts);
await super.transaction(async (relay, kysely) => {
await updateStats({ event, relay, kysely: kysely as unknown as Kysely<DittoTables> });
await relay.event(event, opts);
});
} catch (e) {
// 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 { 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]();
},
};
}

View file

@ -9,14 +9,14 @@ import { findQuoteTag, findReplyTag, getTagSet } from '@/utils/tags.ts';
interface UpdateStatsOpts {
kysely: Kysely<DittoTables>;
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<void> {
export async function updateStats({ event, kysely, relay, x = 1 }: UpdateStatsOpts): Promise<void> {
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<DittoTables>, event: NostrEvent, x: n
}
/** 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');
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<DittoTables>, event: NostrEvent, x: n
}
/** 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];
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<DittoTables['author_stats']> {
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<DittoTables>;
store: SetRequired<NStore, 'count'>;
relay: SetRequired<NStore, 'count'>;
}
/** Refresh the author's stats in the database. */
export async function refreshAuthorStats(
{ pubkey, kysely, store }: RefreshAuthorStatsOpts,
{ pubkey, kysely, relay }: RefreshAuthorStatsOpts,
): Promise<DittoTables['author_stats']> {
const stats = await countAuthorStats({ store, pubkey, kysely });
const stats = await countAuthorStats({ relay, pubkey, kysely });
await kysely.insertInto('author_stats')
.values(stats)