import { describe, it, beforeEach } from 'node:test'; import assert from 'node:assert/strict'; import { readFileSync } from 'node:fs'; import { dirname, resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; const __dirname = dirname(fileURLToPath(import.meta.url)); const rawSrc = readFileSync(resolve(__dirname, '..', 'src', 'services', 'runtime.ts'), 'utf-8'); function stripTS(src) { let out = src; out = out.replace(/\bexport\s+type\s+\w+\s*=[^;]+;/g, ''); out = out.replace(/\bexport\s+interface\s+\w+\s*\{[^}]*\}/g, ''); out = out.replace(/\bexport\s+/g, ''); out = out.replace(/\bas\s+\{[^}]+\}/g, ''); out = out.replace(/:\s*ReturnType\s*\|\s*null/g, ''); out = out.replace(/:\s*AbortController\s*\|\s*null/g, ''); out = out.replace(/:\s*\(\(\)\s*=>\s*void\)\s*\|\s*null/g, ''); out = out.replace(/:\s*SmartPollReason/g, ''); out = out.replace(/:\s*SmartPollContext/g, ''); out = out.replace(/:\s*SmartPollOptions/g, ''); out = out.replace(/:\s*SmartPollLoopHandle/g, ''); out = out.replace(/:\s*Promise/g, ''); out = out.replace(/:\s*Promise\s*\|\s*boolean\s*\|\s*void/g, ''); out = out.replace(/\(\s*ctx\s*\)\s*=>/g, '(ctx) =>'); out = out.replace(/:\s*number\s*\|\s*null/g, ''); out = out.replace(/:\s*(?:number|boolean|string|unknown|void)\b/g, ''); out = out.replace(/\?\.\s*/g, '?.'); return out; } const runtimeSrc = stripTS(rawSrc); function extractBody(source, funcName) { const sig = new RegExp(`function\\s+${funcName}\\s*\\(`); const match = sig.exec(source); if (!match) throw new Error(`Could not find function ${funcName}`); const openBrace = source.indexOf('{', match.index); if (openBrace === -1) throw new Error(`No body found for ${funcName}`); const bodyStart = openBrace + 1; let depth = 1; let state = 'code'; let escaped = false; for (let j = bodyStart; j < source.length; j++) { const ch = source[j]; const next = source[j + 1]; if (state === 'line-comment') { if (ch === '\n') state = 'code'; continue; } if (state === 'block-comment') { if (ch === '*' && next === '/') { state = 'code'; j++; } continue; } if (state === 'single-quote') { if (escaped) { escaped = false; } else if (ch === '\\') { escaped = true; } else if (ch === "'") { state = 'code'; } continue; } if (state === 'double-quote') { if (escaped) { escaped = false; } else if (ch === '\\') { escaped = true; } else if (ch === '"') { state = 'code'; } continue; } if (state === 'template') { if (escaped) { escaped = false; } else if (ch === '\\') { escaped = true; } else if (ch === '`') { state = 'code'; } continue; } if (ch === '/' && next === '/') { state = 'line-comment'; j++; continue; } if (ch === '/' && next === '*') { state = 'block-comment'; j++; continue; } if (ch === "'") { state = 'single-quote'; continue; } if (ch === '"') { state = 'double-quote'; continue; } if (ch === '`') { state = 'template'; continue; } if (ch === '{') { depth++; continue; } if (ch === '}') { depth--; if (depth === 0) return source.slice(bodyStart, j); } } throw new Error(`Could not extract body for ${funcName}`); } function createFakeTimers(startMs = 1_000_000) { const tasks = new Map(); let now = startMs; let nextId = 1; const sortedDueTasks = (target) => Array.from(tasks.entries()) .filter(([, task]) => task.at <= target) .sort((a, b) => (a[1].at - b[1].at) || (a[0] - b[0])); return { get now() { return now; }, get pendingCount() { return tasks.size; }, setTimeout(fn, delay = 0) { const id = nextId++; tasks.set(id, { at: now + Math.max(0, delay), fn }); return id; }, clearTimeout(id) { tasks.delete(id); }, advanceBy(ms) { const target = now + Math.max(0, ms); while (true) { const due = sortedDueTasks(target); if (!due.length) break; const [id, task] = due[0]; tasks.delete(id); now = task.at; task.fn(); } now = target; }, async advanceByAsync(ms) { const target = now + Math.max(0, ms); while (true) { const due = sortedDueTasks(target); if (!due.length) break; const [id, task] = due[0]; tasks.delete(id); now = task.at; task.fn(); await Promise.resolve(); } now = target; }, runAll() { let safety = 0; while (tasks.size > 0 && safety < 500) { const [[id, task]] = Array.from(tasks.entries()).sort( (a, b) => (a[1].at - b[1].at) || (a[0] - b[0]) ); tasks.delete(id); now = task.at; task.fn(); safety++; } }, }; } function buildSmartPollLoop(timers, docMock) { const isAbortErrorBody = extractBody(runtimeSrc, 'isAbortError'); const hasVisibilityApiBody = extractBody(runtimeSrc, 'hasVisibilityApi'); const isDocumentHiddenBody = extractBody(runtimeSrc, 'isDocumentHidden'); const mainBody = extractBody(runtimeSrc, 'startSmartPollLoop'); const factory = new Function( 'setTimeout', 'clearTimeout', 'Math', 'AbortController', 'document', ` function isAbortError(error) { ${isAbortErrorBody} } function hasVisibilityApi() { ${hasVisibilityApiBody} } function isDocumentHidden() { ${isDocumentHiddenBody} } return function startSmartPollLoop(poll, opts) { ${mainBody} }; ` ); return factory( timers.setTimeout.bind(timers), timers.clearTimeout.bind(timers), Math, AbortController, docMock, ); } function createDocMock(hidden = false) { const listeners = new Map(); return { visibilityState: hidden ? 'hidden' : 'visible', addEventListener(evt, fn) { if (!listeners.has(evt)) listeners.set(evt, []); listeners.get(evt).push(fn); }, removeEventListener(evt, fn) { if (!listeners.has(evt)) return; const arr = listeners.get(evt); const idx = arr.indexOf(fn); if (idx !== -1) arr.splice(idx, 1); }, _fire(evt) { for (const fn of (listeners.get(evt) || [])) fn(); }, _setHidden(h) { this.visibilityState = h ? 'hidden' : 'visible'; }, _listenerCount(evt) { return (listeners.get(evt) || []).length; }, }; } describe('startSmartPollLoop', () => { let timers; let doc; let startSmartPollLoop; beforeEach(() => { timers = createFakeTimers(); doc = createDocMock(); startSmartPollLoop = buildSmartPollLoop(timers, doc); }); describe('scheduling', () => { it('fires first tick after intervalMs', async () => { let calls = 0; startSmartPollLoop(() => { calls++; }, { intervalMs: 5_000, jitterFraction: 0 }); assert.equal(calls, 0); timers.advanceBy(4_999); await Promise.resolve(); assert.equal(calls, 0); timers.advanceBy(1); await Promise.resolve(); assert.equal(calls, 1); }); it('subsequent ticks continue firing', async () => { let calls = 0; startSmartPollLoop(() => { calls++; }, { intervalMs: 5_000, jitterFraction: 0 }); timers.advanceBy(5_000); await Promise.resolve(); assert.equal(calls, 1); timers.advanceBy(5_000); await Promise.resolve(); assert.equal(calls, 2); timers.advanceBy(5_000); await Promise.resolve(); assert.equal(calls, 3); }); }); describe('jitter', () => { it('delay varies within ±jitterFraction of base interval', async () => { const delays = []; let lastCall = timers.now; const poll = () => { delays.push(timers.now - lastCall); lastCall = timers.now; }; startSmartPollLoop(poll, { intervalMs: 10_000, jitterFraction: 0.2 }); for (let i = 0; i < 250; i++) { timers.advanceBy(500); await Promise.resolve(); } assert.ok(delays.length >= 8, `expected at least 8 calls, got ${delays.length}`); for (const d of delays) { assert.ok(d >= 8_000, `delay ${d} should be >= 8000`); assert.ok(d <= 13_000, `delay ${d} should be <= 13000`); } }); }); describe('backoff', () => { it('doubles interval on false return, resets on success', async () => { let returnVal = false; let calls = 0; startSmartPollLoop(() => { calls++; return returnVal; }, { intervalMs: 1_000, jitterFraction: 0, maxBackoffMultiplier: 8, }); timers.advanceBy(1_000); await Promise.resolve(); assert.equal(calls, 1); timers.advanceBy(2_000); await Promise.resolve(); assert.equal(calls, 2); timers.advanceBy(4_000); await Promise.resolve(); assert.equal(calls, 3); returnVal = true; timers.advanceBy(8_000); await Promise.resolve(); assert.equal(calls, 4); timers.advanceBy(1_000); await Promise.resolve(); assert.equal(calls, 5); }); it('caps at maxBackoffMultiplier', async () => { let calls = 0; startSmartPollLoop(() => { calls++; return false; }, { intervalMs: 1_000, jitterFraction: 0, maxBackoffMultiplier: 4, }); timers.advanceBy(1_000); await Promise.resolve(); // 1x timers.advanceBy(2_000); await Promise.resolve(); // 2x timers.advanceBy(4_000); await Promise.resolve(); // 4x (cap) assert.equal(calls, 3); timers.advanceBy(4_000); await Promise.resolve(); // still 4x assert.equal(calls, 4); }); }); describe('error backoff', () => { it('thrown errors trigger backoff and onError', async () => { const errors = []; let calls = 0; startSmartPollLoop(() => { calls++; throw new Error('fail'); }, { intervalMs: 1_000, jitterFraction: 0, maxBackoffMultiplier: 4, onError: (e) => errors.push(e), }); timers.advanceBy(1_000); await Promise.resolve(); assert.equal(calls, 1); assert.equal(errors.length, 1); assert.equal(errors[0].message, 'fail'); timers.advanceBy(2_000); await Promise.resolve(); assert.equal(calls, 2); }); }); describe('shouldRun gating', () => { it('poll skipped when shouldRun returns false', async () => { let gate = false; let calls = 0; startSmartPollLoop(() => { calls++; }, { intervalMs: 1_000, jitterFraction: 0, shouldRun: () => gate, }); timers.advanceBy(1_000); await Promise.resolve(); assert.equal(calls, 0); gate = true; timers.advanceBy(1_000); await Promise.resolve(); assert.equal(calls, 1); }); }); describe('runImmediately', () => { it('fires at t=0 with reason startup', async () => { let capturedReason = null; startSmartPollLoop((ctx) => { capturedReason = ctx.reason; }, { intervalMs: 5_000, runImmediately: true, jitterFraction: 0, }); await Promise.resolve(); assert.equal(capturedReason, 'startup'); }); }); describe('pauseWhenHidden', () => { it('no ticks while hidden', async () => { doc._setHidden(true); let calls = 0; startSmartPollLoop(() => { calls++; }, { intervalMs: 1_000, jitterFraction: 0, pauseWhenHidden: true, }); timers.advanceBy(10_000); await Promise.resolve(); assert.equal(calls, 0); }); it('resumes on visibility change to visible', async () => { doc._setHidden(false); let calls = 0; const handle = startSmartPollLoop(() => { calls++; }, { intervalMs: 1_000, jitterFraction: 0, pauseWhenHidden: true, visibilityDebounceMs: 0, }); timers.advanceBy(1_000); await Promise.resolve(); assert.equal(calls, 1); doc._setHidden(true); doc._fire('visibilitychange'); timers.advanceBy(5_000); await Promise.resolve(); assert.equal(calls, 1); doc._setHidden(false); doc._fire('visibilitychange'); await Promise.resolve(); assert.ok(calls >= 2, `expected resume poll, got ${calls}`); handle.stop(); }); it('aborts in-flight on hide', async () => { let aborted = false; const handle = startSmartPollLoop(async (ctx) => { ctx.signal?.addEventListener('abort', () => { aborted = true; }); return new Promise(() => { }); }, { intervalMs: 1_000, jitterFraction: 0, pauseWhenHidden: true, runImmediately: true, visibilityDebounceMs: 0, }); await Promise.resolve(); doc._setHidden(true); doc._fire('visibilitychange'); assert.equal(aborted, true); handle.stop(); }); }); describe('hiddenMultiplier', () => { it('interval scaled when hidden (not paused)', async () => { doc._setHidden(true); let calls = 0; startSmartPollLoop(() => { calls++; }, { intervalMs: 1_000, hiddenMultiplier: 5, jitterFraction: 0, pauseWhenHidden: false, }); timers.advanceBy(4_999); await Promise.resolve(); assert.equal(calls, 0); timers.advanceBy(1); await Promise.resolve(); assert.equal(calls, 1); }); }); describe('hiddenIntervalMs', () => { it('explicit hidden interval overrides multiplier', async () => { doc._setHidden(true); let calls = 0; startSmartPollLoop(() => { calls++; }, { intervalMs: 1_000, hiddenMultiplier: 100, hiddenIntervalMs: 3_000, jitterFraction: 0, pauseWhenHidden: false, }); timers.advanceBy(2_999); await Promise.resolve(); assert.equal(calls, 0); timers.advanceBy(1); await Promise.resolve(); assert.equal(calls, 1); }); }); describe('refreshOnVisible', () => { it('immediate run with reason resume on tab visible', async () => { let capturedReason = null; startSmartPollLoop((ctx) => { capturedReason = ctx.reason; }, { intervalMs: 60_000, refreshOnVisible: true, jitterFraction: 0, visibilityDebounceMs: 0, }); doc._setHidden(true); doc._fire('visibilitychange'); doc._setHidden(false); doc._fire('visibilitychange'); await Promise.resolve(); assert.equal(capturedReason, 'resume'); }); }); describe('visibility debounce', () => { it('rapid show events coalesced within visibilityDebounceMs', async () => { let calls = 0; startSmartPollLoop(() => { calls++; }, { intervalMs: 60_000, refreshOnVisible: true, visibilityDebounceMs: 500, jitterFraction: 0, }); doc._setHidden(true); doc._fire('visibilitychange'); doc._setHidden(false); doc._fire('visibilitychange'); doc._setHidden(true); doc._fire('visibilitychange'); doc._setHidden(false); doc._fire('visibilitychange'); await Promise.resolve(); assert.equal(calls, 0); timers.advanceBy(500); await Promise.resolve(); assert.equal(calls, 1); }); }); describe('trigger()', () => { it('manual trigger fires immediately and resets schedule', async () => { let calls = 0; let lastReason = null; const handle = startSmartPollLoop((ctx) => { calls++; lastReason = ctx.reason; }, { intervalMs: 10_000, jitterFraction: 0, }); handle.trigger(); await Promise.resolve(); assert.equal(calls, 1); assert.equal(lastReason, 'manual'); timers.advanceBy(10_000); await Promise.resolve(); assert.equal(calls, 2); assert.equal(lastReason, 'interval'); }); }); describe('stop()', () => { it('clears timers, aborts in-flight, removes listener, isActive false', async () => { let aborted = false; const handle = startSmartPollLoop(async (ctx) => { ctx.signal?.addEventListener('abort', () => { aborted = true; }); return new Promise(() => { }); }, { intervalMs: 1_000, jitterFraction: 0, runImmediately: true, }); await Promise.resolve(); assert.equal(handle.isActive(), true); handle.stop(); assert.equal(handle.isActive(), false); assert.equal(aborted, true); assert.equal(doc._listenerCount('visibilitychange'), 0); timers.advanceBy(10_000); await Promise.resolve(); }); }); describe('AbortSignal', () => { it('signal provided to poll fn', async () => { let receivedSignal = null; startSmartPollLoop((ctx) => { receivedSignal = ctx.signal; }, { intervalMs: 1_000, jitterFraction: 0, }); timers.advanceBy(1_000); await Promise.resolve(); assert.ok(receivedSignal instanceof AbortSignal); }); it('abort errors do not trigger backoff', async () => { let calls = 0; startSmartPollLoop((ctx) => { calls++; const err = new Error('aborted'); err.name = 'AbortError'; throw err; }, { intervalMs: 1_000, jitterFraction: 0, maxBackoffMultiplier: 4, }); timers.advanceBy(1_000); await Promise.resolve(); assert.equal(calls, 1); timers.advanceBy(1_000); await Promise.resolve(); assert.equal(calls, 2); }); }); describe('in-flight guard', () => { it('concurrent calls are deferred, not dropped', async () => { let calls = 0; let resolvers = []; const handle = startSmartPollLoop(() => { calls++; return new Promise(r => resolvers.push(r)); }, { intervalMs: 1_000, jitterFraction: 0, }); timers.advanceBy(1_000); await Promise.resolve(); assert.equal(calls, 1); timers.advanceBy(1_000); await Promise.resolve(); assert.equal(calls, 1); resolvers[0](); await Promise.resolve(); await Promise.resolve(); timers.advanceBy(1_000); await Promise.resolve(); assert.equal(calls, 2); resolvers[1]?.(); handle.stop(); }); }); describe('visibilityHub integration', () => { it('subscribes to a provided hub on start and unsubscribes on stop()', () => { let subscribeCalls = 0; let unsubscribeCalls = 0; const fakeHub = { subscribe(cb) { subscribeCalls++; return () => { unsubscribeCalls++; }; }, }; const handle = startSmartPollLoop(() => {}, { intervalMs: 1_000, jitterFraction: 0, visibilityHub: fakeHub, }); assert.equal(subscribeCalls, 1, 'subscribe() called once on start'); assert.equal(unsubscribeCalls, 0, 'unsubscribe not called before stop'); handle.stop(); assert.equal(unsubscribeCalls, 1, 'unsubscribe() called once on stop'); assert.equal(subscribeCalls, 1, 'subscribe() not called again after stop'); }); it('uses hub callbacks for visibility changes instead of direct DOM listener', () => { let hubCallback = null; const fakeHub = { subscribe(cb) { hubCallback = cb; return () => {}; }, }; let ticks = 0; const handle = startSmartPollLoop(() => { ticks++; return true; }, { intervalMs: 60_000, jitterFraction: 0, pauseWhenHidden: true, visibilityHub: fakeHub, }); assert.ok(hubCallback, 'hub subscriber callback was registered'); assert.equal(doc._listenerCount('visibilitychange'), 0, 'no direct DOM listener added when hub provided'); handle.stop(); }); }); }); // Build a plain-JS VisibilityHub that mirrors the contract of the real class, // used to test hub behavior without needing to strip TypeScript class syntax. function buildVisibilityHub(doc) { const hasVisibilityApiBody = extractBody(runtimeSrc, 'hasVisibilityApi'); const factory = new Function('document', ` function hasVisibilityApi() { ${hasVisibilityApiBody} } let _listeners = new Set(); let _handler = null; function ensureListening() { if (_handler || !hasVisibilityApi()) return; _handler = () => { for (const cb of _listeners) cb(); }; document.addEventListener('visibilitychange', _handler); } function stopListening() { if (!_handler) return; document.removeEventListener('visibilitychange', _handler); _handler = null; } return { subscribe(cb) { _listeners.add(cb); ensureListening(); return () => { _listeners.delete(cb); if (_listeners.size === 0) stopListening(); }; }, destroy() { stopListening(); _listeners.clear(); }, }; `); return factory(doc); } describe('VisibilityHub', () => { let doc; beforeEach(() => { doc = createDocMock(); }); it('subscribe fans out to the callback on visibilitychange', () => { const hub = buildVisibilityHub(doc); let fired = 0; hub.subscribe(() => { fired++; }); doc._fire('visibilitychange'); assert.equal(fired, 1); hub.destroy(); }); it('unsubscribe callback prevents further notifications', () => { const hub = buildVisibilityHub(doc); let fired = 0; const unsub = hub.subscribe(() => { fired++; }); unsub(); doc._fire('visibilitychange'); assert.equal(fired, 0, 'unsubscribed callback must not fire'); hub.destroy(); }); it('removes the DOM listener when the last subscriber unsubscribes', () => { const hub = buildVisibilityHub(doc); const unsub = hub.subscribe(() => {}); assert.equal(doc._listenerCount('visibilitychange'), 1, 'listener added on first subscribe'); unsub(); assert.equal(doc._listenerCount('visibilitychange'), 0, 'listener removed when subscriber count reaches 0'); hub.destroy(); }); it('fans out to all subscribers on visibilitychange', () => { const hub = buildVisibilityHub(doc); const fired = []; hub.subscribe(() => fired.push('a')); hub.subscribe(() => fired.push('b')); doc._fire('visibilitychange'); assert.deepEqual(fired.sort(), ['a', 'b']); hub.destroy(); }); it('destroy clears all subscribers and removes the DOM listener', () => { const hub = buildVisibilityHub(doc); let fired = 0; hub.subscribe(() => { fired++; }); hub.destroy(); doc._fire('visibilitychange'); assert.equal(fired, 0, 'no callbacks after destroy'); assert.equal(doc._listenerCount('visibilitychange'), 0, 'DOM listener removed after destroy'); }); it('multiple subscribers share one DOM listener', () => { const hub = buildVisibilityHub(doc); hub.subscribe(() => {}); hub.subscribe(() => {}); hub.subscribe(() => {}); assert.equal(doc._listenerCount('visibilitychange'), 1, 'N subscribers → exactly 1 DOM listener'); hub.destroy(); }); });