@struktoai/mirage-core
Version:
A Unified Virtual Filesystem For AI Agents
277 lines • 9.03 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 { 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