UNPKG

@struktoai/mirage-core

Version:

A Unified Virtual Filesystem For AI Agents

176 lines • 6.67 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. ========= import { isNode } from "./loader.js"; // The watchdog body, identical logic in both hosts: one pending // countdown at a time, generation-checked on every tick. The reason // cell is stored BEFORE the interrupt cell: the interrupt write can // wake pyodide immediately, and disarm() must never observe the trip // without its reason (it would report a timeout as a plain failure). // Reassert until disarmed: Pyodide can consume SIGINT without unwinding // the running WASM call, leaving a one-shot deadline permanently lost. const WATCHDOG_BODY = ` let timer = null; function onMessage(msg) { if (timer !== null) { clearTimeout(timer); timer = null; } if (msg.cancel) return; const cells = new Int32Array(msg.buffer); const gen = msg.gen; const deadline = msg.delayMs == null ? Infinity : Date.now() + msg.delayMs; function tick() { if (Atomics.load(cells, 2) !== gen) return; const reason = Atomics.load(cells, 1) || (Atomics.load(cells, 3) === 1 ? 2 : Date.now() >= deadline ? 1 : 0); if (reason !== 0) { Atomics.store(cells, 1, reason); Atomics.store(cells, 0, 2); } timer = setTimeout(tick, 10); } tick(); } `; const NODE_WATCHDOG = `${WATCHDOG_BODY} require('node:worker_threads').parentPort.on('message', onMessage); `; const BROWSER_WATCHDOG = `${WATCHDOG_BODY} self.onmessage = (event) => { if (event.data.ping) self.postMessage('ready'); else onMessage(event.data); }; `; async function createNodeWatchdog() { try { const { Worker } = await import('node:worker_threads'); const worker = new Worker(NODE_WATCHDOG, { eval: true }); // The watchdog must never keep the process alive on its own. worker.unref(); return { post: (message) => { worker.postMessage(message); }, terminate: () => void worker.terminate(), }; } catch { // No worker_threads (exotic host): degrade to unbounded runs. return null; } } async function createBrowserWatchdog() { if (typeof Worker === 'undefined' || typeof Blob === 'undefined') return null; const url = URL.createObjectURL(new Blob([BROWSER_WATCHDOG], { type: 'text/javascript' })); try { const worker = new Worker(url); // A nested browser worker needs its creator's event loop to finish // startup, and until then the browser holds what this thread posts // to it (Chrome holds it even past the worker's first message). Wait // for its answer to a message before synchronous WASM can block that // loop; a deadline posted sooner can wait behind the guest for good. worker.postMessage({ ping: true }); await new Promise((resolve, reject) => { worker.onmessage = () => { worker.onmessage = null; resolve(); }; worker.onerror = (event) => { worker.terminate(); reject(new Error(`pyodide watchdog failed: ${event.message}`)); }; }); return { post: (message) => { worker.postMessage(message); }, terminate: () => { worker.terminate(); }, }; } catch { // Workers can exist in the host API while CSP rejects their startup. // Preserve the same unbounded eager fallback as a worker-less host. return null; } finally { URL.revokeObjectURL(url); } } export async function createPyodideInterrupter(buffer) { if (typeof SharedArrayBuffer === 'undefined') return null; const watchdog = isNode() ? await createNodeWatchdog() : await createBrowserWatchdog(); if (watchdog === null) return null; const shared = buffer ?? new SharedArrayBuffer(16); const cells = new Int32Array(shared); let generation = 0; return { view: cells, arm(timeoutSeconds, signal) { generation += 1; Atomics.store(cells, 0, 0); Atomics.store(cells, 1, 0); Atomics.store(cells, 2, generation); if (Atomics.load(cells, 3) === 1) { Atomics.store(cells, 1, 2); Atomics.store(cells, 0, 2); } watchdog.post({ buffer: shared, delayMs: timeoutSeconds !== null && timeoutSeconds > 0 ? timeoutSeconds * 1000 : null, gen: generation, }); let removeAbort = null; if (signal !== undefined) { const onAbort = () => { Atomics.store(cells, 1, 2); Atomics.store(cells, 0, 2); }; if (signal.aborted) { onAbort(); } else { signal.addEventListener('abort', onAbort, { once: true }); removeAbort = () => { signal.removeEventListener('abort', onAbort); }; } } let done = false; let reason = null; return { disarm: () => { if (done) return reason; done = true; generation += 1; Atomics.store(cells, 2, generation); watchdog.post({ cancel: true }); if (removeAbort !== null) removeAbort(); const why = Atomics.load(cells, 1); Atomics.store(cells, 0, 0); Atomics.store(cells, 1, 0); reason = why === 1 ? 'deadline' : why === 2 ? 'signal' : null; return reason; }, }; }, close() { watchdog.terminate(); }, }; } //# sourceMappingURL=interrupt.js.map