UNPKG

@struktoai/mirage-core

Version:

A Unified Virtual Filesystem For AI Agents

150 lines • 5.94 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 { END_OF_CONSOLE } from "./constants.js"; import { Channel } from "./types.js"; /** * In-memory console storage, the default in every topology. * * Simpler than its Python counterpart for one reason: JavaScript runs * this on a single event loop, so a waiter is just a pending promise * resolver. Python has to park futures per loop and wake them through * call_soon_threadsafe, because its callers arrive on pool threads with * their own loops. * * The ending chunk is terminal here as it is in Redis: an append after a * CONTROL chunk is dropped, so a runner that outlived an ending written * through another handle on this store (a kill from another workspace * sharing it) cannot append past it. */ export class RAMConsoleStore { chunks; baseSeq; nextSeq; bytes; ended; maxBytes; waiters = []; isClosed = false; get closed() { return this.isClosed; } /** * @param maxBytes retention budget. When set, the oldest chunks are * dropped once the total exceeds it, and a reader whose cursor was * dropped is told rather than shorted. * @param chunks pre-existing chunks, used to rebuild a finished job's * console from a snapshot. */ constructor(maxBytes = null, chunks = null) { this.chunks = chunks === null ? [] : [...chunks]; const first = this.chunks[0]; const last = this.chunks[this.chunks.length - 1]; this.baseSeq = first === undefined ? 0 : first.seq; this.nextSeq = last === undefined ? 0 : last.seq + 1; this.bytes = this.chunks.reduce((sum, c) => sum + c.data.byteLength, 0); this.ended = this.chunks.some((c) => c.channel === Channel.CONTROL); this.maxBytes = maxBytes; } append(channel, data) { const last = this.chunks[this.chunks.length - 1]; if (this.ended && last !== undefined) return Promise.resolve(last); this.ended = channel === Channel.CONTROL; const chunk = { seq: this.nextSeq, ts: Date.now() / 1000, channel, data, }; this.chunks.push(chunk); this.nextSeq += 1; this.bytes += data.byteLength; this.trim(); this.wake(); return Promise.resolve(chunk); } readFrom(seq, limit) { const truncated = seq < this.baseSeq; const start = truncated ? 0 : Math.min(seq - this.baseSeq, this.chunks.length); const window = limit === undefined ? this.chunks.slice(start) : this.chunks.slice(start, start + limit); return Promise.resolve([window, this.baseSeq + start + window.length, truncated]); } wait(seq, signal) { // A closed store is checked here too, so a reader that re-arms after // close() released it resolves instead of parking forever. if (this.isClosed || this.nextSeq > seq || signal?.aborted === true) return Promise.resolve(); return new Promise((resolve) => { // The abort listener goes with the waiter, so a reader waiting on // chunk after chunk under one signal leaves none behind. const waiter = { seq, resolve: () => { signal?.removeEventListener('abort', abandon); resolve(); }, }; const abandon = () => { this.waiters = this.waiters.filter((w) => w !== waiter); resolve(); }; this.waiters.push(waiter); signal?.addEventListener('abort', abandon, { once: true }); }); } waitEnded(signal) { if (this.isClosed || this.ended || signal?.aborted === true) return Promise.resolve(); return new Promise((resolve) => { const waiter = { seq: END_OF_CONSOLE, resolve }; this.waiters.push(waiter); signal?.addEventListener('abort', () => { this.waiters = this.waiters.filter((w) => w !== waiter); resolve(); }, { once: true }); }); } close() { this.isClosed = true; const waiters = this.waiters; this.waiters = []; for (const w of waiters) w.resolve(); return Promise.resolve(); } trim() { if (this.maxBytes === null) return; while (this.bytes > this.maxBytes) { const head = this.chunks[0]; // The terminal chunk is what releases waitFinished() and ends // follow(); dropping it would leave both blocked forever, so it // outranks the byte budget. if (head === undefined || head.channel === Channel.CONTROL) break; this.chunks.shift(); this.bytes -= head.data.byteLength; this.baseSeq += 1; } } wake() { const matured = this.waiters.filter((w) => w.seq < this.nextSeq || this.ended); if (matured.length === 0) return; this.waiters = this.waiters.filter((w) => !matured.includes(w)); for (const w of matured) w.resolve(); } } //# sourceMappingURL=ram.js.map