@ava/cooperate
Version:
Plugin to enable cooperation between AVA test files
268 lines • 8.65 kB
JavaScript
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