feat: Prevent UI hanging during video save or playlist add
Task #1 completed by ClaudeQueue ClaudeQueue
This commit is contained in:
68
frontend/async-guard.test.js
Normal file
68
frontend/async-guard.test.js
Normal file
@@ -0,0 +1,68 @@
|
||||
'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('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);
|
||||
});
|
||||
Reference in New Issue
Block a user