d2a34d0f4861 | atagen | 2026-10-25 07:29 UTC | 1 | import type { TimelineItem } from './types'; |
d2a34d0f4861 | atagen | 2026-10-25 07:29 UTC | 2 | |
d2a34d0f4861 | atagen | 2026-10-25 07:29 UTC | 3 | export const FEDERATION_LAG_MS = 60 * 1000; |
d2a34d0f4861 | atagen | 2026-10-25 07:29 UTC | 4 | |
a802a95814e6 | atagen | 2026-10-26 03:48 UTC | 5 | const decorated = new WeakMap<TimelineItem, { lag: number; item: TimelineItem }>(); |
a802a95814e6 | atagen | 2026-10-26 03:48 UTC | 6 | |
a802a95814e6 | atagen | 2026-10-26 03:48 UTC | 7 | function withLag(item: TimelineItem & { kind: 'event' }, lag: number): TimelineItem { |
a802a95814e6 | atagen | 2026-10-26 03:48 UTC | 8 | const hit = decorated.get(item); |
a802a95814e6 | atagen | 2026-10-26 03:48 UTC | 9 | if (hit && hit.lag === lag) return hit.item; |
a802a95814e6 | atagen | 2026-10-26 03:48 UTC | 10 | const next: TimelineItem = { ...item, event: { ...item.event, delivery_lag_ms: lag } }; |
a802a95814e6 | atagen | 2026-10-26 03:48 UTC | 11 | decorated.set(item, { lag, item: next }); |
a802a95814e6 | atagen | 2026-10-26 03:48 UTC | 12 | return next; |
a802a95814e6 | atagen | 2026-10-26 03:48 UTC | 13 | } |
a802a95814e6 | atagen | 2026-10-26 03:48 UTC | 14 | |
d2a34d0f4861 | atagen | 2026-10-25 07:29 UTC | 15 | /** |
d2a34d0f4861 | atagen | 2026-10-25 07:29 UTC | 16 | * Carries delivery lag forward from the previous list and measures it for events appended |
d2a34d0f4861 | atagen | 2026-10-25 07:29 UTC | 17 | * after the last one already known. `since` is when the room last became able to receive |
d2a34d0f4861 | atagen | 2026-10-25 07:29 UTC | 18 | * events, so catch-up after being offline or closed doesn't count as federation delay. |
d2a34d0f4861 | atagen | 2026-10-25 07:29 UTC | 19 | * Returns null when nothing in `items` was known before, meaning the list was replaced. |
d2a34d0f4861 | atagen | 2026-10-25 07:29 UTC | 20 | */ |
d2a34d0f4861 | atagen | 2026-10-25 07:29 UTC | 21 | export function observeArrivals(previous: TimelineItem[], items: TimelineItem[], since: number, now: number): TimelineItem[] | null { |
d2a34d0f4861 | atagen | 2026-10-25 07:29 UTC | 22 | const lags = new Map<string, number | undefined>(); |
d2a34d0f4861 | atagen | 2026-10-25 07:29 UTC | 23 | for (const item of previous) if (item.kind === 'event') lags.set(item.event.item_id, item.event.delivery_lag_ms); |
d2a34d0f4861 | atagen | 2026-10-25 07:29 UTC | 24 | let lastKnown = -1; |
d2a34d0f4861 | atagen | 2026-10-25 07:29 UTC | 25 | items.forEach((item, index) => { |
d2a34d0f4861 | atagen | 2026-10-25 07:29 UTC | 26 | if (item.kind === 'event' && lags.has(item.event.item_id)) lastKnown = index; |
d2a34d0f4861 | atagen | 2026-10-25 07:29 UTC | 27 | }); |
d2a34d0f4861 | atagen | 2026-10-25 07:29 UTC | 28 | if (lastKnown === -1) return null; |
a802a95814e6 | atagen | 2026-10-26 03:48 UTC | 29 | const next = items.map((item, index) => { |
d2a34d0f4861 | atagen | 2026-10-25 07:29 UTC | 30 | if (item.kind !== 'event') return item; |
d2a34d0f4861 | atagen | 2026-10-25 07:29 UTC | 31 | const event = item.event; |
d2a34d0f4861 | atagen | 2026-10-25 07:29 UTC | 32 | if (lags.has(event.item_id)) { |
d2a34d0f4861 | atagen | 2026-10-25 07:29 UTC | 33 | const carried = lags.get(event.item_id); |
a802a95814e6 | atagen | 2026-10-26 03:48 UTC | 34 | return carried === undefined ? item : withLag(item, carried); |
d2a34d0f4861 | atagen | 2026-10-25 07:29 UTC | 35 | } |
d2a34d0f4861 | atagen | 2026-10-25 07:29 UTC | 36 | if (index < lastKnown || event.own) return item; |
d2a34d0f4861 | atagen | 2026-10-25 07:29 UTC | 37 | const lag = now - Math.max(event.timestamp_ms, since); |
a802a95814e6 | atagen | 2026-10-26 03:48 UTC | 38 | return lag < FEDERATION_LAG_MS ? item : withLag(item, lag); |
d2a34d0f4861 | atagen | 2026-10-25 07:29 UTC | 39 | }); |
a802a95814e6 | atagen | 2026-10-26 03:48 UTC | 40 | return previous.length === next.length && next.every((item, index) => item === previous[index]) ? previous : next; |
d2a34d0f4861 | atagen | 2026-10-25 07:29 UTC | 41 | } |