UNPKG

@struktoai/mirage-core

Version:

A Unified Virtual Filesystem For AI Agents

277 lines • 9.03 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 { KeyLock } from "../cache/lock.js"; import { concat } from "../utils/bytes.js"; import { SharedInput } from "./async_line_iterator.js"; /** * One lazy byte cursor shared by commands inheriting an input descriptor. * Serializing reads prevents concurrent consumers from replaying bytes or * pulling the source simultaneously. A consumer stopping early leaves the * cursor open; byte-sized pulls preserve the unread suffix for its successor. */ export class SharedStdin { chunks; buffer = new Uint8Array(); pos = 0; lock = new KeyLock(); constructor(source) { this.chunks = ensureStream(source)[Symbol.asyncIterator](); } [Symbol.asyncIterator]() { return { next: () => this.lock.withLock('read', async () => { while (this.pos >= this.buffer.byteLength) { if (this.chunks === null) return { done: true, value: undefined }; const step = await this.chunks.next(); if (step.done === true) { this.chunks = null; return { done: true, value: undefined }; } this.buffer = step.value; this.pos = 0; } const chunk = this.buffer.subarray(this.pos, this.pos + 1); this.pos += 1; return { done: false, value: chunk }; }), }; } } export async function drain(stream) { if (stream === null || stream instanceof Uint8Array) return; for await (const _chunk of stream) { void _chunk; } } export async function closeQuietly(stream) { if (stream === null || stream instanceof Uint8Array) return; const closer = stream.return; if (typeof closer !== 'function') return; try { await closer.call(stream); } catch (err) { console.debug('stream closer failed:', err); } } /** Own producer cleanup even when its byte iterator was never started. */ export class OutputStream { source; close; closing = null; constructor(source, close) { this.source = source; this.close = close; } [Symbol.asyncIterator]() { return this; } async next() { if (this.closing !== null) return { done: true, value: undefined }; try { const next = await this.source.next(); if (next.done === true) await this.return(); return next; } catch (error) { await this.return(); throw error; } } return() { this.closing ??= this.finish(); return this.closing; } async finish() { await this.close(); return (await this.source.return?.()) ?? { done: true, value: undefined }; } } /** Discard failed reads without changing normal early-consumer close semantics. */ export async function discardStreams(...streams) { for (const stream of new Set(streams)) { if (stream instanceof SharedInput) { await stream.discard(); } else { await closeQuietly(stream); } } } export async function discardIo(io) { await discardStreams(io.stdout, io.stderr); } export function asyncChain(streams) { const sources = [...streams]; async function* output() { for (const source of sources) { if (source !== null) yield* ensureStream(source); } } return new OutputStream(output(), async () => { for (const source of sources) await closeQuietly(source); }); } /** * Write a program's input as it is read, then end it: a finite command * waits for that end. Writing stops once `stopped` says the program has * gone, and a program that closed its input (`closed` says which errors * mean that) has taken what it wanted, so neither fails the run; an input * that fails as it is read does. Mirrors Python's `feed`. */ export async function feed(stdin, write, end, stopped, closed = () => false) { try { for await (const chunk of asyncChain([stdin])) { if (stopped()) return; if (!(await delivered(write(chunk), closed))) return; } if (!stopped()) await delivered(end(), closed); } catch (error) { if (!stopped()) throw error; console.debug('a program ended before consuming its input:', error); } } /** * Whether one step of a program's input landed; false when the program had * closed its input. Mirrors Python's `_delivered`. */ async function delivered(step, closed) { try { await step; return true; } catch (error) { if (!closed(error)) throw error; console.debug('a program closed its input:', error); return false; } } export async function* yieldBytes(data) { await Promise.resolve(); yield data; } /** * Present a byte source as a stream. An iterable is returned as itself, so * closing the consumer closes its source; bytes become a one-chunk stream. */ export function ensureStream(src) { return src instanceof Uint8Array ? yieldBytes(src) : src; } /** Open a reader before creating its destination; retain ownership before the first pull. */ export async function primeStream(source, failed) { const cursor = source[Symbol.asyncIterator](); let first; try { first = await cursor.next(); } catch (error) { await cursor.return?.(); throw error; } async function* rest() { try { if (first.done !== true) yield first.value; for (;;) { const step = await cursor.next(); if (step.done === true) return; yield step.value; } } catch (error) { failed?.(error); throw error; } finally { await cursor.return?.(); } } return new OutputStream(rest(), async () => { await cursor.return?.(); }); } /** Take a byte window without materializing its prefix or suffix. */ export function sliceStream(source, offset = 0, size = null) { if (offset < 0 || (size !== null && size < 0)) throw new RangeError('stream window must be nonnegative'); async function* window() { let skipped = offset; let remaining = size; if (remaining === 0) return; for await (let part of ensureStream(source)) { const cut = Math.min(skipped, part.length); skipped -= cut; part = part.subarray(cut); if (remaining !== null) { part = part.subarray(0, remaining); remaining -= part.length; } if (part.length > 0) yield part; if (remaining === 0) return; } } return new OutputStream(window(), () => closeQuietly(source)); } /** * Re-cut a stream into parts of up to `size` bytes, joining short ones, so a * consumer pays once per block rather than once per record. Mirrors Python's * rechunk. */ export function rechunk(source, size) { async function* parts() { let pending = []; let held = 0; for await (let part of ensureStream(source)) { if (held > 0) { const fill = part.subarray(0, size - held); pending.push(fill); held += fill.byteLength; part = part.subarray(fill.byteLength); if (held < size) continue; yield concat(pending); pending = []; held = 0; } for (; part.byteLength >= size; part = part.subarray(size)) yield part.subarray(0, size); if (part.byteLength > 0) { pending = [part]; held = part.byteLength; } } if (held > 0) yield concat(pending); } return new OutputStream(parts(), () => closeQuietly(source)); } //# sourceMappingURL=stream.js.map