@struktoai/mirage-core
Version:
A Unified Virtual Filesystem For AI Agents
826 lines • 42.5 kB
TypeScript
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