UNPKG

@struktoai/mirage-core

Version:

A Unified Virtual Filesystem For AI Agents

147 lines • 5.23 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 { DIR_MODE } from "./constants.js"; /** * The node table for one mount prefix. * * Everything about *which* nodes exist and what they are called lives * here: creating them, naming them, moving them, and translating between * the guest's absolute paths and positions in this tree. It records no * mutations and reports no errno, so the filesystem above it is left with * only the semantics of each call. */ export class NodeTable { host; nodeOps; streamOps; prefix; root = null; /** * Args: * host: the Emscripten FS namespace, for `createNode` and the mode * predicates. * prefix: the mount prefix this tree serves, trailing slash optional. * nodeOps: op table every node created here must carry. * streamOps: stream op table every node created here must carry. */ constructor(host, prefix, nodeOps, streamOps) { this.host = host; this.prefix = prefix.endsWith('/') ? prefix.slice(0, -1) : prefix; this.nodeOps = nodeOps; this.streamOps = streamOps; } /** Build the root. Emscripten calls this through `FSType.mount`. */ mount() { this.root = this.makeNode(null, '/', DIR_MODE); return this.root; } /** * Create a node and file it under its parent. * * Args: * parent: directory to file it under, null only for the root. * name: the child's name. * mode: type and permission bits. */ makeNode(parent, name, mode, rdev = 0) { const node = this.host.createNode(parent, name, mode, rdev); node.node_ops = this.nodeOps; node.stream_ops = this.streamOps; if (this.host.isDir(mode)) node.children = new Map(); else if (this.host.isLink(mode)) node.link = ''; node.atime = node.mtime = node.ctime = Date.now(); if (parent !== null) { parent.children ??= new Map(); parent.children.set(name, node); } return node; } /** * Give a placed node the kind a mount reported for it afterwards. * * Only a node placed from an unclassified listing row is ever built on * a guess, and the guess is a regular file. When the mount says it is * a directory, the node gets a child table, which is what `makeNode` * would have built for one. * * Args: * node: the node to retype. * mode: the mode the mount reported, type bits included. */ retype(node, mode) { const becomesDir = this.host.isDir(mode) && !this.host.isDir(node.mode); node.mode = mode; if (becomesDir) node.children = new Map(); } childOf(parent, name) { return parent.children?.get(name); } /** * Forget every node below the root, so the next lookup asks the mount. * * A child process may have changed anything the tree served. A stream * open on a forgotten node reads on through its core handle, as a * descriptor keeps its inode. */ invalidate() { if (this.root === null) return; const pending = [...(this.root.children?.values() ?? [])]; while (pending.length > 0) { const node = pending.pop(); if (node === undefined) break; for (const item of node.children?.values() ?? []) pending.push(item); this.host.destroyNode?.(node); } this.root.children?.clear(); delete this.root.listed; } childNames(node) { return [...(node.children?.keys() ?? [])]; } detach(parent, name) { parent.children?.delete(name); } /** * Move a node to a new parent and name. * * Args: * node: the node being moved. * newDir: its new parent directory. * newName: its new name. */ move(node, newDir, newName) { node.parent.children?.delete(node.name); newDir.children ??= new Map(); newDir.children.set(newName, node); node.parent = newDir; node.name = newName; } /** The guest-absolute path of a node, which is what the journal names. */ pathOf(node) { const parts = []; let cur = node; while (cur.parent !== cur) { parts.unshift(cur.name); cur = cur.parent; } return parts.length === 0 ? this.prefix : this.prefix + '/' + parts.join('/'); } } //# sourceMappingURL=nodes.js.map