| 1 | import assert from 'node:assert/strict'; |
| 2 | import { test } from 'node:test'; |
| 3 | import { observeArrivals } from '../src/lib/arrivals.ts'; |
| 4 | import type { TimelineEvent, TimelineItem } from '../src/lib/types.ts'; |
| 5 | |
| 6 | const MINUTE = 60 * 1000; |
| 7 | |
| 8 | function event(id: string, timestamp_ms: number, extra: Partial<TimelineEvent> = {}): TimelineItem { |
| 9 | return { kind: 'event', event: { item_id: id, timestamp_ms, own: false, ...extra } as TimelineEvent }; |
| 10 | } |
| 11 | |
| 12 | function lagOf(items: TimelineItem[] | null, id: string): number | undefined { |
| 13 | const found = items?.find((item) => item.kind === 'event' && item.event.item_id === id); |
| 14 | return found?.kind === 'event' ? found.event.delivery_lag_ms : undefined; |
| 15 | } |
| 16 | |
| 17 | test('flags a delayed event appended after newer known events', () => { |
| 18 | const now = 100 * MINUTE; |
| 19 | const previous = [event('a', 90 * MINUTE), event('b', 95 * MINUTE)]; |
| 20 | const next = observeArrivals(previous, [...previous, event('late', 80 * MINUTE)], 0, now); |
| 21 | assert.equal(lagOf(next, 'late'), 20 * MINUTE); |
| 22 | }); |
| 23 | |
| 24 | test('ignores backfill and prompt arrivals', () => { |
| 25 | const now = 100 * MINUTE; |
| 26 | const previous = [event('a', 99 * MINUTE)]; |
| 27 | const next = observeArrivals(previous, [event('old', 10 * MINUTE), ...previous, event('new', now - 1000)], 0, now); |
| 28 | assert.equal(lagOf(next, 'old'), undefined); |
| 29 | assert.equal(lagOf(next, 'new'), undefined); |
| 30 | }); |
| 31 | |
| 32 | test('measures from when the room became live, not from the send time', () => { |
| 33 | const now = 100 * MINUTE; |
| 34 | const previous = [event('a', 10 * MINUTE)]; |
| 35 | const caughtUp = observeArrivals(previous, [...previous, event('offline', 20 * MINUTE)], now - 5000, now); |
| 36 | assert.equal(lagOf(caughtUp, 'offline'), undefined); |
| 37 | const delayed = observeArrivals(previous, [...previous, event('stuck', 20 * MINUTE)], now - 3 * MINUTE, now); |
| 38 | assert.equal(lagOf(delayed, 'stuck'), 3 * MINUTE); |
| 39 | }); |
| 40 | |
| 41 | test('carries a flag across later updates', () => { |
| 42 | const now = 100 * MINUTE; |
| 43 | const previous = [event('a', 99 * MINUTE), event('late', 50 * MINUTE, { delivery_lag_ms: 50 * MINUTE })]; |
| 44 | const next = observeArrivals(previous, [...previous.map((item) => item.kind === 'event' ? event(item.event.item_id, item.event.timestamp_ms) : item), event('c', now)], 0, now + MINUTE); |
| 45 | assert.equal(lagOf(next, 'late'), 50 * MINUTE); |
| 46 | }); |
| 47 | |
| 48 | test('skips own events and replaced lists', () => { |
| 49 | const now = 100 * MINUTE; |
| 50 | const previous = [event('a', 99 * MINUTE)]; |
| 51 | const own = observeArrivals(previous, [...previous, event('mine', 10 * MINUTE, { own: true })], 0, now); |
| 52 | assert.equal(lagOf(own, 'mine'), undefined); |
| 53 | assert.equal(observeArrivals(previous, [event('x', 10 * MINUTE)], 0, now), null); |
| 54 | }); |