UNPKG

@struktoai/mirage-core

Version:

A Unified Virtual Filesystem For AI Agents

176 lines • 6.89 kB
// ========= Copyright 2026 @ Strukto.AI All Rights Reserved. ========= // Licensed 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. // ========= Copyright 2026 @ Strukto.AI All Rights Reserved. ========= /** * The one error an aborted invocation rejects with. A signal's own reason * rides along as `cause` (a caller's `abort(x)`, a timeout's TimeoutError) * so every gate keys on one name and the caller still sees why. */ export function makeAbortError(signal) { const reason = signal?.aborted === true ? signal.reason : undefined; // Always a fresh wrapper, even when the reason is itself an AbortError // (a plain `abort()`): the contract is one name and the reason under // `cause`, and a caller reading `cause` must find it on every path. const error = new DOMException('execute aborted', 'AbortError'); if (reason !== undefined) { Object.defineProperty(error, 'cause', { value: reason, configurable: true, writable: true }); } return error; } /** * Whether the signal has fired. A call rather than a property read, so a * check that comes after an earlier one is not narrowed away as stale. */ export function hasAborted(signal) { return signal?.aborted === true; } /** Fold two optional abort signals into one; either aborting aborts. */ export function mergeSignals(a, b) { if (a != null && b != null) return AbortSignal.any([a, b]); return a ?? b ?? undefined; } // `setTimeout` holds a 32-bit signed delay, so anything longer is not merely // imprecise: node warns (`TimeoutOverflowWarning`) and clamps it to 1ms, which // turns a long wait into an immediate return. 2147483647ms is just under 25 // days, which python's `asyncio.sleep` reaches without a word, so a delay past // it is served by re-arming rather than by one timer. `tail`'s own pause // (commands/builtin/generic/tail.ts) documents the infinite case of the same // trap. const MAX_TIMEOUT_MS = 2_147_483_647; export function sleep(ms, signal) { return new Promise((resolve, reject) => { if (signal?.aborted === true) { reject(makeAbortError()); return; } let timer = null; let left = ms; const onAbort = () => { if (timer !== null) clearTimeout(timer); reject(makeAbortError()); }; const arm = () => { const step = Math.min(left, MAX_TIMEOUT_MS); left -= step; timer = setTimeout(() => { if (left > 0) { arm(); return; } signal?.removeEventListener('abort', onAbort); resolve(); }, step); }; arm(); signal?.addEventListener('abort', onAbort, { once: true }); }); } /** * Call `fire` once `seconds` have passed, and return what cancels it. A * delay past what one timer takes is waited out in turns. */ export function atDeadline(seconds, fire) { const at = performance.now() + seconds * 1000; let timer; const arm = () => { timer = setTimeout(() => { if (performance.now() < at) arm(); else fire(); }, Math.min(Math.max(at - performance.now(), 0), MAX_TIMEOUT_MS)); }; arm(); return () => { clearTimeout(timer); }; } /** * What a stop that names a signal aborts with (`timeout`'s TERM): what the * stopped run started gets a stop's grace to finish, a guest landing the * file it left open, as one a `kill` stops does through its process's * signal. Python cancels the run and joins it whole, so it needs none. * It is an AbortError, as a plain `abort()` is, since `throwIfAborted` * throws the reason itself and every check keys on that name. */ export class SignalStop extends DOMException { signal; constructor(signal) { super('execute aborted', 'AbortError'); this.signal = signal; } } /** The signal a stop named, through any signal merged over it; null for a plain abort. */ export function stopSignal(signal) { const reason = signal?.aborted === true ? signal.reason : undefined; return reason instanceof SignalStop ? reason.signal : null; } /** Settle with `promise`, or reject as an abort as soon as `signal` fires. */ export function abortable(promise, signal) { if (signal === undefined) return promise; if (signal.aborted) { // The promise is still ours to settle; a later rejection with no // listener would surface as an unhandled error. void promise.catch(() => undefined); return Promise.reject(makeAbortError(signal)); } let onAbort = () => undefined; return new Promise((resolve, reject) => { onAbort = () => { reject(makeAbortError(signal)); }; signal.addEventListener('abort', onAbort, { once: true }); promise.then(resolve, reject); }).finally(() => { signal.removeEventListener('abort', onAbort); }); } /** How long a cancelled tree gets to unwind before the caller is released anyway. */ export const ABORT_JOIN_MS = 250; /** * The twin of Python's `run_cancellable`: cancel, then join. A cancelled * asyncio task unwinds at its next await, so Python joins it fully. A JS * promise cannot be cancelled, so once `signal` fires the tree is given * `graceMs` to reach a checkpoint, close its producers and settle with * its own error (which keeps the caller's abort reason); a leaf that is * blocked past that is left running and the caller is released. A * function is asked for the grace when the signal fires. */ export async function runCancellable(promise, signal, graceMs = ABORT_JOIN_MS) { if (signal === undefined) return promise; try { return await abortable(promise, signal); } catch (error) { if (!signal.aborted) throw error; let timer; const ms = typeof graceMs === 'function' ? graceMs() : graceMs; const grace = new Promise((_resolve, reject) => { timer = setTimeout(() => { reject(makeAbortError(signal)); }, ms); }); try { return await Promise.race([promise, grace]); } finally { clearTimeout(timer); } } } //# sourceMappingURL=abort.js.map