blob: 34efe9b5c1ee1bda36833a6c1e269c37e26d5f29 [file]
const POISONED_MESSAGE = 'Runtime policy activation is poisoned';
/**
* Coordinates runtime-policy mutation/invalidation with the short backend
* activation window that selects a backend and starts a run.
*/
export class RuntimePolicyActivationGate {
readonly #backendActivations = new Set<Promise<void>>();
#mutationTail = Promise.resolve();
#poisoned = false;
runBackendActivation<T>(operation: () => Promise<T> | T): Promise<T> {
if (this.#poisoned) return Promise.reject(poisonedError());
const precedingMutations = this.#mutationTail;
const completion = deferred();
this.#backendActivations.add(completion.promise);
return this.#executeBackendActivation(precedingMutations, completion, operation);
}
runMutation<T>(operation: () => Promise<T> | T): Promise<T> {
if (this.#poisoned) return Promise.reject(poisonedError());
const precedingMutation = this.#mutationTail;
const precedingBackendActivations = [...this.#backendActivations];
const completion = deferred();
// This tail never rejects, so later registrations cannot create an
// unhandled rejection from an operation failure.
this.#mutationTail = precedingMutation.then(() => completion.promise);
return this.#executeMutation(
precedingMutation,
precedingBackendActivations,
completion,
operation,
);
}
poison(): void {
this.#poisoned = true;
}
async #executeBackendActivation<T>(
precedingMutations: Promise<void>,
completion: Deferred,
operation: () => Promise<T> | T,
): Promise<T> {
try {
await precedingMutations;
this.#assertOpen();
return await operation();
} finally {
completion.resolve();
this.#backendActivations.delete(completion.promise);
}
}
async #executeMutation<T>(
precedingMutation: Promise<void>,
precedingBackendActivations: readonly Promise<void>[],
completion: Deferred,
operation: () => Promise<T> | T,
): Promise<T> {
try {
await Promise.all([precedingMutation, ...precedingBackendActivations]);
this.#assertOpen();
return await operation();
} finally {
completion.resolve();
}
}
#assertOpen(): void {
if (this.#poisoned) throw poisonedError();
}
}
interface Deferred {
readonly promise: Promise<void>;
resolve(): void;
}
function deferred(): Deferred {
let resolve!: () => void;
const promise = new Promise<void>((settle) => {
resolve = settle;
});
return { promise, resolve };
}
function poisonedError(): Error {
return new Error(POISONED_MESSAGE);
}