UNPKG

@ava/cooperate

Version:

Plugin to enable cooperation between AVA test files

268 lines 8.65 kB
import never from 'never'; const factory = async ({ negotiateProtocol }) => { const protocol = negotiateProtocol(['ava-4']).ready(); for await (const message of protocol.subscribe()) { const { data } = message; switch (data.type) { case 10 /* LOCK */: { void acquireLock(message, data); break; } case 20 /* RESERVE */: { reserve(message, data); break; } case 31 /* SEMAPHORE_DOWN */: { void downSemaphore(message, data); break; } case 35 /* SEMAPHORE_UP */: { upSemaphore(message, data); break; } // No default default: continue; } } }; export default factory; const sharedContexts = new Map(); function getContext(id) { var _a; const context = (_a = sharedContexts.get(id)) !== null && _a !== void 0 ? _a : { locks: new Map(), reservedValues: new Set(), semaphores: new Map(), }; sharedContexts.set(id, context); return context; } async function acquireLock(message, { contextId, lockId, wait }) { var _a; const context = getContext(contextId); const release = message.testWorker.teardown(() => { const current = context.locks.get(lockId); if (current === undefined) { // This won't actually happen at runtime. return; } if (current.holderId !== message.id) { // We do not have the lock. Ensure we won't acquire it later. const waiting = current.waiting.filter(({ holderId }) => holderId !== message.id); context.locks.set(lockId, { ...current, waiting, }); return; } const [next, ...waiting] = current.waiting; if (next === undefined) { // We have the lock, but nobody else wants it. Delete it. context.locks.delete(lockId); return; } // Transfer the lock to the next in line. context.locks.set(lockId, { holderId: next.holderId, waiting, }); next.notify(); }); if (!context.locks.has(lockId)) { context.locks.set(lockId, { holderId: message.id, waiting: [], }); for await (const { data } of message.reply({ type: 11 /* LOCK_ACQUIRED */ }).replies()) { if (data.type === 13 /* LOCK_RELEASE */) { release(); break; } } return; } if (!wait) { release(); message.reply({ type: 12 /* LOCK_FAILED */ }); return; } const current = (_a = context.locks.get(lockId)) !== null && _a !== void 0 ? _a : never(); current.waiting.push({ holderId: message.id, async notify() { for await (const { data } of message.reply({ type: 11 /* LOCK_ACQUIRED */ }).replies()) { if (data.type === 13 /* LOCK_RELEASE */) { release(); break; } } }, }); } function reserve(message, { contextId, values }) { const context = getContext(contextId); const indexes = values.map((value, index) => { if (context.reservedValues.has(value)) { return -1; } context.reservedValues.add(value); return index; }).filter(index => index >= 0); message.testWorker.teardown(() => { var _a; for (const index of indexes) { context.reservedValues.delete((_a = values[index]) !== null && _a !== void 0 ? _a : never()); } }); message.reply({ type: 21 /* RESERVED_INDEXES */, indexes }); } // A weighted, counting semaphore. // Waiting threads are woken in FIFO order (the semaphore is "fair"). // tryDown() ignores waiting threads (it may "barge"). class Semaphore { constructor(initialValue, managed) { Object.defineProperty(this, "initialValue", { enumerable: true, configurable: true, writable: true, value: initialValue }); Object.defineProperty(this, "managed", { enumerable: true, configurable: true, writable: true, value: managed }); Object.defineProperty(this, "value", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "queue", { enumerable: true, configurable: true, writable: true, value: void 0 }); this.value = initialValue; this.queue = []; } // Down the semaphore by amount, waiting first if necessary, associating the // acquisition with id. Callback is called once, synchronously, when the // decrement occurs. async down(amount, id, callback) { if (this.queue.length > 0 || !this.tryDown(amount)) { return new Promise(resolve => { this.queue.push({ id, amount, resolve() { callback(); resolve(); }, }); }); } callback(); } tryDown(amount) { if (this.value >= amount) { this.value -= amount; return true; } return false; } up(amount) { this.value += amount; for (const item of this.queue) { if (!this.tryDown(item.amount)) { break; } item.resolve(); this.queue.shift(); } } } function getSemaphore(contextId, id, initialValue, managed) { const context = getContext(contextId); let semaphore = context.semaphores.get(id); if (semaphore !== undefined) { return [semaphore.initialValue === initialValue && semaphore.managed === managed, semaphore]; } semaphore = new Semaphore(initialValue, managed); context.semaphores.set(id, semaphore); return [true, semaphore]; } async function downSemaphore(message, { contextId, semaphore: { managed, id, initialValue }, amount, wait }) { const [ok, semaphore] = getSemaphore(contextId, id, initialValue, managed); if (!ok) { message.reply({ type: 30 /* SEMAPHORE_CREATION_FAILED */, initialValue: semaphore.initialValue, managed: semaphore.managed, }); return; } let acquired = 0; let release; if (wait) { release = message.testWorker.teardown((clearQueue = true) => { if (acquired > 0 && managed) { semaphore.up(acquired); } if (clearQueue) { // The waiter will never be woken, but that's fine since the test // worker's already exited. semaphore.queue = semaphore.queue.filter(({ id }) => id !== message.id); } }); await semaphore.down(amount, message.id, () => { acquired = amount; }); } else if (semaphore.tryDown(amount)) { acquired = amount; release = message.testWorker.teardown(() => { semaphore.up(acquired); }); } else { message.reply({ type: 32 /* SEMAPHORE_FAILED */, }); return; } const reply = message.reply({ type: 34 /* SEMAPHORE_SUCCEEDED */, }); if (managed) { for await (const { data } of reply.replies()) { if (data.type === 33 /* SEMAPHORE_RELEASE */) { const releaseAmount = Math.min(acquired, data.amount); semaphore.up(releaseAmount); acquired -= releaseAmount; if (acquired <= 0) { release(false); break; } } } } } function upSemaphore(message, { contextId, semaphore: { managed, id, initialValue }, amount }) { const [ok, semaphore] = getSemaphore(contextId, id, initialValue, managed); if (!ok) { message.reply({ type: 30 /* SEMAPHORE_CREATION_FAILED */, initialValue: semaphore.initialValue, managed: semaphore.managed, }); return; } semaphore.up(amount); message.reply({ type: 34 /* SEMAPHORE_SUCCEEDED */, }); } //# sourceMappingURL=worker.js.map