@struktoai/mirage-core
Version:
A Unified Virtual Filesystem For AI Agents
161 lines • 6 kB
JavaScript
import { makeAbortError } from "../utils/abort.js";
import { concat } from "../utils/bytes.js";
import { CHUNK_SIZE } from "./cooperative.js";
import { PipeClosed } from "./errors.js";
export const CAPACITY = 4 * CHUNK_SIZE;
/**
* Buffers their producer never writes again, such as mirage-rs output copied
* out of linear memory. A pipe keeps a chunk of one up to twice its capacity
* as written, so a reader holding a few bytes never pins a much larger
* buffer; it copies a chunk of any other buffer, in case the writer reuses it.
*/
export const SEALED = new WeakSet();
/** One reader and bounded asynchronous writes. Closing either endpoint wakes writers. */
export class BytePipe {
capacity;
constructor(capacity = CAPACITY) {
this.capacity = capacity;
if (!Number.isSafeInteger(capacity) || capacity < CHUNK_SIZE)
throw new Error('pipe capacity must be an integer from 16384 to 9007199254740991');
}
chunks = [];
bytes = 0;
ended = false;
readerClosed = false;
reading = false;
delivered = 0;
accepted = 0;
failure;
wake = [];
get bufferedBytes() {
return this.bytes;
}
get closedReader() {
return this.readerClosed;
}
notify() {
for (const resolve of this.wake.splice(0))
resolve();
}
changed(signal) {
signal?.throwIfAborted();
return new Promise((resolve, reject) => {
const changed = () => {
signal?.removeEventListener('abort', aborted);
resolve();
};
const aborted = () => {
this.wake = this.wake.filter((wake) => wake !== changed);
signal?.removeEventListener('abort', aborted);
reject(makeAbortError(signal));
};
this.wake.push(changed);
signal?.addEventListener('abort', aborted, { once: true });
});
}
/** Accept bytes in bounded chunks, waiting for available capacity. */
async write(data, signal) {
for (let start = 0; start < data.byteLength; start += CHUNK_SIZE) {
signal?.throwIfAborted();
const length = Math.min(CHUNK_SIZE, data.byteLength - start);
while (this.bytes + length > this.capacity && !this.readerClosed && !this.ended)
await this.changed(signal);
if (signal?.aborted === true)
throw makeAbortError(signal);
if (this.readerClosed || this.ended)
throw new PipeClosed();
const piece = data.subarray(start, start + length);
const lent = SEALED.has(data.buffer) && data.buffer.byteLength <= 2 * this.capacity;
const chunk = lent ? piece : new Uint8Array(piece);
this.chunks.push(chunk);
this.bytes += length;
this.delivered += 1;
this.notify();
}
}
merged(taken) {
const [first, ...rest] = taken;
if (first === undefined)
return new Uint8Array();
if (rest.length === 0)
return first;
let end = first.byteOffset + first.byteLength;
for (const chunk of rest) {
if (chunk.buffer !== first.buffer || chunk.byteOffset !== end)
return concat(taken);
end += chunk.byteLength;
}
return new Uint8Array(first.buffer, first.byteOffset, end - first.byteOffset);
}
/** Wait until the reader consumes accepted writes or closes. */
async drain() {
while (this.accepted < this.delivered && !this.readerClosed)
await this.changed();
}
/** Close the writer, preserving buffered bytes and a late failure. */
end(error) {
if (this.ended)
return;
if (error !== undefined)
this.failure =
error instanceof Error ? error : new Error('Pipeline producer failed', { cause: error });
this.ended = true;
this.notify();
}
closeReader() {
this.readerClosed = true;
this.chunks = [];
this.bytes = 0;
this.notify();
}
/** Match a kernel pipe: an unblocked writer may finish its current burst. */
release() {
setTimeout(() => {
this.closeReader();
}, 0);
}
/** Wait until the reader first asks for bytes or closes. */
async asked() {
while (!this.reading && !this.readerClosed)
await this.changed();
}
/**
* Read once; abandoning the iterator closes its endpoint. Each read is one
* chunk as written, unless `joined`: then it takes everything buffered, as
* a kernel pipe's read does, for a reader that keeps no write boundaries,
* which behind a writer that filled the pipe wakes once for the lot rather
* than once per chunk. Chunks side by side in one buffer (a sealed write)
* pass as one view of it; others are copied together.
*/
async *stream(joined = false) {
if (this.reading)
throw new Error('byte pipe already has a reader');
this.reading = true;
this.notify();
try {
while (!this.readerClosed) {
const taken = joined ? this.chunks.splice(0) : this.chunks.splice(0, 1);
if (taken.length > 0) {
for (const chunk of taken)
this.bytes -= chunk.byteLength;
this.notify();
yield this.merged(taken);
this.accepted += taken.length;
this.notify();
}
else if (this.ended) {
if (this.failure !== undefined)
throw this.failure;
return;
}
else {
await this.changed();
}
}
}
finally {
this.release();
}
}
}
//# sourceMappingURL=pipe.js.map