@struktoai/mirage-core
Version:
A Unified Virtual Filesystem For AI Agents
100 lines • 4.08 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 { BytePipe } from "../../io/pipe.js";
import { ebadf } from "../../errors/fs.js";
import { abortable, makeAbortError } from "../../utils/abort.js";
import { concat } from "../../utils/bytes.js";
/**
* One directional virtual descriptor, with bounded producer backpressure.
*
* The read end belongs to the scope that opened it, which closes it, and the
* reader streams still open (`readers`, held weakly), when its command ends;
* no reader closes it, so a function can read one substitution twice. Readers
* take turns a chunk at a time, so they split the stream, and a ranged read
* leaves the rest of its chunk to the next. A reader whose signal fires while
* it waits leaves that chunk to the next reader. Mirrors Python's DevicePipe.
*/
export class DevicePipe {
readable;
pipe = new BytePipe();
readers = new Set();
source;
pull;
pending = new Uint8Array();
turn = Promise.resolve();
constructor(readable) {
this.readable = readable;
}
async write(data, path, signal) {
if (this.readable)
throw ebadf(path);
await this.pipe.write(data, signal);
}
async read(path, size = null, signal) {
if (!this.readable)
throw ebadf(path);
const chunks = [];
let remaining = size;
while (remaining === null || remaining > 0) {
const chunk = await this.take(remaining, signal);
if (chunk.byteLength === 0)
break;
chunks.push(chunk);
if (remaining !== null)
remaining -= chunk.byteLength;
}
return concat(chunks);
}
/** `signal` is the reader's own: once it fires, the reader takes nothing more. */
async *stream(path, signal) {
if (!this.readable)
throw ebadf(path);
for (;;) {
const chunk = await this.take(null, signal);
if (chunk.byteLength === 0)
return;
yield chunk;
}
}
/** One reader's turn: the next bytes of the stream, at most `size`, empty at its end. */
take(size, signal) {
const taken = this.turn.then(() => this.next(size, signal));
this.turn = taken.then(() => undefined, () => undefined);
return taken;
}
async next(size, signal) {
if (signal?.aborted === true)
throw makeAbortError(signal);
if (this.pending.byteLength === 0) {
this.source ??= this.pipe.stream()[Symbol.asyncIterator]();
// A pull outlives a reader that gives up while it waits: what it
// brings is left for the next reader, as Python's shielded pull is.
const pull = (this.pull ??= this.source.next());
const step = await abortable(pull, signal).catch((error) => {
if (signal?.aborted !== true)
this.pull = undefined;
throw error;
});
this.pull = undefined;
if (step.done === true)
return new Uint8Array();
this.pending = step.value;
}
const count = size === null ? this.pending.byteLength : Math.min(size, this.pending.byteLength);
const taken = this.pending.subarray(0, count);
this.pending = this.pending.subarray(count);
return taken;
}
}
//# sourceMappingURL=pipe.js.map