blob: d8c3aec6cd209f2b752f4894dfdace2b7646a18a [file]
import { AsyncLocalStorage } from 'node:async_hooks';
const sessionAdmissionLeaseBrand: unique symbol = Symbol('SessionAdmissionLease');
export interface SessionAdmissionLease {
readonly [sessionAdmissionLeaseBrand]: true;
}
interface SessionAdmissionContext {
readonly sessionIds: ReadonlySet<string>;
active: boolean;
}
interface SessionAdmissionLeaseState {
readonly sessionIds: ReadonlySet<string>;
readonly context: SessionAdmissionContext;
readonly tasks: Promise<SessionAdmissionTaskResult>[];
accepting: boolean;
}
type SessionAdmissionTaskResult =
| { readonly ok: true }
| { readonly ok: false; readonly error: unknown };
export class SessionAdmissionGate {
readonly #tails = new Map<string, Promise<void>>();
readonly #context = new AsyncLocalStorage<SessionAdmissionContext>();
readonly #leases = new WeakMap<SessionAdmissionLease, SessionAdmissionLeaseState>();
run<T>(
sessionId: string,
operation: (lease: SessionAdmissionLease) => Promise<T> | T,
): Promise<T> {
if (this.#context.getStore()?.active) {
return Promise.reject(
new Error(
'Cannot enter Session admission from an active admission; reuse its lease instead',
),
);
}
return this.#runQueued([sessionId], operation);
}
runMany<T>(
sessionIds: readonly string[],
operation: (lease: SessionAdmissionLease) => Promise<T> | T,
): Promise<T> {
if (this.#context.getStore()?.active) {
return Promise.reject(
new Error(
'Cannot enter Session admission from an active admission; reuse its lease instead',
),
);
}
return this.#runQueued(sessionIds, operation);
}
enqueueDetached(
sessionId: string,
operation: (lease: SessionAdmissionLease) => Promise<void> | void,
): Promise<void> {
return this.#runQueued([sessionId], operation);
}
runAdmitted<T>(
sessionId: string,
lease: SessionAdmissionLease,
operation: () => Promise<T> | T,
): Promise<T> {
const state = this.#requireLease(sessionId, lease);
if (!state.accepting) {
return Promise.reject(new Error('Session admission lease no longer accepts tasks'));
}
const inherited = this.#context.getStore();
if (inherited?.active && inherited !== state.context) {
return Promise.reject(new Error('Cannot reuse a Session admission lease from another task'));
}
let task: Promise<T>;
try {
task = Promise.resolve(this.#context.run(state.context, operation));
} catch (error) {
task = Promise.reject(error);
}
state.tasks.push(
task.then(
(): SessionAdmissionTaskResult => ({ ok: true }),
(error): SessionAdmissionTaskResult => ({ ok: false, error }),
),
);
return task;
}
async #runQueued<T>(
requestedSessionIds: readonly string[],
operation: (lease: SessionAdmissionLease) => Promise<T> | T,
): Promise<T> {
const sessionIds = [...new Set(requestedSessionIds)].sort();
if (sessionIds.length === 0) {
throw new Error('Session admission requires at least one Session');
}
const previous = sessionIds.map((sessionId) => this.#tails.get(sessionId) ?? Promise.resolve());
let release!: () => void;
const current = new Promise<void>((resolve) => {
release = resolve;
});
const tails = new Map(
sessionIds.map((sessionId, index) => {
const tail = previous[index]!.then(() => current);
this.#tails.set(sessionId, tail);
return [sessionId, tail] as const;
}),
);
if (previous.length === 1) {
await previous[0];
} else {
await Promise.all(previous);
}
const ownedSessionIds = new Set(sessionIds);
const context: SessionAdmissionContext = { sessionIds: ownedSessionIds, active: true };
const lease: SessionAdmissionLease = Object.freeze({
[sessionAdmissionLeaseBrand]: true as const,
});
const state: SessionAdmissionLeaseState = {
sessionIds: ownedSessionIds,
context,
tasks: [],
accepting: true,
};
this.#leases.set(lease, state);
try {
let result!: T;
let operationError: unknown;
let operationFailed = false;
try {
result = await this.#context.run(context, () => operation(lease));
} catch (error) {
operationFailed = true;
operationError = error;
} finally {
state.accepting = false;
}
const taskResults = await Promise.all(state.tasks);
const errors: unknown[] = [];
const collect = (error: unknown) => {
if (!errors.some((existing) => Object.is(existing, error))) errors.push(error);
};
if (operationFailed) collect(operationError);
for (const taskResult of taskResults) {
if (!taskResult.ok) collect(taskResult.error);
}
if (errors.length === 1) throw errors[0];
if (errors.length > 1) {
throw new AggregateError(errors, 'Session admission operation failed');
}
return result;
} finally {
context.active = false;
this.#leases.delete(lease);
release();
for (const [sessionId, tail] of tails) {
if (this.#tails.get(sessionId) === tail) this.#tails.delete(sessionId);
}
}
}
#requireLease(sessionId: string, lease: SessionAdmissionLease): SessionAdmissionLeaseState {
const state = this.#leases.get(lease);
if (!state) throw new Error('Session admission lease was not issued by this gate');
if (!state.sessionIds.has(sessionId)) {
throw new Error('Session admission lease does not match the Session');
}
return state;
}
}