UNPKG

@struktoai/mirage-core

Version:

A Unified Virtual Filesystem For AI Agents

826 lines • 42.5 kB
import { type FileCacheMixin } from '../../cache/file/mixin.ts'; import { type CallerDispatchFn } from '../../context/session_context.ts'; import type { LostPaths, OpRecord } from '../../observe/record.ts'; import type { Decisions } from '../../policy/decisions.ts'; import { Policies } from '../../policy/index.ts'; import { type CacheFacts } from '../../types.ts'; import type { BaseVFS } from '../../vfs/base.ts'; import type { SessionAccess } from '../../view/types.ts'; import type { Namespace } from '../mount/namespace/namespace.ts'; import { Reconciler } from '../reconcile.ts'; import type { DriftQueue } from '../snapshot/drift.ts'; /** Runs a write once the workspace admits it. */ type AdmitWrite = <T>(write: () => Promise<T>) => Promise<T>; export declare class Dispatcher { private readonly namespace; private readonly cache; private readonly policies; private readonly drift; private readonly admitWrite; private readonly decisions; private access; private caller; private state; private static readonly stores; private static storeCount; readonly reconciler: Reconciler; constructor(namespace: Namespace, cache: FileCacheMixin & BaseVFS, policies?: Policies, drift?: DriftQueue, admitWrite?: AdmitWrite, decisions?: Decisions); forSession(session: SessionAccess | null): Dispatcher; /** * This dispatcher run as `caller`: its checks read that caller's * session, line, dry run and call, not the async context's. A session * a `forSession` view already bound stays its own. Mirrors Python's * `caller` keyword on Dispatcher.dispatch. */ private forCaller; private sessionId; /** The dry run the op is part of: the caller's, else the async context's. */ private explaining; /** * The namespace's own answer for a path no backend serves. * * Child mounts and symlinks are structure the dispatcher owns, so a * directory that exists only because a mount or link sits below it * still lists and stats. Null for any other op, or when the * namespace knows nothing at `virtual`. */ private namespaceResult; dispatch: CallerDispatchFn; /** The observation owner known before this call can suspend. */ private readCaller; private dispatchAdmitted; /** Publish only this admitted read's records for its captured caller. */ private observeReads; /** Publish delivered chunks and normal empty completion, retaining stream cleanup. */ private observeStream; /** * A read that refuses a directory with GNU's EISDIR, at the path it was * handed. * * A store answers a read of a directory in its own voice: ENOENT from a * keyed store, whose directory is a set of keys rather than an object, an * opaque error from sftp, no bytes from some drivers. So a read that fails * or answers no bytes asks whether its path is a directory (`readHitADir`), * and any other answer stands. A stream is judged as it is pulled; a live * pipe is no directory. Mirrors Python's Dispatcher._read_refusing_dirs. */ private readRefusingDirs; /** * A streamed read that refuses a directory once it fails or ends with no * bytes. Run to its empty first step before it is handed on, as `primed` * runs a stream, so a caller that closes it before pulling still closes the * read. Mirrors Python's Dispatcher._drain_refusing_dirs. */ private drainRefusingDirs; /** * Whether a read of `path` that failed or answered no bytes reached a * directory, probed as this dispatcher's caller. The probes are the * dispatcher's own, no call of the command's, so its path rules do not * judge them; the caller's hides do. A policy's refusal is the dispatcher's * own answer rather than a store's, so it stands, and a dry run asks the * gates only, so it probes nothing. Mirrors Python's * Dispatcher._read_hit_a_dir. */ private readHitADir; /** The entry at `path` itself, as lstat answers; throws when nothing is there. */ private lstat; /** The entries of the directory at `path`. */ private listing; /** * Whether the file cache holds a copy of the call's path, asked of the store * without fetching the bytes or checking the copy against the backend: a * read of a held file is checked as it is served. Mirrors Python's * Dispatcher._held. */ private held; /** * Rank several scopes as one query, as a store's `searchMany` does. One * call answers for them all where one mount serves every scope and its VFS * ranks a batch, past the same checks each scope's own search meets: each * is walked and followed, a path rule or a coded preVfs policy reaching * below any of them declines the batch (null), and every hit the session * hides is dropped. Otherwise, and while a postVfs policy speaks for the * session (it judges each scope's answer on its own), each scope is * searched on its own and the hits joined in scope order. Mirrors Python's * Dispatcher._search_together. */ private searchTogether; /** * Keep the small files `typed` names in the read cache, for the reads a * command is about to make of them; nothing is answered. Each read that * follows still meets every check of its own and is served what this kept, * so nothing is fetched where a check could refuse that read before its * backend is asked: nothing in a dry run or while a coded preVfs policy * speaks for the session (the rule a one-call walk meets in `declines`), * and no path the session hides or a path rule reaches. A file already * kept, one a renderer answers, one pinned to a revision and one on a mount * that keeps no reads or holds its data locally (read as fast when asked) * is passed over. A store that reads a batch * (`readMany`) is asked once for the rest of its files; any other is asked * for each file its listing sizes within READ_AHEAD_BYTES. A failure is * logged and dropped: the read that follows meets it itself. Mirrors * Python's Dispatcher._read_many. */ private readMany; /** * Load the namespace and run what a snapshot restore left owed. * * Pending fingerprint checks from a strict snapshot restore run before * the op can touch a mount, whichever surface called: FUSE and `ws.vfs` * come straight here, so a drain that lived any higher would let a * first write clobber drifted state. drain() clears pending before it * stats, so its own probes cannot recurse into it. A dry run leaves them * pending, its policies' reads included: the check is no policy's * answer, and the op that does run still owes it. Mirrors Python's * Dispatcher._prepare. */ private prepare; /** * Walk the op's path, as the kernel does before a call sees it. * * Hidden paths answer before anything else can: the typed path is * checked so a link inside hidden space cannot be followed out of it, * the followed path is re-checked (`follow`) so a visible link cannot * lead in, and a rename's or a copy's destination is a create. Every link * above the * final name is then followed, whatever the op does with the name: * command dispatch walks the operands it classifies, and this is the * same walk for every other caller (a relative word ln resolves itself, * `ws.vfs`, a runtime's os.symlink), so a link made, read or * removed under a linked directory lands in the directory the link * names. Mirrors Python's Dispatcher._walk. */ private walk; /** * Refuse a rename the namespace forbids before any backend runs. * * A rename re-anchors everything below its source while the hides stay * where they are written, so hidden content would land at paths the * session can see. Destroying hidden content is silent (rmR, the remnant * rmdir); relocating it into view is refused. Only a directory has * anything below it to re-anchor, so a file source passes. rename(2) * also replaces a destination directory only when it is empty, and the * node table is half of what empty means here: a link is invisible to * every backend, so a destination the backend reads as empty can still * hold one. Left to the backend the rename succeeded and the purge then * deleted the link with it, losing namespace state silently where POSIX * promises ENOTEMPTY. Mirrors Python's Dispatcher._refuse_rename. */ private refuseRename; /** * Follow the final name, unless the op acts on the name itself. * * `nofollow` is the caller's AT_SYMLINK_NOFOLLOW: an op that acts on a * link entry itself (chown -h writing the link's own attrs) keeps the * typed path. Consumed here, never forwarded. A copy writes its * destination as a write does, through a link at the final name; a * rename moves the name itself. A copy whose two ends then name one file * is refused: a backend that replaces its destination would delete the * source first. Mirrors Python's Dispatcher._follow. */ private follow; /** * Walk and follow each other path argument the way the path is: the same * hides, spelling checks and rule gate apply, and the followed spelling * replaces it. Mirrors Python's Dispatcher._walk_operands. */ private walkOperands; /** * The mount a copy's destination is on, when that is another mount whose * store may copy from this one's; null otherwise. Mirrors Python's * Dispatcher._copy_target. */ private copyTarget; /** * Copy a file onto another mount, store to store. * * Admission judged the source as a read and the destination as a create * under its own mount's mode, as for a copy within one mount. The * destination's store copies from the source's where it can (one bucket to * another under one set of credentials); otherwise it answers EXDEV, as a * copy between two mounts always did, and the caller moves the bytes * itself. The destination's name is held, and its caches settled, on its * own mount; a copy past its timeout keeps the name until the store * answers. Mirrors Python's Dispatcher._copy_across. */ private copyAcross; /** * Answer EXDEV for a rename or a copy between two mounts. * * A mount is a filesystem boundary: rename(2) moves a name within one and * answers EXDEV across two, before any permission is weighed, so `mv` * falls back to copy and unlink instead of the source's backend taking * the destination for one of its keys; a backend copies only within * itself, the same way. It resolves both parent directories first, so a * missing one is ENOENT (ENOTDIR through a file) ahead of EXDEV. Mirrors * Python's Dispatcher._refuse_cross_mount. */ private refuseCrossMount; /** * Whether a one-call walk gives way to the caller's own walk. * * A backend's find, du, search, tree copy or tree removal answers for * every entry under its path in one call, past the checks each entry would * meet on its own. Path rules and file policies decline that shortcut so * the caller walks through calls that are each judged, including inside * commands. A path rule declines no `files_containing` on its own: its * answer only rules files out of a walk that still * lists every one, and each file it rules out meets the rule as a warm read, * so one the rule refuses reports `Permission denied` as its read would. A * hide declines the copies and the removal, which would reach hidden * entries; a read in HIDE_FILTERED_OPS * runs and `filter` drops what the session hides from its answer. A file * copy is declined the same way: the bytes it moves never pass through a * read. A tree copy is declined too when a link stands below its * destination: the backend writes each child name as it is, where the * walk's copies follow the link. Mirrors Python's Dispatcher._declines. */ private declines; /** * Run admission for an op on a mounted path. * * Admission policies fire at the dispatcher, before the warm-cache early * return: a cached read must be refused exactly like a cold one, or the * cache becomes a policy bypass. This dispatcher is the one dispatcher in * TypeScript: shell internals, programmatic access, `ws.vfs`, and * FUSE all end up here. A rename's or a copy's destination is a create * there: it passes the same gate as the source, so a path rule holds * against moving or copying into a protected scope (or onto the directory * that holds one) the way it holds against writing there, under the mode * of the mount that owns it. A copy only reads its source. Mirrors * Python's Dispatcher._admit. */ private admit; /** * Answer an op on a path no mount serves. * * The namespace may still know a directory there (a deeper mount, a * link). No mount means no cache to keep straight and no owning prefix * (the gates see ''), but admission still fires: a policy that bounds * readdir or stat by path must cover the synthetic answer too. A real but * ungranted mount is the same case: a granted mount below it already put * this path's name in a listing, so walking down to the grant must * answer, and the merged names are session-filtered individually, so * nothing of the mount's own content leaks. A setattr with no owning * mount lands in the overlay (a link above every mount still takes * chown -h), gated exactly like the mounted overlay write; an ungranted * mount is not that case and keeps the canonical denial. Mirrors Python's * Dispatcher._answer_unmounted. */ private answerUnmounted; /** * Whether a read is answered as it is pulled. A whole read of stored bytes * streams as the VFS's `read` answers it, a stream as it is pulled and bytes * as one chunk. A window, a rendering, a VFS with no `read` of its own and a * postVfs policy that may read the result each get the whole bytes instead, * which are a stream of one chunk, including inside commands and runtimes. Mirrors * Python's Dispatcher._streams. */ private streams; /** * Open a streamed read, its first chunk pulled before it returns. A cold * read fills the cache as it is pulled. A live pipe's read is neither * pulled nor cached: it answers as a `PipeStream`. Mirrors Python's * Dispatcher._open_stream. */ private openStream; /** Whether a filetype renderer answers this read on `vfs`; asked each * time, since a renderer can land while the read runs. */ private rendersRead; /** * Judge bytes or a row its caller already holds, as a cache hit. A runtime * descriptor and a kernel mount's handle keep what they fetched; each later * read or stat of it meets the hides, the rules and the file policies in * force now, through the boundary a fetch and the file cache answer * through, and nothing is fetched. The bytes were recorded when they were * fetched, so this is not. Mirrors Python's Dispatcher._judge_held. */ private judgeHeld; /** * Answer a read from the file cache, or null to read the backend. * * The file cache holds what commands read, keyed on the path alone: the * stored bytes, which are the rendering for a VFS with no `read` of its * own. A read through a filetype renderer (whoever registered it) asks * for a different value under the same key, so it is neither served * from that cache nor kept in it, and neither is a direct read * (`direct: true`), which asks for what the backend holds now: a * follow's poll for bytes the cached copy cannot have yet. The cache * holds the whole object, so a ranged read is answered by slicing it, * never by handing back the whole file: the window is what the caller * asked for instead of the file, and git reads pack indexes this way. * sliceWindow is the same helper the ranged read op falls back to, so * warm and cold agree. * Mirrors Python's Dispatcher._serve_cached. */ private serveCached; /** * The cache manager a cold read fills, or null to keep nothing. * * A cold read keeps the whole file it fetched for the next reader, * through the mount's own manager, the one a command's read fills: a * write racing the fetch retires its generation, so the bytes it read * are not kept. A ranged read comes from the store only where the store * can serve one; elsewhere the read op would fetch the whole file and * slice it for every range, so the whole file is read once, kept, and * each range sliced from it. The op is resolved only once the mount is * ready, so a renderer can land after this check; the fill asks again * before it keeps anything. Mirrors Python's Dispatcher._filler. */ private filler; /** * The running command's `walkLimit` on `mount` (walkBound): its entry, * its declared limit and the session's profile over the mount's and the * workspace's. Mirrors Python's Dispatcher._walk_bound. */ private walkBoundOf; /** * The files a walk guard's path holds, once the command's own `walkLimit` * lets the walk run: past it the walk is refused before any backend rule * is asked. Mirrors Python's Dispatcher._full_walk_files. */ private fullWalkFiles; /** * How many files the session sees under a walk guard's path, counted off * the mount's `readTree` with every hidden file dropped, and every file a * nested mount covers, so a guard's refusal never counts what the command * would not read. Without one, or where * it cannot read the whole tree, a mount with a `walkLimit` is walked * listing by listing, hidden entries skipped, until one file past the * limit; a folder below the path it cannot open counts nothing, as the * command reports it itself. null otherwise. Mirrors Python's * Dispatcher._visible_files. */ private visibleFiles; /** * Run a fresh stat under its path's writer hold, so no local write to the * path lands between the backend's answer and its reconcile; the reconcile * runs to its end under the hold. Mirrors Python's Dispatcher._fresh_call. */ private freshCall; /** * Run the op on its mount and stamp what it moved. * * Returns the op's answer with the rename destination and the arguments * the backend received, which the write's settling reads. Mirrors * Python's Dispatcher._call. */ private callBackend; /** * Keep a whole write's bytes for the next read, under its name's hold. * * The write's own record labels them with the token the backend * answered, so a `fresh` mount does not refetch what it just wrote; a * record moving another length than was sent keeps nothing, and so does a * name another mount took while the write ran, read under the cache's lock. * The bytes kept are a copy, so a caller reusing its buffer cannot change * them. Mirrors Python's Dispatcher._keep_written. */ /** * The cached copy an append may extend, read under its name's hold before * the append runs: its bytes and their token. Mirrors Python's * Dispatcher._cached_before. */ private cachedBefore; /** Drop content without losing the conditional-write baseline. */ private invalidateStream; /** * Keep a write's bytes for the next read, under its name's hold. The * write's own record labels them with the token the backend answered, so * a `fresh` mount does not refetch what it just wrote; a record moving * another length than was sent keeps nothing. An append extends the cached * copy only where the file is proven to have been that copy: the backend * rewrote it whole, its read saw the copy's token, and its write moved the * copy's length plus the appended bytes. A store with no tokens, or one * that appends by itself, proves nothing, so the copy stays dropped. An * append of nothing that wrote nothing (`cmd >> f` opening `f`) leaves the * copy as it was; one that made the file anew drops it, and so does a * streamed one, whose name another writer could take while it waited. * Mirrors Python's Dispatcher._keep_written. */ private keepWritten; /** * Merge the namespace into a backend answer and drop hidden names. * * A listing gains the child mounts and links the namespace holds below * it and loses every name the session hides; a stat gains the attribute * overlay recorded at its path. Mirrors Python's Dispatcher._filter. */ private filter; /** * A stat, or a subtree's stats, on the device of the mount serving * `virtual`. The device is the id a runtime reports as a guest's st_dev, * so `stat %d` and `fs.stat` name one device, and a walk tells a mount * boundary by it. A path no mount serves is on the namespace's own, the * one a root mount would have. Anything but a stat or a subtree's stats * passes unchanged. */ /** * A custom function's answer as the session may see it. A list of paths, * or of records with a `path`, loses what the session hides, as a * `files_containing` answer does. Any other answer cannot be told apart * from what it names, so it is declined while the session hides anything * under the call's path. No answer reveals nothing. Mirrors Python's * Dispatcher._custom_answer. */ private customAnswer; private onDevice; /** * A subtree read with the links the namespace holds below `path` on the * same mount, each as the link it is. No backend stores a link, so a walk * that reads the subtree in one call meets them only here, as a listing * meets them in `readdir`; a link that leads where the session cannot see * is left out, as a listing leaves it out. A link shadows what the backend * holds at its name and below it, as a stat of the name answers the link. * Mirrors Python's Dispatcher._with_links. */ /** A subtree read with the attributes the overlay holds for each entry, the * ones a stat of that entry carries. */ private overlaid; private withLinks; /** * Run one write with its names held, one writer at a time per name. * * The caller's abort covers only its wait for the hold: a writer the * abort finds queued lets go of any name it already holds and never * runs. One that comes after the abort (a stopped run landing what its * guest left open, within the stop's grace) waits its turn. One holding * its names runs to the end and the caller waits for it and for `after` * (the bookkeeping), since an abort cannot stop a store's call under way, * as Python's cancelled line is joined whole; an abort that lands during * that call still makes it late, since the line may have gone on past its * grace. `after` runs for a caller still waiting, and for a late call that * succeeded while every name is still on the mount that ran it (`owners`), * so a rename still moves its links; any other late call runs `landed`, * which drops what the landing made stale where its names still belong to * that mount (a read between the abort and the landing may have cached * the old bytes); a failure nobody waits for any more is reported. * * Args: * keys: the names to hold, in the one order every writer takes them. * label: the op and path, for a late failure's report. * call: runs the op, handing over the store's own call once it starts. * after: the bookkeeping, run while the caller still waits. * landed: run once a call that outlived its timeout settles. * owners: the mount each name was on when the call started. */ private holdWrite; /** * What a write changes beside the store: the caches above the path, and * the node table's links and attributes at its names. Mirrors Python's * Dispatcher._settle_write. */ /** * What a write changes beside the store: the caches above the path, and the * node table's links and attributes at its names. A copy reads its path, so * only its destination changes. A create that answers its new entry's stat * (`created`) has it kept where its parent's listing names it, in place of * dropping that listing for a re-list: the create is the one change to it, * and the next level of a nested create finds its parent there. Mirrors * Python's Dispatcher._settle_write. */ private settleWrite; /** * The mount serving each of a write's names as it starts, keyed by the * name, for the cleanup after a call that lands past its timeout. */ /** Whether each name is still on the mount it was on in `owners`. */ private stillOwned; private owners; /** * Drop what a write that threw may have made stale. * * A write that fails partway may already have changed the store (a * paginated delete, a copy that merged some children), so the caches at * and below every name it reaches go either way; the node table moves only * with a write that landed. The version the line held for each name is * kept, so the next write there stays conditioned on what the agent read, * and so is each version `lost` marks under them, which a refused * conditional write kept: a retry without a read is refused again. * Over-dropping costs a refetch. An eviction error is logged, not thrown, * so the caller still learns why the write failed. `owners`, for a call * that landed after its timeout, holds the mount each name was on when the * call started: a name another mount has taken since is that mount's now, * so nothing at or under it is touched. Mirrors Python's * Dispatcher._evict_failed_write, whose timed-out call is cancelled and * never lands late. */ private evictFailedWrite; /** * What a tree copy changed under `dst`: each entry it wrote. The copy * merges into what `dst` already held, so a file it left keeps its cached * copy and the version a conditional write needs; the files and * directories it wrote are the source's, listed after the copy, so an * empty directory it made drops its folder's listing too. Mirrors Python's * Dispatcher._settle_tree_copy. */ private settleTreeCopy; /** * Every file and directory at any depth under `root`, through the mount's * own listing. */ private entriesBelow; /** * A number naming one store object, the same for every mount of it in every * workspace, as Python's id() of the store is. */ private storeId; /** * Whether the node table answers this op instead of a backend. * * `symlink` and `readlink` always, because a link exists nowhere else. * The rest only when the path itself is a link, and then for the same * reason the create and the read are the dispatcher's: forwarding reaches a * backend that has never heard of the name. A no-follow stat is the * read half of that fact (lstat asks for the link's own row, which * only the table holds); a following stat never arrives here, since * the follow below rewrote it to the target. Mirrors Python's * Dispatcher._table_answers. */ /** * The index kwargs normal dispatch stamps on every registered op, for * the dispatcher's own raw registry calls: an indexed backend cannot * resolve a nested path without it. */ private indexKwargs; /** The policy boundary for an op on a path `mount` owns: the mount's * prefix and mode, or for a path above every mount an empty prefix and * full write, governed by `/` (`MountModePolicy`). Mirrors Python's * Dispatcher._boundary. */ private boundary; /** * The dispatcher's own channel for internal walks: the TS twin of Python's * Mount.call plus the dispatcher-side duties around it. The * same mode fence, index stamping and mount-prefix context normal * dispatch applies, plus the boundary's admission and completion for * writes (Python's `_MountChannel` holds the same `Boundary`) and the * dispatcher's own write invalidation, because a raw mount call * runs outside the cache context dispatch establishes, so the cores' * invalidation cannot land. Invalidation runs even when the op * fails: a missing-path failure means the tree changed under the * walk, and the walk's own earlier listing is exactly the entry that * must not survive. Only the visibility filter stays off, which is * what lets a remnant walk see hidden entries. Every internal * backend call in this class routes through here; a bare * `callKeyed` outside dispatch is a bug. */ private fencedCall; /** * Whether a rename's source stats as a directory. * * Only a directory can carry hidden content into view, so the reveal * refusal probes the source before it fires and lets a file rename * pass. An absent source moves nothing (the rename itself reports * it); a source the mount cannot classify fails toward refusal, the * same stance the pattern arm takes. Mirrors Python's * Dispatcher._moved_source_is_dir. */ private movedSourceIsDir; /** * Take a visibly-empty directory's hidden remnants with it. * * The backend refused the rmdir because entries remain, but when the * session's view of the directory is empty the refusal would leak * that something invisible exists. A session's mutation may destroy * what it cannot see, never learn of it, so the remnants go with the * directory through the shared removeRemnants walk; a visible child, * or any cascade failure (a mode-protected entry, a visible entry * appearing mid-walk), re-raises the backend's refusal. Emptiness is * the dispatcher's own readdir pipeline: backend entries merged with the * namespace's children (nested mounts, links) and judged by * visibility, so a visible child no backend can see keeps the * refusal instead of reporting a successful rmdir while the mounted * child remains. The namespace's own hidden nodes under the subtree * (links, attr overlays) are purged with it, so the removed tree * cannot resurface from the node table once the hide lifts. */ private rmdirRemnants; /** * `path` with the links above its final name followed. * * The walked path answers to the session's hides as the typed one did, * the rule the follow of the final name applies too: a visible link must * not lead into hidden space. Throws `eloop` when a link above the name * loops (ELOOP), as the coded error every caller's per-operand catch * words. Mirrors Python's Dispatcher._walked. */ private walked; private tableAnswers; /** * Answer a node-table op at the dispatcher itself, gated like a backend. * * A symlink is namespace state with no backend behind it, so the dispatcher * owns every verb that names one. Admission still fires exactly as for * a backend write: the link's turf is the longest mount prefix above it * (the same ownership rule the link read filter uses), session grants * and both gates run, and the write leaves an OpRecord — a scoped * kernel mount refuses exactly like a scoped shell. The turf's mode * gates the write too (`MountModePolicy` at the `Boundary`), so a * read-only mount or grant answers EROFS for a link exactly as for a * file; a link above every mount is bare namespace structure, gated * with an empty prefix and governed by `/`. A rename's * destination is judged on its own turf, since the endpoints need not * share one. Also answers the `unlink`, `rename` and no-follow * `stat` of a path the node table holds a link for. Mirrors Python's * Dispatcher._namespace_table_op. */ private tableCall; /** * The error a readlink of something that is not a link answers. * * readlink(2) splits the two misses and callers read them differently: * a path that is there but is not a link is EINVAL, and one that is * not there at all is ENOENT, which is the code a guest's * `except FileNotFoundError` catches. The node table only knows the * first half, so absence is probed here and only here, on the failure * path, where one extra round trip buys the right errno. Mirrors * Python's Dispatcher._readlink_miss. */ private readlinkMiss; /** * Whether anything at all is at `path`, and the parent's listing. * * Four channels, asked in the order of what they prove. The namespace * goes first: a link, and a directory that exists only because a * mount or a link sits below it, are structure no backend can see, * and a mount root is the deployment's own configuration. Then the * backend's row, which settles a file. A directory row settles * nothing, because an API tree synthesizes its directories: a * postgres schema lists `tables/` and `views/` before anything has * asked whether that schema is there, and a grouping mount stats * every path under a live collection as a directory. So a directory * is proven the way the hierarchy kit itself proves one, by appearing * in its parent's listing, which is also the only way a prefix store * can answer for a directory that is nothing but a set of keys. * Cannot reuse `resolvePathStat`: that dispatches, and the dispatcher is * what dispatch is inside of. * * The parent's listing comes back beside the answer, null when no probe * reached it or it gave none, because a listing with entries in it also * proves the parent a directory: a create in a directory that holds * anything costs no round trip beyond this one. Mirrors Python's * Dispatcher._occupancy. */ private occupancy; /** * What symlink(2) answers instead of making a link at `path`. * * Null when the link can be made. The name must be free (EEXIST) and * its parent a directory (`parentRefusal`), both read off the probes * `occupancy` makes: a parent whose listing answered with entries is a * directory, so only an empty or silent parent is walked, which is the * failure path nearly always. Mirrors Python's * Dispatcher._symlink_refusal. */ private symlinkRefusal; /** * The errno the parent chain of a name being created answers. * * symlink(2) and rename(2) resolve the directory a name goes in before * they look at the name: ENOENT when it is absent and ENOTDIR when a * non-directory stands anywhere in the chain. The chain is walked * upward until something is there, as `destKind` walks a copy's * destination, because a store answers a path under a plain file with * the same miss as an absent one: the parent itself being a directory * is the one clean answer, a directory higher up means the components * below it are absent, and anything else is ENOTDIR. Null when the * parent is a directory, and when a policy closes a channel, which * proves nothing either way. Mirrors Python's * Dispatcher._parent_refusal. */ private parentRefusal; /** * The type of what stands at `virtual`, null when nothing does. * * The channels `occupancy` asks, for a path the walk above a new name * reaches: namespace structure and a mount root are directories, then * the backend's row, then the path's own entry in its parent's * listing, which is how a prefix store holds a directory that is * nothing but a set of keys. Mirrors Python's Dispatcher._entry_type. */ private entryType; /** * The backend listing of the directory `virtual` sits in: null when no * mount serves that directory, its backend lists nothing there, or the * path has no name to sit in one. Mirrors Python's * Dispatcher._parent_listing. */ private parentListing; /** * Run one read op for a probe, or null when it found nothing. * * The probe reads on the caller's behalf but not at its request, so it * meets what the op would at the dispatcher: a path the caller cannot * see answers as the walk answers it (`sees`), and a policy that denies * `stat` must not be reachable through a readlink. That refusal is * raised, not swallowed, because only the caller knows what to answer * when a channel goes dark. * * The index and the path's filetype are the two kwargs that decide * which registered op answers, so a probe that omitted them would ask * a different question than the dispatcher does and report a rendered path * as absent. Python needs no twin of that half: its dispatcher routes * through `Mount.call`, which stamps both itself. * * Args: * name: the function to run, `stat` or `readdir`. * resolved: what the namespace resolved the path to. * issuer: the mark on the op being served, carried to the probe's * gate so the probe is judged as its caller's. */ private probeRead; /** * Answer an extended-attribute op from the node table. * * The attributes a caller sets live on the path's node, so they * survive on a backend that has no such slot and move with a rename; * the backend's own facts are read off its stat on every call and * refused to writers with EPERM, the answer for an attribute the * filesystem keeps for itself. The listing is sorted, so both hosts * and every backend agree on its order. Gated like a setattr: both * admission gates fire on the path's turf, and a write needs a * writable turf. Mirrors Python's Dispatcher._xattr_op. * * Args: * name: `getxattr`, `listxattr`, `setxattr` or `removexattr`. * path: the path, already followed unless the caller asked for its * link node itself. * kwargs: `name` for all but listxattr, `value` and the * `create`/`replace` flags for setxattr. * report: the caller's report. * issuer: the mark on the op being served. */ private answerXattr; /** * Answer statfs(2) for a path: the type name and the capacity of the * mount that holds it, which is what df reports for that mount. The path * must exist, as statfs's own walk requires. The namespace above every * mount has no file system behind it, so its type is `-` and its * capacity unknown. Mirrors Python's Dispatcher._statfs. * * Args: * path: the path, already followed. * issuer: the mark on the op being served. */ private statfs; /** * Settle that an attribute op's path exists, which it answers first. A * link node's own attributes and a directory that exists only in the * namespace have no backend behind them; anything else the backend's * stat must find, or the op is ENOENT. Mirrors Python's * Dispatcher._xattr_target. */ private xattrTarget; /** * Apply attributes natively where the backend can, overlay the rest. * * A VFS with a registered setattr op applies what it can and * returns the residual; residual fields go to the overlay and * natively applied ones are dropped from it, so a stale overlay never * shadows a fresh backend value. A VFS without the op, and a * link path (which has no backend inode), overlay everything. The * overlay half is the dispatcher's own write, so it runs inside the same * gates as the native half. Mirrors Python's Dispatcher._apply_setattr. */ private applySetattr; /** Store every requested field in the namespace overlay. */ private overlaySetattr; /** Write one overlay entry, converting an ISO mtime to epoch seconds. */ private writeOverlay; /** * Drop what a change to `rawPath` made stale above the store. * * The mount's caches take it under every mount of its store * (`CacheManager.invalidate`); a removal through the dispatcher knows * nothing was below the name, so its index is not asked. The overlay times * a content write moves go too, under each name: `observed` is the epoch * seconds of a content write to record, null for a removal, and `times` * false keeps them, for a change that moved no times. `row` is a CREATE's * new entry, kept in its parent's listing; null drops that entry. Mirrors * Python's Dispatcher._changed. */ /** * Time the directories a `mkdir -p` made above its path, as the path * itself is timed. After a failure only those that now exist are: a chain * is made outermost first, so the first one missing ends it. Mirrors * Python's `_date_made`. */ private dateMade; private changed; /** * Bind the end of a line to the mounts it started with. * * The mount table is pinned at the line's start, so a version kept when it * ends is judged by the mount that produced it rather than whatever holds * the prefix by then. Mirrors Python's Dispatcher.capture_cache_facts. */ captureCacheFacts(): (path: string) => CacheFacts; /** * Keep the version each path last had on a line that ended; a nested * line's keeps only the versions of paths still lost. Mirrors Python's * Dispatcher.keep_versions. */ keepVersions(records: readonly OpRecord[], cacheFacts: (path: string) => CacheFacts, lost: LostPaths | null, nested?: boolean): Promise<void>; } export {}; //# sourceMappingURL=dispatcher.d.ts.map