UNPKG

@struktoai/mirage-core

Version:

A Unified Virtual Filesystem For AI Agents

181 lines • 7.33 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 { activeDevice, activePipe } from "../dev/device.js"; import { chunks } from "../../io/cooperative.js"; import { materialize } from "../../io/types.js"; import { concat } from "../../utils/bytes.js"; import { STREAM_PUBLISH_SECONDS } from "./constants.js"; import { record, startOp } from "../../observe/context.js"; import { VFSName } from "../../types.js"; import { nowIso } from "../../utils/dates.js"; import { norm } from "../../utils/path.js"; import { checkCapacity, checkDestParents, checkRoom, checkWriteTarget } from "./dest.js"; /** * Replace a file's bytes, or add them to its end. Bytes land at once, as does * a stream under `atomic`, staged until it ends. Any other stream lands as it * arrives, so a reader watches the file grow: what has arrived lands within * `STREAM_PUBLISH_SECONDS`, a writer gone quiet included, and at the end, a * replacement at its own position, as an open file's offset keeps it, an * append at the file's end then, so another appender's bytes stay. Appending * nothing opens the file (`true >> f`): it creates a missing one and leaves an * existing one as it was, modification time included. A device that discards * writes (`/dev/null`) has its bytes counted as they arrive and kept nowhere. * Mirrors Python's `write`. */ export async function write(accessor, path, data, append = false, signal, atomic = false) { const timer = startOp(); const p = norm(path.mountPath); const op = append ? 'append' : 'write'; let size = 0; const pipe = activePipe(accessor, p); if (pipe !== null) { if (data instanceof Uint8Array) { await pipe.write(data, path, signal); size = data.byteLength; } else { for await (const chunk of chunks(data)) { await pipe.write(chunk, path, signal); size += chunk.byteLength; } } record(op, path.virtual, VFSName.RAM, size, timer); return; } checkDestParents(accessor, path, p); checkWriteTarget(accessor, path, p); const { files, modified } = accessor.store; if (activeDevice(accessor, p) !== null) { if (data instanceof Uint8Array) size = data.byteLength; else for await (const chunk of chunks(data)) size += chunk.byteLength; if (size > 0 || !append) modified.set(p, nowIso()); record(op, path.virtual, VFSName.RAM, size, timer); return; } if (data instanceof Uint8Array || atomic) { const whole = data instanceof Uint8Array ? data : await materialize(data); const current = append ? files.get(p) : undefined; if (current !== undefined && whole.byteLength === 0) return; checkCapacity(accessor, path, p, (current?.byteLength ?? 0) + whole.byteLength); files.set(p, current === undefined ? whole : concat([current, whole])); modified.set(p, nowIso()); record(op, path.virtual, VFSName.RAM, whole.byteLength, timer); return; } if (!append || !files.has(p)) { files.set(p, append ? (files.get(p) ?? new Uint8Array()) : new Uint8Array()); modified.set(p, nowIso()); } let pending = []; let position = 0; let last = performance.now(); let later = null; const usage = accessor.store.usage; let held = 0; let queued = 0; // What this write last published: a view of a buffer it grows, so a // publish copies only the bytes that arrived since. A file another writer // changed in between is copied instead, so no view a reader holds changes. const grown = { buffer: new Uint8Array(), view: null, }; const publish = () => { if (later !== null) clearTimeout(later); later = null; last = performance.now(); if (pending.length === 0) return; try { const current = files.get(p) ?? new Uint8Array(); const start = append ? current.byteLength : position; const end = pending.reduce((at, piece) => at + piece.byteLength, start); const length = Math.max(current.byteLength, end); checkRoom(path, length - current.byteLength); const grows = current === grown.view && start === current.byteLength; if (!grows || length > grown.buffer.byteLength) { const size = grows ? Math.max(length, grown.buffer.byteLength * 2) : length; grown.buffer = new Uint8Array(size); grown.buffer.set(current); } let at = start; for (const piece of pending) { grown.buffer.set(piece, at); at += piece.byteLength; } grown.view = grown.buffer.subarray(0, length); files.set(p, grown.view); if (!append) position = end; modified.set(p, nowIso()); } finally { pending = []; queued = 0; usage?.land(held, 0); held = 0; } }; const failed = {}; // A timer callback cannot throw to the write; the write throws it. const publishLater = () => { try { publish(); } catch (err) { failed.error = err; } }; try { for await (const chunk of chunks(data)) { if (failed.error !== undefined) throw failed.error; checkRoom(path, queued + chunk.byteLength); if (usage !== null) { // The whole chunk: bytes another writer put in its way count. usage.hold(path, chunk.byteLength); held += chunk.byteLength; } pending.push(chunk); queued += chunk.byteLength; size += chunk.byteLength; const wait = STREAM_PUBLISH_SECONDS * 1000 - (performance.now() - last); if (wait <= 0) publish(); else later ??= setTimeout(publishLater, wait); } } finally { try { publish(); } finally { // A refused last publish still lets the spare buffer go. const { buffer, view } = grown; if (view !== null && files.get(p) === view && view.byteLength < buffer.byteLength) files.set(p, view.slice()); } } if (failed.error !== undefined) throw failed.error; record(op, path.virtual, VFSName.RAM, size, timer); } //# sourceMappingURL=write.js.map