@nimbus-sh/worker 0.9.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/NOTICE.md +5 -0
- package/README.md +10 -0
- package/dist/_shared/session-router.d.ts +7 -0
- package/dist/_shared/session-router.d.ts.map +1 -1
- package/dist/_shared/session-router.js +7 -0
- package/dist/bindings/body.d.ts +6 -0
- package/dist/bindings/body.d.ts.map +1 -0
- package/dist/bindings/body.js +37 -0
- package/dist/bindings/kv.d.ts +0 -2
- package/dist/bindings/kv.d.ts.map +1 -1
- package/dist/bindings/kv.js +3 -42
- package/dist/bindings/r2.d.ts +0 -2
- package/dist/bindings/r2.d.ts.map +1 -1
- package/dist/bindings/r2.js +4 -44
- package/dist/esbuild-cli-artifact.generated.d.ts +18 -0
- package/dist/esbuild-cli-artifact.generated.d.ts.map +1 -0
- package/dist/esbuild-cli-artifact.generated.js +17 -0
- package/dist/esbuild-wasm-bundle.generated.d.ts +24 -24
- package/dist/esbuild-wasm-bundle.generated.d.ts.map +1 -1
- package/dist/esbuild-wasm-bundle.generated.js +24 -24
- package/dist/facets/cirrus-real.d.ts.map +1 -1
- package/dist/facets/cirrus-real.js +11 -6
- package/dist/facets/compose.d.ts +11 -7
- package/dist/facets/compose.d.ts.map +1 -1
- package/dist/facets/compose.js +8 -31
- package/dist/facets/data-plan.d.ts +79 -0
- package/dist/facets/data-plan.d.ts.map +1 -0
- package/dist/facets/data-plan.js +477 -0
- package/dist/facets/esbuild-bundle-pool.d.ts +1 -1
- package/dist/facets/esbuild-bundle-pool.js +3 -3
- package/dist/facets/esbuild-transform.d.ts +38 -8
- package/dist/facets/esbuild-transform.d.ts.map +1 -1
- package/dist/facets/esbuild-transform.js +219 -19
- package/dist/facets/exec-telemetry.d.ts +7 -3
- package/dist/facets/exec-telemetry.d.ts.map +1 -1
- package/dist/facets/manager.d.ts +198 -69
- package/dist/facets/manager.d.ts.map +1 -1
- package/dist/facets/manager.js +1276 -652
- package/dist/facets/opencode-staging.d.ts +0 -2
- package/dist/facets/opencode-staging.d.ts.map +1 -1
- package/dist/facets/opencode-staging.js +5 -11
- package/dist/facets/process.d.ts +4 -0
- package/dist/facets/process.d.ts.map +1 -1
- package/dist/facets/process.js +3 -0
- package/dist/facets/read-profile.d.ts +149 -0
- package/dist/facets/read-profile.d.ts.map +1 -0
- package/dist/facets/read-profile.js +483 -0
- package/dist/facets/real-vite-hmr.d.ts +2 -0
- package/dist/facets/real-vite-hmr.d.ts.map +1 -1
- package/dist/facets/real-vite-hmr.js +3 -2
- package/dist/facets/resident-identity.d.ts +0 -1
- package/dist/facets/resident-identity.d.ts.map +1 -1
- package/dist/facets/resident-identity.js +0 -3
- package/dist/facets/wasm-swap-registry.d.ts +34 -25
- package/dist/facets/wasm-swap-registry.d.ts.map +1 -1
- package/dist/facets/wasm-swap-registry.js +62 -56
- package/dist/facets/ws-terminal.d.ts +4 -4
- package/dist/facets/ws-terminal.d.ts.map +1 -1
- package/dist/facets/ws-terminal.js +4 -5
- package/dist/git/commands.d.ts +33 -10
- package/dist/git/commands.d.ts.map +1 -1
- package/dist/git/commands.js +1891 -197
- package/dist/git/network-facet.d.ts +12 -2
- package/dist/git/network-facet.d.ts.map +1 -1
- package/dist/git/network-facet.js +69 -18
- package/dist/git/unified-diff.d.ts +74 -0
- package/dist/git/unified-diff.d.ts.map +1 -0
- package/dist/git/unified-diff.js +676 -0
- package/dist/git-bundle.generated.d.ts +7 -11
- package/dist/git-bundle.generated.d.ts.map +1 -1
- package/dist/git-bundle.generated.js +7 -11
- package/dist/hosted/commands.d.ts.map +1 -1
- package/dist/hosted/commands.js +64 -245
- package/dist/hosted/runtime.d.ts +22 -15
- package/dist/hosted/runtime.d.ts.map +1 -1
- package/dist/hosted/runtime.js +72 -14
- package/dist/hosted/services.d.ts +8 -1
- package/dist/hosted/services.d.ts.map +1 -1
- package/dist/hosted/services.js +12 -13
- package/dist/hosted/session.d.ts +165 -0
- package/dist/hosted/session.d.ts.map +1 -0
- package/dist/hosted/session.js +132 -0
- package/dist/loaders/generated-workers.d.ts +6 -3
- package/dist/loaders/generated-workers.d.ts.map +1 -1
- package/dist/loaders/generated-workers.js +8 -5
- package/dist/loaders/npm-resolve-preamble.d.ts.map +1 -1
- package/dist/loaders/npm-resolve-preamble.js +0 -27
- package/dist/loaders/pre-bundle-preamble.d.ts +6 -3
- package/dist/loaders/pre-bundle-preamble.d.ts.map +1 -1
- package/dist/loaders/pre-bundle-preamble.js +13 -8
- package/dist/node-shims-artifact.generated.d.ts +13 -4
- package/dist/node-shims-artifact.generated.d.ts.map +1 -1
- package/dist/node-shims-artifact.generated.js +16 -7
- package/dist/npm/bin-links.d.ts +3 -1
- package/dist/npm/bin-links.d.ts.map +1 -1
- package/dist/npm/bin-links.js +32 -8
- package/dist/npm/cache.d.ts +11 -1
- package/dist/npm/cache.d.ts.map +1 -1
- package/dist/npm/cache.js +21 -7
- package/dist/npm/install-batch-facet.d.ts +4 -2
- package/dist/npm/install-batch-facet.d.ts.map +1 -1
- package/dist/npm/install-batch-facet.js +32 -18
- package/dist/npm/installer.d.ts +23 -3
- package/dist/npm/installer.d.ts.map +1 -1
- package/dist/npm/installer.js +330 -131
- package/dist/npm/npx-install.d.ts +3 -1
- package/dist/npm/npx-install.d.ts.map +1 -1
- package/dist/npm/npx-install.js +4 -1
- package/dist/npm/package-lock.d.ts +32 -0
- package/dist/npm/package-lock.d.ts.map +1 -0
- package/dist/npm/package-lock.js +65 -0
- package/dist/npm/placement.d.ts +16 -0
- package/dist/npm/placement.d.ts.map +1 -0
- package/dist/npm/placement.js +33 -0
- package/dist/npm/pre-bundle-facet.d.ts +1 -30
- package/dist/npm/pre-bundle-facet.d.ts.map +1 -1
- package/dist/npm/pre-bundle-facet.js +3 -5
- package/dist/npm/r2-cache.d.ts +12 -6
- package/dist/npm/r2-cache.d.ts.map +1 -1
- package/dist/npm/r2-cache.js +34 -16
- package/dist/npm/resolve-one-facet.d.ts +3 -0
- package/dist/npm/resolve-one-facet.d.ts.map +1 -1
- package/dist/npm/resolve-one-facet.js +15 -3
- package/dist/npm/resolver.d.ts +11 -13
- package/dist/npm/resolver.d.ts.map +1 -1
- package/dist/npm/resolver.js +12 -25
- package/dist/opencode-artifact.generated.js +2 -2
- package/dist/replica/routing.d.ts +2 -38
- package/dist/replica/routing.d.ts.map +1 -1
- package/dist/replica/routing.js +3 -55
- package/dist/router/index.d.ts.map +1 -1
- package/dist/router/index.js +15 -2
- package/dist/router/public-directory.d.ts +18 -0
- package/dist/router/public-directory.d.ts.map +1 -1
- package/dist/router/public-directory.js +1 -1
- package/dist/router/remote-api.d.ts +3 -1
- package/dist/router/remote-api.d.ts.map +1 -1
- package/dist/router/remote-api.js +42 -79
- package/dist/runtime/bash-repl.js +2 -2
- package/dist/runtime/bun-repl.d.ts.map +1 -1
- package/dist/runtime/bun-repl.js +2 -13
- package/dist/runtime/cpython-resident.d.ts.map +1 -1
- package/dist/runtime/cpython-resident.js +2 -10
- package/dist/runtime/esbuild-wasm-bytes.d.ts +37 -18
- package/dist/runtime/esbuild-wasm-bytes.d.ts.map +1 -1
- package/dist/runtime/esbuild-wasm-bytes.js +66 -92
- package/dist/runtime/facet-loader-host.d.ts.map +1 -1
- package/dist/runtime/facet-loader-host.js +0 -7
- package/dist/runtime/git-bundle-artifact.d.ts +17 -0
- package/dist/runtime/git-bundle-artifact.d.ts.map +1 -0
- package/dist/runtime/git-bundle-artifact.js +25 -0
- package/dist/runtime/node-repl.d.ts.map +1 -1
- package/dist/runtime/node-repl.js +2 -13
- package/dist/runtime/node-shims-artifact.d.ts +22 -23
- package/dist/runtime/node-shims-artifact.d.ts.map +1 -1
- package/dist/runtime/node-shims-artifact.js +48 -78
- package/dist/runtime/node-shims.d.ts.map +1 -1
- package/dist/runtime/node-shims.js +2358 -996
- package/dist/runtime/opencode-artifact.d.ts.map +1 -1
- package/dist/runtime/opencode-artifact.js +17 -53
- package/dist/runtime/opencode-facet-runner.d.ts +3 -6
- package/dist/runtime/opencode-facet-runner.d.ts.map +1 -1
- package/dist/runtime/opencode-facet-runner.js +45 -10
- package/dist/runtime/opentui-wasm-bytes.d.ts.map +1 -1
- package/dist/runtime/opentui-wasm-bytes.js +16 -49
- package/dist/runtime/package-manager.d.ts +0 -2
- package/dist/runtime/package-manager.d.ts.map +1 -1
- package/dist/runtime/package-manager.js +0 -3
- package/dist/runtime/python-repl.d.ts.map +1 -1
- package/dist/runtime/python-repl.js +4 -35
- package/dist/runtime/repl-session.d.ts +10 -0
- package/dist/runtime/repl-session.d.ts.map +1 -1
- package/dist/runtime/repl-session.js +7 -0
- package/dist/runtime/ruby-repl.d.ts.map +1 -1
- package/dist/runtime/ruby-repl.js +2 -2
- package/dist/runtime/ruby-resident.d.ts.map +1 -1
- package/dist/runtime/ruby-resident.js +41 -51
- package/dist/runtime/runtime-catalog.d.ts +10 -3
- package/dist/runtime/runtime-catalog.d.ts.map +1 -1
- package/dist/runtime/runtime-catalog.js +125 -15
- package/dist/runtime/sqlite-wasm-bytes.d.ts +4 -4
- package/dist/runtime/sqlite-wasm-bytes.d.ts.map +1 -1
- package/dist/runtime/sqlite-wasm-bytes.js +21 -57
- package/dist/runtime/staged-source.d.ts +67 -0
- package/dist/runtime/staged-source.d.ts.map +1 -0
- package/dist/runtime/staged-source.js +113 -0
- package/dist/runtime/wasi/preamble.d.ts +0 -22
- package/dist/runtime/wasi/preamble.d.ts.map +1 -1
- package/dist/runtime/wasi/preamble.js +98 -1425
- package/dist/runtime-catalog.generated.js +1 -1
- package/dist/session/agent.d.ts +14 -2
- package/dist/session/agent.d.ts.map +1 -1
- package/dist/session/agent.js +57 -19
- package/dist/session/ai.d.ts +0 -11
- package/dist/session/ai.d.ts.map +1 -1
- package/dist/session/ai.js +0 -4
- package/dist/session/fs-watch.d.ts +0 -11
- package/dist/session/fs-watch.d.ts.map +1 -1
- package/dist/session/fs-watch.js +0 -21
- package/dist/session/hibernation.d.ts +68 -2
- package/dist/session/hibernation.d.ts.map +1 -1
- package/dist/session/hibernation.js +95 -1
- package/dist/session/init-phases.d.ts +1 -2
- package/dist/session/init-phases.d.ts.map +1 -1
- package/dist/session/init-phases.js +1 -2
- package/dist/session/init.d.ts.map +1 -1
- package/dist/session/init.js +7 -20
- package/dist/session/keys.d.ts +0 -3
- package/dist/session/keys.d.ts.map +1 -1
- package/dist/session/keys.js +0 -3
- package/dist/session/legacy-reset.d.ts +11 -0
- package/dist/session/legacy-reset.d.ts.map +1 -0
- package/dist/session/legacy-reset.js +16 -0
- package/dist/session/nimbus-session.d.ts +50 -16
- package/dist/session/nimbus-session.d.ts.map +1 -1
- package/dist/session/nimbus-session.js +77 -71
- package/dist/session/npm-install-port.d.ts.map +1 -1
- package/dist/session/npm-install-port.js +2 -0
- package/dist/session/programmatic.d.ts +13 -18
- package/dist/session/programmatic.d.ts.map +1 -1
- package/dist/session/programmatic.js +53 -39
- package/dist/session/replica-routes.d.ts +2 -3
- package/dist/session/replica-routes.d.ts.map +1 -1
- package/dist/session/replica-routes.js +3 -10
- package/dist/session/routes.d.ts.map +1 -1
- package/dist/session/routes.js +11 -12
- package/dist/session/rpc.d.ts +108 -21
- package/dist/session/rpc.d.ts.map +1 -1
- package/dist/session/rpc.js +194 -39
- package/dist/session/start-real-vite.js +2 -2
- package/dist/session/state-store.d.ts +1 -17
- package/dist/session/state-store.d.ts.map +1 -1
- package/dist/session/state-store.js +3 -53
- package/dist/session/supervisor-op.d.ts +7 -0
- package/dist/session/supervisor-op.d.ts.map +1 -1
- package/dist/session/supervisor-op.js +6 -1
- package/dist/session/supervisor-rpc.d.ts +116 -19
- package/dist/session/supervisor-rpc.d.ts.map +1 -1
- package/dist/session/supervisor-rpc.js +194 -84
- package/dist/session/vite-command.d.ts.map +1 -1
- package/dist/session/vite-command.js +17 -14
- package/dist/session/ws.d.ts.map +1 -1
- package/dist/session/ws.js +20 -8
- package/dist/shell/npm-bin-entrypoints.d.ts.map +1 -1
- package/dist/shell/npm-bin-entrypoints.js +30 -5
- package/dist/vfs/facet-resident-limits.d.ts +33 -0
- package/dist/vfs/facet-resident-limits.d.ts.map +1 -0
- package/dist/vfs/facet-resident-limits.js +33 -0
- package/dist/vfs/facet-resident-store.d.ts +2 -10
- package/dist/vfs/facet-resident-store.d.ts.map +1 -1
- package/dist/vfs/facet-resident-store.js +1594 -219
- package/dist/workspace-host.d.ts +3 -1
- package/dist/workspace-host.d.ts.map +1 -1
- package/dist/workspace-host.js +2 -0
- package/dist/wrangler/nimbus-wrangler.d.ts +0 -2
- package/dist/wrangler/nimbus-wrangler.d.ts.map +1 -1
- package/dist/wrangler/nimbus-wrangler.js +8 -22
- package/package.json +34 -7
- package/public/_assets/esbuild-0.24.2.js +2380 -0
- package/public/_assets/opencode/1.16.2/index-attach.js +2 -2
- package/public/_assets/runtime/esbuild-cli-c4fda380d52befc1.js +6892 -0
- package/public/_assets/runtime/git-de3e60c9f3ff006a.js +103 -0
- package/public/_assets/runtime/{node-shims-acaa037e6d26ec0d.js → node-shims-06565b121555c194.js} +2879 -992
- package/public/_assets/runtime/resident-store-6ffb1939c53d982f.js +2264 -0
- package/public/_assets/runtime/vfs-write-ledger-e501e8fc793bc2c1.js +889 -0
- package/scripts/build-opencode-attach-entry.mjs +51 -1
- package/scripts/bundle-esbuild-wasm.mjs +55 -39
- package/scripts/bundle-facet-workers.mjs +159 -10
- package/scripts/bundle-git.mjs +88 -27
- package/scripts/bundle-node-shims.mjs +72 -46
- package/scripts/bundle-runtime.mjs +18 -421
- package/scripts/cf-git-patch.mjs +137 -0
- package/scripts/patch-install-deps.mjs +49 -96
- package/scripts/runtime-specs.mjs +435 -0
- package/vendor/git-http.generated.d.ts +82 -0
- package/vendor/git-upstream.generated.d.ts +4378 -0
- package/vendor/git.LICENSE +7 -0
- package/vendor/git.generated.d.mts +21 -0
- package/vendor/git.generated.mjs +103 -0
- package/dist/replica/suspension.d.ts +0 -36
- package/dist/replica/suspension.d.ts.map +0 -1
- package/dist/replica/suspension.js +0 -49
- package/dist/runtime/static-server.d.ts +0 -3
- package/dist/runtime/static-server.d.ts.map +0 -1
- package/dist/runtime/static-server.js +0 -133
- package/scripts/clean-dist.mjs +0 -7
|
@@ -0,0 +1,2264 @@
|
|
|
1
|
+
// The store's tables (__residentTablesOnSql or __residentTablesInMemory),
|
|
2
|
+
// bound once the process exists. Module scope has no ctx, so every trap below
|
|
3
|
+
// runs in handler context, which is also the only context that reads the
|
|
4
|
+
// resident set.
|
|
5
|
+
let __residentT = null;
|
|
6
|
+
let __residentReady = false;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Reads are SEALED until this incarnation has reconciled the store against the
|
|
10
|
+
* authority. This is the whole coherence design, and it is a seal rather than a
|
|
11
|
+
* check because a check gets forgotten.
|
|
12
|
+
*
|
|
13
|
+
* The hazard is specific to putting the resident set in storage, and it is
|
|
14
|
+
* created by the property that makes the design affordable: a facet's SQLite
|
|
15
|
+
* survives a fresh module scope (measured — same facet name, new loader key,
|
|
16
|
+
* new module token, 7,141 rows and 45.7 MB intact). So a new incarnation opens
|
|
17
|
+
* onto rows written by a PREVIOUS one, while every in-heap stamp and cursor
|
|
18
|
+
* that described them is gone. Heap-side provenance cannot survive the thing it
|
|
19
|
+
* is meant to describe, so provenance lives in the rows: 'file.rev' per path,
|
|
20
|
+
* '(epoch, rev)' for the store, both in this SQLite.
|
|
21
|
+
*
|
|
22
|
+
* Sealed is the initial state and `__residentAdmit` is the only thing that
|
|
23
|
+
* clears it, so a row cannot be served by an incarnation that has not applied
|
|
24
|
+
* the authority's delta. There is one gate and it is on the only door —
|
|
25
|
+
* `__residentRequire`, which every read, scan and write goes through.
|
|
26
|
+
*/
|
|
27
|
+
let __residentSealed = true;
|
|
28
|
+
/**
|
|
29
|
+
* Set when this facet booted from a snapshot carrying no cursor, so the store
|
|
30
|
+
* holds nothing. Read by residency-miss reporting: without it, "not resident"
|
|
31
|
+
* is indistinguishable from "resident set was never adoptable", and the second
|
|
32
|
+
* is a much more actionable thing to be told.
|
|
33
|
+
*/
|
|
34
|
+
let __residentUndated = false;
|
|
35
|
+
let __residentSealReason = "the store has not reconciled with the authority in this incarnation";
|
|
36
|
+
|
|
37
|
+
const __RESIDENT_CHUNK_BYTES = 1048576;
|
|
38
|
+
const __RESIDENT_BATCH_ROWS = 512;
|
|
39
|
+
const __RESIDENT_BATCH_PATHS = 1024;
|
|
40
|
+
const __RESIDENT_BATCH_BYTES = 4194304;
|
|
41
|
+
const __RESIDENT_LIST_PAGE = 8192;
|
|
42
|
+
/**
|
|
43
|
+
* Pages one enumeration may take before it gives up.
|
|
44
|
+
*
|
|
45
|
+
* A bound rather than an unbounded loop because `fsList` is a network call in
|
|
46
|
+
* a boot path: a supervisor that never reported a null `next` would spin here
|
|
47
|
+
* forever and the process would never start. At the page size above this
|
|
48
|
+
* admits filesystems into the tens of millions of paths, so it bounds a defect
|
|
49
|
+
* rather than a workload.
|
|
50
|
+
*/
|
|
51
|
+
const __RESIDENT_MAX_LIST_PAGES = 4096;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Cell kinds. A cell is one of the three things __vfsBundle has always held,
|
|
55
|
+
* and the kind is stored rather than inferred so a text file whose bytes happen
|
|
56
|
+
* to be valid UTF-8 does not change shape between a write and a read.
|
|
57
|
+
*/
|
|
58
|
+
const __RESIDENT_SCHEMA = "2";
|
|
59
|
+
|
|
60
|
+
// N18: the session's storage ledger admits this facet's database growth
|
|
61
|
+
// before it happens. The supervisor grants an allowance (the ledger records
|
|
62
|
+
// it under this facet's name); the store never grows its database past
|
|
63
|
+
// `__residentCap`, and asks for more, asynchronously, when it would. What
|
|
64
|
+
// does not fit is not held: a fill degrades to reads through the session,
|
|
65
|
+
// and an own write goes to the session only. Null: no ledger (no cap).
|
|
66
|
+
let __residentCap = null;
|
|
67
|
+
let __residentFacet = null;
|
|
68
|
+
let __residentSupervisor = null;
|
|
69
|
+
let __residentGrantAsk = null;
|
|
70
|
+
const __RESIDENT_ROW_BYTES = 256;
|
|
71
|
+
function __namespaceRowBytes(parent, name, target) {
|
|
72
|
+
// JavaScript keeps path/target strings in the heap. Charge UTF-16 storage
|
|
73
|
+
// plus the existing row allowance, even when the engine can compress text.
|
|
74
|
+
return __RESIDENT_ROW_BYTES + 2 * (String(parent).length + String(name).length + (target == null ? 0 : String(target).length));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function __residentDbBytes() {
|
|
78
|
+
// A one-shot's heap is one budget: the store's tables and the own writes
|
|
79
|
+
// held beside them (__residentHold) count against the same cap.
|
|
80
|
+
return __residentT.bytes() + (__residentInHeap ? __residentHeldBytes + __residentNamespaceOverlayBytes + __residentNamespaceReserveBytes : 0);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Whether the store is a one-shot's heap (__residentBindInMemory) rather than a facet's SQLite. */
|
|
84
|
+
let __residentInHeap = false;
|
|
85
|
+
let __residentNamespaceOverlayBytes = 0;
|
|
86
|
+
let __residentNamespaceReserveBytes = 0;
|
|
87
|
+
function __residentNamespaceOverlayDelta(bytes) {
|
|
88
|
+
if (!__residentInHeap) return;
|
|
89
|
+
if (bytes > 0 && __residentCap !== null && __residentDbBytes() + bytes > __residentCap) {
|
|
90
|
+
const cause = "namespace storage budget exceeded (" + __residentCap + " bytes)";
|
|
91
|
+
__nsMarkReady(__residentT, false, cause);
|
|
92
|
+
throw Object.assign(new Error(cause), { code: "ENOSPC" });
|
|
93
|
+
}
|
|
94
|
+
__residentNamespaceOverlayBytes += bytes;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Whether the store may grow by `bytes` now; when not, and `ask`, more room
|
|
99
|
+
* is asked for. A copy of an own write never asks: the session's copy is the
|
|
100
|
+
* one that must have the room, and the cache must not take it first.
|
|
101
|
+
*/
|
|
102
|
+
function __residentFits(bytes, ask = true) {
|
|
103
|
+
if (__residentCap === null) return true;
|
|
104
|
+
if (__residentDbBytes() + bytes <= __residentCap) return true;
|
|
105
|
+
if (ask) void __residentAskGrant(bytes);
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Ask the ledger for `bytes` more, reporting what the database measures.
|
|
111
|
+
* Exactly what is needed: room granted and unused is room the session's own
|
|
112
|
+
* writes cannot have. Resolves to whether `bytes` now fit.
|
|
113
|
+
*/
|
|
114
|
+
function __residentAskGrant(bytes) {
|
|
115
|
+
const supervisor = __residentSupervisor;
|
|
116
|
+
if (__residentCap === null) return Promise.resolve(true);
|
|
117
|
+
// A store with no ledger row (a one-shot's heap) has a fixed budget: no one to ask.
|
|
118
|
+
if (__residentFacet === null || !supervisor || typeof supervisor.fsStorageGrant !== "function") return Promise.resolve(false);
|
|
119
|
+
if (__residentGrantAsk) return __residentGrantAsk.then(() => __residentDbBytes() + bytes <= __residentCap);
|
|
120
|
+
const want = Math.max(0, __residentDbBytes() + bytes - __residentCap);
|
|
121
|
+
__residentGrantAsk = (async () => {
|
|
122
|
+
try {
|
|
123
|
+
const answer = await supervisor.fsStorageGrant(__residentFacet, want, __residentDbBytes());
|
|
124
|
+
const granted = Number(answer && answer.granted) || 0;
|
|
125
|
+
__residentCap += granted;
|
|
126
|
+
if (answer && typeof answer.cap === "number") __residentCap = Math.max(__residentCap, answer.cap);
|
|
127
|
+
} catch {}
|
|
128
|
+
})().finally(() => { __residentGrantAsk = null; });
|
|
129
|
+
return __residentGrantAsk.then(() => __residentDbBytes() + bytes <= __residentCap);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** Room for `bytes` before an asynchronous fill writes them: true when they fit. */
|
|
133
|
+
async function __residentEnsureRoom(bytes) {
|
|
134
|
+
if (__residentCap === null || __residentDbBytes() + bytes <= __residentCap) return true;
|
|
135
|
+
return __residentAskGrant(bytes);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* What holding a cell costs the database: its bytes (a text cell's UTF-8
|
|
140
|
+
* length) on overflow pages (1% over, for page headers and the b-tree pages
|
|
141
|
+
* above them), a row per chunk and head, and a page of slack. The manager
|
|
142
|
+
* charges a launch's module map the same way (manager.ts residentCellCost).
|
|
143
|
+
*/
|
|
144
|
+
function __residentCellCost(cell) {
|
|
145
|
+
const isText = typeof cell === "string";
|
|
146
|
+
const size = isText ? cell.length : __residentBytes(cell).byteLength;
|
|
147
|
+
const limit = isText ? Math.floor(__RESIDENT_CHUNK_BYTES / 3) : __RESIDENT_CHUNK_BYTES;
|
|
148
|
+
const chunks = Math.max(1, Math.ceil(size / limit));
|
|
149
|
+
return Math.ceil((isText ? __residentUtf8Length(cell) : size) * 1.01) + (chunks + 1) * __RESIDENT_ROW_BYTES + 4096;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function __residentUtf8Length(text) {
|
|
153
|
+
let bytes = 0;
|
|
154
|
+
for (let i = 0; i < text.length; i++) {
|
|
155
|
+
const c = text.charCodeAt(i);
|
|
156
|
+
if (c < 0x80) bytes += 1;
|
|
157
|
+
else if (c < 0x800) bytes += 2;
|
|
158
|
+
// A surrogate pair is one 4-byte character; a lone surrogate encodes as 3.
|
|
159
|
+
else if (c >= 0xd800 && c < 0xdc00 && i + 1 < text.length && (text.charCodeAt(i + 1) & 0xfc00) === 0xdc00) { bytes += 4; i++; }
|
|
160
|
+
else bytes += 3;
|
|
161
|
+
}
|
|
162
|
+
return bytes;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** Room for what an ACQUIRE answer pushes, asked for before it is applied. */
|
|
166
|
+
async function __residentRoomForPushed(result) {
|
|
167
|
+
if (__residentCap === null || !result || !Array.isArray(result.paths)) return;
|
|
168
|
+
let bytes = 0;
|
|
169
|
+
for (const entry of result.paths) {
|
|
170
|
+
if (entry && entry.bytes != null) bytes += __residentCellCost(__residentBytes(entry.bytes));
|
|
171
|
+
}
|
|
172
|
+
if (bytes > 0) await __residentEnsureRoom(bytes);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** What the database measures, after a fill batch: the ledger settles to it, or records overshoot. */
|
|
176
|
+
async function __residentReportSize() {
|
|
177
|
+
const supervisor = __residentSupervisor;
|
|
178
|
+
if (__residentCap === null || __residentFacet === null || !supervisor || typeof supervisor.fsStorageGrant !== "function") return;
|
|
179
|
+
try { await supervisor.fsStorageGrant(__residentFacet, 0, __residentDbBytes()); } catch {}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** The allowance this launch was admitted for (the fabric's startArgs.storage). */
|
|
183
|
+
function __residentSetStorage(storage, supervisor) {
|
|
184
|
+
__residentSupervisor = supervisor || null;
|
|
185
|
+
if (!storage || typeof storage.facet !== "string" || typeof storage.grant !== "number") return;
|
|
186
|
+
__residentFacet = storage.facet;
|
|
187
|
+
__residentCap = __residentDbBytes() + storage.grant;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** The cap this store enforces, for startProcess to report. */
|
|
191
|
+
function __residentStorageCap() {
|
|
192
|
+
return __residentCap;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// An own write the store could not hold (no room in the session's storage)
|
|
196
|
+
// is kept here, so the program still reads what it wrote: a bounded LRU
|
|
197
|
+
// (Map order is recency) of { cell, rev }, dated like a file row (own until
|
|
198
|
+
// its write-back is acknowledged, then the write's revision) and judged by
|
|
199
|
+
// every report like one. A path it could not keep either is remembered by
|
|
200
|
+
// name only, so a miss on it says why.
|
|
201
|
+
const __residentHeld = new Map();
|
|
202
|
+
let __residentHeldBytes = 0;
|
|
203
|
+
const __residentUnheld = new Set();
|
|
204
|
+
const __RESIDENT_HELD_MAX_BYTES = 33554432;
|
|
205
|
+
|
|
206
|
+
function __residentCellSize(held) {
|
|
207
|
+
const cell = held.cell;
|
|
208
|
+
return typeof cell === "string" ? cell.length * 3 : __residentBytes(cell).byteLength;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function __residentForgetHeld(path) {
|
|
212
|
+
const cell = __residentHeld.get(path);
|
|
213
|
+
if (cell !== undefined) {
|
|
214
|
+
__residentHeld.delete(path);
|
|
215
|
+
__residentHeldBytes -= __residentCellSize(cell);
|
|
216
|
+
}
|
|
217
|
+
__residentUnheld.delete(path);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function __residentHold(path, cell) {
|
|
221
|
+
__residentForgetHeld(path);
|
|
222
|
+
const size = __residentCellSize({ cell });
|
|
223
|
+
// In a facet the held cells have their own bound. In a one-shot's heap they
|
|
224
|
+
// share the store's: what the cap leaves beside the tables.
|
|
225
|
+
const limit = __residentInHeap ? Math.max(0, Number(__residentCap) - __residentT.bytes() - __residentNamespaceOverlayBytes - __residentNamespaceReserveBytes) : __RESIDENT_HELD_MAX_BYTES;
|
|
226
|
+
if (size > limit) { __residentUnheld.add(path); return; }
|
|
227
|
+
while (__residentHeldBytes + size > limit) {
|
|
228
|
+
const [oldest, old] = __residentHeld.entries().next().value;
|
|
229
|
+
__residentHeld.delete(oldest);
|
|
230
|
+
__residentHeldBytes -= __residentCellSize(old);
|
|
231
|
+
__residentUnheld.add(oldest);
|
|
232
|
+
}
|
|
233
|
+
__residentHeld.set(path, { cell, rev: __RK_OWN_WRITE });
|
|
234
|
+
__residentHeldBytes += size;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/** Whether a miss on `path` is the storage's: its own write was held nowhere. */
|
|
238
|
+
function __residentStorageMiss(path) {
|
|
239
|
+
return __residentUnheld.has(path);
|
|
240
|
+
}
|
|
241
|
+
const __RK_TEXT = 0;
|
|
242
|
+
const __RK_BINARY = 1;
|
|
243
|
+
const __RK_DENIED = 2;
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* The revision of a cell holding this facet's own bytes that the authority has
|
|
247
|
+
* not acknowledged yet: a write that has not flushed, or a cell one of its own
|
|
248
|
+
* mutations holds while that mutation is in flight (`__residentLease`).
|
|
249
|
+
*
|
|
250
|
+
* Read-your-writes: strictly newer than anything the authority could report,
|
|
251
|
+
* so it is never evicted by a delta and never mistaken for something the
|
|
252
|
+
* authority vouched for. A distinct sentinel rather than NULL, because NULL
|
|
253
|
+
* would reintroduce the undated row the NOT NULL constraint exists to forbid.
|
|
254
|
+
*
|
|
255
|
+
* It is the ONE revision a delta cannot evict, so it must mean exactly that
|
|
256
|
+
* and nothing else. The acknowledgement dates the row (`__residentStamp`),
|
|
257
|
+
* and bytes the authority served are dated as they are installed
|
|
258
|
+
* (`__residentFill`). A row left here after either would hold its bytes
|
|
259
|
+
* against every later write by anyone, for the life of the store.
|
|
260
|
+
*/
|
|
261
|
+
const __RK_OWN_WRITE = -1;
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* The store's own operations, over the four tables it keeps: 'file' (one row
|
|
265
|
+
* per held path: kind, size, chunk count, revision, content key), 'chunk'
|
|
266
|
+
* (the bytes), 'meta' (schema, cursor, namespace readiness) and 'ns' (the
|
|
267
|
+
* namespace: a stat per visible name, keyed (parent, name)). Everything above
|
|
268
|
+
* this seam decides; the backing only stores. There are exactly two backings,
|
|
269
|
+
* with one contract:
|
|
270
|
+
*
|
|
271
|
+
* - __residentTablesOnSql: the process facet's own SQLite (ctx.storage.sql),
|
|
272
|
+
* durable across the facet's incarnations. Every statement the store has
|
|
273
|
+
* ever issued lives here, unchanged.
|
|
274
|
+
* - __residentTablesInMemory: a one-shot process's store, in its heap, for
|
|
275
|
+
* the life of the run (runOnce hosts no SQLite). Same rows, same answers,
|
|
276
|
+
* and a byte count that plays the database's size for the store's budget.
|
|
277
|
+
*
|
|
278
|
+
* Rows come back as plain objects with the column names as keys: file heads
|
|
279
|
+
* { kind, size, chunks }, chunks { txt, bin } (bin as the backing holds it;
|
|
280
|
+
* callers normalise with __residentBytes), ns rows with every stat column.
|
|
281
|
+
*/
|
|
282
|
+
function __residentTablesOnSql(sql) {
|
|
283
|
+
let schema = null;
|
|
284
|
+
for (const row of sql.exec("SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'meta'")) schema = "";
|
|
285
|
+
if (schema !== null) for (const row of sql.exec("SELECT v FROM meta WHERE k = 'schema'")) schema = String(row.v);
|
|
286
|
+
if (schema !== __RESIDENT_SCHEMA) {
|
|
287
|
+
for (const table of ["chunk", "file", "ns", "meta"]) sql.exec("DROP TABLE IF EXISTS " + table);
|
|
288
|
+
}
|
|
289
|
+
// 'ckey' is the authority's content identity for the bytes (equal keys,
|
|
290
|
+
// equal bytes), when it said so; null for bytes it did not describe.
|
|
291
|
+
sql.exec(
|
|
292
|
+
"CREATE TABLE IF NOT EXISTS file (" +
|
|
293
|
+
"path TEXT PRIMARY KEY, kind INTEGER NOT NULL, size INTEGER NOT NULL, " +
|
|
294
|
+
"chunks INTEGER NOT NULL, rev INTEGER NOT NULL CHECK (rev >= -1), ckey TEXT" +
|
|
295
|
+
")"
|
|
296
|
+
);
|
|
297
|
+
sql.exec("CREATE INDEX IF NOT EXISTS file_ckey ON file (ckey) WHERE ckey IS NOT NULL");
|
|
298
|
+
sql.exec(
|
|
299
|
+
"CREATE TABLE IF NOT EXISTS chunk (" +
|
|
300
|
+
"path TEXT NOT NULL, part INTEGER NOT NULL, txt TEXT, bin BLOB, " +
|
|
301
|
+
"PRIMARY KEY (path, part)" +
|
|
302
|
+
")"
|
|
303
|
+
);
|
|
304
|
+
// The store's own cursor, in the same storage as the rows it describes, so
|
|
305
|
+
// the two cannot be separated by an isolate restart.
|
|
306
|
+
sql.exec("CREATE TABLE IF NOT EXISTS meta (k TEXT PRIMARY KEY, v TEXT NOT NULL)");
|
|
307
|
+
sql.exec("INSERT OR REPLACE INTO meta (k, v) VALUES ('schema', ?)", __RESIDENT_SCHEMA);
|
|
308
|
+
// The namespace: every name the process's credential can see, with its stat
|
|
309
|
+
// and no content. Keyed (parent, name) so a point lookup and a directory
|
|
310
|
+
// listing are both one index seek, and no path is stored twice.
|
|
311
|
+
sql.exec(
|
|
312
|
+
"CREATE TABLE IF NOT EXISTS ns (" +
|
|
313
|
+
"parent TEXT NOT NULL, name TEXT NOT NULL, kind INTEGER NOT NULL, size INTEGER NOT NULL, " +
|
|
314
|
+
"mode INTEGER NOT NULL, uid INTEGER NOT NULL, gid INTEGER NOT NULL, atime REAL NOT NULL, " +
|
|
315
|
+
"mtime REAL NOT NULL, ctime REAL NOT NULL, ino INTEGER NOT NULL, rev INTEGER NOT NULL, target TEXT, " +
|
|
316
|
+
"PRIMARY KEY (parent, name)" +
|
|
317
|
+
") WITHOUT ROWID"
|
|
318
|
+
);
|
|
319
|
+
const first = (rows) => { for (const row of rows) return row; return undefined; };
|
|
320
|
+
const ranged = (select, from, to, tail = "") => to === null
|
|
321
|
+
? sql.exec(select + " WHERE path >= ?" + tail, from)
|
|
322
|
+
: sql.exec(select + " WHERE path >= ? AND path < ?" + tail, from, to);
|
|
323
|
+
return {
|
|
324
|
+
bytes() {
|
|
325
|
+
const size = sql.databaseSize;
|
|
326
|
+
if (typeof size === "number" && size > 0) return size;
|
|
327
|
+
for (const row of sql.exec("SELECT page_count * page_size AS n FROM pragma_page_count(), pragma_page_size()")) return Number(row.n);
|
|
328
|
+
return 0;
|
|
329
|
+
},
|
|
330
|
+
databaseSize() { return Number(sql.databaseSize ?? 0); },
|
|
331
|
+
metaGet(k) { const row = first(sql.exec("SELECT v FROM meta WHERE k = ?", k)); return row === undefined ? undefined : String(row.v); },
|
|
332
|
+
metaSet(k, v) { sql.exec("INSERT OR REPLACE INTO meta (k, v) VALUES (?, ?)", k, v); },
|
|
333
|
+
fileHead(path) { return first(sql.exec("SELECT kind, size, chunks FROM file WHERE path = ?", path)); },
|
|
334
|
+
fileRev(path) { const row = first(sql.exec("SELECT rev FROM file WHERE path = ?", path)); return row === undefined ? undefined : Number(row.rev); },
|
|
335
|
+
fileRevKey(path) { return first(sql.exec("SELECT rev, ckey FROM file WHERE path = ?", path)); },
|
|
336
|
+
fileOwnSize(path) {
|
|
337
|
+
const row = first(sql.exec("SELECT size FROM file WHERE path = ? AND rev = ?", path, __RK_OWN_WRITE));
|
|
338
|
+
return row === undefined ? undefined : Number(row.size);
|
|
339
|
+
},
|
|
340
|
+
filePut(path, kind, size, chunks, rev, ckey) {
|
|
341
|
+
sql.exec("INSERT OR REPLACE INTO file (path, kind, size, chunks, rev, ckey) VALUES (?, ?, ?, ?, ?, ?)", path, kind, size, chunks, rev, ckey);
|
|
342
|
+
},
|
|
343
|
+
fileSetRev(path, rev) { sql.exec("UPDATE file SET rev = ? WHERE path = ?", rev, path); },
|
|
344
|
+
fileStampOwn(path, rev) { sql.exec("UPDATE file SET rev = ? WHERE path = ? AND rev = ?", rev, path, __RK_OWN_WRITE); },
|
|
345
|
+
fileDelete(path) { sql.exec("DELETE FROM file WHERE path = ?", path); },
|
|
346
|
+
fileByKey(ckey, notPath) {
|
|
347
|
+
return first(sql.exec("SELECT path, kind, size, chunks FROM file WHERE ckey = ? AND rev >= 0 AND path <> ? LIMIT 1", ckey, notPath));
|
|
348
|
+
},
|
|
349
|
+
filePaths(from, to) { return [...ranged("SELECT path FROM file", from, to)].map((row) => String(row.path)); },
|
|
350
|
+
fileAny(from, to) { return first(ranged("SELECT path FROM file", from, to, " LIMIT 1")) !== undefined; },
|
|
351
|
+
fileRevsIn(from, to) { return [...ranged("SELECT path, rev FROM file", from, to)]; },
|
|
352
|
+
// In key order, as every other scan here is: the primary key's index
|
|
353
|
+
// serves it, and both backings then answer in one order.
|
|
354
|
+
fileAllPaths() { return [...sql.exec("SELECT path FROM file ORDER BY path")].map((row) => String(row.path)); },
|
|
355
|
+
fileRows() { return [...sql.exec("SELECT path, rev, ckey FROM file ORDER BY path")]; },
|
|
356
|
+
fileOwnPaths() { return [...sql.exec("SELECT path FROM file WHERE rev = ? ORDER BY path", __RK_OWN_WRITE)].map((row) => String(row.path)); },
|
|
357
|
+
fileStats() { return first(sql.exec("SELECT count(*) AS n, coalesce(sum(size), 0) AS b FROM file")); },
|
|
358
|
+
chunkFirst(path) { return first(sql.exec("SELECT txt, bin FROM chunk WHERE path = ? AND part = 0", path)); },
|
|
359
|
+
chunkParts(path) { return [...sql.exec("SELECT txt, bin FROM chunk WHERE path = ? ORDER BY part", path)]; },
|
|
360
|
+
chunkAddText(path, part, txt) { sql.exec("INSERT INTO chunk (path, part, txt) VALUES (?, ?, ?)", path, part, txt); },
|
|
361
|
+
chunkAddBin(path, part, bin) { sql.exec("INSERT INTO chunk (path, part, bin) VALUES (?, ?, ?)", path, part, bin); },
|
|
362
|
+
chunkSetBin(path, part, bin) { sql.exec("INSERT OR REPLACE INTO chunk (path, part, bin) VALUES (?, ?, ?)", path, part, bin); },
|
|
363
|
+
chunkCopy(to, from) { sql.exec("INSERT INTO chunk (path, part, txt, bin) SELECT ?, part, txt, bin FROM chunk WHERE path = ?", to, from); },
|
|
364
|
+
chunkDelete(path) { sql.exec("DELETE FROM chunk WHERE path = ?", path); },
|
|
365
|
+
// Own rows are the one provenance a new incarnation cannot date.
|
|
366
|
+
dropOwn() {
|
|
367
|
+
sql.exec("DELETE FROM chunk WHERE path IN (SELECT path FROM file WHERE rev = ?)", __RK_OWN_WRITE);
|
|
368
|
+
sql.exec("DELETE FROM file WHERE rev = ?", __RK_OWN_WRITE);
|
|
369
|
+
},
|
|
370
|
+
// Every dated row, and any chunk no own row claims.
|
|
371
|
+
dropDated() {
|
|
372
|
+
sql.exec("DELETE FROM chunk WHERE path NOT IN (SELECT path FROM file WHERE rev = ?)", __RK_OWN_WRITE);
|
|
373
|
+
sql.exec("DELETE FROM file WHERE rev <> ?", __RK_OWN_WRITE);
|
|
374
|
+
},
|
|
375
|
+
// Every dated row and its chunks.
|
|
376
|
+
dropDatedFiles() {
|
|
377
|
+
sql.exec("DELETE FROM chunk WHERE path IN (SELECT path FROM file WHERE rev <> ?)", __RK_OWN_WRITE);
|
|
378
|
+
sql.exec("DELETE FROM file WHERE rev <> ?", __RK_OWN_WRITE);
|
|
379
|
+
},
|
|
380
|
+
clear() {
|
|
381
|
+
sql.exec("DELETE FROM chunk");
|
|
382
|
+
sql.exec("DELETE FROM file");
|
|
383
|
+
sql.exec("DELETE FROM ns");
|
|
384
|
+
sql.exec("DELETE FROM meta");
|
|
385
|
+
},
|
|
386
|
+
nsPut(parent, name, kind, size, mode, uid, gid, atime, mtime, ctime, ino, rev, target) {
|
|
387
|
+
sql.exec(
|
|
388
|
+
"INSERT OR REPLACE INTO ns (parent, name, kind, size, mode, uid, gid, atime, mtime, ctime, ino, rev, target) " +
|
|
389
|
+
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
|
390
|
+
parent, name, kind, size, mode, uid, gid, atime, mtime, ctime, ino, rev, target,
|
|
391
|
+
);
|
|
392
|
+
},
|
|
393
|
+
nsGet(parent, name) {
|
|
394
|
+
return first(sql.exec(
|
|
395
|
+
"SELECT kind, size, mode, uid, gid, atime, mtime, ctime, ino, rev, target FROM ns WHERE parent = ? AND name = ?",
|
|
396
|
+
parent, name,
|
|
397
|
+
));
|
|
398
|
+
},
|
|
399
|
+
nsDelete(parent, name) { sql.exec("DELETE FROM ns WHERE parent = ? AND name = ?", parent, name); },
|
|
400
|
+
nsDeleteChildren(parent) { sql.exec("DELETE FROM ns WHERE parent = ?", parent); },
|
|
401
|
+
nsDeleteParents(from, to) {
|
|
402
|
+
if (to === null) sql.exec("DELETE FROM ns WHERE parent >= ?", from);
|
|
403
|
+
else sql.exec("DELETE FROM ns WHERE parent >= ? AND parent < ?", from, to);
|
|
404
|
+
},
|
|
405
|
+
nsClear() { sql.exec("DELETE FROM ns"); },
|
|
406
|
+
nsKeys() { return [...sql.exec("SELECT parent, name FROM ns")]; },
|
|
407
|
+
nsOfKind(kind) { return [...sql.exec("SELECT parent, name FROM ns WHERE kind = ?", kind)]; },
|
|
408
|
+
nsChildren(parent) { return [...sql.exec("SELECT name, kind FROM ns WHERE parent = ?", parent)]; },
|
|
409
|
+
nsCount() { const row = first(sql.exec("SELECT count(*) AS n FROM ns")); return row === undefined ? 0 : Number(row.n); },
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
function __residentBind(ctx) {
|
|
414
|
+
const sql = ctx && ctx.storage && ctx.storage.sql;
|
|
415
|
+
if (!sql || typeof sql.exec !== "function") {
|
|
416
|
+
throw new Error(
|
|
417
|
+
"Nimbus: this facet has no synchronous SQLite (ctx.storage.sql); " +
|
|
418
|
+
"the resident set cannot be served and every sync read would raise EAGAIN"
|
|
419
|
+
);
|
|
420
|
+
}
|
|
421
|
+
return __residentBindTables(__residentTablesOnSql(sql));
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* The one-shot backing: the same tables, in the process's heap, for the life
|
|
426
|
+
* of one run. A one-shot is hosted by runOnce, which gives it no SQLite, and
|
|
427
|
+
* it keeps nothing after it exits. Its answers are the SQLite backing's:
|
|
428
|
+
* scans come back in key order (a b-tree's), reads hand back copies (a
|
|
429
|
+
* program that writes into a Buffer it read must not change the store), and
|
|
430
|
+
* `bytes()` counts what it holds in the units __residentCellCost charges (a
|
|
431
|
+
* row, and the payload's bytes), so the store's one budget rule serves both.
|
|
432
|
+
*/
|
|
433
|
+
function __residentTablesInMemory() {
|
|
434
|
+
const files = new Map();
|
|
435
|
+
const chunks = new Map();
|
|
436
|
+
const meta = new Map();
|
|
437
|
+
const ns = new Map();
|
|
438
|
+
const byKey = new Map();
|
|
439
|
+
let sorted = null;
|
|
440
|
+
let used = 0;
|
|
441
|
+
let names = 0;
|
|
442
|
+
let namespaceBytes = 0;
|
|
443
|
+
const utf8 = (text) => __residentUtf8Length(text);
|
|
444
|
+
// SQLite's binary collation orders TEXT by its UTF-8 bytes: code-point
|
|
445
|
+
// order, which UTF-16 comparison is not past the BMP.
|
|
446
|
+
const cmp = (a, b) => {
|
|
447
|
+
const n = Math.min(a.length, b.length);
|
|
448
|
+
for (let i = 0; i < n; i++) {
|
|
449
|
+
const x = a.codePointAt(i), y = b.codePointAt(i);
|
|
450
|
+
if (x !== y) return x < y ? -1 : 1;
|
|
451
|
+
if (x > 0xffff) i++;
|
|
452
|
+
}
|
|
453
|
+
return a.length === b.length ? 0 : a.length < b.length ? -1 : 1;
|
|
454
|
+
};
|
|
455
|
+
const payload = (cell) => (cell.txt != null ? utf8(cell.txt) : cell.bin ? cell.bin.byteLength : 0);
|
|
456
|
+
const unkey = (path, row) => {
|
|
457
|
+
if (row && row.ckey != null) {
|
|
458
|
+
const set = byKey.get(row.ckey);
|
|
459
|
+
if (set) { set.delete(path); if (set.size === 0) byKey.delete(row.ckey); }
|
|
460
|
+
}
|
|
461
|
+
};
|
|
462
|
+
const paths = () => {
|
|
463
|
+
if (sorted === null) sorted = [...files.keys()].sort(cmp);
|
|
464
|
+
return sorted;
|
|
465
|
+
};
|
|
466
|
+
const lowerBound = (list, from) => {
|
|
467
|
+
let lo = 0, hi = list.length;
|
|
468
|
+
while (lo < hi) { const mid = (lo + hi) >>> 1; if (cmp(list[mid], from) < 0) lo = mid + 1; else hi = mid; }
|
|
469
|
+
return lo;
|
|
470
|
+
};
|
|
471
|
+
const inRange = (from, to) => {
|
|
472
|
+
const list = paths();
|
|
473
|
+
const out = [];
|
|
474
|
+
for (let i = lowerBound(list, from); i < list.length && (to === null || cmp(list[i], to) < 0); i++) out.push(list[i]);
|
|
475
|
+
return out;
|
|
476
|
+
};
|
|
477
|
+
const copy = (cell) => ({ txt: cell.txt ?? null, bin: cell.bin ? cell.bin.slice() : null });
|
|
478
|
+
const dropChunks = (path) => {
|
|
479
|
+
const parts = chunks.get(path);
|
|
480
|
+
if (!parts) return;
|
|
481
|
+
for (const cell of parts.values()) used -= __RESIDENT_ROW_BYTES + payload(cell);
|
|
482
|
+
chunks.delete(path);
|
|
483
|
+
};
|
|
484
|
+
const dropFile = (path) => {
|
|
485
|
+
const row = files.get(path);
|
|
486
|
+
if (row === undefined) return;
|
|
487
|
+
unkey(path, row);
|
|
488
|
+
files.delete(path);
|
|
489
|
+
sorted = null;
|
|
490
|
+
used -= __RESIDENT_ROW_BYTES;
|
|
491
|
+
};
|
|
492
|
+
const addChunk = (path, part, cell, replace) => {
|
|
493
|
+
let parts = chunks.get(path);
|
|
494
|
+
if (!parts) { parts = new Map(); chunks.set(path, parts); }
|
|
495
|
+
const had = parts.get(part);
|
|
496
|
+
if (had !== undefined) {
|
|
497
|
+
if (!replace) throw new Error("UNIQUE constraint failed: chunk.path, chunk.part");
|
|
498
|
+
used -= __RESIDENT_ROW_BYTES + payload(had);
|
|
499
|
+
}
|
|
500
|
+
parts.set(part, cell);
|
|
501
|
+
used += __RESIDENT_ROW_BYTES + payload(cell);
|
|
502
|
+
};
|
|
503
|
+
const nsRow = (parent, name) => { const dir = ns.get(parent); return dir ? dir.get(name) : undefined; };
|
|
504
|
+
const nsDrop = (parent, name) => {
|
|
505
|
+
const dir = ns.get(parent);
|
|
506
|
+
const row = dir?.get(name);
|
|
507
|
+
if (row === undefined) return;
|
|
508
|
+
dir.delete(name);
|
|
509
|
+
names--;
|
|
510
|
+
const bytes = __namespaceRowBytes(parent, name, row.target);
|
|
511
|
+
used -= bytes; namespaceBytes -= bytes;
|
|
512
|
+
if (dir.size === 0) ns.delete(parent);
|
|
513
|
+
};
|
|
514
|
+
const nsDropParent = (parent) => {
|
|
515
|
+
const dir = ns.get(parent);
|
|
516
|
+
if (!dir) return;
|
|
517
|
+
names -= dir.size;
|
|
518
|
+
for (const [name, row] of dir) {
|
|
519
|
+
const bytes = __namespaceRowBytes(parent, name, row.target);
|
|
520
|
+
used -= bytes; namespaceBytes -= bytes;
|
|
521
|
+
}
|
|
522
|
+
ns.delete(parent);
|
|
523
|
+
};
|
|
524
|
+
const byName = cmp;
|
|
525
|
+
const dropWhere = (keepFile, keepChunkPath) => {
|
|
526
|
+
for (const path of [...chunks.keys()]) if (!keepChunkPath(path)) dropChunks(path);
|
|
527
|
+
for (const [path, row] of [...files]) if (!keepFile(row)) dropFile(path);
|
|
528
|
+
};
|
|
529
|
+
return {
|
|
530
|
+
bytes() { return used; },
|
|
531
|
+
databaseSize() { return used; },
|
|
532
|
+
metaGet(k) { return meta.get(k); },
|
|
533
|
+
metaSet(k, v) { meta.set(k, String(v)); },
|
|
534
|
+
fileHead(path) { const row = files.get(path); return row === undefined ? undefined : { kind: row.kind, size: row.size, chunks: row.chunks }; },
|
|
535
|
+
fileRev(path) { const row = files.get(path); return row === undefined ? undefined : row.rev; },
|
|
536
|
+
fileRevKey(path) { const row = files.get(path); return row === undefined ? undefined : { rev: row.rev, ckey: row.ckey }; },
|
|
537
|
+
fileOwnSize(path) { const row = files.get(path); return row !== undefined && row.rev === __RK_OWN_WRITE ? row.size : undefined; },
|
|
538
|
+
filePut(path, kind, size, chunkCount, rev, ckey) {
|
|
539
|
+
if (!(rev >= -1)) throw new Error("CHECK constraint failed: rev >= -1");
|
|
540
|
+
const had = files.get(path);
|
|
541
|
+
if (had === undefined) { sorted = null; used += __RESIDENT_ROW_BYTES; } else unkey(path, had);
|
|
542
|
+
const row = { kind: Number(kind), size: Number(size), chunks: Number(chunkCount), rev: Number(rev), ckey: ckey == null ? null : String(ckey) };
|
|
543
|
+
files.set(path, row);
|
|
544
|
+
if (row.ckey !== null) { let set = byKey.get(row.ckey); if (!set) { set = new Set(); byKey.set(row.ckey, set); } set.add(path); }
|
|
545
|
+
},
|
|
546
|
+
fileSetRev(path, rev) { const row = files.get(path); if (row !== undefined) row.rev = Number(rev); },
|
|
547
|
+
fileStampOwn(path, rev) { const row = files.get(path); if (row !== undefined && row.rev === __RK_OWN_WRITE) row.rev = Number(rev); },
|
|
548
|
+
fileDelete(path) { dropFile(path); },
|
|
549
|
+
fileByKey(ckey, notPath) {
|
|
550
|
+
const set = byKey.get(String(ckey));
|
|
551
|
+
if (!set) return undefined;
|
|
552
|
+
for (const path of set) {
|
|
553
|
+
const row = files.get(path);
|
|
554
|
+
if (path !== notPath && row && row.rev >= 0) return { path, kind: row.kind, size: row.size, chunks: row.chunks };
|
|
555
|
+
}
|
|
556
|
+
return undefined;
|
|
557
|
+
},
|
|
558
|
+
filePaths(from, to) { return inRange(from, to); },
|
|
559
|
+
fileAny(from, to) { const list = paths(); const i = lowerBound(list, from); return i < list.length && (to === null || cmp(list[i], to) < 0); },
|
|
560
|
+
fileRevsIn(from, to) { return inRange(from, to).map((path) => ({ path, rev: files.get(path).rev })); },
|
|
561
|
+
fileAllPaths() { return [...paths()]; },
|
|
562
|
+
fileRows() { return paths().map((path) => { const row = files.get(path); return { path, rev: row.rev, ckey: row.ckey }; }); },
|
|
563
|
+
fileOwnPaths() { return paths().filter((path) => files.get(path).rev === __RK_OWN_WRITE); },
|
|
564
|
+
fileStats() { let b = 0; for (const row of files.values()) b += row.size; return { n: files.size, b }; },
|
|
565
|
+
chunkFirst(path) { const parts = chunks.get(path); const cell = parts && parts.get(0); return cell === undefined ? undefined : copy(cell); },
|
|
566
|
+
chunkParts(path) {
|
|
567
|
+
const parts = chunks.get(path);
|
|
568
|
+
if (!parts) return [];
|
|
569
|
+
return [...parts.keys()].sort((a, b) => a - b).map((part) => copy(parts.get(part)));
|
|
570
|
+
},
|
|
571
|
+
chunkAddText(path, part, txt) { addChunk(path, Number(part), { txt: String(txt), bin: null }, false); },
|
|
572
|
+
chunkAddBin(path, part, bin) { addChunk(path, Number(part), { txt: null, bin: __residentBytes(bin).slice() }, false); },
|
|
573
|
+
chunkSetBin(path, part, bin) { addChunk(path, Number(part), { txt: null, bin: __residentBytes(bin).slice() }, true); },
|
|
574
|
+
chunkCopy(to, from) {
|
|
575
|
+
const parts = chunks.get(from);
|
|
576
|
+
if (!parts) return;
|
|
577
|
+
// One statement in SQLite: a conflicting part fails the whole copy, and nothing lands.
|
|
578
|
+
const held = chunks.get(to);
|
|
579
|
+
if (held) for (const part of parts.keys()) if (held.has(part)) throw new Error("UNIQUE constraint failed: chunk.path, chunk.part");
|
|
580
|
+
for (const [part, cell] of [...parts]) addChunk(to, part, copy(cell), false);
|
|
581
|
+
},
|
|
582
|
+
chunkDelete(path) { dropChunks(path); },
|
|
583
|
+
dropOwn() {
|
|
584
|
+
const own = new Set(paths().filter((path) => files.get(path).rev === __RK_OWN_WRITE));
|
|
585
|
+
dropWhere((row) => row.rev !== __RK_OWN_WRITE, (path) => !own.has(path));
|
|
586
|
+
},
|
|
587
|
+
dropDated() {
|
|
588
|
+
const own = new Set(paths().filter((path) => files.get(path).rev === __RK_OWN_WRITE));
|
|
589
|
+
dropWhere((row) => row.rev === __RK_OWN_WRITE, (path) => own.has(path));
|
|
590
|
+
},
|
|
591
|
+
dropDatedFiles() {
|
|
592
|
+
const dated = new Set(paths().filter((path) => files.get(path).rev !== __RK_OWN_WRITE));
|
|
593
|
+
dropWhere((row) => row.rev === __RK_OWN_WRITE, (path) => !dated.has(path));
|
|
594
|
+
},
|
|
595
|
+
clear() {
|
|
596
|
+
files.clear(); chunks.clear(); meta.clear(); ns.clear(); byKey.clear();
|
|
597
|
+
sorted = null; used = 0; names = 0; namespaceBytes = 0;
|
|
598
|
+
},
|
|
599
|
+
nsPut(parent, name, kind, size, mode, uid, gid, atime, mtime, ctime, ino, rev, target) {
|
|
600
|
+
let dir = ns.get(parent);
|
|
601
|
+
const previous = dir?.get(name);
|
|
602
|
+
const bytes = __namespaceRowBytes(parent, name, target);
|
|
603
|
+
const growth = bytes - (previous === undefined ? 0 : __namespaceRowBytes(parent, name, previous.target));
|
|
604
|
+
if (__residentInHeap && __residentCap !== null && used + __residentHeldBytes + __residentNamespaceOverlayBytes + __residentNamespaceReserveBytes + growth > __residentCap) {
|
|
605
|
+
const cause = "namespace storage budget exceeded (" + __residentCap + " bytes)";
|
|
606
|
+
__nsMarkReady(__residentT, false, cause);
|
|
607
|
+
throw Object.assign(new Error(cause), { code: "ENOSPC" });
|
|
608
|
+
}
|
|
609
|
+
if (!dir) { dir = new Map(); ns.set(parent, dir); }
|
|
610
|
+
if (previous === undefined) names++;
|
|
611
|
+
used += growth; namespaceBytes += growth;
|
|
612
|
+
dir.set(name, {
|
|
613
|
+
kind: Number(kind), size: Number(size), mode: Number(mode), uid: Number(uid), gid: Number(gid),
|
|
614
|
+
atime: Number(atime), mtime: Number(mtime), ctime: Number(ctime), ino: Number(ino), rev: Number(rev),
|
|
615
|
+
target: target == null ? null : String(target),
|
|
616
|
+
});
|
|
617
|
+
},
|
|
618
|
+
nsGet(parent, name) { const row = nsRow(parent, name); return row === undefined ? undefined : { ...row }; },
|
|
619
|
+
nsDelete(parent, name) { nsDrop(parent, name); },
|
|
620
|
+
nsDeleteChildren(parent) { nsDropParent(parent); },
|
|
621
|
+
nsDeleteParents(from, to) {
|
|
622
|
+
for (const parent of [...ns.keys()]) if (cmp(parent, from) >= 0 && (to === null || cmp(parent, to) < 0)) nsDropParent(parent);
|
|
623
|
+
},
|
|
624
|
+
nsClear() { used -= namespaceBytes; names = 0; namespaceBytes = 0; ns.clear(); },
|
|
625
|
+
nsBytes() { return namespaceBytes; },
|
|
626
|
+
nsKeys() {
|
|
627
|
+
const out = [];
|
|
628
|
+
for (const parent of [...ns.keys()].sort(byName)) for (const name of [...ns.get(parent).keys()].sort(byName)) out.push({ parent, name });
|
|
629
|
+
return out;
|
|
630
|
+
},
|
|
631
|
+
nsOfKind(kind) {
|
|
632
|
+
const out = [];
|
|
633
|
+
for (const parent of [...ns.keys()].sort(byName)) {
|
|
634
|
+
const dir = ns.get(parent);
|
|
635
|
+
for (const name of [...dir.keys()].sort(byName)) if (dir.get(name).kind === Number(kind)) out.push({ parent, name });
|
|
636
|
+
}
|
|
637
|
+
return out;
|
|
638
|
+
},
|
|
639
|
+
nsChildren(parent) {
|
|
640
|
+
const dir = ns.get(parent);
|
|
641
|
+
if (!dir) return [];
|
|
642
|
+
return [...dir.keys()].sort(byName).map((name) => ({ name, kind: dir.get(name).kind }));
|
|
643
|
+
},
|
|
644
|
+
nsCount() { return names; },
|
|
645
|
+
};
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
/**
|
|
649
|
+
* A one-shot's store: the heap backing, capped at `budget` bytes and never
|
|
650
|
+
* growing past it (there is no ledger row to grow).
|
|
651
|
+
*/
|
|
652
|
+
function __residentBindInMemory(budget) {
|
|
653
|
+
const t = __residentBindTables(__residentTablesInMemory());
|
|
654
|
+
__residentInHeap = true;
|
|
655
|
+
__residentFacet = null;
|
|
656
|
+
__residentCap = Number(budget);
|
|
657
|
+
__residentNamespaceOverlayBytes = 0;
|
|
658
|
+
__residentNamespaceReserveBytes = 0;
|
|
659
|
+
return t;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
/**
|
|
663
|
+
* Bind the store to its tables: a facet's SQLite (__residentBind), or a
|
|
664
|
+
* one-shot's heap (__residentTablesInMemory).
|
|
665
|
+
*
|
|
666
|
+
* 'file' carries one row per path, so existence, size and kind are one query
|
|
667
|
+
* that never touches content. 'chunk' carries the bytes. Splitting them is
|
|
668
|
+
* what makes _statLadder and __fileExists cheap: the common case reads a
|
|
669
|
+
* short row and never pages a megabyte of blob in to answer "is it there".
|
|
670
|
+
* 'rev' is never undated, and that IS the coherence guarantee rather than a
|
|
671
|
+
* note about it. An undated row can never be invalidated, so it would be
|
|
672
|
+
* served stale forever. The SQLite backing makes the column NOT NULL, so such
|
|
673
|
+
* a row cannot be written; this is the r2-cache posture (an unverifiable key
|
|
674
|
+
* cannot be constructed) moved to the only storage layer this store has.
|
|
675
|
+
*
|
|
676
|
+
* __RK_OWN_WRITE is the one negative value: this facet's own bytes that the
|
|
677
|
+
* authority has not acknowledged, which are strictly newer than anything it
|
|
678
|
+
* can report and are always readable. It is a real provenance, not an absence
|
|
679
|
+
* of one.
|
|
680
|
+
*/
|
|
681
|
+
function __residentBindTables(t) {
|
|
682
|
+
// A store can outlive its process. Own rows left by an earlier one are
|
|
683
|
+
// writes that never reached the authority or cells whose acknowledgement it
|
|
684
|
+
// never saw: neither can be dated, so neither is a file.
|
|
685
|
+
t.dropOwn();
|
|
686
|
+
__residentT = t;
|
|
687
|
+
__residentReady = true;
|
|
688
|
+
__nsLoadLinks(t);
|
|
689
|
+
__nsOk = t.metaGet("ns") === "1";
|
|
690
|
+
return t;
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
function __residentRequire() {
|
|
694
|
+
if (!__residentReady) {
|
|
695
|
+
throw new Error(
|
|
696
|
+
"Nimbus: the resident store was read before its facet bound its SQLite. " +
|
|
697
|
+
"__residentBind(ctx) must run in the process DO constructor, ahead of the program."
|
|
698
|
+
);
|
|
699
|
+
}
|
|
700
|
+
if (__residentSealed) {
|
|
701
|
+
throw new Error(
|
|
702
|
+
"Nimbus: the resident store is sealed — " + __residentSealReason + ". " +
|
|
703
|
+
"__residentAdmit(fsAcquire(...)) must apply the authority's delta before " +
|
|
704
|
+
"the program runs; serving a row now would serve bytes of unknown age."
|
|
705
|
+
);
|
|
706
|
+
}
|
|
707
|
+
return __residentT;
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
/** The persisted cursor, or null when this store has never been populated. */
|
|
711
|
+
function __residentCursor() {
|
|
712
|
+
if (!__residentReady) return null;
|
|
713
|
+
const epoch = __residentT.metaGet("epoch");
|
|
714
|
+
const rev = __residentT.metaGet("rev");
|
|
715
|
+
return epoch === undefined || rev === undefined ? null : { epoch, rev: Number(rev) };
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
/** Stamp the store with the authority state its rows are known-good at. */
|
|
719
|
+
function __residentWriteCursor(t, cursor) {
|
|
720
|
+
t.metaSet("epoch", String(cursor.epoch));
|
|
721
|
+
t.metaSet("rev", String(Number(cursor.rev)));
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
/**
|
|
725
|
+
* Apply one authority delta and unseal.
|
|
726
|
+
*
|
|
727
|
+
* `result` is exactly what `supervisor.fsAcquire(epoch, cursor)` returns
|
|
728
|
+
* (`VfsAcquireResult` — runtime/os-contracts.ts), so this store slots into the
|
|
729
|
+
* ACQUIRE/RELEASE protocol the shims already run rather than beside it. One
|
|
730
|
+
* cache, one invariant.
|
|
731
|
+
*
|
|
732
|
+
* Returns the paths that WERE held and are now gone, which is what
|
|
733
|
+
* `_acquireAndRefetch` re-reads live — the same contract `_acquireBarrier`
|
|
734
|
+
* already has — and how many named rows were kept because their revision
|
|
735
|
+
* already covers the report (this facet's own writes coming back).
|
|
736
|
+
*/
|
|
737
|
+
function __residentAdmit(result) {
|
|
738
|
+
if (!__residentReady) throw new Error("Nimbus: __residentAdmit before __residentBind");
|
|
739
|
+
const t = __residentT;
|
|
740
|
+
const dropped = [];
|
|
741
|
+
const relist = [];
|
|
742
|
+
let kept = 0, pushed = 0;
|
|
743
|
+
const epoch = result && result.epoch != null ? String(result.epoch) : null;
|
|
744
|
+
const rev = result && result.rev != null ? Number(result.rev) : null;
|
|
745
|
+
if (epoch === null || rev === null) {
|
|
746
|
+
throw new Error(
|
|
747
|
+
"Nimbus: fsAcquire returned no cursor; the resident store stays sealed " +
|
|
748
|
+
"rather than serving rows it cannot date"
|
|
749
|
+
);
|
|
750
|
+
}
|
|
751
|
+
// Answers can be admitted out of the order they were served in (two
|
|
752
|
+
// barriers in flight). One older than the store's cursor describes a state
|
|
753
|
+
// the store has already moved past: admitting it would move the cursor
|
|
754
|
+
// back and install its bytes over newer ones. Within an epoch the cursor
|
|
755
|
+
// only moves forward, and only entries above it are applied.
|
|
756
|
+
const at = __residentCursor();
|
|
757
|
+
const floor = at !== null && !__residentSealed && at.epoch === epoch && !(result && result.poison) ? at.rev : null;
|
|
758
|
+
if (floor !== null && rev <= floor) {
|
|
759
|
+
return { dropped, kept, pushed: 0, relist, cursor: at, stale: rev < floor };
|
|
760
|
+
}
|
|
761
|
+
// The namespace moves with the cursor or not at all: a delta without stats,
|
|
762
|
+
// or a poison, leaves it describing a revision the cursor has left, so it
|
|
763
|
+
// stops answering until a listing restores it.
|
|
764
|
+
// An answer naming no path changes no name, with or without stats.
|
|
765
|
+
const namespaced = __nsOk && !!result && !result.poison
|
|
766
|
+
&& (result.namespace === true || (Array.isArray(result.paths) && result.paths.length === 0))
|
|
767
|
+
&& (!Array.isArray(result.paths) || result.paths.every((entry) => entry.stat === null || __nsDescribes(entry.stat)));
|
|
768
|
+
if (!namespaced) {
|
|
769
|
+
__nsMarkReady(t, false, !result || result.poison ? "the session's change log was truncated past this process's cursor"
|
|
770
|
+
: "a change reported without the stat every name needs");
|
|
771
|
+
}
|
|
772
|
+
if (!result || result.poison) {
|
|
773
|
+
// A delta admission has no absolute listing to vouch for a row, so a
|
|
774
|
+
// poison here means nothing the authority dated can be kept.
|
|
775
|
+
//
|
|
776
|
+
// This is the LAST resort, not the poison policy.
|
|
777
|
+
// `__residentSynchronizeFromSupervisor` repairs the same poison against
|
|
778
|
+
// absolute per-path revisions and keeps every row it can prove current;
|
|
779
|
+
// dropping the store is what made a poison cost a whole filesystem.
|
|
780
|
+
__residentDropDated();
|
|
781
|
+
} else if (Array.isArray(result.paths)) {
|
|
782
|
+
for (const entry of result.paths) {
|
|
783
|
+
if (floor !== null && Number(entry.rev) <= floor) continue;
|
|
784
|
+
const path = entry.path;
|
|
785
|
+
if (namespaced && __nsReady()) {
|
|
786
|
+
const again = __nsApplyEntry(t, entry);
|
|
787
|
+
if (again !== null) relist.push(again);
|
|
788
|
+
}
|
|
789
|
+
// A row at or above the reported revision already holds that mutation —
|
|
790
|
+
// this facet's own write coming back, or bytes fetched after it; anything
|
|
791
|
+
// else is someone else's and is dropped. A row of own bytes
|
|
792
|
+
// (__RK_OWN_WRITE) is newer than any authority revision by construction
|
|
793
|
+
// and is kept — read-your-writes survives a peer's mutation of the same
|
|
794
|
+
// path, and the report is adjudicated when the write's own revision
|
|
795
|
+
// arrives (the shims note it for the ledger before admitting).
|
|
796
|
+
// A subtree-scoped or structural entry covers every row at or under its
|
|
797
|
+
// path, each judged the same way: something beneath it this facet may
|
|
798
|
+
// not see changed, or the directory went, or who may enter it did. A
|
|
799
|
+
// row there may be stale, or no longer this facet's to be served.
|
|
800
|
+
if (entry.subtree || entry.structural) {
|
|
801
|
+
const end = __residentPrefixEnd(path + "/");
|
|
802
|
+
for (const row of t.fileRevsIn(path + "/", end)) {
|
|
803
|
+
const stamped = Number(row.rev);
|
|
804
|
+
if (stamped === __RK_OWN_WRITE || stamped >= Number(entry.rev)) continue;
|
|
805
|
+
t.chunkDelete(row.path);
|
|
806
|
+
t.fileDelete(row.path);
|
|
807
|
+
dropped.push(String(row.path));
|
|
808
|
+
}
|
|
809
|
+
for (const [heldPath, heldCell] of [...__residentHeld]) {
|
|
810
|
+
if (!heldPath.startsWith(path + "/") || heldCell.rev === __RK_OWN_WRITE || heldCell.rev >= Number(entry.rev)) continue;
|
|
811
|
+
__residentForgetHeld(heldPath);
|
|
812
|
+
}
|
|
813
|
+
// Names under it may have become hidden or visible without being
|
|
814
|
+
// named: the namespace relists that subtree.
|
|
815
|
+
if (namespaced && entry.subtree && entry.stat && entry.stat.type === "directory") relist.push(path);
|
|
816
|
+
}
|
|
817
|
+
let held = false, stamped = 0, heldKey = null;
|
|
818
|
+
const row = t.fileRevKey(path);
|
|
819
|
+
if (row !== undefined) {
|
|
820
|
+
held = true; stamped = Number(row.rev); heldKey = row.ckey == null ? null : String(row.ckey);
|
|
821
|
+
}
|
|
822
|
+
if (held && (stamped === __RK_OWN_WRITE || stamped >= Number(entry.rev))) { kept++; continue; }
|
|
823
|
+
// A cell held in memory (an own write the store had no room for) is
|
|
824
|
+
// judged the same way; superseded, it goes, and the report's bytes, if
|
|
825
|
+
// any, are held as the store can.
|
|
826
|
+
const heldCell = __residentHeld.get(path);
|
|
827
|
+
if (heldCell !== undefined) {
|
|
828
|
+
if (heldCell.rev === __RK_OWN_WRITE || heldCell.rev >= Number(entry.rev)) { kept++; continue; }
|
|
829
|
+
__residentForgetHeld(path);
|
|
830
|
+
}
|
|
831
|
+
// The same bytes under a newer revision (a chmod, a touch, a rewrite
|
|
832
|
+
// with identical content): kept, and dated at the report.
|
|
833
|
+
if (held && heldKey !== null && entry.contentKey != null && heldKey === String(entry.contentKey)) {
|
|
834
|
+
t.fileSetRev(path, Number(entry.rev));
|
|
835
|
+
kept++;
|
|
836
|
+
continue;
|
|
837
|
+
}
|
|
838
|
+
// Pushed content: the file's bytes at the answer's revision, so dating
|
|
839
|
+
// them at the path's own revision cannot be newer than they are.
|
|
840
|
+
if (entry.bytes != null && entry.stat && entry.stat.type === "file") {
|
|
841
|
+
if (__residentPut(t, path, __residentBytes(entry.bytes), Number(entry.rev), entry.contentKey)) pushed++;
|
|
842
|
+
else if (held) dropped.push(path);
|
|
843
|
+
continue;
|
|
844
|
+
}
|
|
845
|
+
if (!held) continue;
|
|
846
|
+
t.chunkDelete(path);
|
|
847
|
+
t.fileDelete(path);
|
|
848
|
+
dropped.push(path);
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
__residentWriteCursor(t, { epoch, rev });
|
|
852
|
+
__residentSealed = false;
|
|
853
|
+
__residentSealReason = "";
|
|
854
|
+
return { dropped, kept, pushed, relist, cursor: { epoch, rev } };
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
/**
|
|
858
|
+
* Drop every row the authority dated, and nothing else: what a poison does to
|
|
859
|
+
* the rows when nothing can vouch for them. The cursor is left alone, because
|
|
860
|
+
* a store that holds no dated row cannot hold a stale one whatever its cursor
|
|
861
|
+
* says.
|
|
862
|
+
*
|
|
863
|
+
* This facet's own bytes are not the authority's to vouch for, and stay:
|
|
864
|
+
* dropping a row a write or mutation still in flight owns would hand the next
|
|
865
|
+
* fill the pre-write bytes. Its acknowledgement dates it, or evicts it, when
|
|
866
|
+
* it lands.
|
|
867
|
+
*
|
|
868
|
+
* Two statements rather than a delete per path: at pi scale that is 7,141
|
|
869
|
+
* rows, and this is on the hot path (measured 45 ms whole-store, vs a per-row
|
|
870
|
+
* walk).
|
|
871
|
+
*/
|
|
872
|
+
function __residentDropDated() {
|
|
873
|
+
if (!__residentReady) throw new Error("Nimbus: __residentDropDated before __residentBind");
|
|
874
|
+
__residentT.dropDated();
|
|
875
|
+
for (const [path, held] of [...__residentHeld]) if (held.rev !== __RK_OWN_WRITE) __residentForgetHeld(path);
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
/** Re-seal — for a store whose backing is being replaced (slot handover). */
|
|
879
|
+
function __residentSeal(reason) {
|
|
880
|
+
__residentSealed = true;
|
|
881
|
+
__residentSealReason = String(reason || "resealed");
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
/** One row of 'file', or undefined. The existence/shape query. */
|
|
885
|
+
function __residentHead(path) {
|
|
886
|
+
return __residentRequire().fileHead(path);
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
/**
|
|
890
|
+
* The synchronous read. THE point of this module: a plain, non-async function
|
|
891
|
+
* that returns content, so a fs.readFileSync call stack never has to yield.
|
|
892
|
+
*
|
|
893
|
+
* Returns exactly what __vfsBundle has always returned — a string, a
|
|
894
|
+
* Uint8Array, or an { error: "EACCES" } denial — or undefined for a cell that
|
|
895
|
+
* is not held.
|
|
896
|
+
*/
|
|
897
|
+
function __residentGet(path) {
|
|
898
|
+
if (globalThis.__nimbusProfileStaged) globalThis.__nimbusProfileStaged.delete(String(path).replace(/^\/+/, ""));
|
|
899
|
+
const head = __residentHead(path);
|
|
900
|
+
if (head === undefined) {
|
|
901
|
+
const held = __residentHeld.get(path);
|
|
902
|
+
if (held === undefined) return undefined;
|
|
903
|
+
__residentHeld.delete(path);
|
|
904
|
+
__residentHeld.set(path, held);
|
|
905
|
+
return held.cell;
|
|
906
|
+
}
|
|
907
|
+
const kind = Number(head.kind);
|
|
908
|
+
if (kind === __RK_DENIED) return { error: "EACCES" };
|
|
909
|
+
const t = __residentRequire();
|
|
910
|
+
const chunks = Number(head.chunks);
|
|
911
|
+
if (chunks === 1) {
|
|
912
|
+
const row = t.chunkFirst(path);
|
|
913
|
+
if (row === undefined) return undefined;
|
|
914
|
+
return kind === __RK_TEXT ? row.txt : __residentBytes(row.bin);
|
|
915
|
+
}
|
|
916
|
+
// Chunked: a file past the single-value ceiling. Reassembled here so callers
|
|
917
|
+
// never learn the file was split.
|
|
918
|
+
const parts = [];
|
|
919
|
+
for (const row of t.chunkParts(path)) {
|
|
920
|
+
parts.push(kind === __RK_TEXT ? row.txt : __residentBytes(row.bin));
|
|
921
|
+
}
|
|
922
|
+
if (parts.length === 0) return undefined;
|
|
923
|
+
if (kind === __RK_TEXT) return parts.join("");
|
|
924
|
+
const total = parts.reduce((n, p) => n + p.byteLength, 0);
|
|
925
|
+
const out = new Uint8Array(total);
|
|
926
|
+
let at = 0;
|
|
927
|
+
for (const p of parts) { out.set(p, at); at += p.byteLength; }
|
|
928
|
+
return out;
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
function __residentBytes(value) {
|
|
932
|
+
if (value instanceof Uint8Array) return value;
|
|
933
|
+
if (value instanceof ArrayBuffer) return new Uint8Array(value);
|
|
934
|
+
return new Uint8Array(0);
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
/**
|
|
938
|
+
* Write one cell, chunking past the single-value ceiling.
|
|
939
|
+
*
|
|
940
|
+
* `rev` is the authority revision these bytes were read at, and it is what
|
|
941
|
+
* makes the row datable. Omitting it means the cell is this facet's own
|
|
942
|
+
* unflushed write, stored as __RK_OWN_WRITE — always readable
|
|
943
|
+
* (read-your-writes) and never mistaken for something the authority vouched
|
|
944
|
+
* for. There is no third case: the column refuses one.
|
|
945
|
+
*/
|
|
946
|
+
function __residentPut(t, path, cell, rev, ckey) {
|
|
947
|
+
// Whatever this puts supersedes a held cell, held or not.
|
|
948
|
+
__residentForgetHeld(path);
|
|
949
|
+
t.chunkDelete(path);
|
|
950
|
+
const stamp = typeof rev === "number" ? rev : __RK_OWN_WRITE;
|
|
951
|
+
if (cell && typeof cell === "object" && cell.error) {
|
|
952
|
+
t.filePut(path, __RK_DENIED, 0, 0, stamp, null);
|
|
953
|
+
return true;
|
|
954
|
+
}
|
|
955
|
+
const isText = typeof cell === "string";
|
|
956
|
+
const kind = isText ? __RK_TEXT : __RK_BINARY;
|
|
957
|
+
const body = isText ? cell : __residentBytes(cell);
|
|
958
|
+
const size = isText ? body.length : body.byteLength;
|
|
959
|
+
// Chunk on the encoded length for text, since the ceiling is on bytes and a
|
|
960
|
+
// multi-byte string would otherwise cross it while measuring short.
|
|
961
|
+
const limit = isText ? Math.floor(__RESIDENT_CHUNK_BYTES / 3) : __RESIDENT_CHUNK_BYTES;
|
|
962
|
+
const chunks = Math.max(1, Math.ceil(size / limit));
|
|
963
|
+
// Not admitted: nothing is held for the path (its old bytes are gone above).
|
|
964
|
+
if (!__residentFits(__residentCellCost(cell), stamp !== __RK_OWN_WRITE)) {
|
|
965
|
+
t.fileDelete(path);
|
|
966
|
+
if (stamp === __RK_OWN_WRITE) __residentHold(path, cell);
|
|
967
|
+
return false;
|
|
968
|
+
}
|
|
969
|
+
for (let part = 0; part < chunks; part++) {
|
|
970
|
+
const slice = body.slice(part * limit, (part + 1) * limit);
|
|
971
|
+
if (isText) t.chunkAddText(path, part, slice);
|
|
972
|
+
else t.chunkAddBin(path, part, slice);
|
|
973
|
+
}
|
|
974
|
+
t.filePut(path, kind, size, chunks, stamp, stamp === __RK_OWN_WRITE || ckey == null ? null : String(ckey));
|
|
975
|
+
return true;
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
/** One chunk row, written as it arrives. The streaming filler's primitive. */
|
|
979
|
+
function __residentPutChunk(t, path, part, bytes) {
|
|
980
|
+
if (!__residentFits(Math.ceil(bytes.byteLength * 1.01) + __RESIDENT_ROW_BYTES + 4096)) return false;
|
|
981
|
+
t.chunkSetBin(path, part, bytes);
|
|
982
|
+
return true;
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
/**
|
|
986
|
+
* The head row, written LAST by the streaming filler.
|
|
987
|
+
*
|
|
988
|
+
* Order is the durability rule here: without a head row `__residentHead`
|
|
989
|
+
* returns undefined and the path simply misses, falling through to the
|
|
990
|
+
* supervisor exactly as it would have. So a fill interrupted part way through
|
|
991
|
+
* a large file leaves chunks nothing can read, never a short file something
|
|
992
|
+
* can. Writing the head first would invert that into a truncated read.
|
|
993
|
+
*/
|
|
994
|
+
function __residentPutHead(t, path, kind, size, chunks, rev, ckey) {
|
|
995
|
+
t.filePut(
|
|
996
|
+
path, kind, size, chunks, typeof rev === "number" ? rev : __RK_OWN_WRITE,
|
|
997
|
+
typeof rev === "number" && ckey != null ? String(ckey) : null,
|
|
998
|
+
);
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
/**
|
|
1002
|
+
* Hold `path` with the bytes of a dated row whose content identity is
|
|
1003
|
+
* `ckey`, by copying rows inside this store: equal keys are equal bytes, so
|
|
1004
|
+
* no fetch is needed. Returns whether a source existed.
|
|
1005
|
+
*/
|
|
1006
|
+
function __residentCopyByKey(t, path, ckey, rev) {
|
|
1007
|
+
const source = t.fileByKey(String(ckey), path);
|
|
1008
|
+
if (source === undefined) return false;
|
|
1009
|
+
if (!__residentFits(Number(source.size) + (Number(source.chunks) + 1) * __RESIDENT_ROW_BYTES)) return false;
|
|
1010
|
+
t.chunkDelete(path);
|
|
1011
|
+
t.chunkCopy(path, String(source.path));
|
|
1012
|
+
t.filePut(path, Number(source.kind), Number(source.size), Number(source.chunks), rev, String(ckey));
|
|
1013
|
+
return true;
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
function __residentDelete(t, path) {
|
|
1017
|
+
const had = __residentHead(path) !== undefined || __residentHeld.has(path);
|
|
1018
|
+
__residentForgetHeld(path);
|
|
1019
|
+
t.chunkDelete(path);
|
|
1020
|
+
t.fileDelete(path);
|
|
1021
|
+
return had;
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
/**
|
|
1025
|
+
* What a row is dated at: its revision, __RK_OWN_WRITE for this facet's own
|
|
1026
|
+
* unacknowledged bytes, or undefined when the path is not held.
|
|
1027
|
+
*/
|
|
1028
|
+
function __residentProvenance(path) {
|
|
1029
|
+
return __residentRequire().fileRev(path);
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
/**
|
|
1033
|
+
* Date this facet's own bytes with the revision the authority acknowledged
|
|
1034
|
+
* them at: the revision a flushed write produced, or the one an own mutation's
|
|
1035
|
+
* receipt settles on. The store's half of the stamp `__vfsBundleRevisions`
|
|
1036
|
+
* carries in heap (_shared/vfs-write-ledger.ts), kept in the row it describes
|
|
1037
|
+
* so the two cannot drift.
|
|
1038
|
+
*
|
|
1039
|
+
* Only a row still holding own bytes is dated. One that was dropped or
|
|
1040
|
+
* replaced meanwhile holds something this acknowledgement says nothing about.
|
|
1041
|
+
*
|
|
1042
|
+
* Without it nothing dates an own row, and __RK_OWN_WRITE is the revision a
|
|
1043
|
+
* delta never evicts: a file this process wrote would serve its own bytes over
|
|
1044
|
+
* every later write to it by anyone.
|
|
1045
|
+
*/
|
|
1046
|
+
function __residentStamp(path, rev) {
|
|
1047
|
+
const heldCell = __residentHeld.get(path);
|
|
1048
|
+
if (heldCell !== undefined && heldCell.rev === __RK_OWN_WRITE) heldCell.rev = rev;
|
|
1049
|
+
const t = __residentRequire();
|
|
1050
|
+
const size = t.fileOwnSize(path);
|
|
1051
|
+
if (size === undefined) return;
|
|
1052
|
+
t.fileStampOwn(path, rev);
|
|
1053
|
+
__nsNoteOwnFile(path, size, rev);
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
/**
|
|
1057
|
+
* Hold a dated row as this facet's own for the window of one of its own
|
|
1058
|
+
* mutations (a ranged write, a truncate, a chmod), and return the revision it
|
|
1059
|
+
* was dated at: the mutation's receipt is judged against it when the window
|
|
1060
|
+
* closes (`__nimbusEndOwnMutation`). Undefined when there is no dated row —
|
|
1061
|
+
* an own row is held already, and an absent one has nothing to protect.
|
|
1062
|
+
*
|
|
1063
|
+
* The hold is what keeps a barrier that reports the mutation before its own
|
|
1064
|
+
* response arrives from evicting the cell the mutation is about to overlay.
|
|
1065
|
+
* The supervisor answers fsAcquire at once, while a write's response waits on
|
|
1066
|
+
* the Durable Object's output gate, so that ordering is ordinary.
|
|
1067
|
+
*/
|
|
1068
|
+
function __residentLease(path) {
|
|
1069
|
+
const t = __residentRequire();
|
|
1070
|
+
const rev = t.fileRev(path);
|
|
1071
|
+
if (rev === undefined || rev === __RK_OWN_WRITE) return undefined;
|
|
1072
|
+
t.fileSetRev(path, __RK_OWN_WRITE);
|
|
1073
|
+
return rev;
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
/**
|
|
1077
|
+
* Install bytes a live read returned, dated `rev`: the cursor the read was
|
|
1078
|
+
* issued under, so the bytes hold every mutation at or below it. An async read
|
|
1079
|
+
* must write through, or the next synchronous read goes back in time relative
|
|
1080
|
+
* to what the program was just handed (protocol §5.9).
|
|
1081
|
+
*
|
|
1082
|
+
* Refused over a row of own bytes, which are newer than anything a read can
|
|
1083
|
+
* return, and over a row dated after `rev`, which is newer than this read can
|
|
1084
|
+
* vouch for. Returns whether the bytes were installed.
|
|
1085
|
+
*/
|
|
1086
|
+
function __residentFill(path, cell, rev) {
|
|
1087
|
+
if (typeof rev !== "number" || rev < 0) {
|
|
1088
|
+
throw new Error(
|
|
1089
|
+
"Nimbus: refusing to fill '" + path + "' with no authority revision. " +
|
|
1090
|
+
"An undated row cannot be invalidated, so it would be served stale forever."
|
|
1091
|
+
);
|
|
1092
|
+
}
|
|
1093
|
+
const t = __residentRequire();
|
|
1094
|
+
const held = t.fileRev(path);
|
|
1095
|
+
if (held !== undefined && (held === __RK_OWN_WRITE || held > rev)) return false;
|
|
1096
|
+
const heldCell = __residentHeld.get(path);
|
|
1097
|
+
if (heldCell !== undefined && (heldCell.rev === __RK_OWN_WRITE || heldCell.rev > rev)) return false;
|
|
1098
|
+
return __residentPut(t, path, cell, rev);
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
/** Every held path. Backs the Object.keys / for-in scans in the shims. */
|
|
1102
|
+
function __residentKeys() {
|
|
1103
|
+
return __residentRequire().fileAllPaths();
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
/**
|
|
1107
|
+
* Every path held as this facet's own unacknowledged bytes: the rows a
|
|
1108
|
+
* poison's repair keeps without a revision to vouch for them, whose reports
|
|
1109
|
+
* the shims deliver to their owners (`_settleSkippedReports`).
|
|
1110
|
+
*/
|
|
1111
|
+
function __residentOwnPaths() {
|
|
1112
|
+
return __residentRequire().fileOwnPaths();
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
/**
|
|
1116
|
+
* The exclusive upper bound of a prefix under the BINARY collation SQLite gives
|
|
1117
|
+
* a TEXT PRIMARY KEY — the successor of the last code unit it can increment.
|
|
1118
|
+
* Null when there is none (an empty prefix, or one ending in U+FFFF), and the
|
|
1119
|
+
* caller then scans open-ended, which is correct and merely slower.
|
|
1120
|
+
*
|
|
1121
|
+
* Every caller passes a directory prefix ending in '/', so the increment is
|
|
1122
|
+
* '/' → '0' and UTF-16 and UTF-8 orderings agree exactly. They diverge only
|
|
1123
|
+
* around the surrogate range, which no path segment separator lives in.
|
|
1124
|
+
*/
|
|
1125
|
+
function __residentPrefixEnd(prefix) {
|
|
1126
|
+
for (let i = prefix.length - 1; i >= 0; i--) {
|
|
1127
|
+
const code = prefix.charCodeAt(i);
|
|
1128
|
+
if (code < 0xffff) return prefix.slice(0, i) + String.fromCharCode(code + 1);
|
|
1129
|
+
}
|
|
1130
|
+
return null;
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
/**
|
|
1134
|
+
* Paths under a directory prefix, as a bounded index range scan.
|
|
1135
|
+
*
|
|
1136
|
+
* The shims ask this constantly — readdirSync's union with the manifest,
|
|
1137
|
+
* existsSync of a directory, node's own module resolution, rm -rf, watch
|
|
1138
|
+
* globbing. In heap it was a for-in over every key; here it must be a range
|
|
1139
|
+
* scan on both ends, because that is the only version that is actually cheaper:
|
|
1140
|
+
* an open-ended `path >= prefix` with a LIKE filter still visits every row
|
|
1141
|
+
* after the prefix, and going back through the object protocol visits every row
|
|
1142
|
+
* AND reads every file's bytes.
|
|
1143
|
+
*
|
|
1144
|
+
* Two bounds also mean no LIKE, so a real path containing '%' or '_' needs no
|
|
1145
|
+
* escaping — those are only wildcards to an operator this no longer uses.
|
|
1146
|
+
*/
|
|
1147
|
+
function __residentKeysUnder(prefix) {
|
|
1148
|
+
const from = String(prefix);
|
|
1149
|
+
const to = __residentPrefixEnd(from);
|
|
1150
|
+
return __residentRequire().filePaths(from, to);
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
/**
|
|
1154
|
+
* Whether ANY path is held under a prefix — the same range, stopped at the
|
|
1155
|
+
* first row. The hot half: it is what answers "is this a directory" on node's
|
|
1156
|
+
* module-resolution path, where materialising the subtree would be the cost all
|
|
1157
|
+
* over again.
|
|
1158
|
+
*/
|
|
1159
|
+
function __residentHasUnder(prefix) {
|
|
1160
|
+
const from = String(prefix);
|
|
1161
|
+
const to = __residentPrefixEnd(from);
|
|
1162
|
+
return __residentRequire().fileAny(from, to);
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
// ── The namespace ───────────────────────────────────────────────────────────
|
|
1166
|
+
//
|
|
1167
|
+
// Every name the credential can see, exact at the store's cursor: filled from
|
|
1168
|
+
// a complete listing, then moved forward by each ACQUIRE delta, whose entries
|
|
1169
|
+
// carry the path's stat at the answer's revision (null when it is gone). A
|
|
1170
|
+
// change of presence or stat is always a logged mutation of that path, except
|
|
1171
|
+
// what vanishes beneath a removed or replaced directory, which the delta
|
|
1172
|
+
// application drops by subtree. So a synchronous stat, exists or readdir is
|
|
1173
|
+
// answered exactly, and an absent name is known to be absent.
|
|
1174
|
+
//
|
|
1175
|
+
// This table holds the AUTHORITY's view only. The process's own pending
|
|
1176
|
+
// structural effects are an overlay kept by the shims, retired once a barrier
|
|
1177
|
+
// begun after they settled has applied.
|
|
1178
|
+
|
|
1179
|
+
const __NS_FILE = 0;
|
|
1180
|
+
const __NS_DIR = 1;
|
|
1181
|
+
const __NS_LINK = 2;
|
|
1182
|
+
/** Symlink hops before a resolution is ELOOP, as Linux's MAXSYMLINKS. */
|
|
1183
|
+
const __NS_MAX_HOPS = 40;
|
|
1184
|
+
/** Paths of every symlink in the table: resolution walks only past these. */
|
|
1185
|
+
let __nsLinks = new Set();
|
|
1186
|
+
/** The credential names are judged traversable for. */
|
|
1187
|
+
let __nsCred = null;
|
|
1188
|
+
|
|
1189
|
+
function __nsSetCred(cred) { __nsCred = cred || null; }
|
|
1190
|
+
|
|
1191
|
+
function __nsSplit(k) {
|
|
1192
|
+
const i = k.lastIndexOf("/");
|
|
1193
|
+
return i < 0 ? ["", k] : [k.slice(0, i), k.slice(i + 1)];
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
function __nsKindCode(type) {
|
|
1197
|
+
return type === "directory" ? __NS_DIR : type === "symlink" ? __NS_LINK : __NS_FILE;
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
function __nsLoadLinks(t) {
|
|
1201
|
+
__nsLinks = new Set();
|
|
1202
|
+
for (const row of t.nsOfKind(__NS_LINK)) {
|
|
1203
|
+
const parent = String(row.parent);
|
|
1204
|
+
__nsLinks.add(parent ? parent + "/" + String(row.name) : String(row.name));
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
/** Whether the table describes the store's cursor. */
|
|
1209
|
+
function __nsReady() {
|
|
1210
|
+
return __residentReady && !__residentSealed && __nsOk;
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
/** The persisted 'ns' flag, mirrored in heap because every stat asks it. */
|
|
1214
|
+
let __nsOk = false;
|
|
1215
|
+
/** Why the namespace stopped answering, while it does not: what a refused sync call names. */
|
|
1216
|
+
let __nsCause = "the process has not listed it yet";
|
|
1217
|
+
|
|
1218
|
+
function __nsMarkReady(t, ready, cause) {
|
|
1219
|
+
t.metaSet("ns", ready ? "1" : "0");
|
|
1220
|
+
__nsOk = !!ready;
|
|
1221
|
+
if (!ready) __nsCause = String(cause || "a change it could not follow");
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
/** Why the namespace is not answering (meaningful only while __nsReady() is false). */
|
|
1225
|
+
function __nsNotReadyCause() {
|
|
1226
|
+
if (!__residentReady) return "the process's store is not bound";
|
|
1227
|
+
if (__residentSealed) return __residentSealReason || "the store has not reconciled with the session";
|
|
1228
|
+
return __nsCause;
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
/**
|
|
1232
|
+
* Whether a stat from the authority describes its name: a type, and the
|
|
1233
|
+
* mode, owner and group every permission check reads. One without them is
|
|
1234
|
+
* no record. Stored, its missing fields would become zeros, a root-owned
|
|
1235
|
+
* mode-0 file the authority never reported. The namespace then stops
|
|
1236
|
+
* answering (as for a delta without stats) until a listing that does
|
|
1237
|
+
* describe every name restores it.
|
|
1238
|
+
*/
|
|
1239
|
+
function __nsDescribes(stat) {
|
|
1240
|
+
if (!stat || typeof stat.type !== "string") return false;
|
|
1241
|
+
for (const field of ["mode", "uid", "gid"]) {
|
|
1242
|
+
if (stat[field] === null || stat[field] === undefined || !Number.isInteger(Number(stat[field]))) return false;
|
|
1243
|
+
}
|
|
1244
|
+
return true;
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
function __nsPut(t, k, stat, rev, target) {
|
|
1248
|
+
const [parent, name] = __nsSplit(k);
|
|
1249
|
+
const kind = __nsKindCode(stat.type);
|
|
1250
|
+
t.nsPut(
|
|
1251
|
+
parent, name, kind, Number(stat.size) || 0, Number(stat.mode) || 0, Number(stat.uid) || 0,
|
|
1252
|
+
Number(stat.gid) || 0, Number(stat.atime) || 0, Number(stat.mtime) || 0, Number(stat.ctime) || 0,
|
|
1253
|
+
Number(stat.ino) || 0, Number(rev) || 0, kind === __NS_LINK ? String(target ?? "") : null,
|
|
1254
|
+
);
|
|
1255
|
+
if (kind === __NS_LINK) __nsLinks.add(k); else __nsLinks.delete(k);
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
// The authority operation may succeed while metadata cannot be cached.
|
|
1259
|
+
// nsPut has sealed the sync view on heap ENOSPC; preserve the async result.
|
|
1260
|
+
function __nsTryPut(t, k, stat, rev, target) {
|
|
1261
|
+
try { __nsPut(t, k, stat, rev, target); return true; }
|
|
1262
|
+
catch (error) {
|
|
1263
|
+
if (__residentInHeap && error && error.code === "ENOSPC") return false;
|
|
1264
|
+
throw error;
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
/** Delete everything beneath `k`, and `k` itself when `self`. */
|
|
1269
|
+
function __nsDeleteTree(t, k, self) {
|
|
1270
|
+
if (k === "") {
|
|
1271
|
+
t.nsClear();
|
|
1272
|
+
__nsLinks = new Set();
|
|
1273
|
+
return;
|
|
1274
|
+
}
|
|
1275
|
+
const prefix = k + "/";
|
|
1276
|
+
const end = __residentPrefixEnd(prefix);
|
|
1277
|
+
t.nsDeleteChildren(k);
|
|
1278
|
+
t.nsDeleteParents(prefix, end);
|
|
1279
|
+
if (self) {
|
|
1280
|
+
const [parent, name] = __nsSplit(k);
|
|
1281
|
+
t.nsDelete(parent, name);
|
|
1282
|
+
__nsLinks.delete(k);
|
|
1283
|
+
}
|
|
1284
|
+
for (const link of __nsLinks) if (link.startsWith(prefix)) __nsLinks.delete(link);
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
/** The row at exactly `k` (no symlink resolution), or undefined. */
|
|
1288
|
+
function __nsRowAt(t, k) {
|
|
1289
|
+
if (k === "") {
|
|
1290
|
+
return { kind: __NS_DIR, size: 0, mode: 0o40755, uid: 0, gid: 0, atime: 0, mtime: 0, ctime: 0, ino: 1, rev: 0, target: null };
|
|
1291
|
+
}
|
|
1292
|
+
const [parent, name] = __nsSplit(k);
|
|
1293
|
+
return t.nsGet(parent, name);
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1296
|
+
function __nsJoinTarget(linkPath, target) {
|
|
1297
|
+
const t = String(target);
|
|
1298
|
+
const parts = (t.startsWith("/") ? [] : __nsSplit(linkPath)[0].split("/").filter(Boolean));
|
|
1299
|
+
for (const seg of t.split("/")) {
|
|
1300
|
+
if (seg === "" || seg === ".") continue;
|
|
1301
|
+
if (seg === "..") parts.pop(); else parts.push(seg);
|
|
1302
|
+
}
|
|
1303
|
+
return parts.join("/");
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1306
|
+
/**
|
|
1307
|
+
* Resolve `k` through the symlinks the namespace holds: { path, row } for the
|
|
1308
|
+
* name it denotes, null when nothing is there, or "ELOOP". The leaf is
|
|
1309
|
+
* followed only when `followLeaf`. Walks component by component only when a
|
|
1310
|
+
* symlink sits on the path; otherwise it is one lookup.
|
|
1311
|
+
*/
|
|
1312
|
+
function __nsResolve(k, followLeaf) {
|
|
1313
|
+
const t = __residentRequire();
|
|
1314
|
+
let path = k;
|
|
1315
|
+
for (let hops = 0; hops <= __NS_MAX_HOPS; hops++) {
|
|
1316
|
+
let redirected = false;
|
|
1317
|
+
if (__nsLinks.size > 0) {
|
|
1318
|
+
const segs = path.split("/");
|
|
1319
|
+
for (let i = 1; i <= segs.length; i++) {
|
|
1320
|
+
const prefix = segs.slice(0, i).join("/");
|
|
1321
|
+
if (!__nsLinks.has(prefix)) continue;
|
|
1322
|
+
if (i === segs.length && !followLeaf) break;
|
|
1323
|
+
const row = __nsRowAt(t, prefix);
|
|
1324
|
+
if (!row || Number(row.kind) !== __NS_LINK) break;
|
|
1325
|
+
const rest = segs.slice(i).join("/");
|
|
1326
|
+
const target = __nsJoinTarget(prefix, row.target);
|
|
1327
|
+
path = rest ? (target ? target + "/" + rest : rest) : target;
|
|
1328
|
+
redirected = true;
|
|
1329
|
+
break;
|
|
1330
|
+
}
|
|
1331
|
+
}
|
|
1332
|
+
if (redirected) continue;
|
|
1333
|
+
const row = __nsRowAt(t, path);
|
|
1334
|
+
return row === undefined ? null : { path, row };
|
|
1335
|
+
}
|
|
1336
|
+
return "ELOOP";
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1339
|
+
/** The entries directly under directory `k` (already resolved): [{ name, kind }]. */
|
|
1340
|
+
function __nsChildren(k) {
|
|
1341
|
+
return __residentRequire().nsChildren(k).map((row) => ({ name: String(row.name), kind: Number(row.kind) }));
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
/** Whether the credential may search directory `row` (POSIX x). */
|
|
1345
|
+
function __nsTraversable(row) {
|
|
1346
|
+
const cred = __nsCred;
|
|
1347
|
+
if (!row || Number(row.kind) !== __NS_DIR) return false;
|
|
1348
|
+
const mode = Number(row.mode) & 0o777;
|
|
1349
|
+
if (!cred) return true;
|
|
1350
|
+
if (Number(cred.uid) === 0) return (mode & 0o111) !== 0;
|
|
1351
|
+
const groups = Array.isArray(cred.groups) ? cred.groups.map(Number) : [];
|
|
1352
|
+
const shift = Number(cred.uid) === Number(row.uid) ? 6
|
|
1353
|
+
: (Number(cred.gid) === Number(row.gid) || groups.includes(Number(row.gid))) ? 3 : 0;
|
|
1354
|
+
return ((mode >> shift) & 1) === 1;
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1357
|
+
/**
|
|
1358
|
+
* Apply one delta entry that carries a stat. Returns a directory whose
|
|
1359
|
+
* subtree must be relisted — one that just became searchable, whose
|
|
1360
|
+
* descendants no delta names — or null.
|
|
1361
|
+
*/
|
|
1362
|
+
function __nsApplyEntry(t, entry) {
|
|
1363
|
+
const k = String(entry.path).replace(/^\/+/, "");
|
|
1364
|
+
if (k === "") return null;
|
|
1365
|
+
if (entry.stat === null) { __nsDeleteTree(t, k, true); return null; }
|
|
1366
|
+
const before = __nsRowAt(t, k);
|
|
1367
|
+
if (!__nsTryPut(t, k, entry.stat, entry.rev, entry.linkTarget)) return null;
|
|
1368
|
+
const after = __nsRowAt(t, k);
|
|
1369
|
+
if (Number(after.kind) !== __NS_DIR) { __nsDeleteTree(t, k, false); return null; }
|
|
1370
|
+
const was = before !== undefined && __nsTraversable(before);
|
|
1371
|
+
const now = __nsTraversable(after);
|
|
1372
|
+
if (was && !now) __nsDeleteTree(t, k, false);
|
|
1373
|
+
if (!was && now && before !== undefined) return k;
|
|
1374
|
+
return null;
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1377
|
+
/**
|
|
1378
|
+
* Bring the table to a complete listing: upsert what differs, delete what the
|
|
1379
|
+
* listing no longer names. `entries` are VfsListEntry (path, stat, rev,
|
|
1380
|
+
* linkTarget). Called once per enumeration, after its last page.
|
|
1381
|
+
*/
|
|
1382
|
+
function __nsReplace(t, entries) {
|
|
1383
|
+
const listed = new Set();
|
|
1384
|
+
for (const entry of entries) listed.add(String(entry.path).replace(/^\/+/, ""));
|
|
1385
|
+
// Remove stale rows before admitting replacements; do not transiently
|
|
1386
|
+
// charge both versions of a complete namespace against the fixed cap.
|
|
1387
|
+
for (const row of t.nsKeys()) {
|
|
1388
|
+
const parent = String(row.parent);
|
|
1389
|
+
const k = parent ? parent + "/" + String(row.name) : String(row.name);
|
|
1390
|
+
if (!listed.has(k)) { t.nsDelete(parent, String(row.name)); __nsLinks.delete(k); }
|
|
1391
|
+
}
|
|
1392
|
+
// Free shrinking text before growing another surviving row; final-state
|
|
1393
|
+
// admission must not depend on alphabetical replacement order.
|
|
1394
|
+
if (__residentInHeap) {
|
|
1395
|
+
for (const entry of entries) {
|
|
1396
|
+
const k = String(entry.path).replace(/^\/+/, "");
|
|
1397
|
+
if (!k) continue;
|
|
1398
|
+
const [parent, name] = __nsSplit(k);
|
|
1399
|
+
const held = t.nsGet(parent, name);
|
|
1400
|
+
if (held && __namespaceRowBytes(parent, name, held.target) > __namespaceRowBytes(parent, name, entry.linkTarget)) {
|
|
1401
|
+
__nsPut(t, k, entry.stat, entry.rev, entry.linkTarget);
|
|
1402
|
+
}
|
|
1403
|
+
}
|
|
1404
|
+
}
|
|
1405
|
+
for (const entry of entries) {
|
|
1406
|
+
const k = String(entry.path).replace(/^\/+/, "");
|
|
1407
|
+
if (k === "") continue;
|
|
1408
|
+
listed.add(k);
|
|
1409
|
+
const held = __nsRowAt(t, k);
|
|
1410
|
+
const s = entry.stat;
|
|
1411
|
+
if (held !== undefined
|
|
1412
|
+
&& Number(held.kind) === __nsKindCode(s.type) && Number(held.size) === Number(s.size)
|
|
1413
|
+
&& Number(held.mode) === Number(s.mode) && Number(held.uid) === Number(s.uid)
|
|
1414
|
+
&& Number(held.gid) === Number(s.gid) && Number(held.mtime) === Number(s.mtime)
|
|
1415
|
+
&& Number(held.ctime) === Number(s.ctime) && Number(held.ino) === Number(s.ino)
|
|
1416
|
+
&& Number(held.rev) === Number(entry.rev)
|
|
1417
|
+
&& (held.target ?? null) === (entry.linkTarget ?? null)) continue;
|
|
1418
|
+
__nsPut(t, k, s, entry.rev, entry.linkTarget);
|
|
1419
|
+
}
|
|
1420
|
+
}
|
|
1421
|
+
|
|
1422
|
+
/**
|
|
1423
|
+
* Record this facet's own acknowledged write in the namespace before the
|
|
1424
|
+
* delta that reports it: a file it created is then listed at once. Only
|
|
1425
|
+
* when the table has nothing as new, so an authority stat already applied at
|
|
1426
|
+
* or past `rev` is never overwritten by this approximation; the delta that
|
|
1427
|
+
* reports the write replaces it with the authority's stat.
|
|
1428
|
+
*/
|
|
1429
|
+
function __nsNoteOwnFile(k, size, rev) {
|
|
1430
|
+
if (!__nsReady()) return;
|
|
1431
|
+
const t = __residentT;
|
|
1432
|
+
const held = __nsRowAt(t, k);
|
|
1433
|
+
if (held !== undefined && Number(held.rev) >= rev) return;
|
|
1434
|
+
const now = Date.now();
|
|
1435
|
+
__nsTryPut(t, k, {
|
|
1436
|
+
// A file the process created: its umask's mode, until the delta reports
|
|
1437
|
+
// the authority's own stat for it.
|
|
1438
|
+
type: "file", size, mode: held ? Number(held.mode) : 0o100666 & ~Number(__nsCred?.umask ?? 0o022),
|
|
1439
|
+
uid: held ? Number(held.uid) : Number(__nsCred?.uid ?? 0),
|
|
1440
|
+
gid: held ? Number(held.gid) : Number(__nsCred?.gid ?? 0),
|
|
1441
|
+
atime: now, mtime: now, ctime: now, ino: held ? Number(held.ino) : 0,
|
|
1442
|
+
}, rev, null);
|
|
1443
|
+
}
|
|
1444
|
+
|
|
1445
|
+
/**
|
|
1446
|
+
* Record what the authority just said of a name this process asked about (an
|
|
1447
|
+
* async stat, read or accepted write): the sync view answers it at once,
|
|
1448
|
+
* as it would after the delta that reports it. Dated at the store's cursor,
|
|
1449
|
+
* so any row as new is kept, and the next delta naming the path replaces it
|
|
1450
|
+
* with the authority's own dating. `stat` null: the authority said it is not
|
|
1451
|
+
* there, and a row older than the cursor goes.
|
|
1452
|
+
*/
|
|
1453
|
+
function __nsNoteLiveStat(k, stat) {
|
|
1454
|
+
if (!__nsReady()) return;
|
|
1455
|
+
const cursor = __residentCursor();
|
|
1456
|
+
if (cursor === null) return;
|
|
1457
|
+
const t = __residentT;
|
|
1458
|
+
const held = __nsRowAt(t, k);
|
|
1459
|
+
if (held !== undefined && Number(held.rev) >= cursor.rev) return;
|
|
1460
|
+
if (stat === null) {
|
|
1461
|
+
if (held !== undefined) { const [parent, name] = __nsSplit(k); t.nsDelete(parent, name); __nsLinks.delete(k); }
|
|
1462
|
+
return;
|
|
1463
|
+
}
|
|
1464
|
+
if (!__nsDescribes(stat)) return;
|
|
1465
|
+
__nsTryPut(t, k, stat, cursor.rev, stat.linkTarget ?? stat.target ?? null);
|
|
1466
|
+
}
|
|
1467
|
+
|
|
1468
|
+
/**
|
|
1469
|
+
* Drop every cell AND the cursor, and re-seal.
|
|
1470
|
+
*
|
|
1471
|
+
* This is slot handover, not invalidation: a returned slot must not hand the
|
|
1472
|
+
* next tenant a filesystem, and dropping the rows while leaving a cursor behind
|
|
1473
|
+
* would leave the store claiming to be current at a revision it holds nothing
|
|
1474
|
+
* from. Clearing the cursor forces the next incarnation through
|
|
1475
|
+
* `__residentAdmit` from scratch. `__residentBoot` clears the same way when a
|
|
1476
|
+
* kept store cannot be reconciled.
|
|
1477
|
+
*/
|
|
1478
|
+
function __residentClear() {
|
|
1479
|
+
if (!__residentReady) throw new Error("Nimbus: __residentClear before __residentBind");
|
|
1480
|
+
__residentT.clear();
|
|
1481
|
+
// Held cells go with the rows, but for own writes still in flight.
|
|
1482
|
+
for (const [path, held] of [...__residentHeld]) if (held.rev !== __RK_OWN_WRITE) __residentForgetHeld(path);
|
|
1483
|
+
__nsLinks = new Set();
|
|
1484
|
+
__nsOk = false;
|
|
1485
|
+
__residentSeal("the store was cleared");
|
|
1486
|
+
}
|
|
1487
|
+
|
|
1488
|
+
function __residentStats() {
|
|
1489
|
+
if (!__residentReady) throw new Error("Nimbus: __residentStats before __residentBind");
|
|
1490
|
+
const t = __residentT;
|
|
1491
|
+
const counted = t.fileStats();
|
|
1492
|
+
const files = counted === undefined ? 0 : Number(counted.n);
|
|
1493
|
+
const bytes = counted === undefined ? 0 : Number(counted.b);
|
|
1494
|
+
const names = t.nsCount();
|
|
1495
|
+
return {
|
|
1496
|
+
files, bytes, names,
|
|
1497
|
+
namespace: __nsReady(),
|
|
1498
|
+
databaseSize: t.databaseSize(),
|
|
1499
|
+
sealed: __residentSealed,
|
|
1500
|
+
undatedSnapshot: __residentUndated,
|
|
1501
|
+
cursor: __residentCursor(),
|
|
1502
|
+
};
|
|
1503
|
+
}
|
|
1504
|
+
|
|
1505
|
+
/**
|
|
1506
|
+
* Make this incarnation's store servable before the program's first
|
|
1507
|
+
* instruction. The one boot path the resident body takes.
|
|
1508
|
+
*
|
|
1509
|
+
* A COLD store — no persisted cursor — adopts this spawn's own snapshot, the
|
|
1510
|
+
* module bundle dated at the cursor it was read at, and then fills the rest of
|
|
1511
|
+
* the filesystem from the authority. Its rows are current as of the spawn
|
|
1512
|
+
* whether or not the fill completes, which is all the protocol asks of a
|
|
1513
|
+
* process's first block (§5.3); the first ACQUIRE's delta brings them the rest
|
|
1514
|
+
* of the way.
|
|
1515
|
+
*
|
|
1516
|
+
* A KEPT store holds a PREVIOUS process's rows, dated against a cursor this
|
|
1517
|
+
* incarnation has never applied. A durable application's facet is one: it
|
|
1518
|
+
* keeps its `app-slot-` name across launches and its release never deletes
|
|
1519
|
+
* storage, so every relaunch or re-drive of the application opens what its
|
|
1520
|
+
* last process left. The rows are the asset — inside one supervisor
|
|
1521
|
+
* incarnation a relaunch fetches what changed, not the filesystem; across two,
|
|
1522
|
+
* whose revisions are unrelated, the reconcile rebuilds every dated row — and
|
|
1523
|
+
* they are also arbitrarily old: only the reconcile says which of them still
|
|
1524
|
+
* describe the filesystem, and nothing else runs before the program's first
|
|
1525
|
+
* synchronous reads. So the store stays SEALED through the reconcile, and a
|
|
1526
|
+
* reconcile that cannot vouch for its rows — the listing failed, came back
|
|
1527
|
+
* short, or there is no supervisor to ask — does not open it. The kept store
|
|
1528
|
+
* is emptied instead and this launch boots exactly as a cold one does: the
|
|
1529
|
+
* rare failure costs a relaunch its speedup, never a stale byte.
|
|
1530
|
+
*
|
|
1531
|
+
* The previous process's own unacknowledged rows are the one thing the
|
|
1532
|
+
* reconcile cannot judge, since it keeps own rows whatever the listing says.
|
|
1533
|
+
* `__residentBind` drops them before any of this runs.
|
|
1534
|
+
*
|
|
1535
|
+
* `takeBundle` hands over the module bundle and drops the module's own
|
|
1536
|
+
* reference to it. The parsed bundle is the largest allocation in the facet
|
|
1537
|
+
* before the program starts, so a cold boot releases it the moment it is
|
|
1538
|
+
* adopted, and a kept store that reconciles never adopts it at all.
|
|
1539
|
+
*
|
|
1540
|
+
* Returns the cursor to publish and, when the boot fell short of the whole
|
|
1541
|
+
* filesystem, why.
|
|
1542
|
+
*/
|
|
1543
|
+
async function __residentBoot(takeBundle, moduleCursor, supervisor) {
|
|
1544
|
+
if (!__residentReady) throw new Error("Nimbus: __residentBoot before __residentBind");
|
|
1545
|
+
let failure = null;
|
|
1546
|
+
if (__residentCursor() !== null) {
|
|
1547
|
+
let pass = null;
|
|
1548
|
+
try { pass = await __residentSynchronizeFromSupervisor(supervisor); }
|
|
1549
|
+
catch (e) { failure = (e && e.message) || String(e); }
|
|
1550
|
+
if (pass && pass.cursor) return { cursor: await __residentCatchUp(supervisor, pass.cursor), failure: null };
|
|
1551
|
+
failure = "a kept store could not be reconciled ("
|
|
1552
|
+
+ (failure || (pass && (pass.skipped || pass.incomplete)) || "the listing vouched for nothing")
|
|
1553
|
+
+ "); it was emptied and this launch booted from its own snapshot";
|
|
1554
|
+
}
|
|
1555
|
+
// A store with no cursor holds nothing any incarnation can date.
|
|
1556
|
+
__residentClear();
|
|
1557
|
+
const bundle = takeBundle();
|
|
1558
|
+
// The module map is the program's code: it is held whole or the launch
|
|
1559
|
+
// fails. Spawn admitted it (N18); this is room for it now, asked for when
|
|
1560
|
+
// the store's cap does not already cover it.
|
|
1561
|
+
let moduleBytes = 0;
|
|
1562
|
+
for (const path of Object.keys(bundle || {})) moduleBytes += __residentCellCost(bundle[path]);
|
|
1563
|
+
if (!(await __residentEnsureRoom(moduleBytes))) {
|
|
1564
|
+
throw Object.assign(new Error(
|
|
1565
|
+
"ENOSPC: workspace storage is full: this process's modules (" + Math.ceil(moduleBytes / 1048576)
|
|
1566
|
+
+ " MiB) do not fit in what is left of the session's storage"
|
|
1567
|
+
), { code: "ENOSPC" });
|
|
1568
|
+
}
|
|
1569
|
+
const adopted = __residentAdoptModuleBundle(bundle, moduleCursor);
|
|
1570
|
+
// The pass that just failed is not retried. The store now holds what a
|
|
1571
|
+
// cold launch whose fill failed holds, and its first ACQUIRE's delta
|
|
1572
|
+
// brings it current the same way.
|
|
1573
|
+
if (failure !== null) return { cursor: adopted, failure };
|
|
1574
|
+
let pass = null;
|
|
1575
|
+
try { pass = await __residentSynchronizeFromSupervisor(supervisor); }
|
|
1576
|
+
catch (e) { failure = (e && e.message) || String(e); }
|
|
1577
|
+
if (pass && pass.cursor) return { cursor: await __residentCatchUp(supervisor, pass.cursor), failure };
|
|
1578
|
+
return { cursor: adopted, failure };
|
|
1579
|
+
}
|
|
1580
|
+
|
|
1581
|
+
/**
|
|
1582
|
+
* The acquire options this store asks for: stats with every delta entry, and
|
|
1583
|
+
* the content of files changed under the push roots — the process's working
|
|
1584
|
+
* tree and /tmp, where what a peer writes after launch is what it goes on to
|
|
1585
|
+
* read (build output, manifests, temp files), less dependencies and VCS state.
|
|
1586
|
+
*/
|
|
1587
|
+
let __residentPushRoots = [];
|
|
1588
|
+
/** Dependencies and VCS state: written in bulk, read through the module map or not at all. */
|
|
1589
|
+
const __RESIDENT_PUSH_EXCLUDE = ["node_modules", ".git"];
|
|
1590
|
+
function __residentSetPushRoots(roots) {
|
|
1591
|
+
__residentPushRoots = (Array.isArray(roots) ? roots : []).map((r) => String(r).replace(/^\/+|\/+$/g, ""));
|
|
1592
|
+
}
|
|
1593
|
+
/** Whether a changed file at `path` is one the push roots would carry. */
|
|
1594
|
+
function __residentPushable(path) {
|
|
1595
|
+
for (const root of __residentPushRoots) {
|
|
1596
|
+
if (root !== "" && path !== root && !path.startsWith(root + "/")) continue;
|
|
1597
|
+
const rest = root === "" ? path : path.slice(root.length + 1);
|
|
1598
|
+
if (!rest.split("/").some((seg) => __RESIDENT_PUSH_EXCLUDE.includes(seg))) return true;
|
|
1599
|
+
}
|
|
1600
|
+
return false;
|
|
1601
|
+
}
|
|
1602
|
+
function __residentAcquireOptions() {
|
|
1603
|
+
return __residentPushRoots.length > 0
|
|
1604
|
+
? { namespace: true, push: { roots: __residentPushRoots, exclude: __RESIDENT_PUSH_EXCLUDE } }
|
|
1605
|
+
: { namespace: true };
|
|
1606
|
+
}
|
|
1607
|
+
|
|
1608
|
+
/**
|
|
1609
|
+
* One delta from a listing's cursor, applied before the program runs. A
|
|
1610
|
+
* listing's pages are walked at successive revisions, so the namespace it
|
|
1611
|
+
* builds can mix states no single revision had (a rename seen on both sides
|
|
1612
|
+
* or neither); the delta from its cursor names every path that moved during
|
|
1613
|
+
* the walk, and leaves the table exact at the delta's revision. At boot there
|
|
1614
|
+
* are no own writes, so nothing here needs the ledger's report notes.
|
|
1615
|
+
*/
|
|
1616
|
+
async function __residentCatchUp(supervisor, cursor) {
|
|
1617
|
+
if (!supervisor || typeof supervisor.fsAcquire !== "function") return cursor;
|
|
1618
|
+
let result;
|
|
1619
|
+
try { result = await supervisor.fsAcquire(cursor.epoch, cursor.rev, __residentAcquireOptions()); }
|
|
1620
|
+
catch { result = null; }
|
|
1621
|
+
if (!result || result.poison || typeof result.rev !== "number" || typeof result.epoch !== "string") {
|
|
1622
|
+
// Unanswered: the namespace keeps the listing's revision, which the
|
|
1623
|
+
// program's first barrier brings forward like any other.
|
|
1624
|
+
return cursor;
|
|
1625
|
+
}
|
|
1626
|
+
await __residentRoomForPushed(result);
|
|
1627
|
+
const applied = __residentAdmit(result);
|
|
1628
|
+
for (const dir of applied.relist) await __nsRelist(supervisor, dir);
|
|
1629
|
+
// A dropped row was held, so it is wanted again at its new revision.
|
|
1630
|
+
const refetch = [];
|
|
1631
|
+
for (const path of applied.dropped) {
|
|
1632
|
+
const found = __nsResolve(path, false);
|
|
1633
|
+
if (!found || found === "ELOOP" || Number(found.row.kind) !== __NS_FILE) continue;
|
|
1634
|
+
refetch.push({ path, size: Number(found.row.size), rev: Number(found.row.rev), epoch: applied.cursor.epoch });
|
|
1635
|
+
}
|
|
1636
|
+
if (refetch.length > 0) await __residentFetchFiles(supervisor, refetch);
|
|
1637
|
+
return applied.cursor;
|
|
1638
|
+
}
|
|
1639
|
+
|
|
1640
|
+
/**
|
|
1641
|
+
* Re-read the names beneath `dir` from the authority: a directory that just
|
|
1642
|
+
* became searchable, whose descendants no delta will ever name.
|
|
1643
|
+
*/
|
|
1644
|
+
async function __nsRelist(supervisor, dir) {
|
|
1645
|
+
if (!supervisor || typeof supervisor.fsList !== "function") { __nsMarkReady(__residentT, false, "the session cannot list its filesystem"); return; }
|
|
1646
|
+
const t = __residentT;
|
|
1647
|
+
const prefix = dir + "/";
|
|
1648
|
+
const seen = [];
|
|
1649
|
+
let replaceBytes = 0;
|
|
1650
|
+
let incomingBytes = 0;
|
|
1651
|
+
if (__residentInHeap) {
|
|
1652
|
+
for (const row of t.nsKeys()) {
|
|
1653
|
+
const key = row.parent ? row.parent + "/" + row.name : row.name;
|
|
1654
|
+
if (!key.startsWith(prefix)) continue;
|
|
1655
|
+
replaceBytes += __namespaceRowBytes(row.parent, row.name, t.nsGet(row.parent, row.name)?.target);
|
|
1656
|
+
}
|
|
1657
|
+
}
|
|
1658
|
+
let after = prefix;
|
|
1659
|
+
let done = false;
|
|
1660
|
+
for (let page = 0; page < __RESIDENT_MAX_LIST_PAGES && !done; page++) {
|
|
1661
|
+
let listed;
|
|
1662
|
+
try { listed = await supervisor.fsList(after, __RESIDENT_LIST_PAGE); }
|
|
1663
|
+
catch (e) { __nsMarkReady(t, false, "a listing of /" + dir + " failed (" + ((e && e.message) || String(e)) + ")"); return; }
|
|
1664
|
+
if (!listed || !Array.isArray(listed.entries)) { __nsMarkReady(t, false, "a listing of /" + dir + " came back without entries"); return; }
|
|
1665
|
+
for (const entry of listed.entries) {
|
|
1666
|
+
const k = String(entry.path).replace(/^\/+/, "");
|
|
1667
|
+
if (!k.startsWith(prefix)) {
|
|
1668
|
+
if (k > prefix) { done = true; break; }
|
|
1669
|
+
continue;
|
|
1670
|
+
}
|
|
1671
|
+
if (entry.stat) {
|
|
1672
|
+
if (__residentInHeap && __residentCap !== null) {
|
|
1673
|
+
const [parent, name] = __nsSplit(k);
|
|
1674
|
+
incomingBytes += __namespaceRowBytes(parent, name, entry.linkTarget);
|
|
1675
|
+
if (__residentDbBytes() - replaceBytes + incomingBytes > __residentCap) {
|
|
1676
|
+
__nsMarkReady(t, false, "namespace storage budget exceeded (" + __residentCap + " bytes)");
|
|
1677
|
+
return;
|
|
1678
|
+
}
|
|
1679
|
+
}
|
|
1680
|
+
seen.push(entry);
|
|
1681
|
+
}
|
|
1682
|
+
}
|
|
1683
|
+
if (listed.next === null || listed.next === undefined) done = true;
|
|
1684
|
+
else after = listed.next;
|
|
1685
|
+
}
|
|
1686
|
+
if (!done) { __nsMarkReady(t, false, "a listing of /" + dir + " did not finish"); return; }
|
|
1687
|
+
if (!seen.every((entry) => __nsDescribes(entry.stat))) { __nsMarkReady(t, false, "a listing of /" + dir + " named an entry without its stat"); return; }
|
|
1688
|
+
// Exactly what the listing has under the directory: a name it no longer
|
|
1689
|
+
// shows (hidden now, or gone) leaves, one it shows is written.
|
|
1690
|
+
__nsDeleteTree(t, dir, false);
|
|
1691
|
+
for (const entry of seen) __nsPut(t, String(entry.path).replace(/^\/+/, ""), entry.stat, entry.rev, entry.linkTarget);
|
|
1692
|
+
}
|
|
1693
|
+
|
|
1694
|
+
/**
|
|
1695
|
+
* The launch's data plan (facets/data-plan.ts): the paths whose content a
|
|
1696
|
+
* synchronous read may want, held from boot beside the module map. A launch
|
|
1697
|
+
* with none (a re-entry by an inbound request) holds what it already had.
|
|
1698
|
+
*/
|
|
1699
|
+
let __residentPlan = new Set();
|
|
1700
|
+
function __residentSetPlan(paths) {
|
|
1701
|
+
__residentPlan = new Set(Array.isArray(paths) ? paths.map((p) => String(p).replace(/^\/+/, "")) : []);
|
|
1702
|
+
}
|
|
1703
|
+
|
|
1704
|
+
/**
|
|
1705
|
+
* Adopt the module map's bundle into an EMPTY store — the first fill, and the
|
|
1706
|
+
* one that costs nothing extra, because those bytes are already in the facet.
|
|
1707
|
+
* The cursor the bundle was read at becomes the store's.
|
|
1708
|
+
*
|
|
1709
|
+
* Returns the cursor the caller should publish, so there is one answer to
|
|
1710
|
+
* "what state does this facet cache" rather than two that can disagree.
|
|
1711
|
+
*/
|
|
1712
|
+
function __residentAdoptModuleBundle(bundle, moduleCursor) {
|
|
1713
|
+
if (!__residentReady) throw new Error("Nimbus: __residentAdoptModuleBundle before __residentBind");
|
|
1714
|
+
if (__residentCursor() !== null) {
|
|
1715
|
+
// Adopting over a kept store would re-date its rows at this spawn's
|
|
1716
|
+
// cursor without checking one of them. __residentBoot reconciles a kept
|
|
1717
|
+
// store, or empties it first.
|
|
1718
|
+
throw new Error("Nimbus: a module bundle is adopted only into an empty store");
|
|
1719
|
+
}
|
|
1720
|
+
if (!moduleCursor || moduleCursor.epoch == null || moduleCursor.rev == null) {
|
|
1721
|
+
// An undated snapshot. Adopt NOTHING and serve an empty store.
|
|
1722
|
+
//
|
|
1723
|
+
// Not a throw, because this is a real path rather than a misuse: a facet
|
|
1724
|
+
// whose first contact is an inbound HTTP request runs
|
|
1725
|
+
// `__nimbusEnsureStarted(env, ctx)` with no start args, so it has no
|
|
1726
|
+
// cursor to offer. Refusing would turn a served port into a dead one.
|
|
1727
|
+
//
|
|
1728
|
+
// Not an adopt either, because rows nothing can date are the stale-read
|
|
1729
|
+
// bug. An EMPTY store is safe to serve for the reason the seal exists at
|
|
1730
|
+
// all — it has no row whose age could be wrong — and a miss is an answer
|
|
1731
|
+
// the shims already handle, falling through to the supervisor exactly as
|
|
1732
|
+
// they do today after a poison. So this is strictly no worse than the
|
|
1733
|
+
// current behaviour and cannot serve a stale byte.
|
|
1734
|
+
__residentUndated = true;
|
|
1735
|
+
__residentSealed = false;
|
|
1736
|
+
__residentSealReason = "";
|
|
1737
|
+
return null;
|
|
1738
|
+
}
|
|
1739
|
+
const rev = Number(moduleCursor.rev);
|
|
1740
|
+
for (const path of Object.keys(bundle || {})) {
|
|
1741
|
+
__residentPut(__residentT, path, bundle[path], rev);
|
|
1742
|
+
}
|
|
1743
|
+
return __residentAdmit({ poison: false, paths: [], epoch: String(moduleCursor.epoch), rev }).cursor;
|
|
1744
|
+
}
|
|
1745
|
+
|
|
1746
|
+
/**
|
|
1747
|
+
* Enumerate the whole filesystem, one page at a time.
|
|
1748
|
+
*
|
|
1749
|
+
* WHAT EXISTS has to come from the authority, not from anything the facet was
|
|
1750
|
+
* shipped. That was measured rather than assumed: for a working tree holding a
|
|
1751
|
+
* 25 MiB file outside the cwd, the spawn-time stat table this replaced arrived
|
|
1752
|
+
* with FOUR entries ("home", "opt", "var", "home/user"), every one a
|
|
1753
|
+
* directory. A launch ships only what it staged plus its ancestors; a store
|
|
1754
|
+
* that enumerated from that would hold only what the bundle already had, which
|
|
1755
|
+
* is the admission problem it exists to delete.
|
|
1756
|
+
*
|
|
1757
|
+
* Every page is dated, and a page whose epoch differs from the first ABORTS
|
|
1758
|
+
* the listing. The supervisor was replaced mid-walk, so the pages already
|
|
1759
|
+
* collected describe a filesystem that no longer exists and the revision they
|
|
1760
|
+
* would be dated at is meaningless. Returning short is safe — the rows simply
|
|
1761
|
+
* are not there and the reads miss — while stitching the two halves together
|
|
1762
|
+
* would date one incarnation's bytes at another's clock.
|
|
1763
|
+
*/
|
|
1764
|
+
async function __residentEnumerate(supervisor) {
|
|
1765
|
+
const entries = [];
|
|
1766
|
+
// Every name, whatever its kind: the namespace is built from these.
|
|
1767
|
+
const names = [];
|
|
1768
|
+
let namespaceBytes = 0;
|
|
1769
|
+
let after = null;
|
|
1770
|
+
let cursor = null;
|
|
1771
|
+
for (let page = 0; page < __RESIDENT_MAX_LIST_PAGES; page++) {
|
|
1772
|
+
const listed = await supervisor.fsList(after, __RESIDENT_LIST_PAGE);
|
|
1773
|
+
if (!listed || !Array.isArray(listed.entries)) {
|
|
1774
|
+
throw new Error("Nimbus: fsList returned no page");
|
|
1775
|
+
}
|
|
1776
|
+
if (cursor === null) {
|
|
1777
|
+
cursor = { epoch: String(listed.epoch), rev: Number(listed.rev) };
|
|
1778
|
+
} else if (String(listed.epoch) !== cursor.epoch) {
|
|
1779
|
+
return { entries, names, cursor, complete: false, reason: "the supervisor changed incarnation mid-enumeration" };
|
|
1780
|
+
}
|
|
1781
|
+
for (const entry of listed.entries) {
|
|
1782
|
+
if (entry.stat) {
|
|
1783
|
+
if (__residentInHeap && __residentCap !== null) {
|
|
1784
|
+
const key = String(entry.path).replace(/^\/+/, "");
|
|
1785
|
+
const [parent, name] = __nsSplit(key);
|
|
1786
|
+
if (key) namespaceBytes += __namespaceRowBytes(parent, name, entry.linkTarget);
|
|
1787
|
+
if (__residentDbBytes() - __residentT.nsBytes() + namespaceBytes > __residentCap) {
|
|
1788
|
+
const cause = "namespace storage budget exceeded (" + __residentCap + " bytes)";
|
|
1789
|
+
__nsMarkReady(__residentT, false, cause);
|
|
1790
|
+
throw Object.assign(new Error(cause), { code: "ENOSPC" });
|
|
1791
|
+
}
|
|
1792
|
+
}
|
|
1793
|
+
names.push(entry);
|
|
1794
|
+
}
|
|
1795
|
+
// Directories carry no content, and a symlink's size is its TARGET's
|
|
1796
|
+
// length rather than the resolved file's — reading one by that size
|
|
1797
|
+
// would store a truncated file and call it whole. Content is fetched
|
|
1798
|
+
// by the path a symlink resolves to.
|
|
1799
|
+
if (entry.kind !== "file") continue;
|
|
1800
|
+
const path = String(entry.path).replace(/^\/+/, "");
|
|
1801
|
+
if (!path) continue;
|
|
1802
|
+
const size = Number(entry.size);
|
|
1803
|
+
// The path's own last-mutation revision. It is what makes a held row
|
|
1804
|
+
// verifiable without fetching its bytes, and so what makes a poison
|
|
1805
|
+
// repairable rather than fatal — see __residentSynchronizeFromSupervisor.
|
|
1806
|
+
const rev = Number(entry.rev);
|
|
1807
|
+
if (!Number.isSafeInteger(size) || size < 0 || !Number.isSafeInteger(rev) || rev < 0) {
|
|
1808
|
+
throw new Error("Nimbus: fsList returned unusable metadata for '" + path + "'");
|
|
1809
|
+
}
|
|
1810
|
+
entries.push({ path, size, rev, epoch: cursor.epoch, ckey: entry.contentKey == null ? null : String(entry.contentKey) });
|
|
1811
|
+
}
|
|
1812
|
+
if (listed.next === null || listed.next === undefined) {
|
|
1813
|
+
return { entries, names, namespaceBytes, cursor, complete: true, reason: null };
|
|
1814
|
+
}
|
|
1815
|
+
after = listed.next;
|
|
1816
|
+
}
|
|
1817
|
+
return { entries, names, cursor, complete: false, reason: "the filesystem exceeded the enumeration page bound" };
|
|
1818
|
+
}
|
|
1819
|
+
|
|
1820
|
+
/**
|
|
1821
|
+
* Fetch these files from the authority and install them, each dated at the
|
|
1822
|
+
* revision the listing reported it at.
|
|
1823
|
+
*
|
|
1824
|
+
* THE BATCH BOUNDS ARE THE SUPERVISOR'S AND ARE NOT NEGOTIABLE HERE.
|
|
1825
|
+
* `_rpcFsReadBatch` validates with zod and rejects the WHOLE call, so an
|
|
1826
|
+
* over-packed batch loses every path in it rather than degrading. Three
|
|
1827
|
+
* separate bounds apply at once and the packing below respects all three:
|
|
1828
|
+
*
|
|
1829
|
+
* paths per call __RESIDENT_BATCH_PATHS (FS_READ_BATCH_PATH_LIMIT)
|
|
1830
|
+
* requested bytes __RESIDENT_BATCH_BYTES (FS_READ_BATCH_REQUEST_BYTES)
|
|
1831
|
+
* bytes per range __RESIDENT_CHUNK_BYTES (the single-value ceiling)
|
|
1832
|
+
*
|
|
1833
|
+
* Every file is split into __RESIDENT_CHUNK_BYTES ranges — most files are one
|
|
1834
|
+
* short range — and the ranges are packed into calls. That makes the large
|
|
1835
|
+
* file the SAME path as the small one rather than a special case, and it means
|
|
1836
|
+
* no whole-file buffer is ever held: each range is written straight into its
|
|
1837
|
+
* own chunk row as it lands, and the head row follows only once every range of
|
|
1838
|
+
* that file has.
|
|
1839
|
+
*
|
|
1840
|
+
* A row is stamped with its OWN listed revision rather than with the global
|
|
1841
|
+
* cursor. Both are conservative — a write landing mid-fetch reports a revision
|
|
1842
|
+
* above the stamp, so the next ACQUIRE evicts and refetches, while stamping
|
|
1843
|
+
* newer than the bytes would keep a stale row forever — but the per-path
|
|
1844
|
+
* revision is the tighter of the two, and it is the one that survives a
|
|
1845
|
+
* reconcile: a row dated at the global cursor is indistinguishable from a row
|
|
1846
|
+
* whose path moved at that same revision.
|
|
1847
|
+
*/
|
|
1848
|
+
async function __residentFetchFiles(supervisor, files) {
|
|
1849
|
+
// Content already held under another path, or wanted twice in this batch,
|
|
1850
|
+
// is fetched once: equal keys are equal bytes (the second copy is local).
|
|
1851
|
+
const t = __residentT;
|
|
1852
|
+
const requested = files.length;
|
|
1853
|
+
let copied = 0;
|
|
1854
|
+
const wanted = [];
|
|
1855
|
+
const later = [];
|
|
1856
|
+
const fetching = new Set();
|
|
1857
|
+
for (const file of files) {
|
|
1858
|
+
if (file.ckey != null) {
|
|
1859
|
+
if (__residentCopyByKey(t, file.path, file.ckey, file.rev)) { copied++; continue; }
|
|
1860
|
+
if (fetching.has(file.ckey)) { later.push(file); continue; }
|
|
1861
|
+
fetching.add(file.ckey);
|
|
1862
|
+
}
|
|
1863
|
+
wanted.push(file);
|
|
1864
|
+
}
|
|
1865
|
+
files = wanted;
|
|
1866
|
+
// One flat list of ranges across all files, so packing is a single pass and
|
|
1867
|
+
// a 25 MiB file and a 40-byte one are the same shape.
|
|
1868
|
+
const ranges = [];
|
|
1869
|
+
for (const file of files) {
|
|
1870
|
+
const parts = Math.max(1, Math.ceil(file.size / __RESIDENT_CHUNK_BYTES));
|
|
1871
|
+
for (let part = 0; part < parts; part++) {
|
|
1872
|
+
const offset = part * __RESIDENT_CHUNK_BYTES;
|
|
1873
|
+
ranges.push({
|
|
1874
|
+
path: file.path,
|
|
1875
|
+
rev: file.rev,
|
|
1876
|
+
epoch: file.epoch,
|
|
1877
|
+
part,
|
|
1878
|
+
parts,
|
|
1879
|
+
offset,
|
|
1880
|
+
length: Math.min(__RESIDENT_CHUNK_BYTES, Math.max(0, file.size - offset)),
|
|
1881
|
+
size: file.size,
|
|
1882
|
+
ckey: file.ckey ?? null,
|
|
1883
|
+
});
|
|
1884
|
+
}
|
|
1885
|
+
}
|
|
1886
|
+
|
|
1887
|
+
// Per-file landed-part counts, so a head row is written only once every
|
|
1888
|
+
// range of that file is in. See __residentPutHead.
|
|
1889
|
+
const landed = new Map();
|
|
1890
|
+
let filled = 0, failed = 0, fetchedBytes = 0;
|
|
1891
|
+
const failedPaths = new Set();
|
|
1892
|
+
|
|
1893
|
+
for (let at = 0; at < ranges.length;) {
|
|
1894
|
+
const batch = [];
|
|
1895
|
+
let bytes = 0;
|
|
1896
|
+
while (
|
|
1897
|
+
at < ranges.length
|
|
1898
|
+
&& batch.length < __RESIDENT_BATCH_PATHS
|
|
1899
|
+
&& (batch.length === 0 || bytes + ranges[at].length <= __RESIDENT_BATCH_BYTES)
|
|
1900
|
+
) {
|
|
1901
|
+
bytes += ranges[at].length;
|
|
1902
|
+
batch.push(ranges[at]);
|
|
1903
|
+
at++;
|
|
1904
|
+
}
|
|
1905
|
+
// N18: room for the batch first; what the ledger will not admit is read
|
|
1906
|
+
// through the session instead of held.
|
|
1907
|
+
if (!(await __residentEnsureRoom(bytes + batch.length * __RESIDENT_ROW_BYTES))) {
|
|
1908
|
+
for (const r of batch) failedPaths.add(r.path);
|
|
1909
|
+
continue;
|
|
1910
|
+
}
|
|
1911
|
+
let results;
|
|
1912
|
+
try {
|
|
1913
|
+
results = await supervisor.fsReadBatch(
|
|
1914
|
+
batch.map((r) => ({ path: r.path, offset: r.offset, length: r.length, expectedEpoch: r.epoch, expectedRevision: r.rev }))
|
|
1915
|
+
);
|
|
1916
|
+
} catch {
|
|
1917
|
+
for (const r of batch) failedPaths.add(r.path);
|
|
1918
|
+
continue;
|
|
1919
|
+
}
|
|
1920
|
+
for (let i = 0; i < batch.length; i++) {
|
|
1921
|
+
const range = batch[i];
|
|
1922
|
+
const entry = results && results[i];
|
|
1923
|
+
if (!entry || entry.error) {
|
|
1924
|
+
// A denial is a real answer and is stored as one, so a later read
|
|
1925
|
+
// reports EACCES synchronously instead of missing. Anything else is a
|
|
1926
|
+
// path this store will not hold, and a read of it falls through to
|
|
1927
|
+
// exactly the miss it would have had before.
|
|
1928
|
+
if (entry && entry.error && entry.error.code === "EACCES" && range.part === 0) {
|
|
1929
|
+
__residentPut(t, range.path, { error: "EACCES" }, range.rev);
|
|
1930
|
+
filled++;
|
|
1931
|
+
} else failedPaths.add(range.path);
|
|
1932
|
+
continue;
|
|
1933
|
+
}
|
|
1934
|
+
if (entry.bytes === null || entry.bytes === undefined) { failedPaths.add(range.path); continue; }
|
|
1935
|
+
const chunk = __residentBytes(entry.bytes);
|
|
1936
|
+
if (chunk.byteLength !== range.length) { failedPaths.add(range.path); continue; }
|
|
1937
|
+
fetchedBytes += chunk.byteLength;
|
|
1938
|
+
if (!__residentPutChunk(t, range.path, range.part, chunk)) { failedPaths.add(range.path); continue; }
|
|
1939
|
+
const seen = (landed.get(range.path) || 0) + 1;
|
|
1940
|
+
landed.set(range.path, seen);
|
|
1941
|
+
if (seen === range.parts && !failedPaths.has(range.path)) {
|
|
1942
|
+
__residentPutHead(t, range.path, __RK_BINARY, range.size, range.parts, range.rev, range.ckey);
|
|
1943
|
+
filled++;
|
|
1944
|
+
}
|
|
1945
|
+
}
|
|
1946
|
+
}
|
|
1947
|
+
|
|
1948
|
+
// A file whose ranges did not all land has chunk rows and no head, so it is
|
|
1949
|
+
// unreadable rather than short. Drop them so the slot does not carry bytes
|
|
1950
|
+
// nothing will ever serve.
|
|
1951
|
+
for (const path of failedPaths) {
|
|
1952
|
+
failed++;
|
|
1953
|
+
t.chunkDelete(path);
|
|
1954
|
+
t.fileDelete(path);
|
|
1955
|
+
}
|
|
1956
|
+
|
|
1957
|
+
for (const file of later) {
|
|
1958
|
+
if (__residentCopyByKey(t, file.path, file.ckey, file.rev)) copied++;
|
|
1959
|
+
else failed++;
|
|
1960
|
+
}
|
|
1961
|
+
if (ranges.length > 0) await __residentReportSize();
|
|
1962
|
+
return {
|
|
1963
|
+
requested, filled: filled + copied, failed,
|
|
1964
|
+
bytes: fetchedBytes, ranges: ranges.length, copied,
|
|
1965
|
+
};
|
|
1966
|
+
}
|
|
1967
|
+
|
|
1968
|
+
/**
|
|
1969
|
+
* Bring the store to the authority's current state — the one repair path, used
|
|
1970
|
+
* both to populate a facet before its first instruction and to recover a
|
|
1971
|
+
* poisoned cursor.
|
|
1972
|
+
*
|
|
1973
|
+
* It turns "a capped prefetch" into "the filesystem", which is what makes a
|
|
1974
|
+
* first synchronous read of an untouched file succeed, and it is the only
|
|
1975
|
+
* blocking step: the waiting is done here so the reads that follow never have
|
|
1976
|
+
* to.
|
|
1977
|
+
*
|
|
1978
|
+
* WHY A POISON DOES NOT DROP THE ROWS. A poison says only that
|
|
1979
|
+
* `invalidatedSince` cannot describe the distance from our cursor to now —
|
|
1980
|
+
* the invalidation log is bounded at 256 KiB (sqlite-vfs.ts) and ordinary
|
|
1981
|
+
* write churn trims it past a live cursor as a matter of course. It says
|
|
1982
|
+
* nothing about the rows, and the rows are the asset: at pi scale ~16k files
|
|
1983
|
+
* and ~96 MB that dropping forces back over the wire, awaited inside the
|
|
1984
|
+
* ACQUIRE barrier, on every poison. That is a cost defect, not a coherence
|
|
1985
|
+
* one, and it was measured taking an agent turn past the DO CPU limit.
|
|
1986
|
+
*
|
|
1987
|
+
* `fsList` does not touch the log and reports every path's ABSOLUTE
|
|
1988
|
+
* revision, which is strictly more information than a delta. So the rows are
|
|
1989
|
+
* RECONCILED instead: a row at or above its listed revision is proven current
|
|
1990
|
+
* and kept, a row below it or no longer listed goes, and only what actually
|
|
1991
|
+
* moved is refetched. Every surviving row is vouched for by revision, so the
|
|
1992
|
+
* no-stale-byte guarantee is exactly as unconditional as it was on the drop
|
|
1993
|
+
* path — the delta is simply not the only way to establish it.
|
|
1994
|
+
*
|
|
1995
|
+
* The comparison is sound ONLY within one supervisor incarnation and ONLY
|
|
1996
|
+
* against a complete enumeration:
|
|
1997
|
+
*
|
|
1998
|
+
* - revisions from different epochs are unrelated clocks, and after a
|
|
1999
|
+
* restart an untouched path lists at rev 0, which would vouch for anything;
|
|
2000
|
+
* - a truncated listing cannot tell a path that was REMOVED from one that
|
|
2001
|
+
* was never walked, so dropping unlisted rows against it would delete a
|
|
2002
|
+
* live cache for no reason.
|
|
2003
|
+
*
|
|
2004
|
+
* Neither is verifiable ⇒ nothing is dropped and nothing is vouched for; the
|
|
2005
|
+
* pass degrades to filling what the store does not hold, which is exactly what
|
|
2006
|
+
* it did before rows could be verified. The caller keeps whatever cursor it
|
|
2007
|
+
* had, and a poisoned caller has already taken its cold cache through
|
|
2008
|
+
* `__residentAdmit`.
|
|
2009
|
+
*
|
|
2010
|
+
* A row stamped __RK_OWN_WRITE is this facet's own unacknowledged bytes —
|
|
2011
|
+
* newer than anything the authority can report — and is kept whether listed
|
|
2012
|
+
* or not, the same read-your-writes rule the delta path applies. But the
|
|
2013
|
+
* pass moves the cursor without a delta, so the report a delta would have
|
|
2014
|
+
* carried for such a path is returned instead (`own`): the listed revision,
|
|
2015
|
+
* or the listing's own cursor for a path it no longer lists, which bounds
|
|
2016
|
+
* whatever removed it. The write or mutation that owns the row adjudicates it
|
|
2017
|
+
* when its revision arrives, exactly as it does a delta's report. Across
|
|
2018
|
+
* incarnations, or against a short listing, no report can be dated and each
|
|
2019
|
+
* comes back null.
|
|
2020
|
+
*
|
|
2021
|
+
* Writes landing DURING the pass are covered the way they always were: the
|
|
2022
|
+
* published cursor is the one read BEFORE the walk, so they report revisions
|
|
2023
|
+
* above it and the next ACQUIRE evicts whatever they touched.
|
|
2024
|
+
*/
|
|
2025
|
+
async function __residentSynchronizeFromSupervisor(supervisor) {
|
|
2026
|
+
if (!__residentReady) throw new Error("Nimbus: __residentSynchronizeFromSupervisor before __residentBind");
|
|
2027
|
+
if (!supervisor || typeof supervisor.fsReadBatch !== "function") {
|
|
2028
|
+
return { requested: 0, filled: 0, failed: 0, skipped: "no fsReadBatch on this supervisor" };
|
|
2029
|
+
}
|
|
2030
|
+
if (typeof supervisor.fsList !== "function") {
|
|
2031
|
+
// Without the enumeration the store cannot know what it is missing, so it
|
|
2032
|
+
// fills nothing beyond the adopted bundle and every read behaves exactly
|
|
2033
|
+
// as it did before this store existed — a smaller resident set, never a
|
|
2034
|
+
// wrong one. That is the failure direction to keep.
|
|
2035
|
+
return { requested: 0, filled: 0, failed: 0, skipped: "supervisor cannot enumerate the filesystem (no fsList)" };
|
|
2036
|
+
}
|
|
2037
|
+
|
|
2038
|
+
let listing;
|
|
2039
|
+
try {
|
|
2040
|
+
listing = await __residentEnumerate(supervisor);
|
|
2041
|
+
} catch (e) {
|
|
2042
|
+
return { requested: 0, filled: 0, failed: 0, skipped: "fsList failed: " + ((e && e.message) || String(e)) };
|
|
2043
|
+
}
|
|
2044
|
+
if (!listing.cursor) {
|
|
2045
|
+
return { requested: 0, filled: 0, failed: 0, skipped: "fsList reported no authority cursor" };
|
|
2046
|
+
}
|
|
2047
|
+
|
|
2048
|
+
const t = __residentT;
|
|
2049
|
+
const held = __residentCursor();
|
|
2050
|
+
// May a row this pass cannot prove current be DROPPED? Only against a
|
|
2051
|
+
// complete enumeration: a truncated one cannot tell a path that was removed
|
|
2052
|
+
// from one that was never walked.
|
|
2053
|
+
const judgeable = listing.complete;
|
|
2054
|
+
const describable = judgeable && listing.names.every((entry) => __nsDescribes(entry.stat));
|
|
2055
|
+
// May a held revision be COMPARED with a listed one? Only inside one
|
|
2056
|
+
// supervisor incarnation: across a restart the clocks are unrelated, and an
|
|
2057
|
+
// untouched path lists at rev 0, which would vouch for anything.
|
|
2058
|
+
const comparable = judgeable && !!held && held.epoch === listing.cursor.epoch;
|
|
2059
|
+
|
|
2060
|
+
const listed = new Map();
|
|
2061
|
+
for (const file of listing.entries) listed.set(file.path, file);
|
|
2062
|
+
|
|
2063
|
+
const rows = [];
|
|
2064
|
+
for (const row of t.fileRows()) {
|
|
2065
|
+
rows.push({ path: String(row.path), rev: Number(row.rev), ckey: row.ckey == null ? null : String(row.ckey) });
|
|
2066
|
+
}
|
|
2067
|
+
const current = new Set();
|
|
2068
|
+
const dropped = [];
|
|
2069
|
+
const own = [];
|
|
2070
|
+
let rekeyed = 0;
|
|
2071
|
+
for (const row of rows) {
|
|
2072
|
+
const entry = listed.get(row.path);
|
|
2073
|
+
if (row.rev === __RK_OWN_WRITE) {
|
|
2074
|
+
current.add(row.path);
|
|
2075
|
+
own.push({
|
|
2076
|
+
path: row.path,
|
|
2077
|
+
rev: !comparable ? null : entry !== undefined ? entry.rev : listing.cursor.rev,
|
|
2078
|
+
});
|
|
2079
|
+
continue;
|
|
2080
|
+
}
|
|
2081
|
+
const keep = (comparable && entry !== undefined && row.rev >= entry.rev) || !judgeable;
|
|
2082
|
+
if (keep) { current.add(row.path); continue; }
|
|
2083
|
+
// The listing names the same bytes (a revision moved, the content did
|
|
2084
|
+
// not, or another epoch): kept and dated at the listing. Equal keys are
|
|
2085
|
+
// equal bytes, so this needs no comparable clock.
|
|
2086
|
+
if (entry !== undefined && row.ckey !== null && entry.ckey != null && row.ckey === entry.ckey) {
|
|
2087
|
+
t.fileSetRev(row.path, entry.rev);
|
|
2088
|
+
current.add(row.path);
|
|
2089
|
+
rekeyed++;
|
|
2090
|
+
continue;
|
|
2091
|
+
}
|
|
2092
|
+
dropped.push(row.path);
|
|
2093
|
+
}
|
|
2094
|
+
// Cells held in the heap (own writes the store had no room for) are judged
|
|
2095
|
+
// as file rows are, on every path a file row is (N18). Undated, a write-back
|
|
2096
|
+
// still in flight: newer than anything listed, it stays and is reported as
|
|
2097
|
+
// own. Dated: kept only where the listing proves it current; otherwise it
|
|
2098
|
+
// goes, and is fetched again like any dropped row, so a sync read of it is
|
|
2099
|
+
// a miss rather than bytes the authority has replaced.
|
|
2100
|
+
for (const [path, cell] of [...__residentHeld]) {
|
|
2101
|
+
if (current.has(path)) continue;
|
|
2102
|
+
const entry = listed.get(path);
|
|
2103
|
+
if (cell.rev === __RK_OWN_WRITE) {
|
|
2104
|
+
current.add(path);
|
|
2105
|
+
own.push({ path, rev: !comparable ? null : entry !== undefined ? entry.rev : listing.cursor.rev });
|
|
2106
|
+
continue;
|
|
2107
|
+
}
|
|
2108
|
+
if ((comparable && entry !== undefined && cell.rev >= entry.rev) || !judgeable) { current.add(path); continue; }
|
|
2109
|
+
__residentForgetHeld(path);
|
|
2110
|
+
dropped.push(path);
|
|
2111
|
+
}
|
|
2112
|
+
// Sweep what could not be vouched for. When NOTHING could — a new supervisor
|
|
2113
|
+
// incarnation, whose revisions say nothing about ours — that is every row but
|
|
2114
|
+
// this facet's own unflushed writes, and at pi scale a per-path delete would
|
|
2115
|
+
// be ~32,000 statements in one turn against an object that has been observed
|
|
2116
|
+
// resetting under exactly that load. So it is a predicate there, and per path
|
|
2117
|
+
// where a reconcile has made the set small by construction.
|
|
2118
|
+
if (dropped.length > 0 && !comparable && rekeyed === 0) {
|
|
2119
|
+
t.dropDatedFiles();
|
|
2120
|
+
} else {
|
|
2121
|
+
for (const path of dropped) {
|
|
2122
|
+
t.chunkDelete(path);
|
|
2123
|
+
t.fileDelete(path);
|
|
2124
|
+
}
|
|
2125
|
+
}
|
|
2126
|
+
|
|
2127
|
+
// What to hold: the launch's data plan, every row this pass had to drop as
|
|
2128
|
+
// stale — it was held for a reason — and what the pushes a poison lost
|
|
2129
|
+
// would have carried: files under the push roots changed since the cursor
|
|
2130
|
+
// (all of them there, when the revisions are not comparable).
|
|
2131
|
+
const plan = __residentPlan;
|
|
2132
|
+
const wanted = new Set(dropped);
|
|
2133
|
+
const fetch = [];
|
|
2134
|
+
for (const file of listing.entries) {
|
|
2135
|
+
if (current.has(file.path)) continue;
|
|
2136
|
+
if (plan.has(file.path) || wanted.has(file.path)
|
|
2137
|
+
|| (__residentPushable(file.path) && (!comparable || file.rev > held.rev))) fetch.push(file);
|
|
2138
|
+
}
|
|
2139
|
+
// Reserve mandatory metadata growth while optional data is fetched, but
|
|
2140
|
+
// retain the existing namespace publication order across the await.
|
|
2141
|
+
const namespaceGrowth = __residentInHeap && describable ? Math.max(0, listing.namespaceBytes - t.nsBytes()) : 0;
|
|
2142
|
+
if (__residentInHeap && __residentCap !== null && __residentDbBytes() + namespaceGrowth > __residentCap) {
|
|
2143
|
+
const cause = "namespace storage budget exceeded (" + __residentCap + " bytes)";
|
|
2144
|
+
__nsMarkReady(t, false, cause);
|
|
2145
|
+
throw Object.assign(new Error(cause), { code: "ENOSPC" });
|
|
2146
|
+
}
|
|
2147
|
+
__residentNamespaceReserveBytes += namespaceGrowth;
|
|
2148
|
+
let filled;
|
|
2149
|
+
try { filled = await __residentFetchFiles(supervisor, fetch); }
|
|
2150
|
+
finally { __residentNamespaceReserveBytes -= namespaceGrowth; }
|
|
2151
|
+
|
|
2152
|
+
// The cursor may only advance to a state the rows actually describe, and
|
|
2153
|
+
// after a truncated listing they do not: a path in an unwalked page could
|
|
2154
|
+
// have moved since the held cursor and would never be reported again.
|
|
2155
|
+
if (judgeable) {
|
|
2156
|
+
// A listing that cannot describe every name restores the rows it vouches
|
|
2157
|
+
// for and the cursor, but not the namespace (see __nsDescribes).
|
|
2158
|
+
if (describable) __nsReplace(t, listing.names);
|
|
2159
|
+
__residentWriteCursor(t, listing.cursor);
|
|
2160
|
+
__nsMarkReady(t, describable, "the session's listing named an entry without its stat");
|
|
2161
|
+
__residentUndated = false;
|
|
2162
|
+
__residentSealed = false;
|
|
2163
|
+
__residentSealReason = "";
|
|
2164
|
+
}
|
|
2165
|
+
|
|
2166
|
+
return {
|
|
2167
|
+
requested: filled.requested,
|
|
2168
|
+
filled: filled.filled,
|
|
2169
|
+
failed: filled.failed,
|
|
2170
|
+
bytes: filled.bytes,
|
|
2171
|
+
ranges: filled.ranges,
|
|
2172
|
+
kept: current.size,
|
|
2173
|
+
rekeyed,
|
|
2174
|
+
dropped: dropped.length,
|
|
2175
|
+
own,
|
|
2176
|
+
reconciled: comparable,
|
|
2177
|
+
complete: listing.complete,
|
|
2178
|
+
cursor: judgeable ? listing.cursor : null,
|
|
2179
|
+
...(listing.reason ? { incomplete: listing.reason } : {}),
|
|
2180
|
+
};
|
|
2181
|
+
}
|
|
2182
|
+
|
|
2183
|
+
/**
|
|
2184
|
+
* Refetch, after a delta the store applied without a listing, what it should
|
|
2185
|
+
* hold and does not: the paths the delta dropped and the launch's data plan.
|
|
2186
|
+
* The namespace (current as of that delta) gives each one's size and
|
|
2187
|
+
* revision, as a listing entry would.
|
|
2188
|
+
*/
|
|
2189
|
+
async function __residentRefillFromNamespace(supervisor, dropped) {
|
|
2190
|
+
const t = __residentT;
|
|
2191
|
+
const cursor = __residentCursor();
|
|
2192
|
+
if (cursor === null) return { requested: 0, filled: 0, failed: 0, bytes: 0 };
|
|
2193
|
+
const fetch = [];
|
|
2194
|
+
const seen = new Set();
|
|
2195
|
+
for (const path of [...dropped, ...__residentPlan]) {
|
|
2196
|
+
if (seen.has(path)) continue;
|
|
2197
|
+
seen.add(path);
|
|
2198
|
+
if (__residentHead(path) !== undefined || __residentHeld.has(path)) continue;
|
|
2199
|
+
const row = __nsRowAt(t, path);
|
|
2200
|
+
if (row === undefined || Number(row.kind) !== __NS_FILE) continue;
|
|
2201
|
+
fetch.push({ path, size: Number(row.size), rev: Number(row.rev), epoch: cursor.epoch, ckey: null });
|
|
2202
|
+
}
|
|
2203
|
+
return __residentFetchFiles(supervisor, fetch);
|
|
2204
|
+
}
|
|
2205
|
+
|
|
2206
|
+
/**
|
|
2207
|
+
* The launch's last step before user code: the namespace must answer, since
|
|
2208
|
+
* every synchronous stat, exists and readdir reads it. A boot that left it
|
|
2209
|
+
* unready (its listing failed, came back short, or could not describe every
|
|
2210
|
+
* name) is repaired once more. Resolves to null when it answers, or to the
|
|
2211
|
+
* launch's failure, naming why; nothing is ever run on a namespace that
|
|
2212
|
+
* does not.
|
|
2213
|
+
*/
|
|
2214
|
+
async function __residentRequireNamespace(supervisor, bootFailure) {
|
|
2215
|
+
if (__nsReady()) return null;
|
|
2216
|
+
try { await __residentSynchronizeFromSupervisor(supervisor); } catch {}
|
|
2217
|
+
if (__nsReady()) return null;
|
|
2218
|
+
return "node: the process was not started: its view of the filesystem could not be listed ("
|
|
2219
|
+
+ __nsNotReadyCause() + (bootFailure ? "; " + bootFailure : "") + ")";
|
|
2220
|
+
}
|
|
2221
|
+
|
|
2222
|
+
/**
|
|
2223
|
+
* The resident set, as the shims have always seen it.
|
|
2224
|
+
*
|
|
2225
|
+
* Every trap is synchronous, because every reader of __vfsBundle is.
|
|
2226
|
+
*/
|
|
2227
|
+
const __nimbusResidentBundle = new Proxy(Object.create(null), {
|
|
2228
|
+
get(_t, path) {
|
|
2229
|
+
if (typeof path !== "string") return undefined;
|
|
2230
|
+
return __residentGet(path);
|
|
2231
|
+
},
|
|
2232
|
+
has(_t, path) {
|
|
2233
|
+
if (typeof path !== "string") return false;
|
|
2234
|
+
return __residentHead(path) !== undefined || __residentHeld.has(path);
|
|
2235
|
+
},
|
|
2236
|
+
set(_t, path, cell) {
|
|
2237
|
+
// The program's own write, stamped __RK_OWN_WRITE: not the authority's
|
|
2238
|
+
// yet, newer than anything it could report, and replaced with a real
|
|
2239
|
+
// revision by the ledger when the write-back lands.
|
|
2240
|
+
if (typeof path !== "string") return true;
|
|
2241
|
+
__residentPut(__residentRequire(), path, cell, undefined);
|
|
2242
|
+
return true;
|
|
2243
|
+
},
|
|
2244
|
+
deleteProperty(_t, path) {
|
|
2245
|
+
if (typeof path === "string") __residentDelete(__residentRequire(), path);
|
|
2246
|
+
return true;
|
|
2247
|
+
},
|
|
2248
|
+
ownKeys() { return __residentKeys(); },
|
|
2249
|
+
getOwnPropertyDescriptor(_t, path) {
|
|
2250
|
+
if (typeof path !== "string") return undefined;
|
|
2251
|
+
if (__residentHead(path) === undefined) return undefined;
|
|
2252
|
+
// Configurable so 'delete' is legal and enumerable so for-in and
|
|
2253
|
+
// Object.keys see it — the two things every scan site depends on.
|
|
2254
|
+
//
|
|
2255
|
+
// An ACCESSOR, not a data descriptor, and that is the whole point: a
|
|
2256
|
+
// for-in or Object.keys over a Proxy asks this trap about every key, so a
|
|
2257
|
+
// data descriptor had to produce `value` — reading every file's bytes out
|
|
2258
|
+
// of SQLite to answer a question about NAMES. Measured at pi scale, one
|
|
2259
|
+
// such scan read 87 MiB and threw all of it away. A getter answers the
|
|
2260
|
+
// name question for free and still returns the bytes to anything that
|
|
2261
|
+
// genuinely reads the property.
|
|
2262
|
+
return { configurable: true, enumerable: true, get() { return __residentGet(path); } };
|
|
2263
|
+
},
|
|
2264
|
+
});
|