a802a95814e6 | atagen | 2026-10-26 03:48 UTC | 1 | import assert from 'node:assert/strict'; |
a802a95814e6 | atagen | 2026-10-26 03:48 UTC | 2 | import { test } from 'node:test'; |
a802a95814e6 | atagen | 2026-10-26 03:48 UTC | 3 | import { newShareCache, parseShared } from '../src/lib/sharing.ts'; |
a802a95814e6 | atagen | 2026-10-26 03:48 UTC | 4 | import { observeArrivals } from '../src/lib/arrivals.ts'; |
a802a95814e6 | atagen | 2026-10-26 03:48 UTC | 5 | import type { TimelineEvent, TimelineItem } from '../src/lib/types.ts'; |
a802a95814e6 | atagen | 2026-10-26 03:48 UTC | 6 | |
a802a95814e6 | atagen | 2026-10-26 03:48 UTC | 7 | interface Row { |
a802a95814e6 | atagen | 2026-10-26 03:48 UTC | 8 | id: string; |
a802a95814e6 | atagen | 2026-10-26 03:48 UTC | 9 | count: number; |
a802a95814e6 | atagen | 2026-10-26 03:48 UTC | 10 | } |
a802a95814e6 | atagen | 2026-10-26 03:48 UTC | 11 | |
a802a95814e6 | atagen | 2026-10-26 03:48 UTC | 12 | function parse(entry: unknown): Row | null { |
a802a95814e6 | atagen | 2026-10-26 03:48 UTC | 13 | const raw = entry as Record<string, unknown>; |
a802a95814e6 | atagen | 2026-10-26 03:48 UTC | 14 | return typeof raw['id'] === 'string' ? { id: raw['id'], count: Number(raw['count']) } : null; |
a802a95814e6 | atagen | 2026-10-26 03:48 UTC | 15 | } |
a802a95814e6 | atagen | 2026-10-26 03:48 UTC | 16 | |
a802a95814e6 | atagen | 2026-10-26 03:48 UTC | 17 | test('unchanged entries keep their object and unchanged lists keep their array', () => { |
a802a95814e6 | atagen | 2026-10-26 03:48 UTC | 18 | const cache = newShareCache<Row>(); |
a802a95814e6 | atagen | 2026-10-26 03:48 UTC | 19 | const first = parseShared(cache, [{ id: 'a', count: 1 }, { id: 'b', count: 1 }], parse); |
a802a95814e6 | atagen | 2026-10-26 03:48 UTC | 20 | const second = parseShared(cache, [{ id: 'a', count: 1 }, { id: 'b', count: 1 }], parse); |
a802a95814e6 | atagen | 2026-10-26 03:48 UTC | 21 | assert.equal(second, first); |
a802a95814e6 | atagen | 2026-10-26 03:48 UTC | 22 | }); |
a802a95814e6 | atagen | 2026-10-26 03:48 UTC | 23 | |
a802a95814e6 | atagen | 2026-10-26 03:48 UTC | 24 | test('a changed entry gets a new object while its neighbours keep theirs', () => { |
a802a95814e6 | atagen | 2026-10-26 03:48 UTC | 25 | const cache = newShareCache<Row>(); |
a802a95814e6 | atagen | 2026-10-26 03:48 UTC | 26 | const first = parseShared(cache, [{ id: 'a', count: 1 }, { id: 'b', count: 1 }], parse); |
a802a95814e6 | atagen | 2026-10-26 03:48 UTC | 27 | const second = parseShared(cache, [{ id: 'a', count: 1 }, { id: 'b', count: 2 }], parse); |
a802a95814e6 | atagen | 2026-10-26 03:48 UTC | 28 | assert.notEqual(second, first); |
a802a95814e6 | atagen | 2026-10-26 03:48 UTC | 29 | assert.equal(second[0], first[0]); |
a802a95814e6 | atagen | 2026-10-26 03:48 UTC | 30 | assert.notEqual(second[1], first[1]); |
a802a95814e6 | atagen | 2026-10-26 03:48 UTC | 31 | assert.equal(second[1].count, 2); |
a802a95814e6 | atagen | 2026-10-26 03:48 UTC | 32 | }); |
a802a95814e6 | atagen | 2026-10-26 03:48 UTC | 33 | |
a802a95814e6 | atagen | 2026-10-26 03:48 UTC | 34 | test('appended and removed entries change the array', () => { |
a802a95814e6 | atagen | 2026-10-26 03:48 UTC | 35 | const cache = newShareCache<Row>(); |
a802a95814e6 | atagen | 2026-10-26 03:48 UTC | 36 | const first = parseShared(cache, [{ id: 'a', count: 1 }], parse); |
a802a95814e6 | atagen | 2026-10-26 03:48 UTC | 37 | const grown = parseShared(cache, [{ id: 'a', count: 1 }, { id: 'b', count: 1 }], parse); |
a802a95814e6 | atagen | 2026-10-26 03:48 UTC | 38 | assert.notEqual(grown, first); |
a802a95814e6 | atagen | 2026-10-26 03:48 UTC | 39 | assert.equal(grown[0], first[0]); |
a802a95814e6 | atagen | 2026-10-26 03:48 UTC | 40 | const shrunk = parseShared(cache, [{ id: 'b', count: 1 }], parse); |
a802a95814e6 | atagen | 2026-10-26 03:48 UTC | 41 | assert.equal(shrunk[0], grown[1]); |
a802a95814e6 | atagen | 2026-10-26 03:48 UTC | 42 | }); |
a802a95814e6 | atagen | 2026-10-26 03:48 UTC | 43 | |
a802a95814e6 | atagen | 2026-10-26 03:48 UTC | 44 | test('unparsable entries are skipped and non-arrays yield an empty list', () => { |
a802a95814e6 | atagen | 2026-10-26 03:48 UTC | 45 | const cache = newShareCache<Row>(); |
a802a95814e6 | atagen | 2026-10-26 03:48 UTC | 46 | assert.equal(parseShared(cache, [{ id: 'a', count: 1 }, { nope: true }], parse).length, 1); |
a802a95814e6 | atagen | 2026-10-26 03:48 UTC | 47 | assert.deepEqual(parseShared(cache, 'x', parse), []); |
a802a95814e6 | atagen | 2026-10-26 03:48 UTC | 48 | }); |
a802a95814e6 | atagen | 2026-10-26 03:48 UTC | 49 | |
a802a95814e6 | atagen | 2026-10-26 03:48 UTC | 50 | function event(id: string, timestamp_ms: number): TimelineItem { |
a802a95814e6 | atagen | 2026-10-26 03:48 UTC | 51 | return { kind: 'event', event: { item_id: id, timestamp_ms, own: false } as TimelineEvent }; |
a802a95814e6 | atagen | 2026-10-26 03:48 UTC | 52 | } |
a802a95814e6 | atagen | 2026-10-26 03:48 UTC | 53 | |
a802a95814e6 | atagen | 2026-10-26 03:48 UTC | 54 | test('arrivals keeps array and lagged item identity across identical updates', () => { |
a802a95814e6 | atagen | 2026-10-26 03:48 UTC | 55 | const MINUTE = 60 * 1000; |
a802a95814e6 | atagen | 2026-10-26 03:48 UTC | 56 | const known = event('a', 90 * MINUTE); |
a802a95814e6 | atagen | 2026-10-26 03:48 UTC | 57 | const late = event('late', 80 * MINUTE); |
a802a95814e6 | atagen | 2026-10-26 03:48 UTC | 58 | const first = observeArrivals([known], [known, late], 0, 100 * MINUTE); |
a802a95814e6 | atagen | 2026-10-26 03:48 UTC | 59 | assert.ok(first); |
a802a95814e6 | atagen | 2026-10-26 03:48 UTC | 60 | const second = observeArrivals(first, [known, late], 0, 101 * MINUTE); |
a802a95814e6 | atagen | 2026-10-26 03:48 UTC | 61 | assert.equal(second, first); |
a802a95814e6 | atagen | 2026-10-26 03:48 UTC | 62 | }); |
a802a95814e6 | atagen | 2026-10-26 03:48 UTC | 63 | |
a802a95814e6 | atagen | 2026-10-26 03:48 UTC | 64 | test('identified entries are matched by identity and evicted when absent', () => { |
a802a95814e6 | atagen | 2026-10-26 03:48 UTC | 65 | const cache = newShareCache<Row>(); |
a802a95814e6 | atagen | 2026-10-26 03:48 UTC | 66 | const identify = (entry: unknown) => (entry as Row).id; |
a802a95814e6 | atagen | 2026-10-26 03:48 UTC | 67 | const first = parseShared(cache, [{ id: 'a', count: 1 }, { id: 'b', count: 1 }], parse, identify); |
a802a95814e6 | atagen | 2026-10-26 03:48 UTC | 68 | const second = parseShared(cache, [{ id: 'b', count: 1 }, { id: 'c', count: 1 }], parse, identify); |
a802a95814e6 | atagen | 2026-10-26 03:48 UTC | 69 | assert.equal(second[0], first[1]); |
a802a95814e6 | atagen | 2026-10-26 03:48 UTC | 70 | assert.deepEqual([...cache.byKey.keys()], ['b', 'c']); |
a802a95814e6 | atagen | 2026-10-26 03:48 UTC | 71 | }); |
a802a95814e6 | atagen | 2026-10-26 03:48 UTC | 72 | |
a802a95814e6 | atagen | 2026-10-26 03:48 UTC | 73 | test('a changed identified entry is reparsed under the same key', () => { |
a802a95814e6 | atagen | 2026-10-26 03:48 UTC | 74 | const cache = newShareCache<Row>(); |
a802a95814e6 | atagen | 2026-10-26 03:48 UTC | 75 | const identify = (entry: unknown) => (entry as Row).id; |
a802a95814e6 | atagen | 2026-10-26 03:48 UTC | 76 | const first = parseShared(cache, [{ id: 'a', count: 1 }], parse, identify); |
a802a95814e6 | atagen | 2026-10-26 03:48 UTC | 77 | const second = parseShared(cache, [{ id: 'a', count: 2 }], parse, identify); |
a802a95814e6 | atagen | 2026-10-26 03:48 UTC | 78 | assert.notEqual(second[0], first[0]); |
a802a95814e6 | atagen | 2026-10-26 03:48 UTC | 79 | assert.equal(second[0].count, 2); |
a802a95814e6 | atagen | 2026-10-26 03:48 UTC | 80 | }); |