UNPKG

@struktoai/mirage-core

Version:

A Unified Virtual Filesystem For AI Agents

249 lines • 8.64 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 { edquot } from "../errors/fs.js"; import { chunks } from "../io/cooperative.js"; import { CapacityState } from "../types.js"; import { parseLimit } from "../utils/size.js"; import { VFSConfigError } from "./errors.js"; /** * One store's capacity and the bytes counted against it. * * `used` is null until the store is counted. `pending` holds bytes a stream * or an awaiting write admitted and has not landed yet. `active` holds the * writes in flight. Every method but `counted` is * synchronous, so a check and the change it guards run as one step on the * one thread, which is what makes parallel writers safe without a lock. Two * writes in flight on overlapping paths (a file and its folder) can both * measure the same bytes, so they lose the credit for the bytes they replace * and mark the count for a recount, which waits until no write is in flight. The count lives in this object: another * process, or another VFS on the same disk folder, Redis prefix or OPFS * directory, counts on its own, so together they can pass the limit. * Mirrors Python's `StorageUsage`. */ export class StorageUsage { limit; used; pending = 0; active = []; dirty = false; counting = null; constructor(limit, used = null) { this.limit = limit; this.used = used; } /** Count the store when it is uncounted, or marked for a recount and no * write is in flight; parallel callers wait on one count. */ async counted(count) { while (this.used === null || this.counting !== null || (this.dirty && this.active.length === 0)) { if (this.counting === null) { this.dirty = false; this.counting = this.count(count); } await this.counting; } } async count(count) { try { this.used = await count(); } catch (err) { this.dirty = true; throw err; } finally { this.counting = null; } } /** Count the store again once no write is in flight, as bytes written * past mirage (a native program on a disk folder) went uncounted. */ stale() { this.dirty = true; } /** Mark a write in flight; one overlapping another in flight takes both * off their credit and marks the count for a recount. */ enter(held) { for (const other of this.active) { if (overlaps(other.path, held.path)) other.contended = held.contended = this.dirty = true; } this.active.push(held); } /** Mark a write done. */ leave(held) { this.active.splice(this.active.indexOf(held), 1); } /** Throw `edquot(path)` when positive growth would pass the limit; * reaching it exactly passes. */ admit(path, growth) { if (growth <= 0) return; if (this.used === null) throw new Error('an uncounted usage cannot admit growth'); if (this.used + this.pending + growth > this.limit) throw edquot(path); } /** Admit `size` and keep it in `pending` until it lands. */ hold(path, size) { this.admit(path, size); this.pending += size; } /** Take `held` out of `pending` and add the measured `delta`. */ land(held, delta) { this.pending -= held; if (this.used !== null) this.used += delta; } /** df's QUOTA row: total is the limit, available never below 0. */ report() { if (this.used === null) return { state: CapacityState.UNKNOWN }; return { state: CapacityState.QUOTA, total: this.limit, used: this.used, available: Math.max(this.limit - this.used, 0), }; } } function overlaps(a, b) { const x = a.vfsPath.replace(/\/+$/, ''); const y = b.vfsPath.replace(/\/+$/, ''); return x === y || x.startsWith(y + '/') || y.startsWith(x + '/'); } /** * A backend's usage for its `capacity` setting; null for no cap. `used` is * the bytes it starts with when the backend knows them, as an empty RAM * store does. Mirrors Python's `storage_usage`. * * @throws VFSConfigError when the setting is not a size. */ export function storageUsage(name, capacity, used = null) { if (capacity === undefined || capacity === null) return null; let limit; try { limit = parseLimit(capacity, 'capacity'); } catch (err) { throw new VFSConfigError(`${name}: ${err.message}`, { cause: err }); } return new StorageUsage(limit, used); } /** * What one write reserves against a counted store, and the bytes it held so * far; inert for an uncapped store. A replacement is credited with the bytes * it replaces until another write overlaps it (`contended`), as that write * may move those bytes elsewhere. Mirrors Python's `Charge`. */ export class Charge { usage; path; before; held = 0; written = 0; contended = false; constructor(usage, path, before = 0) { this.usage = usage; this.path = path; this.before = before; } /** Reserve `size` more bytes, or throw EDQUOT. */ hold(size) { if (this.usage === null || size <= 0) return; this.usage.hold(this.path, size); this.held += size; } /** The length the operand may reach within what this charge holds and is * credited with. */ get reserved() { return (this.contended ? 0 : this.before) + this.held; } /** Reserve what growing the operand to `size` bytes adds past what this * charge already holds. */ growTo(size) { this.hold(size - this.reserved); } /** `data`, each chunk reserved as the writer takes it. With `append` * false, a replacement reserves only what passes the bytes it replaces. */ stream(data, append = false) { if (this.usage === null) return data; if (data instanceof Uint8Array) { this.take(data.byteLength, append); return data; } return this.held_(data, append); } take(size, append) { this.written += size; if (append) this.hold(size); else this.growTo(this.written); } async *held_(data, append) { for await (const chunk of chunks(data)) { this.take(chunk.byteLength, append); yield chunk; } } } /** * Reserve before a write's bytes go and land what it really changed. The * reservation is checked and taken in one synchronous step, so parallel * writers can never pass the store's capacity together. After the write, * however it ended, the operand is measured again and the change lands; one * that overlapped another in flight marks the count for a recount. An * uncapped store gets an inert charge and measures nothing. Mirrors Python's * `charge`. */ export async function charge(usage, path, sizeOf, count, write) { if (usage === null) return write(new Charge(null, path)); await usage.counted(count); const held = new Charge(usage, path); usage.enter(held); try { const before = (held.before = await sizeOf(path)); const settle = async () => { let after; try { after = await sizeOf(path); } catch (err) { usage.land(held.held, 0); usage.stale(); throw err; } usage.land(held.held, after - before); }; let result; try { result = await write(held); } catch (err) { await settle(); throw err; } await settle(); return result; } finally { usage.leave(held); } } //# sourceMappingURL=capacity.js.map