UNPKG

@struktoai/mirage-core

Version:

A Unified Virtual Filesystem For AI Agents

183 lines • 7.4 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 { classify } from "../../errors/index.js"; const CAPACITY = 64 * 1024; const HEADER_BYTES = 16; const ENC = new TextEncoder(); const DEC = new TextDecoder(); /** The query parameter that marks a request for the service worker bridge. */ export const PARAM = 'mirage-pyodide'; /** The header that carries an answer's format across the bridge. */ export const FORMAT = 'x-mirage-pyodide-format'; /** * A thrown value, told the way a command can print it. Most guest * failures are Errors, but Emscripten and a crashed worker can surface a * plain object, whose String() is `[object Object]`: its name and message * say more, and failing those its keys. */ export function failureText(value) { if (value instanceof Error) return value.message; if (typeof value !== 'object' || value === null) return String(value); const { name, message } = value; const said = [name, message].filter((part) => typeof part === 'string' && part !== ''); if (said.length > 0) return said.join(': '); const keys = Object.keys(value).join(', '); return `an object with keys: ${keys !== '' ? keys : '(none)'}`; } /** An answer as bytes and their format: 0 JSON, 1 raw bytes, 2 a JSON error. */ async function encode(operation) { try { const value = await operation(); if (value instanceof Uint8Array) return { format: 1, bytes: value }; return { format: 0, bytes: ENC.encode(JSON.stringify(value ?? null)) }; } catch (error) { return { format: 2, bytes: ENC.encode(JSON.stringify({ message: failureText(error), code: classify(error) })), }; } } function decode(format, bytes) { if (format === 1) return bytes; const value = JSON.parse(DEC.decode(bytes)); if (format === 2) { const error = value; throw Object.assign(new Error(error.message), { code: error.code }); } return value === null ? undefined : value; } /** Worker-only blocking half; the host always answers asynchronously. */ export function requestSync(post, request, interrupt) { const buffer = new SharedArrayBuffer(HEADER_BYTES + CAPACITY); const cells = new Int32Array(buffer, 0, 4); const chunk = new Uint8Array(buffer, HEADER_BYTES); post({ ...request, kind: 'vfs', buffer }); let output; let offset = 0; for (;;) { while (Atomics.load(cells, 0) === 0) { if (interrupt !== undefined && (Atomics.load(interrupt, 0) === 2 || Atomics.load(interrupt, 1) !== 0)) { Atomics.store(cells, 0, -1); Atomics.notify(cells, 0); throw new Error("the guest worker's file request was interrupted"); } Atomics.wait(cells, 0, 0, 100); } if (Atomics.load(cells, 0) === -1) throw new Error('the guest worker closed'); output ??= new Uint8Array(Atomics.load(cells, 1)); const length = Atomics.load(cells, 2); const format = Atomics.load(cells, 3); output.set(chunk.subarray(0, length), offset); offset += length; Atomics.store(cells, 0, 0); Atomics.notify(cells, 0); if (offset < output.length) continue; return decode(format, output); } } /** Stream arbitrary-size responses through a bounded shared buffer. */ export async function respond(buffer, operation) { const cells = new Int32Array(buffer, 0, 4); const chunk = new Uint8Array(buffer, HEADER_BYTES); const { format, bytes } = await encode(operation); let offset = 0; do { if (Atomics.load(cells, 0) === -1) return; const length = Math.min(CAPACITY, bytes.length - offset); chunk.set(bytes.subarray(offset, offset + length)); Atomics.store(cells, 1, bytes.length); Atomics.store(cells, 2, length); Atomics.store(cells, 3, format); Atomics.store(cells, 0, 1); Atomics.notify(cells, 0); offset += length; while (Atomics.load(cells, 0) === 1) { // The host must never Atomics.wait: it owns the backend event loop. await new Promise((resolve) => setTimeout(resolve, 0)); } } while (offset < bytes.length); } /** * Worker-only blocking half where no memory is shared: the request goes * to the host as a message, and the worker waits on a synchronous request * that the page's service worker holds until the host's answer arrives. * * Args: * post: sends the request to the host. * request: the call to make. * bridge: the page URL the service worker answers bridge requests at. */ export function requestOverBridge(post, request, bridge) { const id = crypto.randomUUID(); post({ ...request, kind: 'vfs', id }); const xhr = new XMLHttpRequest(); xhr.open('GET', `${bridge}?${PARAM}=wait&id=${id}`, false); xhr.responseType = 'arraybuffer'; xhr.send(); const format = xhr.getResponseHeader(FORMAT); if (format === null) throw new Error(`the guest worker's file bridge answered ${String(xhr.status)}`); return decode(Number(format), new Uint8Array(xhr.response)); } /** * Answer one bridged request: the service worker hands the answer to the * worker's request waiting under `id`. * * Args: * bridge: the page URL the service worker answers bridge requests at. * id: the request being answered. * operation: computes the answer. */ export async function answerOverBridge(bridge, id, operation) { const { format, bytes } = await encode(operation); const response = await fetch(`${bridge}?${PARAM}=answer&id=${id}`, { method: 'POST', body: bytes, headers: { [FORMAT]: String(format) }, }); if (!response.headers.has(FORMAT)) throw new Error(`the guest worker's file bridge answered ${String(response.status)}`); } /** * Whether the service worker controlling the page answers bridge requests. * * Args: * bridge: the page URL the service worker answers bridge requests at. */ export async function bridged(bridge) { const response = await fetch(`${bridge}?${PARAM}=ping`); return response.headers.has(FORMAT); } /** * The page URL its service worker bridges file calls at, or null when no * service worker controlling the page answers the bridge. */ export async function serviceWorkerBridge() { const host = globalThis; if (host.navigator?.serviceWorker?.controller == null || host.location === undefined) return null; const bridge = host.location.origin + host.location.pathname; return (await bridged(bridge)) ? bridge : null; } //# sourceMappingURL=transport.js.map