UNPKG

@struktoai/mirage-core

Version:

A Unified Virtual Filesystem For AI Agents

1,943 lines • 88.8 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 { RAMAccessor } from "../../accessor/ram.js";
import { publishRead, readVersioned, requireSupported } from "../../cache/context.js";
import { latestFingerprint } from "../../cache/file/io.js";
import { LookupStatus } from "../../cache/index/config.js";
import { buildIndex } from "../../cache/index/factory.js";
import { indexLock } from "../../cache/index/lock.js";
import { ListingCheckStore } from "../../cache/index/ram.js";
import { KeyLock } from "../../cache/lock.js";
import {} from "../../cache/types.js";
import { linkView } from "../../commands/builtin/utils/links.js";
import { dispatchStat, linkFollow, vfsStatPath, virtualFlags, virtualKey, } from "../../commands/builtin/utils/paths.js";
import { CLI, noopAccessor } from "../../commands/cli/types.js";
import { Command } from "../../commands/config.js";
import { ROOT_CWD } from "../../commands/constants.js";
import { STDIN_DASH_COMMANDS, STDIN_DASH_LEADING } from "../../commands/spec/constants.js";
import { flagOccurrences } from "../../commands/spec/flag_view.js";
import { hasInjectedHelp, hasInjectedVersion } from "../../commands/spec/standard.js";
import { CommandSpec } from "../../commands/spec/types.js";
import { awaitsAs, callerSignal, currentCaller, effectiveMountMode, requirePathsWritable, runWithCaller, sessionVisibility, strongestModeUnder, withCaller, } from "../../context/session_context.js";
import { activePipe, pipeBelow } from "../../core/dev/device.js";
import { appendByRewrite, expectOffset, pwriteByStreamedRewrite, refuseTaken, truncateByRewrite, } from "../../core/generic/rewrite.js";
import { ebusy, enotdir, enotsup, isEnoent } from "../../errors/fs.js";
import { withEgress } from "../../io/gateway.js";
import { checkMemory } from "../../io/memory.js";
import { MEMORY_LIMIT } from "../../io/spool.js";
import { closeQuietly, OutputStream, rechunk } from "../../io/stream.js";
import { IOResult, materialize } from "../../io/types.js";
import { activeRecords, commandRecords, runWithRecording, withMountContext, } from "../../observe/context.js";
import { encodeText } from "../../shell/bytes.js";
import { NOT_FOUND_STATUS } from "../../shell/constants.js";
import { DEFAULT_READ_SPEC, FileChangeKind, FileStat, FileType, Limit, ListingVersion, MountMode, PathSpec, ReadPolicy, WritePolicy, } from "../../types.js";
import { Activity } from "../../utils/activity.js";
import { getExtension } from "../../utils/filetype.js";
import { pathVisible } from "../../utils/hidden.js";
import { SystemIdSource } from "../../utils/ids.js";
import { mountKey } from "../../utils/key_prefix.js";
import { norm } from "../../utils/path.js";
import { isUnsatisfiableRange, sliceWindow } from "../../utils/ranges.js";
import { rstripSlash } from "../../utils/slash.js";
import { compareCodePoints } from "../../utils/sort.js";
import { declared, declaredCalls, methodName } from "../../vfs/call.js";
import { WRITE_EFFECTS } from "../../vfs/constants.js";
import { DevVFS } from "../../vfs/dev/dev.js";
import { HistoryViewVFS } from "../../vfs/history/history.js";
import { RAMVFS } from "../../vfs/ram/ram.js";
import { Effect } from "../../vfs/types.js";
import { answered, cliVoice, mountVoice, runHandler, } from "../executor/command/handler.js";
import { HANDED, STORED_OPS } from "./constants.js";
import { beyondWindow, entryAt, folderOf, indexKey, keepListing, keepTree, storedListing, storedStat, storedTree, unprovenMiss, } from "./listing.js";
import { coerceReadPolicy } from "./read_policy.js";
import { coerceWritePolicy, createConditions, writeConditions } from "./write_policy.js";
function cmdKey(name, filetype) {
    return `${name}\u0000${filetype ?? ''}`;
}
function writeSource(args) {
    const first = args[0];
    if (first instanceof Uint8Array)
        return first;
    if (first !== null &&
        typeof first === 'object' &&
        Symbol.asyncIterator in first &&
        typeof first[Symbol.asyncIterator] === 'function')
        return first;
    throw new TypeError('write op requires bytes or an async byte source as the first arg');
}
function lengthArg(value) {
    if (typeof value !== 'number') {
        throw new TypeError('truncate op requires a number length as the first arg');
    }
    return value;
}
function offsetArg(value, path) {
    if (typeof value !== 'number') {
        throw new TypeError('pwrite op requires a number offset as the second arg');
    }
    return expectOffset(value, path);
}
function dstArg(value) {
    if (!(value instanceof PathSpec)) {
        throw new TypeError('rename op requires a dst PathSpec as the first arg');
    }
    return value;
}
/** What rides beside the path: the handed stats, and a walk's `limit`. */
function handedOf(kw) {
    const handed = kw.handed;
    return typeof kw.limit === 'number' ? { ...handed, limit: kw.limit } : handed;
}
/**
 * Refuse a keyword the function `name` does not take: the TypeError a direct
 * call raises in Python, never silently dropped. The mount hands every
 * function its `index` and a caller may name the `filetype`; a built-in takes
 * the keywords its Python signature names, and a custom function none, a
 * TypeScript function having no keyword parameters. Mirrors Python's `_taken`.
 */
function taken(name, kwargs) {
    const keywords = {
        readdir: ['limit'],
        read_tree: ['limit'],
        read: ['offset', 'size'],
        write: ['atomic'],
        mkdir: ['parents'],
        truncate: ['no_create'],
        files_containing: ['whole_word', 'ignore_case'],
        lines_containing: ['ignore_case'],
        before_full_walk: ['files'],
        setattr: ['mode', 'uid', 'gid', 'atime', 'mtime'],
    };
    const known = Object.hasOwn(keywords, name) ? (keywords[name] ?? []) : [];
    const [first] = Object.keys(kwargs)
        .filter((key) => key !== 'index' && key !== 'filetype' && !known.includes(key) && kwargs[key] !== undefined)
        .sort(compareCodePoints);
    if (first !== undefined) {
        throw new TypeError(`${name}() got an unexpected keyword argument '${first}'`);
    }
}
/**
 * A backend read's answer as a stream: a promise awaited on the first pull,
 * a stream passed through, bytes as one chunk. Mirrors Python's `_as_stream`.
 */
async function* asStream(answer) {
    const found = await answer;
    if (found instanceof Uint8Array) {
        if (found.byteLength > 0)
            yield found;
        return;
    }
    yield* found;
}
/**
 * A backend read's answer as bytes, a stream collected. A whole read's
 * stream publishes the version it recorded for the bytes collected from it,
 * as a whole read answering bytes publishes its own, so a read-modify-write
 * still sends the version it read. Its records are its own, so a read
 * running beside it never lends it a version, and reach the block around it
 * too, so the write a rewrite makes still sees the read that proves the copy
 * it extends. Bytes answered whole must fit in what the workspace's
 * `max_memory` has left unless they are what a RAM store holds (`counted`),
 * already counted. Mirrors Python's `_collected`.
 */
async function collected(answer, path, whole, clock, counted = false) {
    const found = await answer;
    if (found instanceof Uint8Array) {
        if (!counted)
            checkMemory(found.byteLength);
        return found;
    }
    if (!whole)
        return materialize(found);
    const [data, records] = await recorded(() => materialize(found), clock);
    publishRead(path.virtual, data, latestFingerprint(records, path.virtual));
    return data;
}
/**
 * Run a whole read's pull with the records it makes its own, which reach the
 * block around it too. Mirrors Python's `_recorded`.
 */
async function recorded(pull, clock) {
    const held = currentCaller().commandSink;
    const outer = held !== null && held[1] !== held[0] ? held[1] : null;
    const collect = () => commandRecords(async (records) => {
        try {
            return [await pull(), records];
        }
        finally {
            outer?.push(...records);
        }
    });
    return activeRecords() === undefined
        ? (await runWithRecording(collect, null, clock))[0]
        : await collect();
}
/**
 * A read, honoring a byte window when one is asked for.
 *
 * A VFS that reads ranges natively fetches only the window, which is the
 * whole point on an object store: one ranged GET instead of the whole file.
 * Every other read is whole and sliced, which is the only meaningful
 * behavior for content that is rendered rather than stored. A zero-length
 * read is answered here rather than sent anywhere, and a window starting at
 * or past EOF answers empty, the POSIX answer, where an HTTP store refuses
 * with 416: normalizing here keeps the op's contract one thing whichever
 * path answers it. `counted` says `read` answers bytes a RAM store holds,
 * already counted against the workspace's `max_memory`. Mirrors Python's
 * `_read_window`.
 */
async function readWindow(read, ranges, counted, path, kwargs, handed, clock) {
    const offset = typeof kwargs.offset === 'number' ? kwargs.offset : 0;
    const size = typeof kwargs.size === 'number' ? kwargs.size : null;
    if (size === 0)
        return new Uint8Array(0);
    const whole = offset === 0 && size === null;
    if (ranges && !whole) {
        try {
            return await collected(read(path, handed, offset, size), path, false, clock, counted);
        }
        catch (err) {
            if (!isUnsatisfiableRange(err))
                throw err;
            return new Uint8Array(0);
        }
    }
    const data = await collected(read(path, handed), path, true, clock, counted);
    return whole ? data : sliceWindow(data, offset, size);
}
// What the walk guard proves an operand's `.` and `..` with: the
// dispatcher's stat and link follow, bound around the handler. No session
// files (a mount driven directly) binds nothing. Mirrors the Python
// set_walk_probe binding in Mount.run_command.
function walkProbeOf(ctx) {
    if (ctx.vfs === undefined)
        return null;
    return { stat: dispatchStat(ctx.vfs.dispatch), follow: linkFollow(linkView(ctx.vfs)) };
}
/**
 * A command tree's root, which never runs: the executor walks the tree and
 * runs the leaf the line names (`MountEntry.runLeaf`). Mirrors Python's
 * `_tree_root`.
 */
function treeRoot(_accessor, _paths, _texts, ctx) {
    throw new Error(`${ctx.spec?.name ?? '?'}: a command tree runs the leaf its walk names`);
}
/**
 * The command a mount registers for a command tree: its root, under the
 * tree's name. The tree runs on the mount's accessor, so it declares none of
 * an installation's own: no config, no accessor, no script, no runtime.
 * Mirrors Python's `_tree_command`.
 */
function treeCommand(cli) {
    const name = cli.spec.name;
    for (const [setting, own] of [
        ['configModel', cli.configModel !== null],
        ['accessor', cli.accessor !== noopAccessor],
        ['script', cli.script !== null],
        ['runtime', cli.runtime !== null],
    ]) {
        if (own) {
            throw new Error(`cli '${name}': a command tree on a mount runs on the mount's accessor; it cannot declare ${setting}`);
        }
    }
    return new Command({
        name,
        spec: cli.spec,
        vfs: null,
        filetype: null,
        fn: treeRoot,
        write: Object.values(cli.handlers).some((handler) => handler.write),
    });
}
// The grammar a handler sees at `ctx.spec`, named for the command it runs as
// (python3 or python, js or node). Mirrors Python's _named_spec.
function namedSpec(cmd, cmdName) {
    if (cmd.spec.name === cmdName)
        return cmd.spec;
    const spec = cmd.spec;
    return new CommandSpec({ ...spec, name: cmdName });
}
export class MountEntry {
    visible = null;
    mountId;
    prefix;
    vfs;
    // The backend's own walkLimit, kept from before any mount settles the VFS's.
    backendWalkLimit;
    // The functions the VFS answers itself, read once when it is mounted: each
    // dispatcher call it defines, and `read_range` when a read fetches only the
    // range asked for. A command asks its mount's row (`MountView.answers`).
    calls;
    mode;
    read;
    write;
    // `index` is this same store scoped by the cache manager, which is
    // what ops and commands receive.
    indexStore;
    indexConfig;
    inheritsIndex;
    vfsRef;
    activity = new Activity();
    retiring = false;
    beforeUse = null;
    readyLock = new KeyLock();
    /**
     * Per-path revision pins installed at Workspace.load time. Read
     * functions consult these via the {@link revisionFor} contextvar
     * lookup; on a hit, the backend GET pins to the recorded revision so
     * replay serves the exact bytes the agent saw. Empty during normal
     * runs; populated only by the snapshot loader.
     */
    revisions = new Map();
    cacheManager = null;
    writeContextFor = null;
    cmds = new Map();
    // The keys of `cmds` a user registered (`registerCommands`), not mirage.
    userKeys = new Set();
    generalCmds = new Map();
    // name -> the root command registered and the tree it walks
    trees = new Map();
    cmdSpecs = new Map();
    commandLimits = new Map();
    /**
     * The functions the VFS answers itself, read when it is mounted and again
     * when its `walkLimit` is settled (a backend answers `files_containing` only
     * under a cap), plus `read_range` when a read fetches only the range asked
     * for. Mirrors Python's `answered_calls`.
     */
    answeredCalls() {
        const vfs = this.vfs;
        const calls = new Set([...declaredCalls(vfs.constructor).keys()].filter((name) => vfs.supports(name)));
        if (vfs.readsRanges)
            calls.add('read_range');
        return calls;
    }
    /** The bottom layer of a command's limit on this mount: its backend's `walkLimit`. */
    get defaultLimit() {
        return new Limit({ walkLimit: this.vfs.walkLimit });
    }
    // first token -> descending token counts of multi-word command names
    // (e.g. "gws docs documents get"); backs longest-prefix command
    // resolution. null until first built; invalidated on register.
    prefixIndex = null;
    constructor(init) {
        const prefix = init.prefix;
        if (!prefix.startsWith('/')) {
            throw new Error(`prefix must start with /: ${prefix}`);
        }
        if (!prefix.endsWith('/')) {
            throw new Error(`prefix must end with /: ${prefix}`);
        }
        if (prefix.includes('//')) {
            throw new Error(`prefix must not contain //: ${prefix}`);
        }
        for (const [filetype, renderer] of Object.entries(init.vfs.renderers)) {
            if (typeof init.vfs[renderer] !== 'function') {
                throw new TypeError(`${init.vfs.constructor.name}.renderers maps '${filetype}' to '${renderer}', which is not a method`);
            }
        }
        this.mountId = (init.ids ?? new SystemIdSource()).uuid7();
        this.prefix = prefix;
        this.vfs = init.vfs;
        this.backendWalkLimit = init.vfs.walkLimit;
        this.calls = this.answeredCalls();
        this.mode = init.mode ?? MountMode.READ;
        // A frozen copy carrying the coerced policy, not the caller's object.
        //
        // Frozen because Python's `ReadSpec` is a frozen dataclass, so the
        // same spec cannot be edited after the mount-time verdict passed it;
        // a plain JS object can, which would let a caller flip a RAM mount to
        // `fresh` behind the verdict's back.
        //
        // Coerced because `ReadPolicy` is a string-const object: a runtime
        // spec carrying `'FRESH'` matches no `===` downstream -- the gate, the
        // operand probe -- so the mount would pass its capability check
        // and then read as `bounded` everywhere, which is the silent
        // downgrade the policy exists to remove. The Python twin normalizes
        // at this same point.
        const spec = init.read ?? DEFAULT_READ_SPEC;
        this.read = Object.freeze({ ...spec, policy: coerceReadPolicy(spec.policy) });
        // What the client keeps of its own (a store's search results) is served
        // under the same policy.
        init.vfs.accessor.govern?.(this.read);
        // Coerced so an untyped caller's bare string still matches `===`.
        this.write = coerceWritePolicy(init.write);
        this.indexStore = init.index ?? buildIndex(undefined, init.vfs.indexTtl);
        this.indexConfig = init.indexConfig === undefined ? undefined : { ...init.indexConfig };
        this.inheritsIndex = init.inheritsIndex ?? false;
        this.vfsRef = init.vfsRef ?? null;
    }
    /** Whether this mount answers the op `name`. */
    answers(name) {
        return this.callers(name, null).length > 0;
    }
    /**
     * The VFS as the owner of `access` sees it: the device mount's view of that
     * session's descriptors, or the history view of its commands, which a
     * browser call cannot find any other way.
     */
    viewFor(access) {
        if (access === undefined)
            return this.vfs;
        if (this.vfs instanceof DevVFS || this.vfs instanceof HistoryViewVFS) {
            return this.vfs.forSession(access.sessionId);
        }
        return this.vfs;
    }
    /** The live pipe a read of `path` takes bytes from, which another task is still writing. Mirrors Python's `live_pipe`. */
    livePipe(path, access) {
        const accessor = this.viewFor(access).accessor;
        if (!(accessor instanceof RAMAccessor))
            return null;
        return activePipe(accessor, norm('/' + mountKey(path, rstripSlash(this.prefix))));
    }
    /** Whether a live pipe sits at `path` or below it. Mirrors Python's `holds_live_pipe`. */
    holdsLivePipe(path, access) {
        const accessor = this.viewFor(access).accessor;
        if (!(accessor instanceof RAMAccessor))
            return false;
        return pipeBelow(accessor, norm('/' + mountKey(path, rstripSlash(this.prefix))));
    }
    /** Metadata access bound to this mount's ownership. */
    get index() {
        return this.cacheManager?.scopeIndex() ?? this.indexStore;
    }
    /** Finish deferred mount preparation before any backend or cache read. */
    async use(fn) {
        await this.ensureReady();
        this.checkActive();
        const release = this.activity.acquire();
        try {
            return await fn();
        }
        finally {
            release();
        }
    }
    async ensureReady() {
        this.checkActive();
        if (this.beforeUse === null)
            return;
        await this.readyLock.withLock('', async () => {
            if (this.beforeUse !== null) {
                await this.beforeUse();
                this.beforeUse = null;
            }
        });
        this.checkActive();
    }
    checkActive() {
        if (this.retiring)
            throw ebusy(this.prefix);
    }
    /**
     * This mount's mode narrowed by the owner's cap, or the live sessions'
     * when no owner is supplied. The configured mode is the ceiling; a
     * session's mode can only weaken it.
     */
    effectiveMode(access) {
        return effectiveMountMode(this.prefix, this.mode, access);
    }
    // ── command registration ──────────────────────────
    register(cmd) {
        const key = cmdKey(cmd.name, cmd.filetype);
        this.cmds.set(key, cmd);
        this.userKeys.add(key);
        this.cmdSpecs.set(cmd.name, cmd.spec);
        this.prefixIndex = null;
    }
    registerGeneral(cmd) {
        this.generalCmds.set(cmd.name, cmd);
        this.cmdSpecs.set(cmd.name, cmd.spec);
        this.prefixIndex = null;
    }
    resolveCommand(cmdName, extension = null) {
        if (extension !== null && extension !== '') {
            const specific = this.cmds.get(cmdKey(cmdName, extension));
            if (specific !== undefined)
                return specific;
        }
        const byVfs = this.cmds.get(cmdKey(cmdName, null));
        if (byVfs !== undefined)
            return byVfs;
        const general = this.generalCmds.get(cmdName);
        if (general !== undefined)
            return general;
        // Fall back to any filetype variant so callers without an extension can
        // still find the command; the actual handler is picked by runCommand.
        for (const rc of this.cmds.values()) {
            if (rc.name === cmdName)
                return rc;
        }
        return null;
    }
    /**
     * Whether a run of `cmdName` on an operand with `extension` can reach a
     * handler that declares it may write. Every handler the run's cascade tries
     * counts, the filetype one first, since one that declines hands the run to
     * the next. The directory probe a run makes (`pickHandlers`) is not made,
     * so a directory named like a file counts its filetype handler too.
     * `builtins` is false when the line's options leave mirage's own handlers
     * nothing to write (`LineFacts.writes`); a user command counts either way.
     * Mirrors Python's MountEntry.may_write.
     */
    mayWrite(cmdName, extension = null, builtins = true) {
        return this.resolveCascade(cmdName, extension, this.cmds, this.generalCmds).some((cmd) => cmd.write && (builtins || this.isUserCommand(cmd)));
    }
    /**
     * How many leading words form a registered command name here. Command
     * names may span several words ("gws docs documents get"), git-style.
     * Returns the length of the longest registered name that is a prefix of
     * `words`, or 1 (the bare first token) if no multi-word name matches; 0
     * for no words.
     */
    longestCommandMatch(words) {
        if (words.length === 0)
            return 0;
        if (this.prefixIndex === null) {
            const index = new Map();
            const names = new Set([
                ...this.cmdSpecs.keys(),
                ...[...this.cmds.values()].map((rc) => rc.name),
                ...this.generalCmds.keys(),
            ]);
            for (const name of names) {
                const tokens = name.split(' ');
                const [first] = tokens;
                if (first === undefined || tokens.length <= 1)
                    continue;
                const lengths = index.get(first) ?? new Set();
                lengths.add(tokens.length);
                index.set(first, lengths);
            }
            this.prefixIndex = new Map([...index].map(([k, v]) => [k, [...v].sort((a, b) => b - a)]));
        }
        const [first] = words;
        if (first === undefined)
            return 1;
        for (const length of this.prefixIndex.get(first) ?? []) {
            if (length <= words.length &&
                this.resolveCommand(words.slice(0, length).join(' ')) !== null) {
                return length;
            }
        }
        return 1;
    }
    allCommands() {
        const seen = new Set();
        const out = [];
        for (const rc of this.cmds.values()) {
            if (seen.has(rc.name))
                continue;
            seen.add(rc.name);
            out.push(rc);
        }
        for (const rc of this.generalCmds.values()) {
            if (seen.has(rc.name))
                continue;
            seen.add(rc.name);
            out.push(rc);
        }
        return out;
    }
    specFor(cmdName) {
        return this.cmdSpecs.get(cmdName) ?? null;
    }
    commands() {
        const result = new Map();
        for (const rc of this.cmds.values()) {
            const list = result.get(rc.name) ?? [];
            list.push(rc.filetype);
            result.set(rc.name, list);
        }
        for (const name of this.generalCmds.keys()) {
            if (!result.has(name))
                result.set(name, []);
        }
        return sortFiletypeMap(result);
    }
    /**
     * Whether a ranged read of `path` fetches only that range. False where
     * the read that answers it reads the whole file and slices: a VFS with no
     * native range, or a rendered filetype.
     */
    readsRanges(path) {
        return this.vfs.readsRanges && !this.renders(getExtension(path));
    }
    /** Whether the VFS declares the function `name` a write. */
    /**
     * Refuse a write the mount's mode no longer grants at any path. Admission
     * judged the mode before the call waited for the mount and for its write
     * lock; `setMountMode` can make the mount read-only in between, so the mode
     * is read again as the backend call starts. A call that reaches a subtree
     * (a rename, a tree removal or copy) is refused by a read-only region
     * anywhere below its paths, and a copy only reads its source. Mirrors
     * Python's `require_writable`.
     */
    requireWritable(name, path, values, access) {
        const mark = declared(this.vfs.constructor, name);
        if (mark === null || !WRITE_EFFECTS.includes(mark.effect))
            return;
        const others = values.filter((value) => value instanceof PathSpec);
        requirePathsWritable(mark.effect === Effect.COPY ? others : [path, ...others], this.prefix, this.mode, mark.subtree, access);
    }
    /** Refuse a keyword `name` does not take, as a cold call would. Mirrors Python's `refuse_keywords`. */
    refuseKeywords(name, kwargs) {
        taken(name, kwargs);
    }
    /**
     * Whether the VFS renders a read of `filetype`. A rendered read is never
     * served from or kept in the file cache.
     */
    renders(filetype) {
        return filetype !== null && Object.hasOwn(this.vfs.renderers, filetype);
    }
    /**
     * Batch-register user commands. Mirrors Python's `Mount.register_commands(...)`.
     * Each one is this mount's own, keyed by name and filetype, so it wins
     * over the shared commands of that name, and is a user command
     * (`isUserCommand`): refused before it writes to a read-only one. Multi-VFS entries are filtered as
     * `ownCommands` says. A `CLI` serves its verbs as a command tree on this
     * mount, under its root's name: a line naming it walks the tree as an
     * installed CLI's does and runs the leaf on this mount's accessor
     * (`runLeaf`); a tree that declares an installation's own setting throws.
     */
    registerCommands(items) {
        const [commands, trees] = this.ownCommands(items);
        for (const cmd of commands)
            this.register(cmd);
        for (const [root, cli] of trees)
            this.trees.set(root.name, [root, cli]);
    }
    /**
     * Register mirage's own commands for this mount's VFS (a builtin backend's
     * table), filtered as `registerCommands` filters; they are not user
     * commands. Mirrors Python's `Mount._register_builtins`.
     */
    registerBuiltinCommands(items) {
        for (const cmd of this.ownCommands(items)[0]) {
            const key = cmdKey(cmd.name, cmd.filetype);
            this.cmds.set(key, cmd);
            this.userKeys.delete(key);
            this.cmdSpecs.set(cmd.name, cmd.spec);
            this.prefixIndex = null;
        }
    }
    /**
     * The entries of `items` this mount registers: multi-VFS entries (sharing
     * the same name across mounts) are filtered to this mount's VFS kind; if a
     * name has entries but none match this mount, throw.
     */
    ownCommands(items) {
        const kind = this.vfs.name;
        const groups = new Map();
        const trees = [];
        for (const entry of items) {
            let item;
            if (entry instanceof CLI) {
                item = treeCommand(entry);
                trees.push([item, entry]);
            }
            else {
                item = entry;
            }
            let g = groups.get(item.name);
            if (!g) {
                g = { toRegister: [], attempted: new Set() };
                groups.set(item.name, g);
            }
            if (item.vfs === null || item.vfs === kind)
                g.toRegister.push(item);
            else
                g.attempted.add(item.vfs);
        }
        for (const [name, g] of groups) {
            if (g.toRegister.length === 0) {
                const list = [...g.attempted].sort(compareCodePoints);
                throw new Error(`command '${name}' is for VFS(s) [${list.map((r) => `'${r}'`).join(', ')}], not '${kind}'`);
            }
        }
        return [[...groups.values()].flatMap((g) => g.toRegister), trees];
    }
    /**
     * The command tree this mount serves under `cmdName`, null when the name
     * resolves to any other command here. Mirrors Python's
     * `MountEntry.cli_for`.
     */
    cliFor(cmdName) {
        const tree = this.trees.get(cmdName);
        if (this.resolveCommand(cmdName) !== tree?.[0])
            return null;
        return tree[1];
    }
    /**
     * What answers `name` on this mount, in the order to try.
     *
     * A rendered filetype's renderer answers a read before `read` does,
     * window and all, and the first answer that is not null wins. The rest
     * is the dispatcher's own shape around the VFS's functions: a read takes a
     * window, `append` and `pwrite` are a rewrite where the VFS only writes
     * whole files, and `mkdir` refuses a taken name first. Only a method
     * marked `@vfsCall` is reachable by name, and a custom one is handed the
     * scope and the op's positional arguments.
     * Mirrors Python's `MountEntry._callers`.
     */
    callers(name, filetype, vfs = this.vfs) {
        if (name === 'read') {
            const levels = [];
            const renderer = filetype !== null ? vfs.renderers[filetype] : undefined;
            const render = renderer === undefined
                ? undefined
                : vfs[renderer];
            if (render !== undefined) {
                levels.push((scope, _args, kw) => readWindow(render.bind(vfs), true, false, scope, kw, handedOf(kw), this.indexStore.clock));
            }
            if (vfs.supports('read')) {
                const read = (path, given, offset, size) => vfs.read(path, given, offset, size, callerSignal());
                levels.push(async (scope, _args, kw) => {
                    const data = await readWindow(read, vfs.readsRanges, vfs instanceof RAMVFS, scope, kw, handedOf(kw), this.indexStore.clock);
                    if (kw.size !== 0)
                        await this.observe(kw.index ?? this.index, [scope]);
                    return data;
                });
            }
            return levels;
        }
        if (name === 'write' && vfs.supports('write')) {
            return [(scope, args, kw) => this.writeSource(vfs, scope, writeSource(args), false, kw)];
        }
        if (name === 'append' && (vfs.supports('append') || vfs.supports('write'))) {
            return [(scope, args, kw) => this.writeSource(vfs, scope, writeSource(args), true, kw)];
        }
        if (name === 'truncate' && !vfs.supports(name)) {
            if (!vfs.supports('write'))
                return [];
            return [
                (scope, args, kw) => this.truncateByRewrite(scope, lengthArg(args[0]), kw.no_create === true, kw.index),
            ];
        }
        if (name === 'pwrite' && !vfs.supports(name)) {
            if (!vfs.supports('write'))
                return [];
            return [
                (scope, args, kw) => this.pwriteByRewrite(scope, writeSource(args), offsetArg(args[1], scope), kw.index),
            ];
        }
        if (declared(vfs.constructor, name) === null)
            return [];
        if (!vfs.supports(name))
            return [];
        switch (name) {
            case 'readdir':
                return [(scope, _args, kw) => vfs.readdir(scope, handedOf(kw))];
            case 'stat':
                return [(scope, _args, kw) => vfs.stat(scope, handedOf(kw))];
            case 'pwrite':
                return [
                    (scope, args, kw) => this.pwriteSource(vfs, scope, writeSource(args), offsetArg(args[1], scope), handedOf(kw)),
                ];
            case 'create':
                return [(scope, _args, kw) => vfs.create(scope, handedOf(kw))];
            case 'mkdir':
                return [
                    (scope, _args, kw) => this.mkdir(scope, kw.parents === true, kw.index, handedOf(kw)),
                ];
            case 'unlink':
                return [(scope, _args, kw) => vfs.unlink(scope, handedOf(kw))];
            case 'rmdir':
                return [(scope, _args, kw) => vfs.rmdir(scope, handedOf(kw))];
            case 'rename':
                return [(scope, args, kw) => vfs.rename(scope, dstArg(args[0]), handedOf(kw))];
            case 'copy':
                return [(scope, args, kw) => vfs.copy(scope, dstArg(args[0]), callerSignal(), handedOf(kw))];
            case 'dir_copy':
                return [(scope, args, kw) => vfs.dirCopy(scope, dstArg(args[0]), handedOf(kw))];
            case 'rm_r':
                return [(scope, _args, kw) => vfs.rmR(scope, handedOf(kw))];
            case 'first_lines':
                return [(scope, args, kw) => vfs.firstLines(scope, args[0], handedOf(kw))];
            case 'last_lines':
                return [(scope, args, kw) => vfs.lastLines(scope, args[0], handedOf(kw))];
            case 'line_count':
                return [(scope, _args, kw) => vfs.lineCount(scope, handedOf(kw))];
            case 'checksum':
                return [(scope, args, kw) => vfs.checksum(scope, args[0], handedOf(kw))];
            case 'follow':
                return [
                    (scope, args, kw) => vfs.follow(scope, args[0], args[1], handedOf(kw)),
                ];
            case 'read_tree':
                return [(scope, _args, kw) => vfs.readTree(scope, handedOf(kw))];
            case 'search':
                return [(scope, args, kw) => vfs.search(scope, args[0], handedOf(kw))];
            case 'files_containing':
                return [
                    (scope, args, kw) => vfs.filesContaining(scope, args[0], { wholeWord: kw.whole_word === true, ignoreCase: kw.ignore_case === true }, handedOf(kw)),
                ];
            case 'lines_containing':
                return [
                    (scope, args, kw) => vfs.linesContaining(scope, args[0], { ignoreCase: kw.ignore_case === true }, handedOf(kw)),
                ];
            case 'before_full_walk':
                return [
                    (scope, _args, kw) => vfs.beforeFullWalk(scope, typeof kw.files === 'number' ? kw.files : null, handedOf(kw)),
                ];
            case 'truncate':
                return [
                    (scope, args, kw) => vfs.truncate(scope, lengthArg(args[0]), kw.no_create === true, handedOf(kw)),
                ];
            case 'setattr':
                return [(scope, _args, kw) => vfs.setattr(scope, kw, handedOf(kw))];
            default: {
                const method = vfs[name];
                return method === undefined ? [] : [(scope, args) => method.call(vfs, scope, ...args)];
            }
        }
    }
    /**
     * A write or append of bytes or a stream, handed to the VFS's `write` /
     * `append` as it came, an append by rewrite where it has none. Every
     * `write` is told `atomic`; one that collects first already lands whole.
     * Mirrors Python's `MountEntry._write_op` / `_append_op`.
     */
    async writeSource(vfs, path, data, append, kw) {
        try {
            if (!append) {
                return await vfs.write(path, data, callerSignal(), handedOf(kw), kw.atomic === true);
            }
            if (vfs.supports('append'))
                return await vfs.append(path, data, handedOf(kw), callerSignal());
            return await this.appendByRewrite(path, await materialize(data), kw.index);
        }
        finally {
            await closeQuietly(data);
        }
    }
    appendByRewrite(path, data, index = this.index) {
        return appendByRewrite((p) => this.readAt(index, p), (request) => this.replaceAt(index, request), (p) => this.statAt(index, p), path, data);
    }
    /**
     * A pwrite handed to the VFS's `pwrite`: bytes as they came, a stream a
     * window at a time at increasing offsets, so memory holds one window
     * whatever its length. Mirrors Python's `MountEntry._pwrite`.
     */
    async pwriteSource(vfs, path, data, offset, handed) {
        if (data instanceof Uint8Array)
            return vfs.pwrite(path, data, offset, handed, callerSignal());
        let at = offset;
        let result;
        for await (const window of rechunk(data, MEMORY_LIMIT)) {
            result = await vfs.pwrite(path, window, at, handed, callerSignal());
            at += window.byteLength;
        }
        if (at === offset) {
            result = await vfs.pwrite(path, new Uint8Array(), at, handed, callerSignal());
        }
        return result;
    }
    pwriteByRewrite(path, data, offset, index = this.index) {
        return pwriteByStreamedRewrite((p, spool) => this.readInto(index, p, spool), (request) => this.replaceAt(index, request), (p) => this.statAt(index, p), path, data, offset);
    }
    truncateByRewrite(path, length, noCreate, index = this.index) {
        return truncateByRewrite((p) => this.readAt(index, p), (request) => this.replaceAt(index, request), path, length, noCreate);
    }
    async replaceAt(index, request) {
        try {
            requireSupported(this.vfs.conditionSupport, this.vfs.name, request.path, 'put', request.condition);
            if (request.condition.kind === 'any' && !this.vfs.supports('_replace'))
                return await this.writeAt(index, request.path, request.body);
            return await this.vfs._replace(request, callerSignal(), await this.handedFor('write', request.path, null, index));
        }
        finally {
            await closeQuietly(request.body);
        }
    }
    /** The VFS's read of a whole file, handed what is at its path. */
    readAt(index, path) {
        return collected(this.answer(this.vfs, index, path), path, true, index.clock, this.vfs instanceof RAMVFS);
    }
    /**
     * Fill `spool` with the VFS's read of a whole file, answering the version
     * the read carried, as `readAt` publishes it. Mirrors Python's `_read_into`.
     */
    async readInto(index, path, spool) {
        const [found, version] = await readVersioned(path, () => this.answer(this.vfs, index, path));
        if (found instanceof Uint8Array) {
            await spool.fill(found);
            return version;
        }
        const [, records] = await recorded(() => spool.fill(found), index.clock);
        return latestFingerprint(records, path.virtual);
    }
    /**
     * What `vfs`'s read of a whole file answers, called with the stats it
     * declares. Mirrors Python's `_answer`.
     */
    async answer(vfs, index, path, signal) {
        return vfs.read(path, await this.handedFor('read', path, null, index), 0, null, signal);
    }
    /** The VFS's write of a whole file, handed what is at its path and the directory it goes in. */
    async writeAt(index, path, data) {
        return this.vfs.write(path, data, callerSignal(), await this.handedFor('write', path, null, index));
    }
    async mkdir(path, parents, index = this.index, handed) {
        await refuseTaken((p) => this.statAt(index, p), path, parents);
        if (parents && this.wants('mkdir').includes('parent'))
            return this.mkdirLevels(path, index);
        const made = await this.vfs.mkdir(path, parents, handed);
        // A directory made new holds nothing yet.
        if (!parents && made instanceof FileStat)
            await index.setDir(indexKey(this.prefix, path), []);
        return made;
    }
    /**
     * `mkdir -p` on a backend that makes a directory inside its parent's entry:
     * one level at a time, from the mount root down. The level above does not
     * exist yet when the one below needs it as its parent, so each level's
     * create answers its new stat and the index keeps it where the next level
     * looks for its parent, with its own listing, which is empty. A level that
     * is there already is passed, a directory or not: a file there is ENOTDIR
     * for the level below, as mkdir(2) finds. Mirrors Python's `_mkdir_levels`.
     */
    async mkdirLevels(path, index) {
        const names = path.vfsPath.split('/').filter((name) => name !== '');
        const root = rstripSlash(this.prefix);
        let made = undefined;
        for (let depth = 1; depth <= names.length; depth++) {
            const level = this.scope(`${root}/${names.slice(0, depth).join('/')}`);
            const key = indexKey(this.prefix, level);
            const found = await this.entry(level, index);
            if (found !== null) {
                if (found.type !== FileType.DIRECTORY)
                    throw enotdir(level);
                continue;
            }
            const parent = await this.entry(this.folder(level), index);
            made = await this.vfs.mkdir(level, false, parent === null ? {} : { parent });
            if (made instanceof FileStat) {
                await index.addEntry(key, made);
                await index.setDir(key, []);
            }
        }
        return made;
    }
    /** The index key of this mount's root, which no listing names. */
    /**
     * Whether the VFS lists, so the mount keeps its listings as stats. Mirrors
     * Python's `_lists`.
     */
    get lists() {
        return this.vfs.supports('readdir');
    }
    get rootKey() {
        return rstripSlash(this.prefix) || '/';
    }
    /**
     * The directory holding `scope`, keyed below this mount; a glob word stands
     * for the directory it lists, and the root holds itself.
     */
    folder(scope) {
        const target = scope.pattern !== null ? scope.dir : scope;
        if (indexKey(this.prefix, target) === this.rootKey)
            return target;
        const virtual = rstripSlash(target.virtual);
        return this.scope(virtual.slice(0, virtual.lastIndexOf('/')) || '/');
    }
    /** The stats the VFS's function `name` declares it is handed. */
    wants(name) {
        return this.vfs.handed[methodName(name)] ?? [];
    }
    /**
     * What is at `scope`, as its parent's listing names it, listing the parent
     * when no stored listing does. Under `fresh` the running command's check of
     * `scope` already asked the backend, so its answer stands in for the walk;
     * a check itself always walks. A miss right after a listing of the parent
     * landed, with no listing left now, says only that a check cleared the index
     * mid-lookup, so it is looked up once more. Mirrors Python's `_entry`.
     */
    async entry(scope, index) {
        const key = indexKey(this.prefix, scope);
        if (key === this.rootKey || !this.lists)
            return null;
        if (this.cacheManager !== null && !(index instanceof ListingCheckStore)) {
            const probed = this.cacheManager.probedStat(scope);
            if (probed !== null)
                return probed;
        }
        const listed = [];
        const fetched = new Map([[key, null]]);
        const warm = async () => {
            const names = await this.listed(this.folder(scope), index, fetched);
            listed.push(key);
            return names;
        };
        let row = (await entryAt(index, key, warm)) ?? fetched.get(key) ?? null;
        if (row === null &&
            listed.length > 0 &&
            fetched.has(key) &&
            (await index.listDir(folderOf(key), key)).status === LookupStatus.NOT_FOUND) {
            row = (await entryAt(index, key, warm)) ?? fetched.get(key) ?? null;
        }
        if (row !== null)
            return row;
        if (!fetched.has(key))
            return null;
        if (await beyondWindow(index, key))
            return this.beyond(scope, index);
        if (!(await this.unproven(index, key)))
            return null;
        if (this.asksByPath)
            return this.beyond(scope, index);
        await index.invalidateDir(folderOf(key));
        return (await entryAt(index, key, warm)) ?? fetched.get(key) ?? null;
    }
    /**
     * The row the stored listings answer a stat of `key` with, null for one the
     * backend must answer, a miss they do not prove (`unproven`) included.
     * Mirrors Python's `_stored_row`.
     */
    async storedRow(index, key) {
        return this.whole(await storedStat(index, key, (folder) => this.proves(index, folder)));
    }
    /**
     * Whether a complete listing of `folder` proves a name it leaves out absent:
     * always, but on a VFS that checks such a name again (`unproven`), only a
     * listing the running command fetched. Mirrors Python's `_proves`.
     */
    proves(index, folder) {
        const checks = this.asksByPath || this.vfs.supports('write');
        return !checks || index.listedThisCommand(folder);
    }
    /** Whether the VFS's stat asks by path, handed no stored row. */
    get asksByPath() {
        return this.lists && this.wants('stat').length === 0;
    }
    /**
     * Whether the stored listings leave `key` out without proving it absent
     * (`unprovenMiss`), on a VFS that checks such a name again: one whose stat
     * asks by path (one stat) or one that writes (its folder is listed again),
     * where a stale miss would let a write land over a file another client
     * made. Mirrors Python's `_unproven`.
     */
    async unproven(index, key) {
        return (this.asksByPath || this.vfs.supports('write')) && unprovenMiss(index, key);
    }
    /** Invalidate cached absences disproved by current backend answers. Mirrors Python's _observe. */
    async observe(index, scopes) {
        if (!this.lists || index instanceof ListingCheckStore)
            return;
        const paths = new Set();
        for (const scope of scopes) {
            if (this.revisions.has(scope.virtual) || this.livePipe(scope.virtual) !== null)
                continue;
            let key = indexKey(this.prefix, scope);
            while (key !== this.rootKey && key !== '/') {
                paths.add(key);
                key = folderOf(key);
            }
        }
        if (paths.size > 0) {
            try {
                await index.invalidateMissing([...paths].sort(compareCodePoints));
            }
            catch (err) {
                console.warn(`listing observation not kept for ${this.prefix}`, err);
            }
        }
    }
    /** Observe existence once without pulling beyond the consumer's demand. */
    async *observeStream(scope, index, source) {
        let observed = false;
        try {
            for await (const chunk of source) {
                if (chunk.byteLength > 0 && !observed) {
                    await this.observe(index, [scope]);
                    observed = true;
                }
                yield chunk;
            }
            if (!observed)
                await this.observe(index, [scope]);
        }
        finally {
            await closeQuietly(source);
        }
    }
    /**
     * What is at a path its folder's stored listing leaves out without proving
     * it absent (a window, or a listing another command fetched): the backend's
     * stat of it, handed what is at its parent; null when nothing is. Mirrors
     * Python's `_beyond`.
     */
    async beyond(scope, index) {
        const handed = {};
        if (this.wants('stat').includes('parent')) {
            handed.parent = await this.entry(this.folder(scope), index);
        }
        const generation = this.cacheManager?.generation;
        try {
            const row = await this.vfs.stat(scope, handed);
            if (this.wants('stat').length === 0)
                await this.observe(index, [scope]);
            this.noteLive(scope, index, row, generation);
            return row;
        }
        catch (err) {
            if (!isEnoent(err))
                throw err;
            console.debug(`nothing unlisted at ${scope.virtual}: ${String(err)}`);
            return null;
        }
    }
    /**
     * A directory's children, from its stored listing or listed through the
     * backend and kept. Finding the rows a listing is handed lists its parent,
     * whose fetch may have proved this listing too (a seed). Mirrors Python's
     * `_listed`.
     */
    async listed(path, index, fetched) {
        const key = indexKey(this.prefix, path);
        const glob = path.pattern !== null;
        const stored = await storedListing(index, key, glob);
        if (stored !== null)
            return stored;
        const handed = await this.handedFor('readdir', path, null, index);
        const seeded = await storedListing(index, key, glob);
        if (seeded !== null)
            return seeded;
        const refill = await index.refillToken();
        const listed = await this.vfs.readdir(path, handed);
        return keepListing(index, key, listed, refill, fetched);
    }
    /**
     * One level of a subtree read: the directory's listing, as `readdir`
     * answers it, each child keyed by its name with the row its `stat` answers
     * from that listing. Null where a stat does not come from the listing: a
     * mount that keeps none, or whose index keeps no listing live (a zero ttl),
     * one under `read: fresh`, whose stats check the backend, a listing the
     * index does not hold live once listed, and a child whose row a stat would
     * finish (`whole`). A listing cut at the walk's limit answers for the
     * children it names, as their stats do. Mirrors Python's `_level`.
     */
    async level(virtual, limit, access) {
        const index = this.index;
        if (!this.lists || this.read.policy === ReadPolicy.FRESH || index.ttl <= 0)
            return null;
        const names = (await this.call('readdir', virtual, [], limit === undefined ? {} : { limit }, access));
        const listing = await index.listDir(indexKey(this.prefix, this.scope(virtual)));
        const held = listing.entries ?? listing.partialEntries ?? null;
        if (held === null)
            return null;
        const live = new Set(held);
        const rows = await index.getMany(names);
        const found = new Map();
        for (const child of names) {
            const row = live.has(child) ? this.whole(rows.get(child) ?? null) : null;
            if (row === null)
                return null;
            found.set(child.slice(child.lastIndexOf('/') + 1), row);
        }
        return found;
    }
    /** A stat of `path` on this mount, as `callKeyed` answers one. Mirrors Python's `_stat`. */
    async statAt(index, path) {
        const stored = (await this.stored('stat', path, index));
        if (stored !== null)
            return stored;
        return this.vfs.stat(path, await this.handedFor('stat', path, null, index));
    }
    /**
     * The stats the VFS's function `name` declares, each found in the stored
     * listings. Mirrors Python's `_handed`.
     */
    async handedFor(name, scope, dst, index) {
        const wants = this.wants(name);
        const handed = {};
        if (wants.includes('index')) {
            handed.index = index;
            // Under a fresh check such a function answers from the check's hints:
            // walking for its stats would cost the requests it saves.
            if (index instanceof ListingCheckStore)
                return handed;
        }
        for (const stat of HANDED) {
            if (!wants.includes(stat))
                continue;
            if (stat === 'entry')
                handed.entry = await this.entry(scope, index);
            else if (stat === 'parent')
                handed.parent = await this.entry(this.folder(scope), index);
            else if (dst !== null) {
                const there = this.scope(dst.virtual);
                if (stat === 'dstEntry')
                    handed.dstEntry = await this.entry(there, index);
                else
                    handed.dstParent = await this.entry(this.folder(there), index);
            }
        }
        return handed;
    }
    /**
     * Run one level with the stats its function declares. A stat a stored
     * listing handed in may be stale: the backend answering ENOENT for it drops
     * the listings it came from, which lists them again, and the level runs once
     * more. A level handed nothing is not run again, nor a stat the listings
     * found nothing at: the backend agrees with them. `found` is the stats
     * already found for this level, null to find them here. Mirrors Python's
     * `_run`.
     */
    async run(name, call, scope, args, kwargs, index, found = null) {
        // A rename's or a copy's destination is its first argument; a function
        // is handed a stat there only where it declares one.
        const dst = args[0] instanceof PathSpec ? args[0] : null;
        const handed = found ?? (await this.handedFor(name, scope, dst, index));
        try {
            return await call(scope, args, { ...kwargs, handed });
        }
        catch (err) {
            if (!isEnoent(err) ||
                !HANDED.some((stat) => handed[stat] != null) ||
                (name === 'stat' && handed.entry == null))
                throw err;
            console.debug(`${name} ${scope.virtual} missed its handed rows, asking again: ${String(err)}`);
        }
        await this.forget(scope, dst, index);
        const again = await this.handedFor(name, scope, dst, index);
        return call(scope, args, { ...kwargs, handed: again });
    }
    /** Drop the listings a stale stat was handed from. Mirrors Python's `_forget`. */
    async forget(scope, dst, index) {
        const paths = [scope];
        if (dst !== null)
            paths.push(this.scope(dst.virtual));
        for (const path of paths) {
            const key = indexKey(this.prefix, path);
            await index.invalidateDir(folderOf(key));
            await index.invalidateDir(key);
        }
    }
    /**
     * A listing or a stat the stored listings answer, null for one the backend
     * must. Mirrors Python's `_stored`.
     */
    async stored(name, scope, index) {
        const key = indexKey(this.prefix, scope);
        if (name === 'readdir')
            return storedListing(index, key, scope.pattern !== null);
        if (name === 'read_tree')
            return this.namesOnly ? null : storedTree(index, key);
        if (key === this.rootKey)
            return null;
        const row = await this.storedRow(index, key);
        if (row === null || !this.namesOnly)
            return row;
        return this.cacheManager?.listingTrusted(folderOf(key)) === true ? row : null;
    }
    /**
     * A stored row that answers a stat whole, null for one the backend must
     * finish: a file with no size, on a VFS whose stat sizes every file (chroma
     * sizes a folder's pages on its first stat). Mirrors Python's `_whole`.
     */
    whole(row) {
        if (row !== null &&
            row.size === null &&
            row.type === FileType.FILE &&
            this.vfs.sizesAlwaysKnown) {
            return null;
        }
        return row;
    }
    /**
     * Whether a kept listing vouches for its children's names only. A folder's
     * version moves when its names change, not when a child is edited in place,
     * so under `fresh` a mount whose listings are checked by folder answers a
     * child's stat from its listing only when the running command listed it.
     * Mirrors Python's `_names_only`.
     */
    get namesOnly() {
        return (this.read.policy === ReadPolicy.FRESH && this.vfs.listingVersion === ListingVersion.FOLDER);
    }
    /**
     * The VFS's read of `scope` as a stream, called on the first pull and
     * handed the stats it declares. Mirrors Python's `_streamed`.
     */
    async *streamed(vfs, scope, index, signal) {
        yield* asStream(this.answer(vfs, index, scope, signal));
    }
    /**
     * Run `name` on this mount's VFS over a scope already keyed below the
     * mount: each level in turn until one answers with something other than
     * null. A read resolves its renderer by `kwargs.filetype` when the caller
     * names one (null asks for the stored bytes) and by the path's extension
     * otherwise. A VFS with no `read` of its own (gdocs, gsheets, gslides)
     * stores the rendering, so its stored bytes are the renderer's. A
     * `read_tree` of one level (`level: true`) is the mount's own listing with
     * each child's row (`level`).
     */
    async callKeyed(name, scope, args = [], kwargs = {}, access) {
        if (name === 'read_tree' && kwargs.level === true) {
            return this.level(scope.virtual, kwargs.limit, access);
        }
        const named = kwargs.filetype === undefined ? getExtension(scope.virtual) : kwargs.filetype;
        const filetype = named === null && !this.vfs.supports('read') ? getExtension(scope.virtual) : named;
        const vfs = this.viewFor(access);
        const levels = this.callers(name, filetype, vfs);
        if (levels.length === 0) {
            // A copy only reads its source, so the path it is refused at is the
            // destination it would have made.
            const dst = args[0];
            const copies = declared(this.vfs.constructor, name)?.effect === Effect.COPY;
            throw enotsup(this.vfs.name, name, copies && dst instanceof PathSpec ? dst : scope);
        }
        taken(name, kwargs);
        this.requireWritable(name, scope, [...args, ...Object.values(kwargs)], access);
        const index = kwargs.index ?? this.index;
        const answer = async () => {
            let found = null;
            if (this.lists && STORED_OPS.has(name)) {
                let stored = await this.stored(name, scope, index);
                if (stored === null && name === 'readdir') {
                    // Finding the rows a listing is handed lists its parent, whose
                    // fetch may have proved this listing too (a seed).
                    found = await this.handedFor(name, scope, null, index);
                    stored = await this.stored(name, scope, index);
                }
                if (stored !== null)
                    return stored;
            }
            for (const call of levels) {
                const refill = this.lists && (name === 'readdir' || name === 'read_tree')
                    ? await index.refillToken()
                    : null;
                const generation = this.cacheManager?.generation;
                const result = await this.run(name, call, scope, args, kwargs, index, found);
                found = null;
                if (result === null || result === undefined)
                    continue;
                const kept = this.lists ? await this.kept(name, scope, index, result, refill) : result;
                if (name === 'stat') {
                    if (kept instanceof FileStat && this.wants('stat').length === 0) {
                        await this.observe(index, [scope]);
                    }
                    this.noteLive(scope, index, kept, generation);
                }
                return kept;
            }
            return null;
        };
        // One listing of a folder at a time: a reader arriving while another
        // lists it waits, then answers from what that one kept.
        if (name === 'readdir' && this.lists) {
            return indexLock(index, indexKey(this.prefix, scope), answer);
        }
        return answer();
    }
    /**
     * Keep a current stat the backend answered as the running command's
     * probe answer: the cache gate would ask the backend the same question
     * again. Only a stat that asked the backend counts: one that takes neither
     * an index of its own nor a row the mount hands it, either of which a
     * listing filled. A probe's own store records through the probe. Mirrors
     * Python's `_note_live`.
     */
    noteLive(scope, index, result, generation) {
        const manager = this.cacheManager;
        if (manager !== null &&
            manager.generation === generation &&
            result instanceof FileStat &&
            !(index instanceof ListingCheckStore) &&
            this.lists &&
            this.wants('stat').length === 0) {
            manager.noteProbed(scope, result);
        }
    }
    /**
     * What a listing or a subtree read answered, kept as listings; a listing
     * answers its children's paths once kept. Mirrors Python's `_kept`.
     */
    async kept(name, scope, index, result, refill = null) {
        const key = indexKey(this.prefix, scope);
        if (name === 'readdir') {
            return keepListing(index, key, result, refill);
        }
        if (name === 'read_tree' && result instanceof Map) {
            const tree = result;
            await keepTree(index, key, tree, refill);
            return new Map([...tree].filter(([path]) => path !== ''));
        }
        return result;
    }
    /**
     * The VFS's streamed read of a scope keyed below the mount, framed for later
     * pulls. The backend runs on each pull, after this frame is gone, so every
     * pull gets what a call gets around it: the caller's context (session,
     * recorder, cache scope), the mount's recording context, its write context
     * (a read hands its token on only where a write will send it) and its
     * revision pins. The mount is held until the stream ends or is closed. A read that
     * is neither async nor handed stats is called as the stream opens, so what
     * it binds (a pipe) is bound before the path can be released. Mirrors
     * Python's `read_stream`.
     */
    readStream(scope, access) {
        const vfs = this.viewFor(access);
        if (this.retiring)
            throw ebusy(this.prefix);
        const caller = this.onMount(currentCaller());
        const signal = callerSignal();
        const stream = this.wants('read').length > 0 || vfs.read.constructor.name === 'AsyncFunction'
            ? this.streamed(vfs, scope, this.index, signal)
            : asStream(runWithCaller(caller, async () => vfs.read(scope, {}, 0, null, signal)));
        return this.activity.hold(withEgress(withCaller(this.observeStream(scope, this.index, stream), caller)));
    }
    resolveCascade(name, extension, table, general) {
        const levels = [];
        if (extension !== null && extension !== '') {
            const specific = table.get(cmdKey(name, extension));
            if (specific !== undefined)
                levels.push(specific);
        }
        const byVfs = table.get(cmdKey(name, null));
        if (byVfs !== undefined)
            levels.push(byVfs);
        const generalEntry = general.get(name);
        if (generalEntry !== undefined)
            levels.push(generalEntry);
        return levels;
    }
    // ── execution ─────────────────────────────────────
    /**
     * What a write through this mount must carry, null when its writes are
     * unconditional. Bound by both entry points whatever the policy, so an
     * unconditional mount clears a context an outer command's mount set.
     * Mirrors Python's `MountEntry.write_context`.
     */
    writeContext() {
        const manager = this.cacheManager;
        if (this.write !== WritePolicy.CONDITIONAL || manager === null)
            return null;
        // One object per manager, so this mount's frames compare equal.
        if (this.writeContextFor?.manager !== manager) {
            const vfs = this.vfs;
            this.writeContextFor = {
                manager,
                context: {
                    vfs: vfs.name,
                    // Judged at each write: an S3 mount with no endpoint reads the environment.
                    get conditions() {
                        return writeConditions(vfs);
                    },
                    get creates() {
                        return createConditions(vfs);
                    },
                    readVersion: (path) => manager.readVersion(path),
                    readVersions: (paths) => manager.readVersions(paths),
                    drop: (path) => manager.invalidate(path, FileChangeKind.UPDATE),
                    keep: (path, version) => manager.keepVersion(path, version),
                    observe: (record) => {
                        manager.observe(record);
                    },
                    lose: (path, version) => {
                        manager.lose(path, version);
                    },
                },
            };
        }
        return this.writeContextFor.context;
    }
    /**
     * `caller` running on this mount: its records name this mount, its writes
     * are checked against this mount's write context, its reads keep this
     * mount's revision pins and a connection it opens names this mount.
     * Mirrors Python's `MountEntry._on_mount`.
     */
    onMount(caller) {
        return {
            ...caller,
            recording: caller.recording === null ? null : { ...caller.recording, mountId: this.mountId },
            write: { prefix: this.prefix, context: this.writeContext() },
            revisions: { map: this.revisions.size > 0 ? this.revisions : null },
            vfsMount: this.prefix,
        };
    }
    async runCommand(cmdName, paths, texts, flags, ctx = {}) {
        return this.use(async () => {
            const handlers = await this.pickHandlers(cmdName, paths, ctx);
            if (handlers.length === 0) {
                return [
                    null,
                    new IOResult({
                        exitCode: NOT_FOUND_STATUS,
                        stderr: encodeText(`${cmdName}: command not found`),
                    }),
                ];
            }
            const probe = walkProbeOf(ctx);
            const access = ctx.session?.access();
            return this.inCommandScope(probe, async () => {
                for (const cmd of handlers) {
                    const user = this.isUserCommand(cmd);
                    const operands = user ? this.keyedPaths(cmdName, paths) : paths.map(virtualKey);
                    const runCtx = user
                        ? this.commandCtx(this.keyedFlags(flags), ctx, this.prefix)
                        : this.commandCtx(virtualFlags(flags), ctx, '');
                    const refusal = this.commandRefusal(cmdName, cmd, flags, access);
                    if (refusal !== null)
                        return [null, refusal];
                    const result = await this.runOne(cmdName, cmd, operands, texts, { ...runCtx, spec: namedSpec(cmd, cmdName) }, access, paths);
                    if (result !== null)
                        return result;
                }
                return [null, new IOResult()];
            });
        });
    }
    /**
     * The handlers to try in order.
     *
     * A filetype handler is selected from the operand's NAME, and a
     * directory can carry any extension, so the cascade would hand a
     * renderer a directory to read. One stat settles it, and only when a
     * handler for this exact extension exists, so a mount with no filetype
     * registrations never reaches the probe. The built-in is what a
     * directory should get: it owns GNU's `Is a directory` wording, and the
     * renderer owns nothing but its own format. The DISPATCHER's stat, not
     * the backend's, so a mount root and a namespace-only directory answer
     * too; null means neither plane saw anything, in which case the renderer
     * reports its own miss. Mirrors Python's MountEntry._pick_handlers.
     */
    async pickHandlers(cmdName, paths, ctx) {
        let extension = paths.length > 0 && paths[0] !== undefined ? getExtension(paths[0].virtual) : null;
        const first = paths[0];
        const statPath = vfsStatPath(ctx.vfs);
        if (extension !== null &&
            extension !== '' &&
            first !== undefined &&
            statPath !== undefined &&
            this.cmds.has(cmdKey(cmdName, extension))) {
            const entry = await statPath(first);
            if (entry !== null && entry.type === FileType.DIRECTORY)
                extension = null;
        }
        return this.resolveCascade(cmdName, extension, this.cmds, this.generalCmds);
    }
    /** `p` with this mount's backend key stamped on. */
    keyed(p) {
        return new PathSpec({
            virtual: p.virtual,
            directory: p.directory,
            pattern: p.pattern,
            resolved: p.resolved,
            vfsPath: mountKey(p.virtual, rstripSlash(this.prefix)),
            rawPath: p.rawPath,
            dotted: p.dotted,
            walkError: p.walkError,
        });
    }
    /**
     * Stamp this mount's backend key onto each path operand, for a handler that
     * addresses its backend through the mount's accessor (a user command, a
     * command tree's leaf); mirage's own commands get every path keyed by its
     * whole virtual path (`virtualKey`) and reach it through their files. A
     * stdin `-` routed nowhere, so it rides on whichever mount runs the line,
     * beside the operands that chose it. Mirrors Python's
     * MountEntry._keyed_paths.
     */
    keyedPaths(cmdName, paths) {
        const mountPrefix = rstripSlash(this.prefix);
        const stdinSlots = STDIN_DASH_COMMANDS.has(cmdName)
            ? (STDIN_DASH_LEADING.get(cmdName) ?? paths.length)
            : 0;
        return paths.map((p, index) => index < stdinSlots && p.rawPath === '-'
            ? new PathSpec({
                virtual: `${mountPrefix}/-`,
                directory: p.directory,
                resolved: p.resolved,
                vfsPath: '-',
                rawPath: p.rawPath,
            })
            : this.keyed(p));
    }
    /**
     * Stamp this mount's backend key onto path-shaped flag values so backend
     * reads can address them: a single PathSpec (awk -f, tar -f) or a list
     * (repeated grep -f, jq's --rawfile pairs). Everything else passes
     * through unchanged. Mirrors Python's MountEntry._keyed_flags.
     */
    keyedFlags(flags) {
        const stampedFlags = { ...flags };
        flagOccurrences(stampedFlags).push(...flagOccurrences(flags));
        for (const [key, value] of Object.entries(flags)) {
            if (value instanceof PathSpec)
                stampedFlags[key] = this.keyed(value);
            else if (Array.isArray(value) && value.some((item) => item instanceof PathSpec)) {
                const items = value;
                stampedFlags[key] = items.map((item) => item instanceof PathSpec ? this.keyed(item) : item);
            }
        }
        return stampedFlags;
    }
    /**
     * The one typed bag a handler reads, built here and nowhere else.
     *
     * A pattern operand travels to the handler whole. The handler resolves it
     * once, through its files, which is where the namespace facts (links,
     * nested mount roots, a trailing slash) are in view, so expanding here
     * would lose what the handler needs. Python's dispatcher never expands
     * either. The working directory is keyed below `prefix`: this mount's for a
     * handler `keyedPaths` keys the operands of, empty for mirage's own
     * commands, whose paths keep their whole virtual path. Mirrors Python's
     * MountEntry._command_ctx.
     */
    commandCtx(flags, ctx, mountPrefix) {
        const prefix = rstripSlash(mountPrefix);
        const cwd = (ctx.cwd ?? ROOT_CWD).virtual;
        return {
            ...ctx,
            // Said on the files, not only bound in the async context, so a read of
            // this run's mount after an await answers for this run.
            ...(ctx.vfs !== undefined ? { vfs: ctx.vfs.servedBy([this.prefix, this.mode]) } : {}),
            stdin: ctx.stdin ?? null,
            flags,
            cwd: new PathSpec({
                virtual: cwd,
                directory: cwd,
                vfsPath: mountKey(cwd, prefix),
                resolved: false,
            }),
        };
    }
    /**
     * Run `fn` with what a handler's backend calls read bound: the mode the
     * command tier's mode guard holds each write to (its own region's mode),
     * what the command tier's walk guard proves an operand's `.` and `..`
     * with, the recorder's mount, the mount's cache manager and write context,
     * and the snapshot revision pins. Mirrors Python's MountEntry._command_scope.
     */
    inCommandScope(probe, fn) {
        const outer = currentCaller();
        return runWithCaller({
            ...this.onMount(outer),
            cache: { manager: this.cacheManager },
            mount: [this.prefix, this.mode],
            walkProbe: probe === null ? outer.walkProbe : [this.prefix, probe],
        }, fn);
    }
    /**
     * Refuse a user handler on a hidden mount, or a writer on a read-only one.
     *
     * A user handler can reach its accessor directly, so a hidden mount's
     * handler must never run, even when an explicit operand or cwd selected it.
     * It answers an unknown command without revealing the hidden mount's name.
     *
     * Mirage's own commands write through the dispatcher, which refuses each
     * write in the command's own GNU voice, because only the write knows
     * whether a line writes: `gzip -c`, `tar -t` and `split -n 1/2` read a
     * read-only mount like any reader, and `gzip f` is refused at the write of
     * `f.gz`. A user command (`isUserCommand`) may reach its service some other
     * way, which no gate would see, so one that writes is refused here, before
     * it runs. strongestModeUnder, not effectiveMode: a mount whose only
     * writable region is a show entry still runs it. Only wrapper-owned
     * responses (an injected help or version) bypass it: a handler that
     * declares `--help` itself is handed the flag and runs. The trailing
     * newline is load-bearing: stderr accumulates across a line. `served` is
     * the command this mount serves `cmd` under, which says whether it is a
     * user command: a tree's root for one of its leaves, which is built for its
     * one run and never registered. Mirrors Python's
     * MountEntry._command_refusal.
     */
    commandRefusal(cmdName, cmd, flags, access, served) {
        if (this.isUserCommand(served ?? cmd) && !pathVisible(sessionVisibility(access), this.prefix)) {
            return new IOResult({
                exitCode: NOT_FOUND_STATUS,
                stderr: encodeText(`${cmdName}: command not found\n`),
            });
        }
        const infoOnly = (flags.help === true && hasInjectedHelp(cmd.spec)) ||
            (flags.version === true && hasInjectedVersion(cmd.spec));
        if (cmd.write &&
            this.isUserCommand(served ?? cmd) &&
            !infoOnly &&
            strongestModeUnder(this.prefix, this.mode, access) === MountMode.READ) {
            return new IOResult({
                exitCode: 1,
                stderr: encodeText(`${cmdName}: read-only mount at ${this.prefix}\n`),
            });
        }
        return null;
    }
    /**
     * Whether `cmd` is a user command: one this mount serves that is not
     * mirage's own, registered through `registerCommands` (a VFS's handed
     * commands among them) rather than as a builtin. Mirrors Python's
     * MountEntry._is_user_command.
     */
    isUserCommand(cmd) {
        const key = cmdKey(cmd.name, cmd.filetype);
        return this.userKeys.has(key) && this.cmds.get(key) === cmd;
    }
    /**
     * Run one handler through the runner a CLI leaf shares, in this mount's
     * voice. `typed` are the operands as the line typed them, which a file
     * error names. A user
     * command that declares a write reaches its backend through its own
     * handle, past the dispatcher's per-path invalidation, so this mount drops
     * its caches once it has run; mirage's own commands write through the
     * dispatcher and drop nothing. Mirrors Python's MountEntry._run_one.
     */
    async runOne(cmdName, cmd, paths, texts, ctx, access, typed) {
        const accessor = this.viewFor(access).accessor;
        return this.runIn(cmdName, ctx, mountVoice(cmdName, typed), { command: cmdName, prefixes: [this.prefix], declared: cmd.limit ?? null }, cmd.write && this.isUserCommand(cmd), 
        // Deferred, so a handler that throws before it returns a promise is
        // still the runner's to word and to drop the caches after.
        (runCtx) => Promise.resolve().then(() => cmd.fn(accessor, paths, texts, runCtx)));
    }
    /**
     * Run one leaf of a command tree this mount serves.
     *
     * The executor walked the line and parsed the leaf's words, as it does an
     * installed CLI's; the leaf runs here as a command does: on this mount's
     * accessor, in its scope, under the read-only refusal for its own `write`
     * and, being a user command that holds the raw handle, the refusal a
     * restricting session gets. Its failures speak in a CLI's voice. Mirrors
     * Python's `MountEntry.run_leaf`.
     */
    async runLeaf(prog, cli, handler, paths, texts, ctx) {
        const root = this.resolveCommand(cli.spec.name);
        const fn = handler.fn;
        if (fn === null || root === null)
            throw new Error(`${prog}: no command tree serves it here`);
        const leaf = new Command({
            name: prog,
            spec: ctx.spec ?? cli.spec,
            vfs: null,
            filetype: null,
            fn,
            write: handler.write,
            limit: handler.limit,
        });
        return this.use(async () => {
            const keyedPaths = this.keyedPaths(prog, paths);
            const probe = walkProbeOf(ctx);
            const access = ctx.session?.access();
            const flags = this.keyedFlags(ctx.flags);
            const runCtx = this.commandCtx(flags, ctx, this.prefix);
            return this.inCommandScope(probe, async () => {
                // No flag here is answered for the leaf: walkLeaf answered the
                // tree's own help, and the leaf is handed the rest.
                const refusal = this.commandRefusal(prog, leaf, {}, access, root);
                if (refusal !== null)
                    return [null, refusal];
                const accessor = this.viewFor(access).accessor;
                return this.runIn(prog, runCtx, cliVoice(prog), { command: prog, prefixes: [this.prefix], declared: handler.limit }, handler.write && this.isUserCommand(root), (leafCtx) => answered(Promise.resolve().then(() => fn(accessor, keyedPaths, texts, leafCtx))));
            });
        });
    }
    /**
     * Run a handler body through the shared runner with what this mount binds
     * around it. The runner abandons the promise, it cannot cancel it; the
     * context's signal (a background job's kill channel, the line's deadline)
     * lets a runtime kill what it spawned (python cancels the task instead).
     */
    runIn(name, ctx, voice, producer, writes, call) {
        // No hold past an abort: an unmount waits on this mount's activity
        // with no grace, so a body, or a renderer of the rows it handed over,
        // that ignores the abort is let go rather than waited on. The command's
        // walks resolve their walkLimit with its own declared limit.
        return runWithCaller({ ...currentCaller(), declared: producer.declared }, () => runHandler(call(ctx), name, producer, voice, {
            scope: (result) => wrapMountStreams(result, this.activity),
            ...(writes ? { drop: () => this.dropCaches() } : {}),
            ...(ctx.signal !== undefined ? { signal: ctx.signal } : {}),
            ...(ctx.vfs !== undefined ? { vfs: ctx.vfs } : {}),
        }));
    }
    /**
     * Drop what this mount keeps after a write that went past the dispatcher:
     * its listings and bodies (`CacheManager.drop`), and the answers its client
     * keeps of its own (`Accessor.forget`). Mirrors Python's
     * `MountEntry.drop_caches`.
     *
     * @param bodies drop the bodies too; false when the registry clears the
     *   whole file cache itself.
     */
    async dropCaches(bodies = true) {
        if (this.cacheManager !== null)
            await this.cacheManager.drop(bodies);
        else
            await this.indexStore.invalidate();
        this.accessor.forget?.();
    }
    /** The client this mount's VFS reaches its backend with. */
    get accessor() {
        return this.vfs.accessor;
    }
    /**
     * Run an op on this mount's VFS. A read tries a rendered filetype's
     * renderer first, then the VFS's own function; the first answer that is
     * not null wins. A caller may name the filetype, and null asks for the
     * stored bytes even where the VFS renders the filetype: a
     * read-modify-write hands whatever it read straight back to `write`,
     * which always stores, so reading a rendered form would store the
     * rendering over the file. Mirrors Python's `MountEntry.call`.
     */
    async call(name, path, args = [], kwargs = {}, access) {
        return this.use(async () => {
            const mountPrefix = rstripSlash(this.prefix);
            const lastSlash = path.lastIndexOf('/');
            const scope = new PathSpec({
                virtual: path,
                directory: lastSlash > 0 ? path.slice(0, lastSlash + 1) : '/',
                vfsPath: mountKey(path, mountPrefix),
            });
            const effectiveKwargs = {
                ...kwargs,
                ...(kwargs.index === undefined ? { index: this.index } : {}),
            };
            return this.framed(async () => (await this.callKeyed(name, scope, args, effectiveKwargs, access)) ?? null);
        });
    }
    /** `path` as this mount's VFS addresses it. Mirrors Python's _scope. */
    scope(path) {
        const lastSlash = path.lastIndexOf('/');
        return new PathSpec({
            virtual: path,
            directory: lastSlash > 0 ? path.slice(0, lastSlash) : '/',
            vfsPath: mountKey(path, rstripSlash(this.prefix)),
        });
    }
    /**
     * Whether this mount's store may copy `src` from `source`'s, store to
     * store. Not when this mount's VFS cannot copy from that one, nor when the
     * store's object is not what a move's read and write would carry: a cached,
     * pinned or rendered source reads other bytes, and a conditional mount on
     * either end holds a version for the write or for `mv`'s delete. Asked
     * before the copy's deadline starts. Mirrors Python's `can_copy_from`.
     */
    async canCopyFrom(source, src) {
        return !(!this.vfs.supports('copyFrom') ||
            this.write === WritePolicy.CONDITIONAL ||
            source.write === WritePolicy.CONDITIONAL ||
            source.revisions.has(src) ||
            source.renders(getExtension(src)) ||
            (source.cacheManager !== null && (await source.cacheManager.isCached(source.scope(src)))));
    }
    /**
     * Copy a file from `source`'s store into this mount's, store to store.
     * For `cp` and `mv` between two mounts,
     * once the dispatcher has judged `src` as a read and `dst` as a create and
     * `canCopyFrom` has said yes. The VFS rejects EXDEV when it cannot make the
     * copy after all, and the caller moves the bytes itself. `onCall` receives
     * the store's own call once it starts.
     * Mirrors Python's `copy_from`.
     */
    async copyFrom(source, src, dst, onCall) {
        await this.use(() => source.use(() => this.framed(() => {
            this.requireWritable('write', PathSpec.fromStrPath(dst), []);
            const pending = this.vfs.copyFrom(source.vfs, source.scope(src), this.scope(dst));
            onCall?.(pending);
            return pending;
        })));
    }
    /**
     * Read several whole files on this mount in one request. For the
     * dispatcher's read-ahead, once it has walked and
     * followed each path; keyed by virtual path, a file left out is read on its
     * own later. Mirrors Python's `read_many`.
     */
    async readMany(paths, limit) {
        return this.use(() => this.framed(() => this.batchRead(paths.map((p) => this.scope(p)), limit)));
    }
    /**
     * Run one backend call as `call` frames its own: as the caller on this
     * mount (`onMount`), an answer that streams wrapped to keep that frame and
     * `owner`'s egress. Public for the dispatcher, which keys and calls its ops
     * itself. Mirrors Python's `_framed`.
     */
    framed(work, owner) {
        return runWithCaller(this.onMount(currentCaller()), async () => wrapStream(await work(), this.mountId, this.activity, owner));
    }
    /**
     * The VFS's batch read, handed what is at each path and at its folder as
     * the stored listings name them, where it declares them. Mirrors Python's
     * `_read_many`.
     */
    async batchRead(scopes, limit) {
        const wants = this.wants('readMany');
        const index = this.index;
        const handed = [];
        for (const scope of scopes) {
            const one = {};
            if (wants.includes('entry'))
                one.entry = await this.entry(scope, index);
            if (wants.includes('parent'))
                one.parent = await this.entry(this.folder(scope), index);
            handed.push(one);
        }
        const result = await this.vfs.readMany(scopes, limit, handed);
        await this.observe(index, scopes.filter((scope) => result.has(scope.virtual)));
        return result;
    }
    /**
     * Rank several scopes on this mount as one query. For the dispatcher, once it has walked, followed and
     * admitted each scope as it does a `search`'s one. Mirrors Python's
     * `search_many`.
     */
    async searchMany(paths, query) {
        return this.use(() => this.framed(() => this.vfs.searchMany(paths.map((p) => this.scope(p)), query)));
    }
}
/**
 * Preserve a streaming operation's recording owner, caller and egress after
 * its dispatch frame exits.
 */
function wrapStream(result, mountId, activity, owner) {
    if (result !== null && typeof result === 'object' && Symbol.asyncIterator in result) {
        // The backend's own iterator, which the close reaches first: a pull
        // waiting inside it ends only once it is told.
        const iterator = result[Symbol.asyncIterator]();
        const caller = currentCaller();
        const kept = withCaller(withMountContext({ [Symbol.asyncIterator]: () => iterator }, mountId), caller);
        // The close, a cleanup request it may send among it, runs as the
        // stream's own caller, whoever closes it and however late.
        return activity.hold(withEgress(new OutputStream(kept[Symbol.asyncIterator](), awaitsAs(caller, () => closeQuietly(iterator))), owner));
    }
    return result;
}
// Keep the command's caller on the stream the command handed back, so a
// deferred backend read records under the mount and reads the revisions its
// command bound, as an eager one does. Called inside the mount's command
// scope. Mirrors python's _wrap_mount_streams.
function wrapMountStreams(result, activity) {
    const [stream, io] = result;
    if (stream === null || stream instanceof Uint8Array)
        return [stream, io];
    return [activity.hold(withCaller(stream, currentCaller())), io];
}
function sortFiletypeMap(m) {
    const out = {};
    for (const k of [...m.keys()].sort(compareCodePoints)) {
        const list = m.get(k) ?? [];
        list.sort((a, b) => {
            const aKey = a === null ? 0 : 1;
            const bKey = b === null ? 0 : 1;
            if (aKey !== bKey)
                return aKey - bKey;
            const as = a ?? '';
            const bs = b ?? '';
            return compareCodePoints(as, bs);
        });
        out[k] = list;
    }
    return out;
}
//# sourceMappingURL=mount.js.map