@struktoai/mirage-core
Version:
A Unified Virtual Filesystem For AI Agents
462 lines • 18.7 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. =========
import MIRAGE_RS_WASM from "../generated/mirage_rs.js";
import { chunks } from "../io/cooperative.js";
import { SEALED } from "../io/pipe.js";
import { YieldBudget } from "../io/yield_budget.js";
import { concat } from "../utils/bytes.js";
import { decodeBase64 } from "../utils/base64.js";
import { StreamRefusal } from "./errors.js";
// Linear memory only grows, so an instance a large stream (a big sort) grew
// is dropped once no stream is open and the next stream gets a fresh one.
// Movable streams (the matchers a cache keeps) do not hold it: they close
// with it, and their owners open them again on the next instance. One in the
// middle of a search (busy) does hold it, so the search keeps its progress.
const RECYCLE_BYTES = 32 * 1024 * 1024;
const encoder = new TextEncoder();
const decoder = new TextDecoder();
let compiled;
let compiledSync;
let current;
let synchronous;
const movables = new WeakMap();
// The next stream gets a fresh instance; the movable streams still on this one
// close, and `live` says so to their owners.
function retire(exports, trapped) {
current = undefined;
if (synchronous === exports)
synchronous = undefined;
for (const stream of movables.get(exports) ?? [])
stream.drop(!trapped);
movables.delete(exports);
}
/** The compiled module, for the page to hand the workers it starts without a copy. */
export function compiledModule() {
compiled ??= WebAssembly.compile(decodeBase64(MIRAGE_RS_WASM));
return compiled;
}
/** The compiled module, compiled now if no one has yet, for a caller that cannot wait. */
export function moduleSync() {
compiledSync ??= new WebAssembly.Module(decodeBase64(MIRAGE_RS_WASM));
compiled ??= Promise.resolve(compiledSync);
return compiledSync;
}
/** Run on the module the page compiled: a browser worker's bundle leaves the bytes out. */
export function adoptModule(module) {
compiledSync ??= module;
compiled ??= Promise.resolve(module);
}
async function instance() {
if (synchronous !== undefined)
return synchronous;
current ??= compiledModule().then((module) => {
compiledSync = module;
synchronous ??= new WebAssembly.Instance(module, {}).exports;
return synchronous;
});
return current;
}
function bytesAt(exports, ptr, len) {
const bytes = new Uint8Array(exports.memory.buffer, ptr, len).slice();
SEALED.add(bytes.buffer);
return bytes;
}
/** One mirage-rs stream: chunks go in, the bytes they produce come out. Mirrors Python's OpenStream. */
export class OpenStream {
movable;
exports;
handle;
capacity;
constructor(exports, handle, movable = false) {
this.movable = movable;
this.exports = exports;
this.handle = handle;
this.capacity = exports.mirage_rs_input_capacity();
if (movable) {
const held = movables.get(exports) ?? new Set();
held.add(this);
movables.set(exports, held);
}
}
/** Whether the stream is still open; a movable one closes when its instance retires. */
get live() {
return this.handle !== 0;
}
/** Push one chunk and return what it produced. */
feed(chunk) {
return this.guard(() => this.push(chunk, (len) => this.exports.mirage_rs_feed(this.handle, len)));
}
/** End the input and return what the stream held back. */
finish() {
return this.guard(() => this.collect(this.exports.mirage_rs_finish(this.handle), []));
}
/** Whether `drain` has work or output left. */
busy() {
return this.guard(() => this.exports.mirage_rs_busy(this.handle) === 1);
}
/** Stop processing and report whether bounded retirement is available. */
cancel() {
return this.guard(() => this.exports.mirage_rs_cancel(this.handle) === 1);
}
/** Bounded steps of the work `busy` reports, up to a piece of output, and that output. */
drain() {
return this.guard(() => this.collect(this.exports.mirage_rs_drain(this.handle), []));
}
/** The input a multi-input stream needs next, null for none. */
wants() {
const index = this.guard(() => this.exports.mirage_rs_wants(this.handle));
return index < 0 ? null : index;
}
/** Push one chunk of input `index` to a multi-input stream. */
feedInput(index, chunk) {
return this.guard(() => this.push(chunk, (len) => this.exports.mirage_rs_feed_input(this.handle, index, len)));
}
/** End input `index` of a multi-input stream. */
endInput(index) {
return this.guard(() => this.collect(this.exports.mirage_rs_end_input(this.handle, index), []));
}
/** Start the next input of a stream that reads one after another. */
begin(config) {
const bytes = encoder.encode(JSON.stringify(config));
if (bytes.byteLength > this.capacity)
throw new StreamRefusal('stream input configuration too large', new Uint8Array());
this.guard(() => {
const window = this.exports.mirage_rs_input(this.handle);
new Uint8Array(this.exports.memory.buffer, window, bytes.byteLength).set(bytes);
this.collect(this.exports.mirage_rs_begin(this.handle, bytes.byteLength), []);
});
}
/** Whether the input fed since the last answer, then `chunk`, passes the stream's test, in one call. */
test(chunk) {
return this.guard(() => {
const cut = Math.max(0, chunk.byteLength - this.capacity);
if (cut > 0)
this.push(chunk.subarray(0, cut), (len) => this.exports.mirage_rs_feed(this.handle, len));
const tail = chunk.subarray(cut);
const window = this.exports.mirage_rs_input(this.handle);
new Uint8Array(this.exports.memory.buffer, window, tail.byteLength).set(tail);
const answer = this.exports.mirage_rs_test(this.handle, tail.byteLength);
if (answer < 0)
this.collect(1, []);
return answer === 1;
});
}
/** Whether the stream wants no more of the current input. */
done() {
return this.guard(() => this.exports.mirage_rs_done(this.handle) === 1);
}
/** The command's exit status for the input so far. */
status() {
return this.guard(() => this.exports.mirage_rs_status(this.handle));
}
/** The stream's current progress as its typed JSON record. */
metadata() {
return this.guard(() => this.collect(this.exports.mirage_rs_metadata(this.handle), []));
}
/** What the stream wrote to stderr since the last call. */
takeStderr() {
return this.guard(() => this.collect(this.exports.mirage_rs_take_stderr(this.handle), []));
}
/** The host operation the stream waits on and the bytes it carries, null while it waits on none. */
request() {
return this.guard(() => {
const len = this.exports.mirage_rs_request(this.handle);
if (len < 0)
return null;
const taken = this.collect(0, []);
const header = JSON.parse(decoder.decode(taken.subarray(0, len)));
return [header, taken.subarray(len)];
});
}
/** Answer the request taken last with the host's reply and the bytes it carries. */
respond(reply, data) {
const header = encoder.encode(JSON.stringify(reply));
return this.guard(() => {
for (const part of [header, ...(data instanceof Uint8Array ? [data] : data)]) {
for (let offset = 0; offset < part.byteLength; offset += this.capacity) {
const piece = part.subarray(offset, offset + this.capacity);
const window = this.exports.mirage_rs_input(this.handle);
new Uint8Array(this.exports.memory.buffer, window, piece.byteLength).set(piece);
this.exports.mirage_rs_stage(this.handle, piece.byteLength);
}
}
return this.collect(this.exports.mirage_rs_respond(this.handle, header.byteLength), []);
});
}
close() {
if (this.handle === 0)
return;
this.drop(true);
const held = movables.get(this.exports);
if (this.exports.mirage_rs_open_count() === (held?.size ?? 0) &&
this.exports.memory.buffer.byteLength > RECYCLE_BYTES &&
![...(held ?? [])].some((stream) => stream.busy()))
retire(this.exports, false);
}
/** Close the handle, on the instance itself unless it trapped. */
drop(release) {
if (this.handle === 0)
return;
if (release)
this.exports.mirage_rs_close(this.handle);
this.handle = 0;
movables.get(this.exports)?.delete(this);
}
// A trap (a Rust panic) stops the instance mid-call with its state still
// borrowed, so every later call on it would trap too: this stream is
// done, and the next stream gets a fresh instance.
guard(call) {
try {
return call();
}
catch (err) {
if (err instanceof WebAssembly.RuntimeError) {
this.drop(false);
retire(this.exports, true);
}
throw err;
}
}
push(chunk, feed) {
// An empty chunk still reaches the stream: a sort starts a buffered run.
if (chunk.byteLength === 0)
return this.collect(feed(0), []);
const parts = [];
for (let offset = 0; offset < chunk.byteLength; offset += this.capacity) {
const piece = chunk.subarray(offset, offset + this.capacity);
const window = this.exports.mirage_rs_input(this.handle);
new Uint8Array(this.exports.memory.buffer, window, piece.byteLength).set(piece);
parts.push(this.collect(feed(piece.byteLength), parts));
}
const [only] = parts;
return parts.length === 1 && only !== undefined ? only : concat(parts);
}
collect(status, before) {
const { exports, handle } = this;
const output = bytesAt(exports, exports.mirage_rs_output(handle), exports.mirage_rs_output_len(handle));
if (status === 0)
return output;
const message = decoder.decode(bytesAt(exports, exports.mirage_rs_error(handle), exports.mirage_rs_error_len(handle)));
throw new StreamRefusal(message, concat([...before, output]), exports.mirage_rs_error_status(handle));
}
}
/**
* Run a stream until it waits on neither work nor its host, a bounded call
* at a time, serving each request it makes. Mirrors Python's serve_requests.
*/
export async function* serveRequests(stream, host, signal) {
const budget = new YieldBudget(signal);
for (;;) {
while (stream.busy()) {
await budget.run();
const output = stream.drain();
await host.errors.append(stream.takeStderr());
if (output.byteLength > 0)
yield output;
}
const taken = stream.request();
if (taken === null)
break;
await budget.run();
const served = await host.serve(...taken);
if (served.output !== undefined && served.output !== null)
yield* chunks(served.output, signal);
const output = stream.respond(served.reply, served.data ?? new Uint8Array());
// Each diagnostic is collected before the output that follows it, so a
// reader that closes on that output still leaves it reported.
await host.errors.append(stream.takeStderr());
if (output.byteLength > 0)
yield output;
}
const tail = stream.finish();
await host.errors.append(stream.takeStderr());
if (tail.byteLength > 0)
yield tail;
}
/** Open the mirage-rs stream `kind`. Mirrors Python's open_stream. */
export async function openStream(kind, config) {
const exports = await instance();
return open(exports, kind, config);
}
/**
* Open a stream for synchronous pure utilities, initializing Wasm on first use.
* A `movable` stream does not keep a grown instance alive: it closes when the
* instance retires, and its owner opens it again.
*/
export function openStreamSync(kind, config, movable = false) {
synchronous ??= new WebAssembly.Instance(moduleSync(), {}).exports;
return open(synchronous, kind, config, movable);
}
const movableFinalizer = new FinalizationRegistry((stream) => {
stream.close();
});
/**
* A movable synchronous stream for an object a cache keeps, such as a compiled
* matcher: it opens again on the live instance once the one it was on retired,
* and closes when its owner is collected. A retired stream is unregistered, so
* it never holds its instance's memory.
*/
export class MovableStream {
kind;
config;
stream;
constructor(kind, config) {
this.kind = kind;
this.config = config;
this.stream = this.open();
}
/** Close the stream now rather than when its owner is collected. */
close() {
movableFinalizer.unregister(this);
this.stream.close();
}
/** The stream, opened again first when its instance retired. */
get live() {
if (!this.stream.live) {
movableFinalizer.unregister(this);
this.stream = this.open();
}
return this.stream;
}
open() {
const stream = openStreamSync(this.kind, this.config, true);
movableFinalizer.register(this, stream, this);
return stream;
}
}
function open(exports, kind, config, movable = false) {
const kindBytes = encoder.encode(kind);
const configBytes = encoder.encode(JSON.stringify(config));
const args = exports.mirage_rs_args(kindBytes.byteLength + configBytes.byteLength);
const view = new Uint8Array(exports.memory.buffer, args, kindBytes.byteLength + configBytes.byteLength);
view.set(kindBytes);
view.set(configBytes, kindBytes.byteLength);
const handle = exports.mirage_rs_open(kindBytes.byteLength, configBytes.byteLength);
if (handle === 0) {
const message = decoder.decode(bytesAt(exports, exports.mirage_rs_error(0), exports.mirage_rs_error_len(0)));
throw new StreamRefusal(message, new Uint8Array(), exports.mirage_rs_error_status(0));
}
return new OpenStream(exports, handle, movable);
}
/**
* Stream `source` through `kind`, one bounded chunk at a time. A refusal
* throws StreamRefusal holding the output written before it, so the caller
* records the failure before it passes that output on: a reader that stops
* at the output would otherwise never see it. Mirrors Python's run_stream.
*/
export async function* runStream(kind, config, source, signal) {
const stream = await openStream(kind, config);
yield* streamOutputChunks(stream, source, signal);
}
/**
* Feed a multi-input stream the input its `wants` names until it wants none,
* handing on its output a bounded call at a time. Mirrors Python's
* merge_output.
*/
export async function* mergeOutput(stream, read, signal) {
const budget = new YieldBudget(signal);
for (;;) {
while (stream.busy()) {
await budget.run();
const output = stream.drain();
if (output.byteLength > 0)
yield output;
}
const index = stream.wants();
if (index === null)
break;
await budget.run();
const chunk = await read(index);
const output = chunk === null ? stream.endInput(index) : stream.feedInput(index, chunk);
if (output.byteLength > 0)
yield output;
}
const tail = stream.finish();
if (tail.byteLength > 0)
yield tail;
while (stream.busy()) {
await budget.run();
const output = stream.drain();
if (output.byteLength > 0)
yield output;
}
}
/**
* Feed a stream that reads its inputs one after another each source in turn,
* through mergeOutput, and close the sources. Mirrors Python's chain_output.
*/
export async function* chainOutput(stream, sources, signal) {
const inputs = sources.map((source) => chunks(source, signal));
try {
yield* mergeOutput(stream, async (index) => {
const next = await inputs[index]?.next();
return next === undefined || next.done === true ? null : next.value;
}, signal);
}
finally {
for (const [index, input] of inputs.entries()) {
await input.return?.(undefined);
await sources[index].return?.();
}
}
}
export async function* streamOutputChunks(stream, source, signal, ownsStream = true) {
const budget = new YieldBudget(signal);
try {
for await (const chunk of chunks(source, signal)) {
const output = stream.feed(chunk);
if (output.byteLength > 0)
yield output;
while (stream.busy()) {
await budget.run();
const output = stream.drain();
if (output.byteLength > 0)
yield output;
}
if (stream.done())
break;
}
const tail = stream.finish();
if (tail.byteLength > 0)
yield tail;
while (stream.busy()) {
await budget.run();
const output = stream.drain();
if (output.byteLength > 0)
yield output;
}
}
finally {
if (ownsStream)
await closeStream(stream);
}
}
/** Retire cancelled records through bounded calls before releasing the handle. */
export async function closeStream(stream) {
if (!stream.live)
return;
try {
if (stream.cancel()) {
const budget = new YieldBudget();
while (stream.busy()) {
await budget.run();
stream.drain();
}
}
}
finally {
stream.close();
}
}
//# sourceMappingURL=stream.js.map