/** Typed identity for one value carried by a {@link Context}. */ export interface ContextKey { readonly token: symbol; /** Type-only marker that prevents keys with different value types from being interchangeable. */ readonly valueType?: (value: T) => T; } /** Immutable invocation-scoped values passed explicitly through operations. */ export interface Context { readonly abortSignal: AbortSignal | undefined; value(key: ContextKey): T | undefined; toString(): string; } const ABORT_SIGNAL_CONTEXT_KEY: ContextKey = Object.freeze({ token: Symbol("chord.abortSignal"), }); abstract class BaseContext implements Context { abstract value(key: ContextKey): T | undefined; abstract toString(): string; get abortSignal(): AbortSignal | undefined { return this.value(ABORT_SIGNAL_CONTEXT_KEY); } } class EmptyContext extends BaseContext { readonly #name: string; constructor(name: string) { super(); this.#name = name; } value(_key: ContextKey): T | undefined { return undefined; } toString(): string { return this.#name; } } class ContextValue extends BaseContext { readonly #parent: Context; readonly #key: ContextKey; readonly #value: T; constructor(parent: Context, key: ContextKey, value: T) { super(); this.#parent = parent; this.#key = key; this.#value = value; } value(key: ContextKey): Value | undefined { if (key.token === this.#key.token) return this.#value as unknown as Value; return this.#parent.value(key); } toString(): string { return `${this.#parent}.WithValue(${this.#key.token.description ?? "anonymous"})`; } } export const BACKGROUND_CONTEXT: Context = new EmptyContext("[Context BACKGROUND_CONTEXT]"); export const TODO_CONTEXT: Context = new EmptyContext("[Context TODO_CONTEXT]"); export function createContextKey(description: string): ContextKey { return Object.freeze({ token: Symbol(description) }); } /** Derive a context containing one additional or replaced value. */ export function withContextValue(key: ContextKey, value: T, parent: Context): Context { return new ContextValue(parent, key, value); } /** * Derive a context cancelled by either the parent signal or the supplied signal. * The parent context remains unchanged. */ export function withAbortSignal(signal: AbortSignal, context: Context): Context { const parentSignal = context.abortSignal; const combined = parentSignal === undefined ? signal : AbortSignal.any([parentSignal, signal]); return withContextValue(ABORT_SIGNAL_CONTEXT_KEY, combined, context); } /** Derive a context retaining all values except caller cancellation. Intended for mandatory cleanup only. */ export function withoutAbortSignal(context: Context): Context { return withContextValue(ABORT_SIGNAL_CONTEXT_KEY, undefined, context); } /** Derive an independently cancellable child context. */ export function withCancel(context: Context): { readonly context: Context; readonly cancel: (reason?: unknown) => void; } { const controller = new AbortController(); return { context: withAbortSignal(controller.signal, context), cancel: (reason?: unknown) => controller.abort(reason), }; } /** * Observe a promise until it settles or the invocation is cancelled. * Cancellation rejects only this waiter; it does not cancel the underlying promise. */ export function awaitWithContext(promise: Promise, context: Context): Promise { const signal = context.abortSignal; if (signal === undefined) return promise; if (signal.aborted) return Promise.reject(abortError(signal)); return new Promise((resolve, reject) => { const onAbort = (): void => reject(abortError(signal)); signal.addEventListener("abort", onAbort, { once: true }); void promise.then( (value) => { signal.removeEventListener("abort", onAbort); resolve(value); }, (error: unknown) => { signal.removeEventListener("abort", onAbort); reject(error); }, ); }); } function abortError(signal: AbortSignal): Error { const reason: unknown = signal.reason; return reason instanceof Error ? reason : new DOMException("The operation was aborted", "AbortError"); }