UNPKG

@struktoai/mirage-core

Version:

A Unified Virtual Filesystem For AI Agents

185 lines • 6.86 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 { FileChangeKind, FileEvent, OverflowPolicy, PathSpec } from "../../types.js"; import { DEFAULT_MAX_PENDING } from "../constants.js"; import { QueueClosed, QueueOverflowError } from "../errors.js"; export function signal() { let wake = () => undefined; const promise = new Promise((resolve) => { wake = resolve; }); return { promise, wake }; } export class RAMEventQueue { roots; label; maxPending; onOverflow; changes = new Map(); overflowed = false; closed = false; ready = signal(); constructor(roots, options = {}) { this.roots = roots instanceof PathSpec ? [roots] : [...roots]; this.label = this.roots.map((root) => root.virtual).join(', '); this.maxPending = options.maxPending ?? DEFAULT_MAX_PENDING; this.onOverflow = options.onOverflow ?? OverflowPolicy.COLLAPSE; } merge(old, change) { if (old === undefined) return change; if (old.kind === FileChangeKind.UNKNOWN) return old; if (old.kind === FileChangeKind.CREATE) { if (change.kind === FileChangeKind.DELETE) return null; return new FileEvent({ kind: FileChangeKind.CREATE, path: change.path, timestamp: change.timestamp, previousPath: change.previousPath, metadata: change.metadata, }); } if (old.kind === FileChangeKind.MOVE && change.kind !== FileChangeKind.DELETE) { return new FileEvent({ kind: FileChangeKind.MOVE, path: change.path, timestamp: change.timestamp, previousPath: old.previousPath, metadata: change.metadata, }); } if (old.kind === FileChangeKind.DELETE && change.kind === FileChangeKind.CREATE) { return new FileEvent({ kind: FileChangeKind.UPDATE, path: change.path, timestamp: change.timestamp, previousPath: change.previousPath, metadata: change.metadata, }); } return change; } absorbSource(change) { if (change.kind !== FileChangeKind.MOVE || change.previousPath === null) return change; const source = this.changes.get(change.previousPath.virtual); if (source === undefined || source.kind === FileChangeKind.UNKNOWN) return change; this.changes.delete(change.previousPath.virtual); if (source.kind === FileChangeKind.CREATE) { return new FileEvent({ kind: FileChangeKind.CREATE, path: change.path, timestamp: change.timestamp, metadata: change.metadata, }); } if (source.kind === FileChangeKind.MOVE && source.previousPath !== null) { return new FileEvent({ kind: FileChangeKind.MOVE, path: change.path, timestamp: change.timestamp, previousPath: source.previousPath, metadata: change.metadata, }); } return change; } push(change) { if (this.closed) return Promise.resolve(); const event = this.absorbSource(change); const key = event.path.virtual; const old = this.changes.get(key); this.changes.delete(key); if (old?.kind === FileChangeKind.MOVE && old.previousPath !== null && event.kind === FileChangeKind.DELETE) { const source = old.previousPath.virtual; if (!this.changes.has(source)) { this.changes.set(source, new FileEvent({ kind: FileChangeKind.DELETE, path: old.previousPath, timestamp: event.timestamp, })); } } else { const merged = this.merge(old, event); if (merged !== null) this.changes.set(key, merged); } if (this.changes.size > this.maxPending) this.applyOverflow(); if (this.changes.size > 0 || this.overflowed) this.ready.wake(); return Promise.resolve(); } applyOverflow() { if (this.onOverflow === OverflowPolicy.DROP_OLDEST) { const oldest = this.changes.keys().next().value; if (oldest !== undefined) this.changes.delete(oldest); return; } this.changes.clear(); if (this.onOverflow === OverflowPolicy.ERROR) { this.overflowed = true; return; } const timestamp = new Date(); for (const root of this.roots) { this.changes.set(root.virtual, new FileEvent({ kind: FileChangeKind.UNKNOWN, path: root, timestamp })); } } async pop() { for (;;) { if (this.closed) throw new QueueClosed(this.label); if (this.overflowed) { this.overflowed = false; if (this.changes.size === 0) this.ready = signal(); throw new QueueOverflowError(`event queue for ${this.label} exceeded ${String(this.maxPending)} pending changes`); } const first = this.changes.entries().next().value; if (first !== undefined) { this.changes.delete(first[0]); if (this.changes.size === 0) this.ready = signal(); return first[1]; } this.ready = signal(); await this.ready.promise; } } pending() { return Promise.resolve(this.changes.size); } clear() { this.changes.clear(); this.overflowed = false; return Promise.resolve(); } close() { this.closed = true; this.changes.clear(); this.overflowed = false; this.ready.wake(); return Promise.resolve(); } } //# sourceMappingURL=ram.js.map