blob: fe2f3c8b7d8e254e3ef2d708c19ac58b757e70f3 [file]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { randomUUID } from 'node:crypto';
import {
decodeAnyEventWaitRecord,
EVENT_WAIT_LIMITS,
type AnyEventWaitRecord as EventWaitRecord,
type EventWaitObservation,
type ConsumerEventWaitObservation,
} from '@maka/core/event-wait';
import {
EVENT_WAIT_OBSERVATION_HINT_LIMIT,
EVENT_WAIT_PAGE_LIMIT,
eventWaitSourceKey,
type ExtendEventWaitSourceCooldownInput,
type ReadEventWaitInput,
} from '@maka/storage/event-wait-authority';
import type {
EventWaitObservationStore,
ObservationWaitSnapshot,
} from './event-wait-observation-store.js';
import { StorageRootAuthorityError } from '@maka/storage/root-authority';
import {
EventWaitAdapterRegistry,
type EventWaitObservationAccess,
type EventWaitObservationResult,
type EventWaitSourceAdapter,
type EventWaitSubscription,
} from './event-wait-adapter.js';
type EventWaitSnapshot = ObservationWaitSnapshot<EventWaitRecord>;
export interface EventWaitClock {
now(): number;
schedule(callback: () => void, delayMs: number): () => void;
}
const systemClock: EventWaitClock = {
now: Date.now,
schedule(callback, delayMs) {
const timer = setTimeout(callback, Math.min(delayMs, 2_147_483_647));
timer.unref();
return () => clearTimeout(timer);
},
};
export interface EventWaitObservationPolicy {
readonly scanIntervalMs: number;
readonly pageSize: number;
readonly minIntervalMs: number;
readonly pollIntervalMs: number;
/** Caps local error backoff before jitter, not a source's Retry-After. */
readonly maxBackoffMs: number;
readonly readTimeoutMs: number;
readonly closeTimeoutMs: number;
/** Total observation budget, including healthy pending reads; exhaustion ends the wait. */
readonly maxAttempts: number;
/** Independent lifetime ceiling, not a guarantee that the attempt budget lasts this long. */
readonly maxLifetimeMs: number;
readonly maxConcurrent: number;
readonly maxPerSource: number;
readonly maxSubscriptions: number;
/** Bounds the cache; unpersisted source facts reaching this threshold pause new observations. */
readonly maxSourceCooldowns: number;
readonly jitterRatio: number;
}
/**
* For a promptly responding source that stays pending, ordinary 15–18 second polling
* (including jitter) exhausts 10,000 attempts in roughly 42–50 hours. Exhaustion resolves
* the wait as source_unavailable with reason observation_budget_exhausted.
* This estimate excludes scheduling/I/O delays and shared-source contention or throttling;
* notification-triggered reads can exhaust the budget sooner. The seven-day lifetime is
* an independent ceiling, not a guarantee of seven days of observation.
*/
export const DEFAULT_EVENT_WAIT_OBSERVATION_POLICY: EventWaitObservationPolicy = Object.freeze({
scanIntervalMs: 1_000,
pageSize: 64,
minIntervalMs: 1_000,
pollIntervalMs: 15_000,
maxBackoffMs: 300_000,
readTimeoutMs: 30_000,
closeTimeoutMs: 5_000,
maxAttempts: 10_000,
maxLifetimeMs: 7 * 24 * 60 * 60_000,
maxConcurrent: 8,
maxPerSource: 1,
maxSubscriptions: 128,
maxSourceCooldowns: 1024,
jitterRatio: 0.2,
});
export interface HostEventWaitObserverOptions {
/** Semantic projection hint only; pending polling does not broadcast activity. */
readonly onChanged?: (wait: EventWaitRecord) => void;
readonly store: EventWaitObservationStore<EventWaitRecord>;
readonly decodeRecord?: (value: unknown) => EventWaitRecord;
readonly adapters: EventWaitAdapterRegistry;
/** Trusted composition must check current Session/Goal identity and source access. */
readonly authorize: (
wait: EventWaitRecord,
signal: AbortSignal,
) => Promise<EventWaitObservationAccess>;
readonly clock?: EventWaitClock;
readonly random?: () => number;
readonly policy?: Partial<EventWaitObservationPolicy>;
/** Diagnostics only. No result-delivery or execution-admission callback exists here. */
readonly onError: (error: unknown) => void;
}
type Flight = {
readonly readTimeoutMs: number;
readonly controller: AbortController;
readonly source: string;
readonly sessionId: string;
readonly waitId: string;
claimed: boolean;
cooldownUntil: number;
/** Source Retry-After also gates authorization; read spacing and reservations do not. */
retryAfterUntil: number;
task: Promise<void>;
};
type Subscription = {
readonly identity: ReadEventWaitInput;
readonly controller: AbortController;
handle?: EventWaitSubscription;
dirty: boolean;
isPending?: () => boolean;
};
type DispatchPreparation = {
readonly waiters: Set<() => void>;
};
class ObservationTimeout extends Error {
constructor(
message: string,
readonly isPending: () => boolean = () => false,
) {
super(message);
}
}
class SourceCooldownWriteError extends Error {
constructor(cause: unknown) {
super('Unable to persist event wait source cooldown', { cause });
}
}
const identity = (r: EventWaitRecord): ReadEventWaitInput => ({
sessionId: r.sessionId,
waitId: r.waitId,
});
const deadlineOf = (r: EventWaitRecord): number => r.deadlineAt ?? Number.MAX_SAFE_INTEGER;
const sourceKey = (r: EventWaitRecord): string =>
JSON.stringify([r.resource.providerId, r.resource.connectionId]);
const addTime = (now: number, delay: number): number =>
Math.min(Number.MAX_SAFE_INTEGER, now + delay);
/**
* Internal, explicitly composed observation service. PR2 has no production adapters or tool
* registration. Durable records are the only authority; timers and subscriptions are hints.
* Ready candidates normally use durable observation age. Failed preparations get a bounded
* traversal by immutable ID, so neither failed writes nor cache capacity can pin selection.
*/
export class HostEventWaitObserver {
readonly #store: EventWaitObservationStore<EventWaitRecord>;
readonly #options: HostEventWaitObserverOptions;
readonly #clock: EventWaitClock;
readonly #policy: EventWaitObservationPolicy;
readonly #flights = new Map<string, Flight>();
/** Registration/delivery authorization shares observation capacity and source facts. */
readonly #accessFlights = new Set<Flight>();
readonly #subscriptions = new Map<string, Subscription>();
readonly #cooldowns = new Map<string, number>();
/** Dispatch preparation is serialized per source, independently of in-flight reads. */
readonly #dispatching = new Map<string, DispatchPreparation>();
/** Failed source writes retain their absolute deadline independently of wait cleanup. */
readonly #pendingCooldowns = new Map<string, ExtendEventWaitSourceCooldownInput>();
/** Interrupted calls retain their owning flight's capacity through late-fact processing. */
readonly #lingering = new Map<Promise<unknown>, Flight>();
/** Source facts returned after interruption may still be writing their shared cooldown. */
readonly #lateResults = new Set<Promise<void>>();
#expiryCursor: string | undefined;
/** A failed write cannot advance durable observation age. Recover with an O(1) ID cursor. */
#observationScan: { readonly afterWaitId?: string } | undefined;
#timer: (() => void) | undefined;
#scan: Promise<void> | undefined;
#start: Promise<void> | undefined;
#close: Promise<void> | undefined;
#closed = false;
#ready = false;
constructor(options: HostEventWaitObserverOptions) {
this.#store = options.store;
this.#options = options;
this.#clock = options.clock ?? systemClock;
this.#policy = { ...DEFAULT_EVENT_WAIT_OBSERVATION_POLICY, ...options.policy };
for (const [name, value] of Object.entries(this.#policy)) {
if (name === 'jitterRatio') {
if (!Number.isFinite(value) || value < 0 || value > 1)
throw new TypeError('Invalid wait jitter');
} else if (
!Number.isSafeInteger(value) ||
value < 1 ||
(name !== 'maxLifetimeMs' && value > 2_147_483_647)
)
throw new TypeError(`Invalid wait policy: ${name}`);
}
if (
this.#policy.pageSize > EVENT_WAIT_PAGE_LIMIT ||
this.#policy.maxAttempts > EVENT_WAIT_LIMITS.observationAttempts ||
this.#policy.minIntervalMs > this.#policy.pollIntervalMs ||
this.#policy.pollIntervalMs > this.#policy.maxBackoffMs ||
this.#policy.maxPerSource > this.#policy.maxConcurrent ||
2 * (this.#policy.maxConcurrent + this.#policy.maxSourceCooldowns) +
this.#policy.maxSubscriptions +
this.#policy.pageSize >
EVENT_WAIT_OBSERVATION_HINT_LIMIT
)
throw new TypeError('Inconsistent event wait policy');
}
#decode(value: unknown): EventWaitRecord {
return (this.#options.decodeRecord ?? decodeAnyEventWaitRecord)(value);
}
/** Validate source templates; neutral v1 observations have a bounded lifetime. */
validate(wait: EventWaitRecord): void {
const record = this.#decode(wait);
if (
record.schemaVersion === 1 &&
record.deadlineAt - record.createdAt > this.#policy.maxLifetimeMs
)
throw new TypeError('Event wait exceeds maximum lifetime');
this.#options.adapters.validate(record);
}
/** Bounded source access outside observation; no wait attempt or work Turn is charged. */
async checkSourceAccess(
wait: EventWaitRecord,
parent: AbortSignal,
authorize: HostEventWaitObserverOptions['authorize'],
onTimeout?: () => Exclude<EventWaitObservationAccess, { kind: 'allowed' }>,
): Promise<EventWaitObservationAccess> {
const unknown = { kind: 'unknown' as const, reason: 'authorization_unavailable' };
const source = sourceKey(wait);
const occupied = this.#occupiedFlights();
if (
this.#closed ||
parent.aborted ||
this.#pendingCooldowns.size >= this.#policy.maxSourceCooldowns ||
this.#sourceCooldown(source) > this.#clock.now() ||
occupied.size >= this.#policy.maxConcurrent ||
[...occupied].filter((f) => f.source === source).length >= this.#policy.maxPerSource
)
return unknown;
const flight: Flight = {
readTimeoutMs: this.#options.adapters.get(wait)?.readTimeoutMs ?? this.#policy.readTimeoutMs,
controller: new AbortController(),
source,
sessionId: wait.sessionId,
waitId: wait.waitId,
claimed: false,
cooldownUntil: 0,
retryAfterUntil: 0,
task: Promise.resolve(),
};
this.#accessFlights.add(flight);
const abort = () => flight.controller.abort();
parent.addEventListener('abort', abort, { once: true });
const task = (async () => {
const cooldown = await this.#store.readSourceCooldown({
providerId: wait.resource.providerId,
connectionId: wait.resource.connectionId,
});
if (
this.#closed ||
flight.controller.signal.aborted ||
Math.max(cooldown ?? 0, this.#sourceCooldown(source)) > this.#clock.now()
)
return unknown;
let started = false;
try {
// Expired waits still need delivery authorization, so the source deadline
// does not bound this separate request. #call retains the request timeout.
return (
(
await this.#tryAuthorize(wait, flight, Number.MAX_SAFE_INTEGER, (record, signal) => {
started = true;
return authorize(record, signal);
})
)?.access ?? unknown
);
} catch (error) {
if (error instanceof SourceCooldownWriteError || error instanceof StorageRootAuthorityError)
throw error;
if (
error instanceof ObservationTimeout &&
started &&
onTimeout &&
!this.#closed &&
!parent.aborted
) {
const access = onTimeout();
if (access.kind !== 'unknown' && access.kind !== 'revoked')
throw new TypeError('Timeout cannot authorize access');
this.#validateAccess(access);
if (access.kind === 'unknown') {
this.#validateResult(access, wait);
await this.#recordSourceCooldown(wait, access);
}
return access;
}
return unknown;
}
})();
flight.task = task.then(
() => undefined,
() => undefined,
);
try {
return await task;
} finally {
parent.removeEventListener('abort', abort);
this.#accessFlights.delete(flight);
// #call retains interrupted requests in #lingering until late facts settle.
}
}
start(): Promise<void> {
if (this.#closed) return Promise.reject(new Error('Event wait manager is closed'));
return (this.#start ??= this.#recover());
}
async #recover(): Promise<void> {
// Every source read has a durable source reservation before dispatch. Recover it
// on demand while scanning bounded pages; per-wait checkpoints retain their own backoff.
if (this.#closed) return;
this.#ready = true;
await this.reconcile();
}
/** Also used by the future registration/control bridge; notifications never bypass limits. */
reconcile(): Promise<void> {
if (this.#closed || !this.#ready) return Promise.resolve();
if (this.#scan) return this.#scan;
this.#timer?.();
this.#timer = undefined;
const task = this.#scanPage();
this.#scan = task;
void task
.finally(() => {
this.#scan = undefined;
this.#schedule();
})
.catch(() => undefined);
return task;
}
#schedule(): void {
if (this.#closed || this.#timer) return;
this.#timer = this.#clock.schedule(() => {
this.#timer = undefined;
return this.reconcile().catch((e: unknown) => this.#report(e));
}, this.#policy.scanIntervalMs);
}
#occupiedFlights(): Set<Flight> {
// One observation owns one slot even while its interrupted request and checkpoint
// persistence overlap. Keep that slot until both the flight and its late work retire.
return new Set([
...this.#flights.values(),
...this.#accessFlights,
...this.#lingering.values(),
]);
}
async #scanPage(): Promise<void> {
// Session cleanup can remove waits without calling cancel(). Include polling-only
// flights and release invalid work before scheduling peers into bounded capacity.
const active = new Map<string, ReadEventWaitInput>();
const seenWaitIds = new Set<string>();
for (const [waitId, subscription] of this.#subscriptions)
active.set(waitId, subscription.identity);
for (const [waitId, flight] of this.#flights)
active.set(waitId, { waitId, sessionId: flight.sessionId });
for (const input of active.values()) {
if (this.#closed) return;
try {
const current = await this.#store.read(input);
if (!current || current.record.status !== 'waiting') this.#stopObservation(input);
} catch (error) {
// An unreadable record does not authorize continued use of its cached state.
// Stop only its work; a later authoritative selection can retry it normally.
this.#stopObservation(input);
seenWaitIds.add(input.waitId);
this.#report(error);
}
}
// Retry a bounded, rotating batch without reinterpreting source facts as read errors.
for (const [key, input] of [...this.#pendingCooldowns].slice(0, this.#policy.pageSize)) {
if (this.#closed) return;
if (this.#pendingCooldowns.get(key) !== input) continue;
this.#pendingCooldowns.delete(key);
if (input.until <= this.#clock.now()) continue;
this.#pendingCooldowns.set(key, input);
try {
await this.#writeSourceCooldown(input);
} catch (error) {
this.#report(error);
}
}
if (this.#closed) return;
// Expiry has its own monotonic scan; unavailable sources cannot hide deadlines.
const page = await this.#store.listByStatus({
status: 'waiting',
limit: this.#policy.pageSize,
...(this.#expiryCursor ? { afterWaitId: this.#expiryCursor } : {}),
});
for (const snapshot of page.items) {
if (this.#closed) return;
if (this.#clock.now() >= deadlineOf(snapshot.record)) {
try {
await this.#expire(snapshot);
} catch (error) {
// A record-local failure must not pin the cursor or hide other deadlines.
// Keep the durable record for a later pass; never manufacture an expiry.
this.#report(error);
}
}
}
this.#expiryCursor = page.nextCursor ?? undefined;
// Pending cooldown saturation pauses new observations globally until a write
// succeeds or its absolute cooldown expires. Expiry and persistence retries above
// continue, preserving source limits without admitting unbounded pending facts.
// Fill source lanes separately: one source with many old waits must not occupy
// the candidate page while another source could use a free global slot.
for (
let selected = 0;
selected < Math.min(this.#policy.pageSize, this.#policy.maxConcurrent);
selected++
) {
const occupiedFlights = this.#occupiedFlights();
if (
this.#closed ||
this.#pendingCooldowns.size >= this.#policy.maxSourceCooldowns ||
occupiedFlights.size >= this.#policy.maxConcurrent
)
return;
const now = this.#clock.now();
const excludedWaitIds = new Set(seenWaitIds);
const occupied = new Map<string, number>();
for (const flight of occupiedFlights) {
excludedWaitIds.add(flight.waitId);
occupied.set(flight.source, (occupied.get(flight.source) ?? 0) + 1);
}
const dirtyWaitIds: string[] = [];
for (const [waitId, subscription] of this.#subscriptions) {
if (subscription.isPending?.()) excludedWaitIds.add(waitId);
else if (subscription.dirty) dirtyWaitIds.push(waitId);
}
const excludedSources = new Set<string>();
for (const [source, count] of occupied)
if (count >= this.#policy.maxPerSource) excludedSources.add(source);
for (const [source, until] of this.#cooldowns) if (until > now) excludedSources.add(source);
for (const [source, input] of this.#pendingCooldowns)
if (input.until > now) excludedSources.add(source);
// Normally a durable claim moves the oldest winner behind its peers. After a
// failed claim, traverse immutable IDs instead so progress does not depend on
// that write succeeding or on a bounded cache retaining every failed wait.
// Eligibility and occupied lanes are still filtered before LIMIT in both modes.
const observationScan = this.#observationScan;
const candidates = await this.#store.listForObservation({
limit: 1,
now,
...(observationScan ? { scan: observationScan } : {}),
dirtyWaitIds,
excludedWaitIds: [...excludedWaitIds],
excludedSources: [...excludedSources].map((key) => {
const [providerId, connectionId] = JSON.parse(key) as [string, string | null];
return { providerId, connectionId };
}),
});
const snapshot = candidates[0];
if (this.#closed) return;
if (!snapshot) {
if (observationScan && this.#observationScan === observationScan)
this.#observationScan = undefined;
return;
}
if (observationScan && this.#observationScan === observationScan)
this.#observationScan = { afterWaitId: snapshot.record.waitId };
seenWaitIds.add(snapshot.record.waitId);
const r = snapshot.record;
const dispatchAt = this.#clock.now();
if (dispatchAt >= deadlineOf(r)) {
try {
await this.#expire(snapshot);
} catch (error) {
this.#report(error);
}
continue;
}
const currentFlights = this.#occupiedFlights();
if ([...currentFlights].some((flight) => flight.waitId === r.waitId)) continue;
const subscription = this.#subscriptions.get(r.waitId);
if (subscription?.isPending?.()) continue;
const o = r.observation;
const due = o ? (subscription?.dirty ? o.nextObserveAt : o.nextPollAt) : r.createdAt;
if (dispatchAt < due) continue;
const source = sourceKey(r);
if (
dispatchAt < this.#sourceCooldown(source) ||
this.#pendingCooldowns.size >= this.#policy.maxSourceCooldowns ||
currentFlights.size >= this.#policy.maxConcurrent ||
[...currentFlights].filter((v) => v.source === source).length >= this.#policy.maxPerSource
)
continue;
// Reserve capacity before asynchronous setup; rate limits apply at read dispatch.
const flight: Flight = {
readTimeoutMs: this.#options.adapters.get(r)?.readTimeoutMs ?? this.#policy.readTimeoutMs,
controller: new AbortController(),
source,
sessionId: r.sessionId,
waitId: r.waitId,
claimed: false,
cooldownUntil: 0,
retryAfterUntil: 0,
task: Promise.resolve(),
};
this.#flights.set(r.waitId, flight);
flight.task = this.#observe(snapshot, flight)
.catch((error: unknown) => {
this.#report(error);
if (!this.#closed && !flight.claimed) this.#observationScan ??= {};
})
.finally(() => {
if (this.#flights.get(r.waitId) === flight) this.#flights.delete(r.waitId);
});
}
}
async #observe(snapshot: EventWaitSnapshot, flight: Flight): Promise<void> {
const projectionBefore = snapshot.record;
let r = projectionBefore;
if (r.status !== 'waiting' || this.#closed) return;
const cooldown = await this.#store.readSourceCooldown({
providerId: r.resource.providerId,
connectionId: r.resource.connectionId,
});
if (this.#closed || flight.controller.signal.aborted) return;
if (cooldown !== null && cooldown > this.#clock.now()) {
return;
}
const previous = r.observation;
if (r.schemaVersion === 1 && (previous?.attempts ?? 0) >= this.#policy.maxAttempts) {
const finished = await this.#finish(
snapshot,
{ kind: 'source_unavailable', receiptKey: `${r.deliveryKey}/budget`, evidenceRefs: [] },
{ reason: 'observation_budget_exhausted' },
);
if (!finished && !this.#closed && !flight.controller.signal.aborted)
this.#observationScan ??= {};
return;
}
let adapter: EventWaitSourceAdapter;
try {
this.validate(r);
adapter = this.#options.adapters.get(r)!;
} catch {
const finished = await this.#finish(snapshot, {
kind: 'source_unavailable',
receiptKey: `${r.deliveryKey}/unsupported`,
evidenceRefs: [],
});
if (!finished && !this.#closed && !flight.controller.signal.aborted)
this.#observationScan ??= {};
return;
}
const now = Math.max(r.updatedAt, this.#clock.now());
if (now >= deadlineOf(r)) {
await this.#expire(snapshot);
return;
}
const rolls =
r.schemaVersion === 2 && previous?.attempts === EVENT_WAIT_LIMITS.observationAttempts;
const reserved: EventWaitObservation | ConsumerEventWaitObservation = {
...(r.schemaVersion === 2
? { schemaVersion: 2 as const, window: (r.observation?.window ?? 0) + (rolls ? 1 : 0) }
: { schemaVersion: 1 as const }),
attempts: rolls ? 1 : (previous?.attempts ?? 0) + 1,
lastAttemptAt: now,
lastObservedAt: previous?.lastObservedAt ?? null,
nextObserveAt: Math.min(
deadlineOf(r),
addTime(now, Math.max(flight.readTimeoutMs, this.#policy.minIntervalMs)),
),
nextPollAt: Math.min(
deadlineOf(r),
addTime(now, Math.max(flight.readTimeoutMs, this.#policy.minIntervalMs)),
),
// The bounded counter also preserves the last unknown during checking.
// Rolling may shrink its count, but cannot turn that observation into pending.
consecutiveErrors: rolls
? Math.min(1, previous.consecutiveErrors)
: (previous?.consecutiveErrors ?? 0),
outcome: 'checking',
cursor: previous?.cursor ?? null,
eventKey: previous?.eventKey ?? null,
// Reconnecting projections still need the last completed observation's
// diagnostic while this reservation's source request is in flight.
reason: previous?.reason ?? null,
};
const claim = await this.#commit(
snapshot,
this.#decode({ ...r, updatedAt: now, observation: reserved }),
);
if (this.#closed || flight.controller.signal.aborted) return;
if (!claim) {
// Conflict responses also need progress if a backend keeps returning the same
// ready row. Reselect from authority during the next bounded recovery pass.
this.#observationScan ??= {};
return;
}
flight.claimed = true;
snapshot = claim;
r = claim.record;
let result: EventWaitObservationResult;
let subscriptionPending = false;
try {
for (;;) {
const access = await this.#authorize(r, flight);
if (access.kind === 'revoked') {
await this.cancel(identity(r), access.reason);
return;
}
if (access.kind === 'unknown') {
result = access;
break;
}
const readiness = await this.#subscribe(adapter, r, access.context, flight);
// A peer can throttle the source while authorization is in flight. Wait
// outside the request timeout, then obtain fresh access before subscribing.
if (readiness === 'deferred') continue;
subscriptionPending = readiness === 'pending';
if (subscriptionPending) {
result = { kind: 'unknown', reason: 'observation_timeout' };
} else {
const read = await this.#read(adapter, r, flight);
if ('access' in read) {
if (read.access.kind === 'revoked') {
await this.cancel(identity(r), read.access.reason);
return;
}
// No source result needs revalidation; unknown access must back off immediately.
result = read.access;
} else {
result = read.result;
// A rate-limited read has no condition facts to accept. Preserve its backoff
// without issuing another potentially remote authorization request.
if (result.kind !== 'unknown' || (result.retryAfterMs ?? 0) === 0) {
const latestAccess = await this.#authorize(r, flight);
if (latestAccess.kind === 'revoked') {
await this.cancel(identity(r), latestAccess.reason);
return;
}
if (latestAccess.kind === 'unknown') {
result = latestAccess;
}
}
}
}
break;
}
} catch (error) {
// A source fact must not become a weaker observation_failed checkpoint just
// because its persistence failed. Leave the reservation intact and report it.
if (error instanceof SourceCooldownWriteError || error instanceof StorageRootAuthorityError)
throw error;
if (this.#closed || flight.controller.signal.aborted) return;
result = {
kind: 'unknown',
reason: error instanceof ObservationTimeout ? 'observation_timeout' : 'observation_failed',
};
}
if (this.#closed || flight.controller.signal.aborted) return;
try {
this.#validateResult(result, r);
} catch {
result = { kind: 'unknown', reason: 'invalid_observation_result' };
}
// Release live notification intake when observation or access is uncertain, but
// keep a failed subscription's polling-only marker across transient read errors.
if (
result.kind === 'unknown' &&
!subscriptionPending &&
!this.#subscriptions.get(r.waitId)?.controller.signal.aborted
)
this.#detach(r.waitId);
await this.#finish(snapshot, result, {
retryAfterUntil: flight.retryAfterUntil,
projectionBefore,
});
}
#validateAccess(access: EventWaitObservationAccess): void {
if (!access || !['allowed', 'revoked', 'unknown'].includes(access.kind))
throw new TypeError('Invalid observation access');
}
async #authorize(r: EventWaitRecord, flight: Flight): Promise<EventWaitObservationAccess> {
for (;;) {
// Every authorization boundary may issue a remote request. A peer can establish
// or extend Retry-After during any earlier await, including the source read.
// Intentional throttling stays outside the request timeout and attempt budget.
if (flight.retryAfterUntil > this.#clock.now())
await this.#waitForCooldown(flight, Math.min(flight.retryAfterUntil, deadlineOf(r)));
const checked = await this.#tryAuthorize(r, flight);
if (checked) return checked.access;
}
}
/** Null defers dispatch to the caller without starting an authorization request. */
async #tryAuthorize(
r: EventWaitRecord,
flight: Flight,
deadline = deadlineOf(r),
authorize = this.#options.authorize,
): Promise<{ access: EventWaitObservationAccess } | null> {
const recordCooldown = async (checked: { access: EventWaitObservationAccess } | null) => {
if (!checked || this.#closed) return;
this.#validateAccess(checked.access);
if (checked.access.kind === 'unknown') {
this.#validateResult(checked.access, r);
await this.#recordSourceCooldown(r, checked.access);
}
};
const checked = await this.#call<{ access: EventWaitObservationAccess } | null>(
flight,
deadline,
(signal) => {
// Recheck at actual dispatch; cooldown can change after the timer wakes or
// before #call invokes this callback. Active flights retain it across cache eviction.
if (flight.retryAfterUntil > this.#clock.now()) return Promise.resolve(null);
return authorize(structuredClone(r), signal).then((access) => ({ access }));
},
recordCooldown,
);
// Shared facts outlive a cancelled wait, including cancellation during the
// handoff from #call. Normal and late results use the same validation/persistence
// path, outside the request timeout and before releasing their request capacity.
await recordCooldown(checked);
return checked;
}
async #read(
adapter: EventWaitSourceAdapter,
r: EventWaitRecord,
flight: Flight,
): Promise<
| { result: EventWaitObservationResult }
| { access: Exclude<EventWaitObservationAccess, { kind: 'allowed' }> }
> {
for (;;) {
if (this.#closed || flight.controller.signal.aborted)
throw new Error('Observation cancelled');
if (this.#clock.now() >= deadlineOf(r))
throw new ObservationTimeout('Observation deadline reached');
const preparing = this.#dispatching.get(flight.source);
if (preparing) {
await this.#waitForPreparation(flight, deadlineOf(r), preparing);
continue;
}
const preparation: DispatchPreparation = { waiters: new Set() };
this.#dispatching.set(flight.source, preparation);
const unlock = () => {
if (this.#dispatching.get(flight.source) === preparation)
this.#dispatching.delete(flight.source);
for (const resume of preparation.waiters) resume();
preparation.waiters.clear();
};
const token = randomUUID();
let reserved = false;
let completion: Promise<{ error: unknown } | null> | undefined;
const complete = (at: number, until: number) => {
const writing = this.#completeDispatch(r, token, at, until).then(
() => null,
(error: unknown) => ({ error }),
);
// Only dispatch preparation is serialized; the source read may continue.
void writing.then(unlock);
return writing;
};
try {
// The cache contains actual source facts only. A durable dispatch reservation
// is checked here, but never cached as though it were a source Retry-After.
const persisted = await this.#store.readSourceCooldown({
providerId: r.resource.providerId,
connectionId: r.resource.connectionId,
});
if (this.#closed || flight.controller.signal.aborted)
throw new Error('Observation cancelled');
if (this.#clock.now() >= deadlineOf(r))
throw new ObservationTimeout('Observation deadline reached');
const until = Math.max(
persisted ?? 0,
this.#sourceCooldown(flight.source),
flight.cooldownUntil,
);
if (until > this.#clock.now()) {
unlock();
await this.#waitForCooldown(flight, Math.min(until, deadlineOf(r)));
continue;
}
const now = this.#clock.now();
// Persist a bounded preparation window before any source request. It survives
// a crash before actual dispatch timing is committed, without permanently
// extending the ordinary minimum interval after a successful dispatch.
const dispatchBy = addTime(now, flight.readTimeoutMs);
reserved = await this.#reserveDispatch(
r,
token,
now,
addTime(dispatchBy, this.#policy.minIntervalMs),
);
if (!reserved) continue;
// Never wait out Retry-After while holding a bounded dispatch reservation.
// Release it in finally, then wait and reserve afresh within this observation.
const checked = await this.#tryAuthorize(r, flight);
if (!checked) continue;
const { access } = checked;
if (access.kind !== 'allowed') return { access };
const read = await this.#call(
flight,
deadlineOf(r),
(signal) => {
const at = this.#clock.now();
if (at > dispatchBy || at >= deadlineOf(r))
throw new ObservationTimeout('Dispatch reservation elapsed');
if (at < flight.cooldownUntil || at < this.#sourceCooldown(flight.source))
return Promise.resolve(null);
const subscription = this.#subscriptions.get(r.waitId);
if (subscription) subscription.dirty = false;
this.#cool(flight.source, addTime(at, this.#policy.minIntervalMs), flight);
// The reservation already protects restart. Persist actual timing beside
// the source request; a synchronous source throw also consumes an interval.
try {
return adapter
.read({
wait: structuredClone(r),
access: access.context,
cursor: r.observation?.cursor ?? null,
signal,
})
.then((result) => ({ result }));
} finally {
completion = complete(at, addTime(at, this.#policy.minIntervalMs));
}
},
async (lateRead) => {
if (!lateRead) return;
this.#validateResult(lateRead.result, r);
await this.#recordSourceCooldown(r, lateRead.result);
},
);
if (read) {
this.#validateResult(read.result, r);
await this.#recordSourceCooldown(r, read.result);
return read;
}
} finally {
try {
// An aborted preparation has no source request to account for. Release only
// our own token; newer Retry-After facts and later reservations remain intact.
if (reserved && !completion && !this.#closed) {
const at = this.#clock.now();
completion = complete(at, at);
}
const failed = await completion;
if (failed) throw failed.error;
} finally {
unlock();
}
}
}
}
async #reserveDispatch(
r: EventWaitRecord,
token: string,
observedAt: number,
until: number,
): Promise<boolean> {
for (let attempt = 0; ; attempt++) {
if (this.#closed) throw new Error('Observation cancelled');
try {
return await this.#store.reserveSourceDispatch({
providerId: r.resource.providerId,
connectionId: r.resource.connectionId,
token,
observedAt,
until,
});
} catch (error) {
if (error instanceof StorageRootAuthorityError) throw error;
if (attempt === 1) throw new SourceCooldownWriteError(error);
}
}
}
async #completeDispatch(
r: EventWaitRecord,
token: string,
observedAt: number,
until: number,
): Promise<void> {
for (let attempt = 0; attempt < 2; attempt++) {
if (this.#closed) return;
try {
await this.#store.completeSourceDispatch({
providerId: r.resource.providerId,
connectionId: r.resource.connectionId,
token,
observedAt,
until,
});
return;
} catch (error) {
if (error instanceof StorageRootAuthorityError) throw error;
if (attempt === 1) throw new SourceCooldownWriteError(error);
}
}
}
async #waitForPreparation(
flight: Flight,
deadline: number,
preparation: DispatchPreparation,
): Promise<void> {
if (this.#closed || flight.controller.signal.aborted) throw new Error('Observation cancelled');
if (this.#clock.now() >= deadline) throw new ObservationTimeout('Observation deadline reached');
if (this.#dispatching.get(flight.source) !== preparation) return;
// This wait owns no source request or dispatch reservation. Detach on interruption
// rather than retaining a Promise reaction or counting it as a lingering request.
let resume!: () => void;
let abort!: () => void;
let cancelTimer: (() => void) | undefined;
try {
await new Promise<void>((resolve, reject) => {
resume = resolve;
abort = () => reject(new Error('Observation cancelled'));
preparation.waiters.add(resume);
flight.controller.signal.addEventListener('abort', abort, { once: true });
cancelTimer = this.#clock.schedule(
() => reject(new ObservationTimeout('Observation deadline reached')),
Math.max(0, deadline - this.#clock.now()),
);
});
} finally {
preparation.waiters.delete(resume);
flight.controller.signal.removeEventListener('abort', abort);
cancelTimer?.();
}
}
async #waitForCooldown(flight: Flight, until: number): Promise<void> {
if (this.#closed || flight.controller.signal.aborted) throw new Error('Observation cancelled');
if (this.#clock.now() >= until) return;
// Intentional throttling does not consume read timeouts or additional attempts.
await new Promise<void>((resolve, reject) => {
const abort = () => {
cancelTimer();
flight.controller.signal.removeEventListener('abort', abort);
reject(new Error('Observation cancelled'));
};
const cancelTimer = this.#clock.schedule(() => {
flight.controller.signal.removeEventListener('abort', abort);
resolve();
}, until - this.#clock.now());
flight.controller.signal.addEventListener('abort', abort, { once: true });
});
}
#validateResult(result: EventWaitObservationResult, r: EventWaitRecord): void {
if (
!result ||
!['pending', 'unknown', 'satisfied', 'invalidated', 'source_unavailable'].includes(
result.kind,
)
)
throw new TypeError('Invalid observation result');
if (
result.kind === 'unknown' &&
result.retryAfterMs !== undefined &&
(!Number.isSafeInteger(result.retryAfterMs) || result.retryAfterMs < 0)
)
throw new TypeError('Invalid retry delay');
const now = Math.max(r.updatedAt, this.#clock.now());
// Registration and delivery authorization may have no observation checkpoint.
// Validate the prospective fields without writing an attempt or changing the wait.
const observation = r.observation ?? {
...(r.schemaVersion === 2
? { schemaVersion: 2 as const, window: 0 }
: { schemaVersion: 1 as const }),
attempts: 1,
lastAttemptAt: now,
lastObservedAt: now,
nextObserveAt: now,
nextPollAt: now,
consecutiveErrors: 0,
outcome: 'checking',
cursor: null,
eventKey: null,
reason: null,
};
this.#decode({
...r,
updatedAt: now,
observation: {
...observation,
outcome: result.kind,
lastObservedAt: now,
cursor: result.cursor ?? observation.cursor,
eventKey: result.eventKey ?? observation.eventKey,
reason: result.reason ?? null,
},
...(result.kind === 'pending' || result.kind === 'unknown'
? {}
: {
status: 'resolved',
resolvedAt: now,
resolution: {
outcome: result.kind,
receiptKey: result.receiptKey,
observedAt: now,
evidenceRefs: result.evidenceRefs,
},
}),
});
}
async #recordSourceCooldown(
r: EventWaitRecord,
result: EventWaitObservationResult,
): Promise<void> {
if (this.#closed) return;
const now = Math.max(r.updatedAt, this.#clock.now());
const retry = result.kind === 'unknown' ? (result.retryAfterMs ?? 0) : 0;
if (retry > 0) {
// Persist the source fact before changing the wait, including a late expiry
// or a losing cancellation CAS. Another wait still owes the full Retry-After.
const until = addTime(now, retry);
this.#cool(sourceKey(r), until);
await this.#persistSourceCooldown(r, now, until);
}
}
async #persistSourceCooldown(
r: EventWaitRecord,
observedAt: number,
until: number,
): Promise<void> {
await this.#writeSourceCooldown({
providerId: r.resource.providerId,
connectionId: r.resource.connectionId,
observedAt,
until,
});
}
async #writeSourceCooldown(input: ExtendEventWaitSourceCooldownInput): Promise<void> {
const key = eventWaitSourceKey(input);
// Keep unacknowledged facts outside the evictable cache. A peer may finish setup
// while this write is pending, and wait cleanup must not erase the source limit.
if ((this.#pendingCooldowns.get(key)?.until ?? 0) < input.until)
this.#pendingCooldowns.set(key, input);
// Retry a transient write once using the same absolute deadline. A lost
// acknowledgement is safe because the store only extends the existing limit.
for (let attempt = 0; attempt < 2; attempt++) {
if (this.#closed) return;
try {
const persisted = await this.#store.extendSourceCooldown(input);
if (!this.#closed) {
this.#cacheCooldown(key, persisted);
if ((this.#pendingCooldowns.get(key)?.until ?? 0) <= persisted)
this.#pendingCooldowns.delete(key);
}
return;
} catch (error) {
if (error instanceof StorageRootAuthorityError) throw error;
// The fact was registered before the first await. If it has since been
// removed, a successful dominating write or expiry already retired it.
if (attempt === 1) throw new SourceCooldownWriteError(error);
}
}
}
async #finish(
snapshot: EventWaitSnapshot,
result: EventWaitObservationResult,
options: {
reason?: string;
retryAfterUntil?: number;
projectionBefore?: EventWaitRecord;
} = {},
): Promise<EventWaitSnapshot | null> {
const r = snapshot.record;
if (this.#closed || r.status !== 'waiting') return null;
// Source reads and authorization already recorded their shared facts. Finishing
// this wait must neither discard those facts nor restart their absolute cooldown.
const now = Math.max(r.updatedAt, this.#clock.now());
if (now >= deadlineOf(r)) {
await this.#expire(snapshot);
return null;
}
const previous = r.observation;
let observation = previous;
if (previous) {
const errors =
result.kind === 'unknown' ? Math.min(previous.attempts, previous.consecutiveErrors + 1) : 0;
const interval = this.#options.adapters.get(r)?.pollIntervalMs ?? this.#policy.pollIntervalMs;
const backoff =
result.kind === 'unknown'
? Math.min(this.#policy.maxBackoffMs, interval * 2 ** Math.min(errors - 1, 20))
: interval;
const random = this.#options.random?.() ?? Math.random();
if (!Number.isFinite(random) || random < 0 || random > 1)
throw new TypeError('Invalid wait randomness');
const delay = Math.ceil(backoff * (1 + this.#policy.jitterRatio * random));
const nextObserveAt = Math.min(
deadlineOf(r),
Math.max(
options.retryAfterUntil ?? 0,
addTime(
now,
result.kind === 'unknown'
? Math.max(delay, this.#policy.minIntervalMs)
: this.#policy.minIntervalMs,
),
),
);
observation = {
...previous,
lastObservedAt: now,
nextObserveAt,
nextPollAt: Math.min(deadlineOf(r), Math.max(nextObserveAt, addTime(now, delay))),
consecutiveErrors: errors,
outcome: result.kind,
// Failed reads never advance a replay checkpoint.
cursor: result.kind === 'unknown' ? previous.cursor : (result.cursor ?? previous.cursor),
eventKey:
result.kind === 'unknown' ? previous.eventKey : (result.eventKey ?? previous.eventKey),
reason: result.reason ?? options.reason ?? null,
};
}
let record = this.#decode({ ...r, updatedAt: now, ...(observation ? { observation } : {}) });
if (result.kind !== 'pending' && result.kind !== 'unknown') {
record = {
...record,
status: 'resolved',
resolvedAt: now,
resolution: {
outcome: result.kind,
receiptKey: result.receiptKey,
observedAt: now,
evidenceRefs: result.evidenceRefs,
},
};
}
const committed = await this.#commit(snapshot, record, options.projectionBefore);
if (!committed || committed.record.status !== 'waiting') this.#detach(r.waitId);
return committed;
}
async #subscribe(
adapter: EventWaitSourceAdapter,
r: EventWaitRecord,
access: unknown,
flight: Flight,
): Promise<'ready' | 'pending' | 'deferred'> {
if (
!adapter.subscribe ||
this.#subscriptions.has(r.waitId) ||
this.#subscriptions.size >= this.#policy.maxSubscriptions
)
return 'ready';
if (flight.retryAfterUntil > this.#clock.now()) return 'deferred';
const subscription: Subscription = {
identity: identity(r),
controller: new AbortController(),
dirty: false,
};
this.#subscriptions.set(r.waitId, subscription);
try {
const subscribed = await this.#call(flight, deadlineOf(r), async (signal) => {
// Check at the side-effect boundary too: #call dispatches in a microtask.
if (flight.retryAfterUntil > this.#clock.now()) return false;
const abort = () => subscription.controller.abort();
signal.addEventListener('abort', abort, { once: true });
try {
const handle = await adapter.subscribe!(
{
wait: structuredClone(r),
access,
cursor: r.observation?.cursor ?? null,
signal: subscription.controller.signal,
},
() => {
if (
!this.#closed &&
this.#subscriptions.get(r.waitId) === subscription &&
!subscription.controller.signal.aborted
)
subscription.dirty = true;
},
);
if (
this.#closed ||
subscription.controller.signal.aborted ||
this.#subscriptions.get(r.waitId) !== subscription
)
handle.close();
else subscription.handle = handle;
return true;
} finally {
signal.removeEventListener('abort', abort);
}
});
if (!subscribed) {
this.#detach(r.waitId);
return 'deferred';
}
} catch (error) {
subscription.controller.abort();
if (this.#closed || flight.controller.signal.aborted) throw error;
// Retain the bounded, aborted entry as a polling-only fallback. Retrying a
// broken optional subscription before every read can consume the entire TTL.
// A timed-out setup still running must first release its source capacity.
if (error instanceof ObservationTimeout) subscription.isPending = error.isPending;
return subscription.isPending?.() ? 'pending' : 'ready';
}
return 'ready';
}
async #call<T>(
flight: Flight,
deadline: number,
operation: (signal: AbortSignal) => Promise<T>,
onLateResult?: (result: T) => Promise<void>,
): Promise<T> {
if (this.#closed || flight.controller.signal.aborted) throw new Error('Observation cancelled');
if (this.#clock.now() >= deadline) throw new ObservationTimeout('Observation deadline reached');
const controller = new AbortController();
let settled = false;
let delivered = false;
let finishHandoff!: () => void;
const handoff = new Promise<void>((resolve) => {
finishHandoff = resolve;
});
let rejectWait!: (reason: unknown) => void;
const interrupted = new Promise<never>((_, reject) => {
rejectWait = reject;
});
const task = Promise.resolve().then(() => {
if (controller.signal.aborted) throw new Error('Observation cancelled');
return operation(controller.signal);
});
const interrupt = (error: Error) => {
controller.abort();
if (!settled) this.#lingering.set(task, flight);
rejectWait(error);
};
const abort = () => interrupt(new Error('Observation cancelled'));
flight.controller.signal.addEventListener('abort', abort, { once: true });
const cancelTimer = this.#clock.schedule(
() => interrupt(new ObservationTimeout('Observation timed out', () => !settled)),
Math.max(0, Math.min(flight.readTimeoutMs, deadline - this.#clock.now())),
);
void task
.then(
async (result) => {
// Abort can fire after task settlement while its result still wins the
// race. Only the actual handoff outcome decides who processes the fact.
await handoff;
if (delivered || this.#closed || !onLateResult) return;
// Keep source capacity until its late facts are processed. This persistence
// is outside the source timeout and participates in bounded Host shutdown.
const processing = Promise.resolve().then(() => {
if (!this.#closed) return onLateResult(result);
});
this.#lateResults.add(processing);
try {
await processing;
} catch (error) {
this.#report(error);
} finally {
this.#lateResults.delete(processing);
}
},
() => undefined,
)
.finally(() => {
settled = true;
this.#lingering.delete(task);
})
.catch(() => undefined);
try {
const result = await Promise.race([task, interrupted]);
delivered = true;
return result;
} finally {
cancelTimer();
flight.controller.signal.removeEventListener('abort', abort);
finishHandoff();
}
}
async #commit(
snapshot: EventWaitSnapshot,
record: EventWaitRecord,
projectionBefore: EventWaitRecord = snapshot.record,
): Promise<EventWaitSnapshot | null> {
if (this.#closed) return null;
const result = await this.#store.commit({
...identity(record),
expectedAuthorityRevision: snapshot.authorityRevision,
record,
});
if (result.kind !== 'committed') return null;
const after = result.snapshot.record;
// Compare durable semantics before the attempt, not its temporary checking
// reservation. Cache eviction must not turn unchanged polling into activity.
if (
(after.status !== 'waiting' || after.observation?.outcome !== 'checking') &&
(projectionBefore.status !== after.status ||
projectionBefore.observation?.outcome !== after.observation?.outcome ||
projectionBefore.observation?.reason !== after.observation?.reason)
) {
try {
this.#options.onChanged?.(after);
} catch {
/* Projection hints are best effort. */
}
}
return result.snapshot;
}
async #expire(snapshot: EventWaitSnapshot): Promise<void> {
if (this.#closed || snapshot.record.status !== 'waiting') return;
const r = snapshot.record;
const now = Math.max(r.updatedAt, this.#clock.now());
if (r.deadlineAt === null || now < deadlineOf(r)) return;
// The immutable deadline already forbids more observation, even if recording
// expiry fails or its acknowledgement is lost. The store still owns the status.
this.#stopObservation(identity(r));
await this.#commit(snapshot, {
...r,
status: 'expired',
updatedAt: now,
expiredAt: now,
});
}
/** Trusted internal control entry point; the consumer authenticates the caller. */
async cancel(input: ReadEventWaitInput, reason: string): Promise<void> {
if (this.#closed) throw new Error('Event wait manager is closed');
for (let attempt = 0; attempt < 4 && !this.#closed; attempt++) {
const snapshot = await this.#store.read(input);
if (
!snapshot ||
snapshot.record.delivery ||
['cancelled', 'expired'].includes(snapshot.record.status)
) {
this.#stopObservation(input);
return;
}
const r = snapshot.record;
const now = Math.max(r.updatedAt, this.#clock.now());
const base = {
...r,
status: 'cancelled' as const,
updatedAt: now,
cancelledAt: now,
reason,
...(r.status === 'resolved'
? { priorResolution: { resolvedAt: r.resolvedAt, resolution: r.resolution } }
: {}),
};
// Construct from the base contract; terminal-only keys must not leak across variants.
const {
resolvedAt: _resolvedAt,
resolution: _resolution,
...record
} = base as typeof base & {
resolvedAt?: number;
resolution?: unknown;
};
try {
if (await this.#commit(snapshot, record)) {
this.#stopObservation(input);
return;
}
} catch (error) {
// A lost acknowledgement may still have durably cancelled the wait.
// Release resources once that fact is confirmed; preserve the original error.
try {
const current = await this.#store.read(input);
if (!current || current.record.status !== 'waiting') this.#stopObservation(input);
} catch {
/* Failed storage remains authoritative; the caller receives the failure. */
}
throw error;
}
}
// Only an authoritative outcome above acknowledges cancellation. Shutdown
// may stop observation without durably cancelling the wait.
throw new Error(
this.#closed ? 'Event wait manager is closed' : 'Event wait cancellation conflicted',
);
}
#stopObservation(input: ReadEventWaitInput): void {
const flight = this.#flights.get(input.waitId);
if (flight?.sessionId === input.sessionId) flight.controller.abort();
if (this.#subscriptions.get(input.waitId)?.identity.sessionId === input.sessionId)
this.#detach(input.waitId);
}
#sourceCooldown(source: string): number {
return Math.max(
this.#cooldowns.get(source) ?? 0,
this.#pendingCooldowns.get(source)?.until ?? 0,
);
}
#cool(source: string, until: number, owner?: Flight): void {
// Prepared reads retain new source facts even if their cache entry is evicted.
for (const flight of this.#occupiedFlights()) {
if (flight.source === source && flight !== owner) {
flight.cooldownUntil = Math.max(flight.cooldownUntil, until);
// An owner marks ordinary read spacing. Source-reported Retry-After has no
// owner and must also defer authorization in every already-admitted flight.
if (!owner) flight.retryAfterUntil = Math.max(flight.retryAfterUntil, until);
}
}
this.#cacheCooldown(source, until);
}
#cacheCooldown(source: string, until: number): void {
const now = this.#clock.now();
for (const [key, time] of this.#cooldowns) if (time <= now) this.#cooldowns.delete(key);
if (until <= now) return;
const latest = Math.max(this.#cooldowns.get(source) ?? 0, until);
this.#cooldowns.delete(source);
if (this.#cooldowns.size >= this.#policy.maxSourceCooldowns)
this.#cooldowns.delete(this.#cooldowns.keys().next().value!);
this.#cooldowns.set(source, latest);
}
#detach(waitId: string): void {
const subscription = this.#subscriptions.get(waitId);
if (!subscription) return;
this.#subscriptions.delete(waitId);
subscription.controller.abort();
try {
subscription.handle?.close();
} catch (error) {
this.#report(error);
}
}
#report(error: unknown): void {
if (error instanceof StorageRootAuthorityError && error.code === 'invalid_lease')
void this.close();
try {
this.#options.onError(error);
} catch {
/* Diagnostic consumers cannot stop cleanup. */
}
}
/** Stop observation synchronously while final close still owns cleanup waits. */
beginDrain(): void {
if (this.#closed) return;
this.#closed = true;
this.#timer?.();
this.#timer = undefined;
for (const flight of this.#flights.values()) flight.controller.abort();
for (const flight of this.#accessFlights) flight.controller.abort();
for (const waitId of this.#subscriptions.keys()) this.#detach(waitId);
}
close(): Promise<void> {
if (this.#close) return this.#close;
this.beginDrain();
this.#close = (async () => {
let cancelTimer: (() => void) | undefined;
try {
await Promise.race([
Promise.allSettled([
this.#start,
this.#scan,
...[...this.#flights.values()].map((v) => v.task),
...[...this.#accessFlights].map((v) => v.task),
...this.#lateResults,
]),
new Promise<void>((resolve) => {
cancelTimer = this.#clock.schedule(resolve, this.#policy.closeTimeoutMs);
}),
]);
} finally {
cancelTimer?.();
}
})();
return this.#close;
}
}