import type * as ShTangledActorDefs from "$lib/api/lexicons/types/sh/tangled/actor/defs"; import type * as ShTangledFeedGetTimeline from "$lib/api/lexicons/types/sh/tangled/feed/getTimeline"; type TimelineItem = ShTangledFeedGetTimeline.$output["feed"][number]; export interface TimelineEvent { event: E; eventAt: string; } interface Group { kind: K; key: string; items: TimelineEvent[]; } interface ActorGroup extends Group { actor: ShTangledActorDefs.ProfileViewBasic; } type ActorEntry = | ActorGroup<"follow", ShTangledFeedGetTimeline.FollowEvent> | ActorGroup<"star", ShTangledFeedGetTimeline.StarEvent> | ActorGroup<"create" | "fork", ShTangledFeedGetTimeline.RepoEvent>; type TargetEntry = | Group<"followers", ShTangledFeedGetTimeline.FollowEvent> | Group<"stargazers", ShTangledFeedGetTimeline.StarEvent> | Group<"forkers", ShTangledFeedGetTimeline.RepoEvent>; export type TimelineEntry = ActorEntry | TargetEntry; const TARGET_THRESHOLD = 3; const TARGET_KIND = { follow: "followers", star: "stargazers", fork: "forkers" } as const; const GROUP_WINDOW_MS = 60 * 60 * 1000; const withinWindow = (a: string, b: string) => Math.abs(Date.parse(a) - Date.parse(b)) < GROUP_WINDOW_MS; const toEntry = ({ event, eventAt }: TimelineItem): ActorEntry | undefined => { switch (event.$type) { case "sh.tangled.feed.getTimeline#followEvent": return { kind: "follow", key: event.uri, actor: event.actor, items: [{ event, eventAt }] }; case "sh.tangled.feed.getTimeline#starEvent": return { kind: "star", key: event.uri, actor: event.actor, items: [{ event, eventAt }] }; case "sh.tangled.feed.getTimeline#repoEvent": return { kind: event.source ? "fork" : "create", key: event.uri, actor: event.repo.owner, items: [{ event, eventAt }] }; } }; const targetOf = (entry: ActorEntry): string | undefined => { switch (entry.kind) { case "follow": return `follow ${entry.items[0].event.subject.did}`; case "star": { const { owner, slug } = entry.items[0].event.repo; return `star ${owner.did}/${slug}`; } case "fork": { const { owner, slug } = entry.items[0].event.source!; return `fork ${owner.did}/${slug}`; } } }; const popularTargets = (entries: (ActorEntry | undefined)[]) => { const actors = new Map>(); for (const entry of entries) { const target = entry && targetOf(entry); if (!target) continue; actors.set(target, (actors.get(target) ?? new Set()).add(entry.actor.did)); } return new Set( [...actors].filter(([, dids]) => dids.size >= TARGET_THRESHOLD).map(([target]) => target) ); }; export const groupTimeline = (feed: TimelineItem[]): TimelineEntry[] => { const singles = feed.map(toEntry); const popular = popularTargets(singles); const entries: TimelineEntry[] = []; const targets = new Map(); const open = new Map(); let runActor: string | undefined; let runStart = 0; for (const [index, next] of singles.entries()) { if (!next) continue; const target = targetOf(next); if (target && popular.has(target) && next.kind !== "create") { const group = targets.get(target); if (group) { (group.items as unknown[]).push(...next.items); } else { const entry = { kind: TARGET_KIND[next.kind], key: next.key, items: next.items } as TargetEntry; targets.set(target, entry); entries.push(entry); } continue; } if (next.actor.did !== runActor) { runActor = next.actor.did; runStart = index; } const groupKey = `${next.actor.did} ${next.kind}`; const group = open.get(groupKey); if ( group && (group.lastIndex >= runStart || withinWindow(group.entry.items.at(-1)!.eventAt, next.items[0].eventAt)) ) { (group.entry.items as unknown[]).push(...next.items); group.lastIndex = index; } else { entries.push(next); open.set(groupKey, { entry: next, lastIndex: index }); } } return entries; };