@struktoai/mirage-core
Version:
A Unified Virtual Filesystem For AI Agents
176 lines • 6.89 kB
JavaScript
// ========= 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