@struktoai/mirage-core
Version:
A Unified Virtual Filesystem For AI Agents
129 lines • 6.29 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 } from "../../types.js";
import { compareCodePoints } from "../../utils/sort.js";
import { eventAt, field, textField } from "../../events/payload.js";
import { EntityKind, KIND_TO_DIR, SYSTEM_DATABASES } from "./types.js";
// Change events that rewrite a collection's documents.jsonl.
const DATA_EVENTS = new Set(['insert', 'update', 'replace', 'delete']);
// Change events that rewrite a collection's schema.json.
const SCHEMA_EVENTS = new Set(['createIndexes', 'dropIndexes', 'modify']);
// Change events that change which collections and views a database lists, so
// its views must be listed again.
const LISTING_EVENTS = new Set(['create', 'drop', 'rename', 'modify']);
const COLLECTIONS = KIND_TO_DIR[EntityKind.COLLECTION];
const VIEWS = KIND_TO_DIR[EntityKind.VIEW];
/**
* Map one MongoDB change stream event onto mount paths.
*
* The consumer owns the stream (`watch()` on the client or one database, with
* `showExpandedEvents` so collection and index changes come through) and its
* resume token, and passes each event's `operationType` with the event.
* Nothing here runs a loop.
*
* A collection is one `documents.jsonl` and one `schema.json`, so an event
* says exactly which files moved: a document written or removed rewrites its
* collection's documents.jsonl and the schema.json sampled from it, and both
* files of every view in that database (a view reads other collections); an
* index or validator change rewrites its schema.json, and a view's new
* pipeline (its `modify` names `viewOn`) both of the view's files; a
* collection created, dropped or renamed is its directory appearing or
* leaving, and its database's database.json, which lists it. A dropped
* database leaves, and an invalidated stream leaves the whole mount UNKNOWN.
* The views of a database are listed once and listed again after any event
* that changes what the database holds.
*
* Mirrors Python `MongoDBEventHook` (`core/mongodb/events.py`).
*/
export class MongoDBEventHook {
accessor;
views = new Map();
constructor(accessor) {
this.accessor = accessor;
}
/** The views `db` holds, listed once until it changes. */
async viewsOf(db) {
let found = this.views.get(db);
if (found === undefined) {
found = [...(await this.accessor.driver.listCollections(db, EntityKind.VIEW))].sort(compareCodePoints);
this.views.set(db, found);
}
return found;
}
async toEvents(root, operationType, payload) {
if (operationType === 'invalidate')
return [eventAt(root, '', FileChangeKind.UNKNOWN)];
const ns = field(payload, 'ns');
const db = textField(ns, 'db');
const name = textField(ns, 'coll');
const allowed = this.accessor.config.databases;
if (db === null ||
SYSTEM_DATABASES.has(db) ||
(allowed !== null && allowed.length > 0 && !allowed.includes(db))) {
return [];
}
if (operationType === 'dropDatabase') {
this.views.delete(db);
return [eventAt(root, db, FileChangeKind.DELETE)];
}
if (name === null)
return [];
const details = field(payload, 'operationDescription');
const changed = new Map();
const place = `${db}/${COLLECTIONS}/${name}`;
if (LISTING_EVENTS.has(operationType))
this.views.delete(db);
if (DATA_EVENTS.has(operationType)) {
changed.set(`${place}/documents.jsonl`, FileChangeKind.UPDATE);
changed.set(`${place}/schema.json`, FileChangeKind.UPDATE);
for (const view of await this.viewsOf(db)) {
for (const file of ['documents.jsonl', 'schema.json']) {
changed.set(`${db}/${VIEWS}/${view}/${file}`, FileChangeKind.UPDATE);
}
}
}
else if (operationType === 'modify' && field(details, 'viewOn') !== null) {
const view = `${db}/${VIEWS}/${name}`;
changed.set(`${view}/schema.json`, FileChangeKind.UPDATE);
changed.set(`${view}/documents.jsonl`, FileChangeKind.UPDATE);
}
else if (SCHEMA_EVENTS.has(operationType)) {
changed.set(`${place}/schema.json`, FileChangeKind.UPDATE);
}
else if (operationType === 'create') {
const dirname = field(details, 'viewOn') !== null ? VIEWS : COLLECTIONS;
changed.set(`${db}/${dirname}/${name}`, FileChangeKind.CREATE);
changed.set(`${db}/database.json`, FileChangeKind.UPDATE);
}
else if (operationType === 'drop') {
changed.set(place, FileChangeKind.DELETE);
changed.set(`${db}/${VIEWS}`, FileChangeKind.UNKNOWN);
changed.set(`${db}/database.json`, FileChangeKind.UPDATE);
}
else if (operationType === 'rename') {
const to = field(payload, 'to');
changed.set(place, FileChangeKind.DELETE);
changed.set(`${db}/database.json`, FileChangeKind.UPDATE);
const targetDb = textField(to, 'db');
const target = textField(to, 'coll');
if (targetDb !== null && target !== null) {
this.views.delete(targetDb);
changed.set(`${targetDb}/${COLLECTIONS}/${target}`, FileChangeKind.CREATE);
changed.set(`${targetDb}/database.json`, FileChangeKind.UPDATE);
}
}
return [...changed].map(([path, kind]) => eventAt(root, path, kind));
}
}
//# sourceMappingURL=events.js.map