stats: handle follow/following counts
This commit is contained in:
parent
2d3f12dc72
commit
4f79b7ec29
|
@ -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,
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
94
src/stats.ts
94
src/stats.ts
|
@ -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 };
|
||||||
|
|
Loading…
Reference in New Issue