stats: handle follow/following counts

This commit is contained in:
Alex Gleason 2023-12-10 17:42:44 -06:00
parent 2d3f12dc72
commit 4f79b7ec29
No known key found for this signature in database
GPG Key ID: 7211D1F99744FBB7
3 changed files with 86 additions and 11 deletions

View File

@ -63,6 +63,7 @@ export {
type CompiledQuery, type CompiledQuery,
FileMigrationProvider, FileMigrationProvider,
type Insertable, type Insertable,
type InsertQueryBuilder,
Kysely, Kysely,
Migrator, Migrator,
type NullableInsertKeys, type NullableInsertKeys,

View File

@ -71,7 +71,7 @@ async function storeEvent(event: Event, data: EventData): Promise<void> {
} else { } else {
await Promise.all([ await Promise.all([
eventsDB.insertEvent(event, data).catch(console.warn), eventsDB.insertEvent(event, data).catch(console.warn),
updateStats(event), updateStats(event).catch(console.warn),
]); ]);
} }
} else { } else {

View File

@ -1,5 +1,6 @@
import { type AuthorStatsRow, db, type EventStatsRow } from '@/db.ts'; import { type AuthorStatsRow, db, type DittoDB, type EventStatsRow } from '@/db.ts';
import { Event, findReplyTag } from '@/deps.ts'; import * as eventsDB from '@/db/events.ts';
import { type Event, findReplyTag, type InsertQueryBuilder } from '@/deps.ts';
type AuthorStat = keyof Omit<AuthorStatsRow, 'pubkey'>; type AuthorStat = keyof Omit<AuthorStatsRow, 'pubkey'>;
type EventStat = keyof Omit<EventStatsRow, 'event_id'>; type EventStat = keyof Omit<EventStatsRow, 'event_id'>;
@ -9,21 +10,29 @@ type EventStatDiff = ['event_stats', eventId: string, stat: EventStat, diff: num
type StatDiff = AuthorStatDiff | EventStatDiff; type StatDiff = AuthorStatDiff | EventStatDiff;
/** Store stats for the event in LMDB. */ /** Store stats for the event in LMDB. */
async function updateStats(event: Event) { async function updateStats<K extends number>(event: Event<K> & { prev?: Event<K> }) {
const statDiffs = getStatsDiff(event); const queries: InsertQueryBuilder<DittoDB, any, unknown>[] = [];
if (!statDiffs.length) return;
// Kind 3 is a special case - replace the count with the new list.
if (event.kind === 3) {
await maybeSetPrev(event);
queries.push(updateFollowingCountQuery(event as Event<3>));
}
const statDiffs = getStatsDiff(event);
const pubkeyDiffs = statDiffs.filter(([table]) => table === 'author_stats') as AuthorStatDiff[]; const pubkeyDiffs = statDiffs.filter(([table]) => table === 'author_stats') as AuthorStatDiff[];
const eventDiffs = statDiffs.filter(([table]) => table === 'event_stats') as EventStatDiff[]; const eventDiffs = statDiffs.filter(([table]) => table === 'event_stats') as EventStatDiff[];
await Promise.all([ if (pubkeyDiffs.length) queries.push(authorStatsQuery(pubkeyDiffs));
pubkeyDiffs.length ? authorStatsQuery(pubkeyDiffs).execute() : undefined, if (eventDiffs.length) queries.push(eventStatsQuery(eventDiffs));
eventDiffs.length ? eventStatsQuery(eventDiffs).execute() : undefined,
]); if (queries.length) {
await Promise.all(queries.map((query) => query.execute()));
}
} }
/** Calculate stats changes ahead of time so we can build an efficient query. */ /** Calculate stats changes ahead of time so we can build an efficient query. */
function getStatsDiff(event: Event): StatDiff[] { function getStatsDiff<K extends number>(event: Event<K> & { prev?: Event<K> }): StatDiff[] {
const statDiffs: StatDiff[] = []; const statDiffs: StatDiff[] = [];
const firstTaggedId = event.tags.find(([name]) => name === 'e')?.[1]; const firstTaggedId = event.tags.find(([name]) => name === 'e')?.[1];
@ -36,6 +45,9 @@ function getStatsDiff(event: Event): StatDiff[] {
statDiffs.push(['event_stats', inReplyToId, 'replies_count', 1]); statDiffs.push(['event_stats', inReplyToId, 'replies_count', 1]);
} }
break; break;
case 3:
statDiffs.push(...getFollowDiff(event as Event<3>, event.prev as Event<3> | undefined));
break;
case 6: case 6:
if (firstTaggedId) { if (firstTaggedId) {
statDiffs.push(['event_stats', firstTaggedId, 'reposts_count', 1]); statDiffs.push(['event_stats', firstTaggedId, 'reposts_count', 1]);
@ -50,6 +62,7 @@ function getStatsDiff(event: Event): StatDiff[] {
return statDiffs; return statDiffs;
} }
/** Create an author stats query from the list of diffs. */
function authorStatsQuery(diffs: AuthorStatDiff[]) { function authorStatsQuery(diffs: AuthorStatDiff[]) {
const values: AuthorStatsRow[] = diffs.map(([_, pubkey, stat, diff]) => { const values: AuthorStatsRow[] = diffs.map(([_, pubkey, stat, diff]) => {
const row: AuthorStatsRow = { const row: AuthorStatsRow = {
@ -75,6 +88,7 @@ function authorStatsQuery(diffs: AuthorStatDiff[]) {
); );
} }
/** Create an event stats query from the list of diffs. */
function eventStatsQuery(diffs: EventStatDiff[]) { function eventStatsQuery(diffs: EventStatDiff[]) {
const values: EventStatsRow[] = diffs.map(([_, event_id, stat, diff]) => { const values: EventStatsRow[] = diffs.map(([_, event_id, stat, diff]) => {
const row: EventStatsRow = { const row: EventStatsRow = {
@ -100,4 +114,64 @@ function eventStatsQuery(diffs: EventStatDiff[]) {
); );
} }
/** Set the `prev` value on the event to the last version of the event, if any. */
async function maybeSetPrev<K extends number>(event: Event<K> & { prev?: Event<K> }): Promise<void> {
if (event.prev?.kind === event.kind) return;
const [prev] = await eventsDB.getFilters([
{ kinds: [event.kind], authors: [event.pubkey], limit: 1 },
]);
if (prev.created_at < event.created_at) {
event.prev = prev;
}
}
/** Set the following count to the total number of unique "p" tags in the follow list. */
function updateFollowingCountQuery({ pubkey, tags }: Event<3>) {
const following_count = new Set(
tags
.filter(([name]) => name === 'p')
.map(([_, value]) => value),
).size;
return db.insertInto('author_stats')
.values({
pubkey,
following_count,
followers_count: 0,
notes_count: 0,
})
.onConflict((oc) =>
oc
.column('pubkey')
.doUpdateSet({ following_count })
);
}
/** Compare the old and new follow events (if any), and return a diff array. */
function getFollowDiff(event: Event<3>, prev?: Event<3>): AuthorStatDiff[] {
const prevTags = prev?.tags ?? [];
const prevPubkeys = new Set(
prevTags
.filter(([name]) => name === 'p')
.map(([_, value]) => value),
);
const pubkeys = new Set(
event.tags
.filter(([name]) => name === 'p')
.map(([_, value]) => value),
);
const added = [...pubkeys].filter((pubkey) => !prevPubkeys.has(pubkey));
const removed = [...prevPubkeys].filter((pubkey) => !pubkeys.has(pubkey));
return [
...added.map((pubkey): AuthorStatDiff => ['author_stats', pubkey, 'followers_count', 1]),
...removed.map((pubkey): AuthorStatDiff => ['author_stats', pubkey, 'followers_count', -1]),
];
}
export { updateStats }; export { updateStats };