Something went wrong. Try again.
Monorepo for Tangled tangled.org
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141import 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<E> { event: E; eventAt: string;}
interface Group<K extends string, E> { kind: K; key: string; items: TimelineEvent<E>[];}
interface ActorGroup<K extends string, E> extends Group<K, E> { 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<string, Set<string>>(); 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<string, TargetEntry>(); const open = new Map<string, { entry: ActorEntry; lastIndex: number }>(); 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;};