Files
ytplayer/frontend/async-guard.test.js

118 lines
4.9 KiB
JavaScript
Executable File

'use strict';
const { test } = require('node:test');
const assert = require('node:assert');
const { runExclusive } = require('./async-guard');
// A controllable promise so a test can hold an operation "in flight".
function deferred() {
let resolve, reject;
const promise = new Promise((res, rej) => { resolve = res; reject = rej; });
return { promise, resolve, reject };
}
test('runs fn and returns its resolved value', async () => {
const set = new Set();
const out = await runExclusive(set, 'a', async () => 42);
assert.strictEqual(out, 42);
assert.strictEqual(set.size, 0, 'key freed after success');
});
test('skips a second concurrent call for the same key (fn runs once)', async () => {
const set = new Set();
let calls = 0;
const d = deferred();
const first = runExclusive(set, 'k', async () => { calls++; await d.promise; return 'first'; });
// Second call while the first is still in flight — must be a no-op.
const second = await runExclusive(set, 'k', async () => { calls++; return 'second'; });
assert.strictEqual(second, undefined, 'skipped call resolves to undefined');
assert.strictEqual(calls, 1, 'fn invoked only once');
d.resolve();
assert.strictEqual(await first, 'first');
});
test('different keys run in parallel', async () => {
const set = new Set();
const dA = deferred();
const dB = deferred();
let aDone = false;
const a = runExclusive(set, 'A', async () => { await dA.promise; aDone = true; return 'A'; });
const b = runExclusive(set, 'B', async () => { await dB.promise; return 'B'; });
assert.strictEqual(set.size, 2, 'both keys in flight at once');
// B can finish before A — proves they are not serialized.
dB.resolve();
assert.strictEqual(await b, 'B');
assert.strictEqual(aDone, false, 'A still in flight while B completed');
dA.resolve();
assert.strictEqual(await a, 'A');
assert.strictEqual(set.size, 0);
});
test('frees the key after failure so a retry is possible', async () => {
const set = new Set();
await assert.rejects(
runExclusive(set, 'x', async () => { throw new Error('boom'); }),
/boom/,
);
assert.strictEqual(set.has('x'), false, 'key freed even on throw');
// Retry now succeeds because the key was released.
const out = await runExclusive(set, 'x', async () => 'ok');
assert.strictEqual(out, 'ok');
});
test('bounded GET retries recover transient network and server failures', async () => {
const { getJson } = require('./async-guard');
let calls = 0;
const result = await getJson('/api/search', {}, { sleep: async () => {}, fetch: async () => {
calls++;
if (calls === 1) throw new TypeError('connection reset');
if (calls === 2) return new Response('{}', { status: 503 });
return Response.json({ ok: true, results: [] });
} });
assert.equal(result.ok, true);
assert.equal(calls, 3);
});
test('GET retry stops at the bound and does not retry permanent HTTP errors', async () => {
const { getJson } = require('./async-guard');
let calls = 0;
await assert.rejects(getJson('/api/search', {}, { sleep: async () => {}, fetch: async () => {
calls++; return new Response('{}', { status: 404 });
} }), /HTTP 404/);
assert.equal(calls, 1);
await assert.rejects(getJson('/api/search', {}, { attempts: 2, sleep: async () => {}, fetch: async () => {
calls++; throw new TypeError('offline');
} }), /offline/);
assert.equal(calls, 3);
});
test('retryable API error payloads recover but permanent application errors do not retry', async () => {
const { getJson } = require('./async-guard'); let calls = 0;
const retryResult = value => value?.ok === false && /temporarily unavailable/i.test(value.error || '');
const result = await getJson('/api/streams', {}, { retryResult, sleep: async () => {}, fetch: async () => {
calls++;
return Response.json(calls === 1 ? { ok: false, error: 'temporarily unavailable' } : { ok: true, data: [] });
} });
assert.equal(result.ok, true); assert.equal(calls, 2);
calls = 0;
const absent = await getJson('/api/streams', {}, { retryResult, sleep: async () => {}, fetch: async () => { calls++; return Response.json({ ok: false, error: 'video not found' }); } });
assert.equal(absent.ok, false); assert.equal(calls, 1);
});
test('timed out GET bodies retry within the configured bound', async () => {
const { getJson } = require('./async-guard'); let calls = 0;
const result = await getJson('/api/related', {}, { attempts: 2, timeoutMs: 5, sleep: async () => {}, fetch: async (_url, { signal }) => {
calls++;
if (calls === 1) return new Promise((resolve, reject) => signal.addEventListener('abort', () => reject(new DOMException('aborted', 'AbortError')), { once: true }));
return Response.json({ ok: true });
} });
assert.equal(result.ok, true); assert.equal(calls, 2);
});
test('supports a synchronous fn', async () => {
const set = new Set();
const out = await runExclusive(set, 's', () => 7);
assert.strictEqual(out, 7);
assert.strictEqual(set.size, 0);
});