'use strict'; const { test } = require('node:test'); const assert = require('node:assert/strict'); const LatestWinsGate = require('../../src/domain/LatestWinsGate'); // Helper: a deferred promise so a test can pause a dispatch and inspect // gate state before resolving. Avoids real timers entirely. function deferred() { let resolve; let reject; const promise = new Promise((res, rej) => { resolve = res; reject = rej; }); return { promise, resolve, reject }; } test('single fire calls dispatch with the value', async () => { const calls = []; const gate = new LatestWinsGate(async (v) => { calls.push(v); }); gate.fire('a'); await gate.drain(); assert.deepEqual(calls, ['a']); }); test('two fires while in-flight: second value runs after first settles', async () => { const calls = []; const gates = [deferred(), deferred()]; const started = [deferred(), deferred()]; let n = 0; const gate = new LatestWinsGate(async (v) => { const slot = n++; calls.push(v); started[slot].resolve(); await gates[slot].promise; }); gate.fire('first'); gate.fire('second'); // parks while 'first' is in flight await started[0].promise; assert.deepEqual(calls, ['first']); assert.equal(gate.size, 2); gates[0].resolve(); await started[1].promise; assert.deepEqual(calls, ['first', 'second']); gates[1].resolve(); await gate.drain(); }); test('three fires back-to-back: only the last runs after the first settles', async () => { const calls = []; const first = deferred(); const firstStarted = deferred(); let count = 0; const gate = new LatestWinsGate(async (v) => { calls.push(v); if (count++ === 0) { firstStarted.resolve(); await first.promise; } }); gate.fire(1); gate.fire(2); // parked gate.fire(3); // overwrites 2 await firstStarted.promise; assert.deepEqual(calls, [1]); first.resolve(); await gate.drain(); assert.deepEqual(calls, [1, 3]); }); test('drain() resolves only after all queued work has run', async () => { const calls = []; const d = deferred(); let started = 0; const gate = new LatestWinsGate(async (v) => { calls.push(v); if (started++ === 0) await d.promise; }); gate.fire('x'); gate.fire('y'); let drained = false; const p = gate.drain().then(() => { drained = true; }); // While first is paused, drain must not have resolved yet. await Promise.resolve(); await Promise.resolve(); assert.equal(drained, false); d.resolve(); await p; assert.deepEqual(calls, ['x', 'y']); assert.equal(drained, true); }); test('error in dispatch does not prevent subsequent fire from working', async () => { const calls = []; let throwNext = true; const errors = []; const logger = { error: (e) => errors.push(e) }; const gate = new LatestWinsGate(async (v) => { calls.push(v); if (throwNext) { throwNext = false; throw new Error('boom'); } }, { logger }); gate.fire('a'); await gate.drain(); assert.equal(calls.length, 1); assert.equal(errors.length, 1); assert.match(errors[0].message, /boom/); assert.ok(gate.lastError instanceof Error); // Gate must still accept further work. gate.fire('b'); await gate.drain(); assert.deepEqual(calls, ['a', 'b']); }); test('error is recorded on lastError when no logger is supplied', async () => { const gate = new LatestWinsGate(async () => { throw new Error('silent'); }); gate.fire('only'); await gate.drain(); assert.ok(gate.lastError instanceof Error); assert.match(gate.lastError.message, /silent/); }); test('size reports 0 / 1 / 2 across the lifecycle', async () => { const d1 = deferred(); const gate = new LatestWinsGate(async () => { await d1.promise; }); assert.equal(gate.size, 0); gate.fire('one'); // fire is sync, but _dispatch starts on a microtask. Either way the // gate is marked in-flight synchronously. assert.equal(gate.size, 1); gate.fire('two'); // parked assert.equal(gate.size, 2); d1.resolve(); await gate.drain(); assert.equal(gate.size, 0); }); test('fireAndWait resolves when the dispatch for that value settles', async () => { const calls = []; const gate = new LatestWinsGate(async (v) => { calls.push(v); return `done:${v}`; }); const result = await gate.fireAndWait('a'); assert.deepEqual(calls, ['a']); assert.equal(result, 'done:a'); }); test('fireAndWait while in-flight: caller awaits OWN settlement, not the first call', async () => { const calls = []; const d = deferred(); let count = 0; const gate = new LatestWinsGate(async (v) => { calls.push(v); if (count++ === 0) await d.promise; return `r:${v}`; }); const p1 = gate.fireAndWait('first'); // p1 in flight. Park second; second's promise should resolve only // after second's OWN dispatch runs, not after first's. const p2 = gate.fireAndWait('second'); let p2Settled = false; p2.then(() => { p2Settled = true; }); await Promise.resolve(); await Promise.resolve(); assert.equal(p2Settled, false); d.resolve(); const r1 = await p1; assert.equal(r1, 'r:first'); const r2 = await p2; assert.equal(r2, 'r:second'); assert.deepEqual(calls, ['first', 'second']); }); test('fireAndWait superseded by a later fireAndWait resolves with { superseded: true }', async () => { const calls = []; const d = deferred(); let count = 0; const gate = new LatestWinsGate(async (v) => { calls.push(v); if (count++ === 0) await d.promise; }); const p1 = gate.fireAndWait('first'); // in flight const pParked = gate.fireAndWait('parked'); // gets superseded const pLatest = gate.fireAndWait('latest'); // wins d.resolve(); const supersedeRes = await pParked; assert.equal(supersedeRes.superseded, true); await p1; await pLatest; assert.deepEqual(calls, ['first', 'latest']); // 'parked' dropped }); test('fireAndWait + fire intermix: a plain fire supersedes a pending fireAndWait', async () => { const d = deferred(); let count = 0; const calls = []; const gate = new LatestWinsGate(async (v) => { calls.push(v); if (count++ === 0) await d.promise; }); gate.fire('first'); // in flight, no settle const pParked = gate.fireAndWait('parked'); gate.fire('latest'); // supersedes parked d.resolve(); const res = await pParked; assert.equal(res.superseded, true); await gate.drain(); assert.deepEqual(calls, ['first', 'latest']); }); test('fireAndWait still resolves (with undefined) when the dispatch throws', async () => { const errors = []; const logger = { error: (e) => errors.push(e) }; const gate = new LatestWinsGate(async () => { throw new Error('kaboom'); }, { logger }); const r = await gate.fireAndWait('only'); assert.equal(r, undefined); assert.equal(errors.length, 1); assert.ok(gate.lastError instanceof Error); });