import type * as OrgTangledActorDefs from "#lib/api/lexicons/types/org/tangled/actor/defs.ts";
import type * as OrgTangledFeedGetTimeline from "#lib/api/lexicons/types/org/tangled/feed/getTimeline.ts";
type TimelineItem = OrgTangledFeedGetTimeline.$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: OrgTangledActorDefs.ProfileViewBasic;
}
type ActorEntry =
| ActorGroup<"follow", OrgTangledFeedGetTimeline.FollowEvent>
| ActorGroup<"star", OrgTangledFeedGetTimeline.StarEvent>
| ActorGroup<"create" | "fork", OrgTangledFeedGetTimeline.RepoEvent>;
type TargetEntry =
| Group<"followers", OrgTangledFeedGetTimeline.FollowEvent>
| Group<"stargazers", OrgTangledFeedGetTimeline.StarEvent>
| Group<"forkers", OrgTangledFeedGetTimeline.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 "org.tangled.feed.getTimeline#followEvent":
return {
kind: "follow",
key: event.uri,
actor: event.actor,
items: [{ event, eventAt }]
};
case "org.tangled.feed.getTimeline#starEvent":
return {
kind: "star",
key: event.uri,
actor: event.actor,
items: [{ event, eventAt }]
};
case "org.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;
};