@struktoai/mirage-core
Version:
A Unified Virtual Filesystem For AI Agents
185 lines • 6.86 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 { 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