@nimbus-sh/worker 0.10.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/facets/cirrus-real.d.ts.map +1 -1
- package/dist/facets/cirrus-real.js +9 -4
- package/dist/facets/compose.d.ts +11 -7
- package/dist/facets/compose.d.ts.map +1 -1
- package/dist/facets/compose.js +8 -38
- 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-transform.d.ts +36 -10
- package/dist/facets/esbuild-transform.d.ts.map +1 -1
- package/dist/facets/esbuild-transform.js +216 -20
- 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 +179 -78
- package/dist/facets/manager.d.ts.map +1 -1
- package/dist/facets/manager.js +1221 -649
- 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/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 +16 -9
- package/dist/git/commands.d.ts.map +1 -1
- package/dist/git/commands.js +1818 -188
- package/dist/git/network-facet.d.ts +10 -2
- package/dist/git/network-facet.d.ts.map +1 -1
- package/dist/git/network-facet.js +62 -16
- 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 +62 -243
- package/dist/hosted/runtime.d.ts +19 -13
- package/dist/hosted/runtime.d.ts.map +1 -1
- package/dist/hosted/runtime.js +71 -14
- package/dist/hosted/services.d.ts +6 -7
- package/dist/hosted/services.d.ts.map +1 -1
- package/dist/hosted/services.js +10 -14
- 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 +5 -2
- package/dist/loaders/generated-workers.d.ts.map +1 -1
- package/dist/loaders/generated-workers.js +6 -3
- package/dist/loaders/npm-resolve-preamble.d.ts.map +1 -1
- package/dist/loaders/npm-resolve-preamble.js +0 -27
- 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 +5 -0
- package/dist/npm/cache.d.ts.map +1 -1
- package/dist/npm/cache.js +13 -1
- package/dist/npm/install-batch-facet.d.ts +2 -2
- package/dist/npm/install-batch-facet.js +3 -3
- package/dist/npm/installer.d.ts +17 -1
- package/dist/npm/installer.d.ts.map +1 -1
- package/dist/npm/installer.js +159 -20
- 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/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 +2 -0
- package/dist/npm/r2-cache.d.ts.map +1 -1
- package/dist/npm/r2-cache.js +2 -0
- package/dist/npm/resolve-one-facet.d.ts.map +1 -1
- package/dist/npm/resolve-one-facet.js +14 -3
- package/dist/npm/resolver.d.ts +0 -7
- package/dist/npm/resolver.d.ts.map +1 -1
- package/dist/npm/resolver.js +0 -22
- 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/esbuild-wasm-bytes.d.ts +17 -5
- package/dist/runtime/esbuild-wasm-bytes.d.ts.map +1 -1
- package/dist/runtime/esbuild-wasm-bytes.js +43 -94
- 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 +1567 -864
- 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 +2 -2
- 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 +36 -35
- 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.map +1 -1
- package/dist/runtime/wasi/preamble.js +0 -24
- 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 +34 -11
- package/dist/session/hibernation.d.ts.map +1 -1
- package/dist/session/hibernation.js +46 -29
- 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 +41 -15
- package/dist/session/nimbus-session.d.ts.map +1 -1
- package/dist/session/nimbus-session.js +64 -70
- package/dist/session/npm-install-port.d.ts.map +1 -1
- package/dist/session/npm-install-port.js +1 -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 +7 -10
- package/dist/session/rpc.d.ts +104 -20
- package/dist/session/rpc.d.ts.map +1 -1
- package/dist/session/rpc.js +184 -37
- 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 +104 -15
- package/dist/session/supervisor-rpc.d.ts.map +1 -1
- package/dist/session/supervisor-rpc.js +188 -71
- 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 +2 -0
- 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 +5 -22
- package/package.json +8 -7
- 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-7b4dd7a76d95d21e.js → node-shims-06565b121555c194.js} +2089 -861
- 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-facet-workers.mjs +114 -3
- 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,889 @@
|
|
|
1
|
+
const __vfsWriteGenerations = Object.create(null);
|
|
2
|
+
// Per-path: the authority revision the resident cell in __vfsBundle is
|
|
3
|
+
// known-good at. Only a flush of this facet's own bytes sets one, this
|
|
4
|
+
// facet's own partial mutations of a stamped cell advance it, and the
|
|
5
|
+
// ACQUIRE barrier is the only reader. An unstamped cell is simply evicted,
|
|
6
|
+
// so a mutation path that forgets to stamp costs a refetch and never a
|
|
7
|
+
// stale byte. Infinity is not a revision: it is the lease below, held
|
|
8
|
+
// while one of this facet's own mutations of the path is in flight.
|
|
9
|
+
// Heap cells only: the resident store dates each row in the row itself
|
|
10
|
+
// (__nimbusResidentRows), and this map says nothing about those.
|
|
11
|
+
const __vfsBundleRevisions = Object.create(null);
|
|
12
|
+
// Own mutations in flight per path: the stamp the first lease began with,
|
|
13
|
+
// how many are outstanding, whether a receipt has already proven a peer
|
|
14
|
+
// wrote inside the window, and the highest revision a barrier reported for
|
|
15
|
+
// the path while the lease suppressed it.
|
|
16
|
+
//
|
|
17
|
+
// Overlapping own mutations of one path are legal — chown and the chmod
|
|
18
|
+
// ride-along are not queued behind each other — so a second begin reuses
|
|
19
|
+
// the record rather than opening a second window, and an end whose receipt
|
|
20
|
+
// reports a `before` past the record's stamp marks `peer`. That verdict
|
|
21
|
+
// cannot distinguish a stranger's write from the sibling own mutation still
|
|
22
|
+
// in flight, and does not try to: it is a conservative refetch, never
|
|
23
|
+
// staleness.
|
|
24
|
+
const __vfsOwnLeases = Object.create(null);
|
|
25
|
+
// Per path with a PARKED write: the highest revision a barrier reported for
|
|
26
|
+
// it while that write was in flight. A parked cell is unstamped, so the
|
|
27
|
+
// barrier evicts it and consumes the report; the flush's own revision is
|
|
28
|
+
// what finally says whether that report was its own write coming back.
|
|
29
|
+
// Bounded by the parked set — the entry is retired with the cell below.
|
|
30
|
+
const __vfsParkedReports = Object.create(null);
|
|
31
|
+
const __vfsAppendWrites = Object.create(null);
|
|
32
|
+
const __vfsWrites = new Proxy(Object.create(null), {
|
|
33
|
+
set(target, path, value) {
|
|
34
|
+
target[path] = value;
|
|
35
|
+
delete __vfsAppendWrites[path];
|
|
36
|
+
delete __vfsParkedReports[path];
|
|
37
|
+
__vfsWriteGenerations[path] = (__vfsWriteGenerations[path] || 0) + 1;
|
|
38
|
+
// Parking a cell is the only signal a synchronous write leaves. It is
|
|
39
|
+
// therefore the one place a write-back can be scheduled from, and it
|
|
40
|
+
// covers every sync mutation — writeFileSync, appendFileSync, the fd
|
|
41
|
+
// writes, rename — with no per-call-site duplication.
|
|
42
|
+
__nimbusScheduleVfsWriteBack();
|
|
43
|
+
return true;
|
|
44
|
+
},
|
|
45
|
+
deleteProperty(target, path) {
|
|
46
|
+
delete __vfsAppendWrites[path];
|
|
47
|
+
delete __vfsParkedReports[path];
|
|
48
|
+
if (Object.prototype.hasOwnProperty.call(target, path)) {
|
|
49
|
+
delete target[path];
|
|
50
|
+
__vfsWriteGenerations[path] = (__vfsWriteGenerations[path] || 0) + 1;
|
|
51
|
+
}
|
|
52
|
+
return true;
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
const __vfsMutationTails = new Map();
|
|
57
|
+
const __nimbusPendingVfsMutations = new Set();
|
|
58
|
+
let __nimbusPendingVfsMutationFailure;
|
|
59
|
+
let __nimbusHasPendingVfsMutationFailure = false;
|
|
60
|
+
const __vfsWriteClaims = new Map();
|
|
61
|
+
// Per path: this facet's own writes and mutations the authority has been
|
|
62
|
+
// asked to apply whose acknowledgement is not adjudicated yet
|
|
63
|
+
// (__nimbusOwnAcknowledgement). Each entry settles, never rejects.
|
|
64
|
+
const __vfsOwnAcks = new Map();
|
|
65
|
+
const __nimbusVfsAppendRangeResult = {};
|
|
66
|
+
// Operation sequences reset when this generated module is evaluated again.
|
|
67
|
+
// The nonce namespaces those retries without pretending a new application
|
|
68
|
+
// request is the same logical append. Minted lazily: this source is spliced
|
|
69
|
+
// into the opencode runner's module scope, where workerd forbids global-scope
|
|
70
|
+
// RNG; the first append always runs in handler context.
|
|
71
|
+
let __nimbusVfsModuleIncarnationNonce;
|
|
72
|
+
function __nimbusVfsModuleIncarnation() {
|
|
73
|
+
return (__nimbusVfsModuleIncarnationNonce ??= crypto.randomUUID());
|
|
74
|
+
}
|
|
75
|
+
let __nimbusVfsAppendOperationSequence = 0;
|
|
76
|
+
|
|
77
|
+
function __nimbusVfsPathKey(path) {
|
|
78
|
+
return String(path).replace(/^\/+/, "");
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Write-backs in flight at once. Unbounded, a burst of ~250 left some
|
|
82
|
+
// SupervisorRPC-to-DO calls undelivered with no error, so the process never
|
|
83
|
+
// exited (measured 2026-09-22); capped at 6, none stalled.
|
|
84
|
+
const __NIMBUS_VFS_RPC_MAX_IN_FLIGHT = 6;
|
|
85
|
+
let __nimbusVfsRpcInFlight = 0;
|
|
86
|
+
const __nimbusVfsRpcWaiters = [];
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* A supervisor round trip the ledger issues on its own account.
|
|
90
|
+
*
|
|
91
|
+
* A facet's event loop exits when no handle is live, and an in-flight
|
|
92
|
+
* supervisor RPC is one of the handles it counts — but the debounced
|
|
93
|
+
* write-back runs from a raw timer, outside any call the program is awaiting,
|
|
94
|
+
* so nothing else was counting it. Measured: a template copy parked a cell,
|
|
95
|
+
* the debounce fired 10ms later and issued the write, the explicit flush
|
|
96
|
+
* behind it joined that same in-flight promise rather than starting its own,
|
|
97
|
+
* and the loop saw zero handles and ended the program with two files copied
|
|
98
|
+
* out of twenty-eight — silently, exit 0. fs.promises.cp is how
|
|
99
|
+
* create-cloudflare copies its template.
|
|
100
|
+
*
|
|
101
|
+
* The counter is the shims' __nimbusPendingOps, reached through globalThis
|
|
102
|
+
* rather than by calling their RPC helper: this source is spliced ahead of
|
|
103
|
+
* them and into embeddings that are not the one-shot entrypoint, and an
|
|
104
|
+
* absent counter must not be an error.
|
|
105
|
+
*/
|
|
106
|
+
async function __nimbusVfsRpc(issue) {
|
|
107
|
+
if (typeof globalThis.__nimbusPendingOps !== "number") globalThis.__nimbusPendingOps = 0;
|
|
108
|
+
globalThis.__nimbusPendingOps++;
|
|
109
|
+
try {
|
|
110
|
+
if (__nimbusVfsRpcInFlight < __NIMBUS_VFS_RPC_MAX_IN_FLIGHT) {
|
|
111
|
+
__nimbusVfsRpcInFlight++;
|
|
112
|
+
} else {
|
|
113
|
+
const slot = Promise.withResolvers();
|
|
114
|
+
__nimbusVfsRpcWaiters.push(slot.resolve);
|
|
115
|
+
await slot.promise;
|
|
116
|
+
}
|
|
117
|
+
try { return await issue(); }
|
|
118
|
+
finally {
|
|
119
|
+
// Hand the slot straight to the next waiter; the count only drops when none waits.
|
|
120
|
+
const next = __nimbusVfsRpcWaiters.shift();
|
|
121
|
+
if (next) next();
|
|
122
|
+
else __nimbusVfsRpcInFlight--;
|
|
123
|
+
}
|
|
124
|
+
} finally { globalThis.__nimbusPendingOps--; }
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Errno values that are the filesystem ANSWERING the syscall: the path is not
|
|
129
|
+
* there, it is a directory, the descriptor is closed. The operation did not
|
|
130
|
+
* apply, no bytes were in flight, and nothing the program believes is saved
|
|
131
|
+
* has been lost. Node hands these to the caller and lets it decide — which is
|
|
132
|
+
* why `fs.truncate(missing).catch(() => {})` is ordinary, correct code.
|
|
133
|
+
*
|
|
134
|
+
* ENOSPC is one of them: the session's storage ledger (N18) refuses a write
|
|
135
|
+
* before any of it is made.
|
|
136
|
+
*
|
|
137
|
+
* Everything else — EIO, a dropped RPC, an authority that died, an
|
|
138
|
+
* error carrying no errno at all — is not an answer. It means the outcome of
|
|
139
|
+
* a write is UNKNOWN, and that is a durability event no matter what the
|
|
140
|
+
* program caught. Unrecognised is treated as durability-class on purpose: the
|
|
141
|
+
* safe direction is to surface.
|
|
142
|
+
*/
|
|
143
|
+
const __NIMBUS_SYSCALL_VERDICT_CODES = new Set([
|
|
144
|
+
"ENOENT", "EEXIST", "EISDIR", "ENOTDIR", "ENOTEMPTY",
|
|
145
|
+
"EBADF", "EINVAL", "EPERM", "EACCES", "ELOOP", "ENAMETOOLONG", "ENOSPC",
|
|
146
|
+
]);
|
|
147
|
+
|
|
148
|
+
function __nimbusIsDurabilityFailure(error) {
|
|
149
|
+
if (error && typeof error === "object" && error.nimbusRefusedWriteBack === true) return true;
|
|
150
|
+
const code = error && typeof error === "object" ? error.code : undefined;
|
|
151
|
+
return typeof code !== "string" || !__NIMBUS_SYSCALL_VERDICT_CODES.has(code);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Everything already queued for `path` and for every proper ancestor of it,
|
|
156
|
+
* as of NOW — a snapshot, not a subscription.
|
|
157
|
+
*
|
|
158
|
+
* The queue orders mutations per path and nothing else, so `mkdirSync(a)`
|
|
159
|
+
* followed by anything under `a` — `mkdirSync(a/b)`, a flushed write of
|
|
160
|
+
* `a/f`, an `fs.promises.open(a/f, "w")` — could reach the authority
|
|
161
|
+
* ahead of the directory it lives in and be answered ENOENT for a parent
|
|
162
|
+
* the program had demonstrably created. node-tar does exactly this for
|
|
163
|
+
* every entry it extracts.
|
|
164
|
+
*
|
|
165
|
+
* Snapshotting at call time is what keeps the graph acyclic: a mutation
|
|
166
|
+
* only ever waits on mutations that were queued before it, in program
|
|
167
|
+
* order. Capturing the tails inside the mutation body instead would let a
|
|
168
|
+
* rename queued behind a slow ancestor pick up a descendant that was
|
|
169
|
+
* queued after it — and that descendant is already waiting on the rename.
|
|
170
|
+
*
|
|
171
|
+
* Map lookups only; resolves on the next tick when nothing is pending.
|
|
172
|
+
* Tails never reject, so neither does this.
|
|
173
|
+
*/
|
|
174
|
+
function __nimbusAwaitAncestorMutations(path) {
|
|
175
|
+
const pending = [];
|
|
176
|
+
let prefix = "";
|
|
177
|
+
for (const segment of __nimbusVfsPathKey(path).split("/")) {
|
|
178
|
+
if (!segment) continue;
|
|
179
|
+
prefix = prefix ? prefix + "/" + segment : segment;
|
|
180
|
+
const tail = __vfsMutationTails.get(prefix);
|
|
181
|
+
if (tail) pending.push(tail);
|
|
182
|
+
}
|
|
183
|
+
return pending.length === 0 ? Promise.resolve() : Promise.all(pending).then(() => undefined);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Everything already queued strictly BELOW `path`, as of now. The
|
|
188
|
+
* complement of the ancestor wait, for the two mutations that act on a
|
|
189
|
+
* whole subtree: rmdir needs the children gone first, and a rename must
|
|
190
|
+
* not carry the old name across while a mutation under it is still bound
|
|
191
|
+
* for the old name.
|
|
192
|
+
*/
|
|
193
|
+
function __nimbusAwaitSubtreeMutations(path) {
|
|
194
|
+
const prefix = __nimbusVfsPathKey(path) + "/";
|
|
195
|
+
const pending = [];
|
|
196
|
+
for (const [key, tail] of __vfsMutationTails) {
|
|
197
|
+
if (key.startsWith(prefix)) pending.push(tail);
|
|
198
|
+
}
|
|
199
|
+
return pending.length === 0 ? Promise.resolve() : Promise.all(pending).then(() => undefined);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Order a mutation behind the others queued for the same path.
|
|
204
|
+
*
|
|
205
|
+
* A rejection is ALSO reported to the drain when it is durability-class. That
|
|
206
|
+
* second channel is not redundancy: the tail handler below marks `result`
|
|
207
|
+
* handled, which suppresses the platform's own `unhandledrejection` signal,
|
|
208
|
+
* so retention is the only thing that can reach a durability boundary. Losing
|
|
209
|
+
* it is how a handler whose write failed still answers 200 — silent wrong
|
|
210
|
+
* data, which is what this ledger exists to prevent.
|
|
211
|
+
*
|
|
212
|
+
* What must NOT be retained is a plain syscall verdict. The queue used to
|
|
213
|
+
* retain those too, so an error the program had already caught was delivered
|
|
214
|
+
* a second time at teardown and killed the process: `opencode --help`
|
|
215
|
+
* rendered its whole help surface and then exited 1 on the
|
|
216
|
+
* `fs.truncate(logfile).catch(() => {})` in its logger init.
|
|
217
|
+
*
|
|
218
|
+
* The seam is the ERROR, not the call site. The same source line —
|
|
219
|
+
* `fs.promises.truncate(p).catch(() => {})` — must exit 0 when the file was
|
|
220
|
+
* simply absent, and must fail the response when the authority could not say
|
|
221
|
+
* whether the write landed. No per-call-site flag can express that, because
|
|
222
|
+
* both cases arrive through the same call site.
|
|
223
|
+
*/
|
|
224
|
+
function __nimbusQueueVfsMutation(path, mutation, retainFailure = true) {
|
|
225
|
+
const key = __nimbusVfsPathKey(path);
|
|
226
|
+
const previous = __vfsMutationTails.get(key) || Promise.resolve();
|
|
227
|
+
// Every queued mutation is ordered behind the structural mutations
|
|
228
|
+
// pending for its ancestors — one place, so a flushed write, an fd write
|
|
229
|
+
// and a queued mkdir all obey the same rule without each site knowing it.
|
|
230
|
+
const ancestors = __nimbusAwaitAncestorMutations(key);
|
|
231
|
+
const result = previous.then(() => ancestors).then(mutation);
|
|
232
|
+
__nimbusPendingVfsMutations.add(result);
|
|
233
|
+
// A failed mutation rejects its own caller but must not poison later writes
|
|
234
|
+
// for the same path or become an unhandled queue-cleanup rejection.
|
|
235
|
+
let tail;
|
|
236
|
+
const clearTail = () => {
|
|
237
|
+
__nimbusPendingVfsMutations.delete(result);
|
|
238
|
+
if (__vfsMutationTails.get(key) === tail) {
|
|
239
|
+
__vfsMutationTails.delete(key);
|
|
240
|
+
}
|
|
241
|
+
};
|
|
242
|
+
tail = result.then(clearTail, (error) => {
|
|
243
|
+
if (retainFailure
|
|
244
|
+
&& __nimbusIsDurabilityFailure(error)
|
|
245
|
+
&& !__nimbusHasPendingVfsMutationFailure) {
|
|
246
|
+
__nimbusHasPendingVfsMutationFailure = true;
|
|
247
|
+
__nimbusPendingVfsMutationFailure = error;
|
|
248
|
+
}
|
|
249
|
+
clearTail();
|
|
250
|
+
});
|
|
251
|
+
__vfsMutationTails.set(key, tail);
|
|
252
|
+
return result;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Run `work` as an acknowledgement in flight for `path`: one of this
|
|
257
|
+
* facet's own writes or mutations of it, from being issued to the authority
|
|
258
|
+
* through adjudicating what the authority answered — the stamp that dates
|
|
259
|
+
* the cell, or the eviction when a barrier's report outran it.
|
|
260
|
+
*
|
|
261
|
+
* Until then the facet cannot tell whether the authority has applied it, or
|
|
262
|
+
* at which revision, and a barrier that reported the path cannot tell whether
|
|
263
|
+
* that report was this very write or a peer's after it. Served, the facet's
|
|
264
|
+
* own bytes would then be read past a peer that overwrote them — beside
|
|
265
|
+
* another path the same peer wrote afterwards, read new. So no resumption
|
|
266
|
+
* runs while such a report is outstanding against one of these
|
|
267
|
+
* (__nimbusReportedOwnAcknowledgements). A write that is only parked is not
|
|
268
|
+
* in flight: it has not reached the authority, cannot have been applied
|
|
269
|
+
* before any report, and will be applied after every one it has missed.
|
|
270
|
+
*
|
|
271
|
+
* `generation` is the parked cell a whole-file write-back carries
|
|
272
|
+
* (__vfsWriteGenerations): once a newer one is parked over it, that newer
|
|
273
|
+
* cell is what the facet serves, and this acknowledgement no longer says
|
|
274
|
+
* anything about the bytes a resumption would read.
|
|
275
|
+
*/
|
|
276
|
+
function __nimbusOwnAcknowledgement(path, work, generation) {
|
|
277
|
+
const key = __nimbusVfsPathKey(path);
|
|
278
|
+
const acked = work();
|
|
279
|
+
const ack = { settled: acked.then(() => undefined, () => undefined), generation };
|
|
280
|
+
let held = __vfsOwnAcks.get(key);
|
|
281
|
+
if (!held) {
|
|
282
|
+
held = new Set();
|
|
283
|
+
__vfsOwnAcks.set(key, held);
|
|
284
|
+
}
|
|
285
|
+
held.add(ack);
|
|
286
|
+
ack.settled.then(() => {
|
|
287
|
+
held.delete(ack);
|
|
288
|
+
if (held.size === 0 && __vfsOwnAcks.get(key) === held) __vfsOwnAcks.delete(key);
|
|
289
|
+
});
|
|
290
|
+
return acked;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* The own acknowledgements in flight for every path that carries a report
|
|
295
|
+
* noted while its write or mutation was out (__nimbusNoteVfsReport) — by the
|
|
296
|
+
* barrier asking or by any before it — as of NOW, settling together; null
|
|
297
|
+
* when there are none. A barrier waits on these before its resumption runs.
|
|
298
|
+
*
|
|
299
|
+
* The reports an answer names are noted before it is admitted, so they are
|
|
300
|
+
* among them. So is one an earlier barrier noted and is still waiting on:
|
|
301
|
+
* that barrier moved the cursor past it, and an answer asked for from there
|
|
302
|
+
* does not name it again, but the own bytes are no fresher for that.
|
|
303
|
+
* Once an acknowledgement lands, its adjudication has decided — a report at
|
|
304
|
+
* or below its revision was this facet's own write coming back and the cell
|
|
305
|
+
* is dated; one above it was a peer writing after, and the cell is evicted
|
|
306
|
+
* and owed a refetch — and the report is retired with it.
|
|
307
|
+
*
|
|
308
|
+
* A write-back whose cell has been superseded is left out: a newer write of
|
|
309
|
+
* the path is parked over it, and no mutation of the path is out beside it.
|
|
310
|
+
* The facet serves that newer cell, which is only parked, so it will be
|
|
311
|
+
* applied above every report made so far. Waiting on the older one would
|
|
312
|
+
* stall a program that writes and yields in a loop on each of its own writes
|
|
313
|
+
* coming back, for nothing it reads.
|
|
314
|
+
*/
|
|
315
|
+
function __nimbusReportedOwnAcknowledgements() {
|
|
316
|
+
const pending = [];
|
|
317
|
+
for (const [key, held] of __vfsOwnAcks) {
|
|
318
|
+
const lease = __vfsOwnLeases[key];
|
|
319
|
+
if (__vfsParkedReports[key] === undefined && !(lease && lease.reported !== -1)) continue;
|
|
320
|
+
const parkedOver = lease === undefined && Object.prototype.hasOwnProperty.call(__vfsWrites, key);
|
|
321
|
+
for (const ack of held) {
|
|
322
|
+
if (parkedOver && ack.generation !== undefined && ack.generation !== __vfsWriteGenerations[key]) continue;
|
|
323
|
+
pending.push(ack.settled);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
return pending.length === 0 ? null : Promise.all(pending);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Note a report no answer could name, on every path with an own
|
|
331
|
+
* acknowledgement in flight. A poison, a barrier with no answer, a repair
|
|
332
|
+
* that vouched for nothing: each moves on without saying what changed, so
|
|
333
|
+
* each of those writes is adjudicated as though a peer wrote after it — a
|
|
334
|
+
* refetch, never a stale byte.
|
|
335
|
+
*/
|
|
336
|
+
function __nimbusNoteUnnamedReports() {
|
|
337
|
+
for (const key of __vfsOwnAcks.keys()) __nimbusNoteVfsReport(key, Infinity);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
async function __nimbusDrainVfsMutations() {
|
|
341
|
+
while (__nimbusPendingVfsMutations.size > 0) {
|
|
342
|
+
await Promise.allSettled([...__nimbusPendingVfsMutations]);
|
|
343
|
+
}
|
|
344
|
+
if (__nimbusHasPendingVfsMutationFailure) {
|
|
345
|
+
const failure = __nimbusPendingVfsMutationFailure;
|
|
346
|
+
__nimbusHasPendingVfsMutationFailure = false;
|
|
347
|
+
__nimbusPendingVfsMutationFailure = undefined;
|
|
348
|
+
throw failure;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function __nimbusCapturePendingVfsAppend(path) {
|
|
353
|
+
const key = __nimbusVfsPathKey(path);
|
|
354
|
+
const append = __vfsAppendWrites[key];
|
|
355
|
+
return append && append.generation === __vfsWriteGenerations[key]
|
|
356
|
+
? append
|
|
357
|
+
: null;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function __nimbusConcatVfsBytes(left, right) {
|
|
361
|
+
const bytes = new Uint8Array(left.byteLength + right.byteLength);
|
|
362
|
+
bytes.set(left, 0);
|
|
363
|
+
bytes.set(right, left.byteLength);
|
|
364
|
+
return bytes;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function __nimbusRecordVfsAppend(path, delta, fragment, previous) {
|
|
368
|
+
const key = __nimbusVfsPathKey(path);
|
|
369
|
+
const chain = previous ? previous.chain : { pending: [] };
|
|
370
|
+
let operation;
|
|
371
|
+
if (previous &&
|
|
372
|
+
!previous.claimed &&
|
|
373
|
+
!chain.pending.includes(previous.operation)) {
|
|
374
|
+
operation = previous.operation;
|
|
375
|
+
operation.bytes = __nimbusConcatVfsBytes(operation.bytes, delta);
|
|
376
|
+
} else {
|
|
377
|
+
operation = {
|
|
378
|
+
id: String(++__nimbusVfsAppendOperationSequence),
|
|
379
|
+
bytes: delta.slice(),
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
__vfsAppendWrites[key] = {
|
|
383
|
+
generation: __vfsWriteGenerations[key],
|
|
384
|
+
fragment,
|
|
385
|
+
chain,
|
|
386
|
+
operation,
|
|
387
|
+
claimed: false,
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function __nimbusCaptureVfsWrite(path) {
|
|
392
|
+
const key = __nimbusVfsPathKey(path);
|
|
393
|
+
if (!Object.prototype.hasOwnProperty.call(__vfsWrites, key)) return null;
|
|
394
|
+
return {
|
|
395
|
+
key,
|
|
396
|
+
content: __vfsWrites[key],
|
|
397
|
+
generation: __vfsWriteGenerations[key],
|
|
398
|
+
append: __nimbusCapturePendingVfsAppend(key),
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
function __nimbusVfsAppendOperations(snapshot) {
|
|
403
|
+
const operations = snapshot.append.chain.pending.slice();
|
|
404
|
+
if (!operations.includes(snapshot.append.operation)) {
|
|
405
|
+
operations.push(snapshot.append.operation);
|
|
406
|
+
}
|
|
407
|
+
return operations;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
function __nimbusBeginVfsAppendOperation(snapshot, operation) {
|
|
411
|
+
if (!snapshot.append.chain.pending.includes(operation)) {
|
|
412
|
+
snapshot.append.chain.pending.push(operation);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function __nimbusCommitVfsAppendOperation(snapshot, operation) {
|
|
417
|
+
const index = snapshot.append.chain.pending.indexOf(operation);
|
|
418
|
+
if (index !== -1) snapshot.append.chain.pending.splice(index, 1);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
function __nimbusUnsupportedVfsAppend(path) {
|
|
422
|
+
const error = new Error(
|
|
423
|
+
"ENOSYS: preserving a nonresident append requires fsAppend and fsAppendAck: " + path,
|
|
424
|
+
);
|
|
425
|
+
error.code = "ENOSYS";
|
|
426
|
+
return error;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* The authority refused a parked write outright (a syscall verdict:
|
|
431
|
+
* EACCES, EPERM, EISDIR...). The bytes are not the file's and never will
|
|
432
|
+
* be, so the process stops serving them: the parked cell of that generation
|
|
433
|
+
* and any resident copy go, and the shims forget what they recorded of the
|
|
434
|
+
* path, so the next read asks the authority. When no caller will see the
|
|
435
|
+
* rejection (`unseen`: a sync write carried across later), the refusal is
|
|
436
|
+
* retained and reported at exit; an async writer gets it as its verdict.
|
|
437
|
+
*/
|
|
438
|
+
function __nimbusRefuseParkedWrite(snapshot, error, unseen) {
|
|
439
|
+
if (__vfsWriteGenerations[snapshot.key] === snapshot.generation) {
|
|
440
|
+
delete __vfsWrites[snapshot.key];
|
|
441
|
+
if (typeof __vfsBundle !== "undefined" && __vfsBundle) delete __vfsBundle[snapshot.key];
|
|
442
|
+
const refused = globalThis.__nimbusVfsWriteRefused;
|
|
443
|
+
if (typeof refused === "function") refused(snapshot.key);
|
|
444
|
+
}
|
|
445
|
+
if (unseen && error && typeof error === "object") {
|
|
446
|
+
try { error.nimbusRefusedWriteBack = true; } catch {}
|
|
447
|
+
}
|
|
448
|
+
return error;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
function __nimbusRunVfsWriteMutation(snapshot, mutation, retainFailure, unseen) {
|
|
452
|
+
return __nimbusQueueVfsMutation(snapshot.key, () => __nimbusOwnAcknowledgement(snapshot.key, async () => {
|
|
453
|
+
let value;
|
|
454
|
+
try {
|
|
455
|
+
value = await mutation(snapshot.content, snapshot);
|
|
456
|
+
} catch (error) {
|
|
457
|
+
// A verdict is the authority's answer; anything else (a lost RPC) may
|
|
458
|
+
// have landed, and the parked bytes stay until that is known.
|
|
459
|
+
if (!__nimbusIsDurabilityFailure(error)) throw __nimbusRefuseParkedWrite(snapshot, error, unseen);
|
|
460
|
+
throw error;
|
|
461
|
+
}
|
|
462
|
+
if (__vfsWriteGenerations[snapshot.key] === snapshot.generation) {
|
|
463
|
+
// What the barriers reported for this path while the write was in
|
|
464
|
+
// flight. Read before the parked cell is retired below, which drops it.
|
|
465
|
+
const reported = __vfsParkedReports[snapshot.key];
|
|
466
|
+
if (snapshot.append &&
|
|
467
|
+
value === __nimbusVfsAppendRangeResult &&
|
|
468
|
+
typeof __vfsBundle !== "undefined" &&
|
|
469
|
+
__vfsBundle) {
|
|
470
|
+
delete __vfsBundle[snapshot.key];
|
|
471
|
+
} else if (typeof value === "number" &&
|
|
472
|
+
typeof __vfsBundle !== "undefined" &&
|
|
473
|
+
__vfsBundle &&
|
|
474
|
+
!(snapshot.key in __vfsBundle) &&
|
|
475
|
+
!(reported > value)) {
|
|
476
|
+
// The barrier that evicted the resident copy was answered AHEAD of
|
|
477
|
+
// this very write (see __nimbusBeginOwnMutation for why that
|
|
478
|
+
// ordering is ordinary): it reported the path at the revision this
|
|
479
|
+
// write produced, which is the revision about to be stamped, and
|
|
480
|
+
// the bytes in hand are the authority's content AT that revision.
|
|
481
|
+
// So reinstall them rather than let the identity check below
|
|
482
|
+
// decline and leave the facet holding nothing — a sync read of a
|
|
483
|
+
// file the program just wrote whole would fail EAGAIN.
|
|
484
|
+
//
|
|
485
|
+
// A peer's earlier write was overwritten by this whole-file write.
|
|
486
|
+
// A peer's LATER one is reported ABOVE this revision, and the guard
|
|
487
|
+
// above is that test: reported later, the bytes in hand are not
|
|
488
|
+
// what the authority serves and the eviction stands — a refetch,
|
|
489
|
+
// never a stale byte. The parked cell is dropped right after, so a
|
|
490
|
+
// reinstalled copy is the one the sync view serves.
|
|
491
|
+
__vfsBundle[snapshot.key] = snapshot.content;
|
|
492
|
+
}
|
|
493
|
+
delete __vfsWrites[snapshot.key];
|
|
494
|
+
__nimbusStampFlushedCell(snapshot, value, reported);
|
|
495
|
+
}
|
|
496
|
+
return value;
|
|
497
|
+
}, snapshot.generation), retainFailure);
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
/**
|
|
501
|
+
* Whether this facet's resident set is the SQLite store
|
|
502
|
+
* (vfs/facet-resident-store.ts), which the resident node body splices ahead
|
|
503
|
+
* of this ledger. Its rows carry their own revision, so a stamp is written
|
|
504
|
+
* into the row, and __vfsBundleRevisions — which describes heap cells — says
|
|
505
|
+
* nothing about them.
|
|
506
|
+
*/
|
|
507
|
+
function __nimbusResidentRows() {
|
|
508
|
+
return typeof __residentStamp === "function"
|
|
509
|
+
&& typeof __residentReady !== "undefined"
|
|
510
|
+
&& __residentReady === true;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
/**
|
|
514
|
+
* Record the authority revision a just-flushed cell is known-good at.
|
|
515
|
+
*
|
|
516
|
+
* The barrier reports back every path mutated since the facet's cursor,
|
|
517
|
+
* which includes the facet's OWN writes — the invalidation log has no way
|
|
518
|
+
* to know who caused an entry. Without a stamp the facet drops the cells it
|
|
519
|
+
* authored the instant it flushes them, and a resumption then refetches
|
|
520
|
+
* bytes it is already holding: a self-inflicted cold cache on exactly the
|
|
521
|
+
* files a scaffolder or a build just wrote.
|
|
522
|
+
*
|
|
523
|
+
* A "skip paths I wrote" rule would be unsound — a peer may write the same
|
|
524
|
+
* path after us, and that invalidation is real. The revision separates
|
|
525
|
+
* them: a report AT our revision is our own write coming back, a report
|
|
526
|
+
* ABOVE it is somebody else's and still evicts.
|
|
527
|
+
*
|
|
528
|
+
* Guarded on cell identity rather than on the flush alone. A read that
|
|
529
|
+
* raced the flush may have refilled the cell from an older revision, and
|
|
530
|
+
* stamping that with our newer one would pin a stale byte — the one
|
|
531
|
+
* outcome this whole protocol exists to prevent.
|
|
532
|
+
*
|
|
533
|
+
* Only the written path is stamped, never its parent — deliberately, and it
|
|
534
|
+
* is why a batch of writes still costs ONE invalidation rather than none.
|
|
535
|
+
* Every mutation reports its parent directory too, so stamping the parent
|
|
536
|
+
* here looks like the obvious way to reach zero. It is not: the same
|
|
537
|
+
* revision on the directory would also vouch for a peer's earlier change to
|
|
538
|
+
* the DIRECTORY ITSELF — a chmod at a revision this facet never acquired —
|
|
539
|
+
* and that stale mode would then survive the barrier. The write knows what
|
|
540
|
+
* it did to the file and nothing about the directory. Do not "finish" this
|
|
541
|
+
* by stamping the parent.
|
|
542
|
+
*/
|
|
543
|
+
function __nimbusStampFlushedCell(snapshot, revision, reported) {
|
|
544
|
+
if (typeof revision !== "number") return;
|
|
545
|
+
if (typeof __vfsBundle === "undefined" || !__vfsBundle) return;
|
|
546
|
+
if (__nimbusResidentRows()) {
|
|
547
|
+
// The row held this write's bytes as the store's own-write revision,
|
|
548
|
+
// which no barrier evicts, so every barrier inside the window KEPT it and
|
|
549
|
+
// consumed its report. Those reports are judged here, against the
|
|
550
|
+
// revision this write produced: at or below it is this write coming
|
|
551
|
+
// back, or a write it overwrote; above it, a peer wrote after, and the
|
|
552
|
+
// eviction those barriers could not perform is owed now. The store dates
|
|
553
|
+
// only a row still holding own bytes, which is the identity guard below
|
|
554
|
+
// in the form rows allow: a fill never replaces own bytes, and a later
|
|
555
|
+
// write of the path fails the generation test before this is reached.
|
|
556
|
+
if (reported > revision) {
|
|
557
|
+
__nimbusEvictLeasedCell(snapshot.key);
|
|
558
|
+
return;
|
|
559
|
+
}
|
|
560
|
+
__residentStamp(snapshot.key, revision);
|
|
561
|
+
return;
|
|
562
|
+
}
|
|
563
|
+
if (__vfsBundle[snapshot.key] !== snapshot.content) return;
|
|
564
|
+
__vfsBundleRevisions[snapshot.key] = revision;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
/**
|
|
568
|
+
* Take an own-mutation lease on a held cell.
|
|
569
|
+
*
|
|
570
|
+
* One of the facet's OWN partial mutations (ranged write, truncate, utimes,
|
|
571
|
+
* chmod, chown) bumps the path's revision exactly as a flush does, and the
|
|
572
|
+
* ACQUIRE barrier that reports it back cannot tell who caused it. Stamping
|
|
573
|
+
* once the RPC answers is not enough, because the two are CONCURRENT: the
|
|
574
|
+
* supervisor answers `fsAcquire` from memory at once, while a write's
|
|
575
|
+
* response waits on the Durable Object's output gate for durability. So a
|
|
576
|
+
* barrier issued AFTER this facet's own write can be ANSWERED BEFORE that
|
|
577
|
+
* write's response arrives. Its delta lists the path at the write's new
|
|
578
|
+
* revision, the facet's stamp is still the old one, the cell the facet is
|
|
579
|
+
* about to overlay is evicted out from under it, and the next
|
|
580
|
+
* `readFileSync` fails EAGAIN on bytes the program wrote itself.
|
|
581
|
+
* Intermittent, and it is what create-astro shows on staging — the README
|
|
582
|
+
* its extract wrote, read synchronously right after.
|
|
583
|
+
*
|
|
584
|
+
* So the cell is HELD for the whole window instead, and the receipt decides
|
|
585
|
+
* at the end (`__nimbusEndOwnMutation`). `Infinity` is what the barrier's
|
|
586
|
+
* `stamp >= entry.rev` reads as "mine" for every revision it could report
|
|
587
|
+
* while the RPC is in flight.
|
|
588
|
+
*
|
|
589
|
+
* An UNSTAMPED cell is not leased: it keeps today's evict-and-refetch, so a
|
|
590
|
+
* mutation path that never stamps still costs a refetch and never a stale
|
|
591
|
+
* byte. `false` says no lease was taken and the end is a no-op.
|
|
592
|
+
*
|
|
593
|
+
* In the resident store the row is the stamp, so the store holds the row as
|
|
594
|
+
* own bytes for the window (`__residentLease`, which the barrier keeps the
|
|
595
|
+
* way it keeps an Infinity stamp) and hands back the revision it was dated
|
|
596
|
+
* at; the lease record carries that revision exactly as it does here.
|
|
597
|
+
*/
|
|
598
|
+
function __nimbusBeginOwnMutation(key) {
|
|
599
|
+
if (__nimbusResidentRows()) {
|
|
600
|
+
const open = __vfsOwnLeases[key];
|
|
601
|
+
if (open) { open.pending++; return true; }
|
|
602
|
+
const dated = __residentLease(key);
|
|
603
|
+
if (dated === undefined) return false;
|
|
604
|
+
__vfsOwnLeases[key] = { stamp: dated, pending: 1, peer: false, reported: -1 };
|
|
605
|
+
return true;
|
|
606
|
+
}
|
|
607
|
+
const stamp = __vfsBundleRevisions[key];
|
|
608
|
+
if (stamp === undefined) return false;
|
|
609
|
+
const lease = __vfsOwnLeases[key]
|
|
610
|
+
|| (__vfsOwnLeases[key] = { stamp, pending: 0, peer: false, reported: -1 });
|
|
611
|
+
lease.pending++;
|
|
612
|
+
__vfsBundleRevisions[key] = Infinity;
|
|
613
|
+
return true;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
/**
|
|
617
|
+
* Remember a revision the ACQUIRE barrier has just reported for a path one
|
|
618
|
+
* of this facet's own writes is in flight for. Called for every reported
|
|
619
|
+
* path; a no-op for the ones nothing of ours is touching.
|
|
620
|
+
*
|
|
621
|
+
* A barrier answered inside that window is the whole problem this file is
|
|
622
|
+
* solving, and its report is CONSUMED — the cursor advances past it and
|
|
623
|
+
* nothing will ever raise that revision again. Whether it was this facet's
|
|
624
|
+
* own write coming back or a peer's cannot be decided then, so the number
|
|
625
|
+
* is kept until the write's own revision arrives and can decide it:
|
|
626
|
+
*
|
|
627
|
+
* - a leased cell had the report SUPPRESSED (the stamp reads Infinity for
|
|
628
|
+
* every revision), so `__nimbusEndOwnMutation` applies the barrier's own
|
|
629
|
+
* test against the stamp the receipt settles on.
|
|
630
|
+
* - a PARKED cell is unstamped, so the barrier evicted it legitimately;
|
|
631
|
+
* `__nimbusRunVfsWriteMutation` uses this to tell a report of its own
|
|
632
|
+
* write (put the bytes back) from a peer's later one (leave them gone).
|
|
633
|
+
*
|
|
634
|
+
* Neither can be answered by the receipt alone: it is read in the
|
|
635
|
+
* mutation's own turn, so a peer writing after the mutation landed and
|
|
636
|
+
* before its response arrived is invisible to it.
|
|
637
|
+
*/
|
|
638
|
+
function __nimbusNoteVfsReport(key, revision) {
|
|
639
|
+
const lease = __vfsOwnLeases[key];
|
|
640
|
+
if (lease && revision > lease.reported) lease.reported = revision;
|
|
641
|
+
if (Object.prototype.hasOwnProperty.call(__vfsWrites, key) &&
|
|
642
|
+
!(__vfsParkedReports[key] >= revision)) {
|
|
643
|
+
__vfsParkedReports[key] = revision;
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
/**
|
|
648
|
+
* `__nimbusNoteVfsReport` for a report that covers a subtree: a
|
|
649
|
+
* subtree-scoped or structural delta entry, which stands for changes at or
|
|
650
|
+
* under `prefix` that it does not name. Every path there that one of this
|
|
651
|
+
* facet's own writes or mutations is in flight for gets the report.
|
|
652
|
+
*/
|
|
653
|
+
function __nimbusNoteVfsReportUnder(prefix, revision) {
|
|
654
|
+
const under = prefix + "/";
|
|
655
|
+
const keys = new Set([...Object.keys(__vfsOwnLeases), ...Object.keys(__vfsWrites)]);
|
|
656
|
+
for (const key of keys) {
|
|
657
|
+
if (key === prefix || key.startsWith(under)) __nimbusNoteVfsReport(key, revision);
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
/**
|
|
662
|
+
* End one own-mutation lease, and settle the stamp when it was the last.
|
|
663
|
+
*
|
|
664
|
+
* The receipt carries the path's revision on either side of the mutation,
|
|
665
|
+
* both read in the RPC's own synchronous turn:
|
|
666
|
+
*
|
|
667
|
+
* - `before` at or below the stamp the lease began with — nobody else
|
|
668
|
+
* touched the path in the window, so the cell with the local effect
|
|
669
|
+
* applied IS what the authority serves at `after`, and the stamp
|
|
670
|
+
* advances there.
|
|
671
|
+
* - `before` past it — a peer wrote inside the window, and the barrier
|
|
672
|
+
* that would have evicted was suppressed by this lease. The end owes
|
|
673
|
+
* that eviction, so it performs it.
|
|
674
|
+
* - no receipt at all (the RPC threw) — the outcome is unknown, which is
|
|
675
|
+
* handled exactly as a peer's write is.
|
|
676
|
+
*
|
|
677
|
+
* Then every report the lease suppressed is adjudicated against the stamp
|
|
678
|
+
* it settled on: a report ABOVE it is a mutation this receipt does not
|
|
679
|
+
* account for, so the eviction the barrier did not perform is performed
|
|
680
|
+
* here (see `__nimbusNoteVfsReport`).
|
|
681
|
+
*
|
|
682
|
+
* A cell that stopped being held during the window (a poison drop, a
|
|
683
|
+
* parked sync write retiring the stamp) has nothing left to vouch for, so
|
|
684
|
+
* no stamp is restored for it. In the resident store a write parked over
|
|
685
|
+
* the row inside the window owns it from then on, and its flush dates it.
|
|
686
|
+
*/
|
|
687
|
+
function __nimbusEndOwnMutation(key, held, receipt) {
|
|
688
|
+
if (!held) return;
|
|
689
|
+
const lease = __vfsOwnLeases[key];
|
|
690
|
+
if (!lease) return;
|
|
691
|
+
lease.pending--;
|
|
692
|
+
if (receipt && typeof receipt.before === "number" && typeof receipt.after === "number") {
|
|
693
|
+
if (lease.stamp >= receipt.before) lease.stamp = receipt.after;
|
|
694
|
+
else lease.peer = true;
|
|
695
|
+
} else {
|
|
696
|
+
lease.peer = true;
|
|
697
|
+
}
|
|
698
|
+
if (lease.pending > 0) return;
|
|
699
|
+
delete __vfsOwnLeases[key];
|
|
700
|
+
const evict = lease.peer || lease.reported > lease.stamp;
|
|
701
|
+
if (__nimbusResidentRows()) {
|
|
702
|
+
if (evict) __nimbusEvictLeasedCell(key);
|
|
703
|
+
else if (!Object.prototype.hasOwnProperty.call(__vfsWrites, key)) __residentStamp(key, lease.stamp);
|
|
704
|
+
return;
|
|
705
|
+
}
|
|
706
|
+
const resident = typeof __vfsBundle !== "undefined" && __vfsBundle && key in __vfsBundle;
|
|
707
|
+
if (evict || !resident) {
|
|
708
|
+
delete __vfsBundleRevisions[key];
|
|
709
|
+
if (evict) __nimbusEvictLeasedCell(key);
|
|
710
|
+
return;
|
|
711
|
+
}
|
|
712
|
+
__vfsBundleRevisions[key] = lease.stamp;
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
/**
|
|
716
|
+
* Drop a cell the way the shims' own invalidation does.
|
|
717
|
+
*
|
|
718
|
+
* Content view, stat view and the invalidation count move together in the
|
|
719
|
+
* shims' `_evictResident`, and this source is spliced AHEAD of that
|
|
720
|
+
* closure, so the lease asks through the hook it publishes rather than
|
|
721
|
+
* keeping a second eviction path in step with it. A ledger embedded without
|
|
722
|
+
* the shims has no stat view to keep coherent, and the content view is then
|
|
723
|
+
* all there is to drop.
|
|
724
|
+
*/
|
|
725
|
+
function __nimbusEvictLeasedCell(key) {
|
|
726
|
+
const evict = globalThis.__nimbusEvictResidentCell;
|
|
727
|
+
if (typeof evict === "function") { evict(key); return; }
|
|
728
|
+
if (typeof __vfsBundle !== "undefined" && __vfsBundle) delete __vfsBundle[key];
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
/** `unseen`: no caller awaits this flush, so a refusal must be retained to be heard. */
|
|
732
|
+
function __nimbusFlushVfsWrite(path, mutation, retainFailure = true, unseen = false) {
|
|
733
|
+
const snapshot = __nimbusCaptureVfsWrite(path);
|
|
734
|
+
if (!snapshot) return Promise.resolve(undefined);
|
|
735
|
+
const existing = __vfsWriteClaims.get(snapshot.key);
|
|
736
|
+
if (existing && existing.generation === snapshot.generation) {
|
|
737
|
+
return existing.promise;
|
|
738
|
+
}
|
|
739
|
+
if (snapshot.append) snapshot.append.claimed = true;
|
|
740
|
+
const result = __nimbusRunVfsWriteMutation(snapshot, mutation, retainFailure, unseen);
|
|
741
|
+
const claim = { generation: snapshot.generation, promise: result };
|
|
742
|
+
__vfsWriteClaims.set(snapshot.key, claim);
|
|
743
|
+
const release = () => {
|
|
744
|
+
if (__vfsWriteClaims.get(snapshot.key) === claim) {
|
|
745
|
+
__vfsWriteClaims.delete(snapshot.key);
|
|
746
|
+
}
|
|
747
|
+
};
|
|
748
|
+
result.then(() => {
|
|
749
|
+
release();
|
|
750
|
+
// The authority accepted the bytes: the shims learn what it made of the
|
|
751
|
+
// path (owner, mode) for their sync view.
|
|
752
|
+
const landed = globalThis.__nimbusVfsWriteLanded;
|
|
753
|
+
if (typeof landed === "function") landed(snapshot.key);
|
|
754
|
+
}, () => {
|
|
755
|
+
release();
|
|
756
|
+
if (snapshot.append &&
|
|
757
|
+
__vfsAppendWrites[snapshot.key] === snapshot.append) {
|
|
758
|
+
snapshot.append.claimed = false;
|
|
759
|
+
}
|
|
760
|
+
});
|
|
761
|
+
return result;
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
async function __nimbusPersistVfsWrite(supervisor, path, content, snapshot) {
|
|
765
|
+
if (snapshot.append) {
|
|
766
|
+
if (typeof supervisor.fsAppend !== "function" ||
|
|
767
|
+
typeof supervisor.fsAppendAck !== "function") {
|
|
768
|
+
throw __nimbusUnsupportedVfsAppend(path);
|
|
769
|
+
}
|
|
770
|
+
for (const operation of __nimbusVfsAppendOperations(snapshot)) {
|
|
771
|
+
__nimbusBeginVfsAppendOperation(snapshot, operation);
|
|
772
|
+
await __nimbusVfsRpc(() => supervisor.fsAppend(
|
|
773
|
+
path,
|
|
774
|
+
__nimbusVfsModuleIncarnation(),
|
|
775
|
+
operation.id,
|
|
776
|
+
operation.bytes,
|
|
777
|
+
));
|
|
778
|
+
__nimbusCommitVfsAppendOperation(snapshot, operation);
|
|
779
|
+
try {
|
|
780
|
+
await __nimbusVfsRpc(() => supervisor.fsAppendAck(__nimbusVfsModuleIncarnation(), operation.id));
|
|
781
|
+
} catch {
|
|
782
|
+
// The client has already relinquished retry ownership after the
|
|
783
|
+
// append success. A lost acknowledgement may retain a receipt, but
|
|
784
|
+
// must never turn a committed append into a failed/retried write.
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
return __nimbusVfsAppendRangeResult;
|
|
788
|
+
}
|
|
789
|
+
// The revision this write produced. It is what lets the ACQUIRE barrier
|
|
790
|
+
// tell this facet's own mutation apart from a peer's.
|
|
791
|
+
return __nimbusVfsRpc(() => supervisor.writeFile(path, content));
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
/**
|
|
795
|
+
* Write back the cells parked at THIS instant, and only those.
|
|
796
|
+
*
|
|
797
|
+
* Bounded on purpose, and deliberately not routed through
|
|
798
|
+
* `__nimbusDrainVfsWrites`, whose `while (pending > 0)` waits for the mutation
|
|
799
|
+
* queue to be EMPTY. That wait is correct at process exit, where no new writes
|
|
800
|
+
* are coming. Anywhere else it is a livelock: a facet unpacking a tarball adds
|
|
801
|
+
* mutations faster than the loop retires them, so the loop never returns.
|
|
802
|
+
* Sited ahead of egress — where it was — that stopped the request from ever
|
|
803
|
+
* leaving the facet, and `npx sv create` ran, printed its intro, and then
|
|
804
|
+
* never reported an exit at all. A barrier may delay a request; it may not
|
|
805
|
+
* wait on a condition a busy process never reaches.
|
|
806
|
+
*
|
|
807
|
+
* Failures are retained rather than thrown. The two callers — the debounce
|
|
808
|
+
* below, and the RELEASE barrier ahead of egress — have no frame that could
|
|
809
|
+
* act on one: rejecting the fetch that happened to trigger the flush would
|
|
810
|
+
* blame the wrong operation. The exit drain reports what is retained, so a
|
|
811
|
+
* lost write is loud exactly once and never silent.
|
|
812
|
+
*/
|
|
813
|
+
async function __nimbusFlushVfsWriteBack(supervisor) {
|
|
814
|
+
if (!supervisor) return;
|
|
815
|
+
const paths = Object.keys(__vfsWrites);
|
|
816
|
+
if (paths.length === 0) return;
|
|
817
|
+
await Promise.allSettled(paths.map((path) => __nimbusFlushVfsWrite(
|
|
818
|
+
path,
|
|
819
|
+
(content, snapshot) => __nimbusPersistVfsWrite(supervisor, path, content, snapshot),
|
|
820
|
+
true,
|
|
821
|
+
true,
|
|
822
|
+
)));
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
/**
|
|
826
|
+
* A synchronous write can only park bytes in `__vfsWrites`: a sync syscall
|
|
827
|
+
* has no channel to the authority. Something else therefore has to carry
|
|
828
|
+
* them across, and the only thing that did was the drain at process exit —
|
|
829
|
+
* so a resident server that writes synchronously never wrote back at all,
|
|
830
|
+
* and a peer reading the same path got the pre-write bytes for the whole
|
|
831
|
+
* life of the process. Measured, not theorised: `writeFileSync` then 50 ms
|
|
832
|
+
* left the authority at null with zero write RPCs issued.
|
|
833
|
+
*
|
|
834
|
+
* Flushing on every write is not the repair — 500 sync writes would become
|
|
835
|
+
* 500 round trips, and an npm install writes thousands. Debounce instead:
|
|
836
|
+
* parking a cell schedules one write-back, and every write that lands before
|
|
837
|
+
* it fires joins that same batch. Steady state costs no more round trips
|
|
838
|
+
* than the exit drain already paid; what changes is when they happen.
|
|
839
|
+
*
|
|
840
|
+
* The timer is the raw platform one, captured before the shims wrap
|
|
841
|
+
* `setTimeout` with the VFS resumption barrier: a write-back is the shim's
|
|
842
|
+
* own infrastructure, not a user resumption, and must not pay an ACQUIRE to
|
|
843
|
+
* deliver an ACQUIRE.
|
|
844
|
+
*/
|
|
845
|
+
const __NIMBUS_VFS_WRITE_BACK_DELAY_MS = 10;
|
|
846
|
+
const __nimbusRawTimer = globalThis.setTimeout;
|
|
847
|
+
let __nimbusVfsWriteBackTimer = null;
|
|
848
|
+
function __nimbusScheduleVfsWriteBack() {
|
|
849
|
+
if (__nimbusVfsWriteBackTimer !== null) return;
|
|
850
|
+
if (typeof __nimbusRawTimer !== 'function') return;
|
|
851
|
+
__nimbusVfsWriteBackTimer = __nimbusRawTimer(() => {
|
|
852
|
+
__nimbusVfsWriteBackTimer = null;
|
|
853
|
+
const supervisor = typeof __supervisor !== 'undefined' ? __supervisor : null;
|
|
854
|
+
if (!supervisor) return;
|
|
855
|
+
// Not registered in __nimbusPendingVfsMutations: each write it starts
|
|
856
|
+
// registers itself there through __nimbusQueueVfsMutation, so the exit
|
|
857
|
+
// drain already awaits the work. Registering the orchestration too would
|
|
858
|
+
// add an entry nothing ever removes, and that set is drained by a
|
|
859
|
+
// while-loop on its size.
|
|
860
|
+
void __nimbusFlushVfsWriteBack(supervisor);
|
|
861
|
+
}, __NIMBUS_VFS_WRITE_BACK_DELAY_MS);
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
async function __nimbusDrainVfsWrites(supervisor) {
|
|
865
|
+
const paths = Object.keys(__vfsWrites);
|
|
866
|
+
const outcomes = await Promise.allSettled([
|
|
867
|
+
...paths.map(async (path) => {
|
|
868
|
+
const persist = () => __nimbusFlushVfsWrite(
|
|
869
|
+
path,
|
|
870
|
+
(content, snapshot) =>
|
|
871
|
+
__nimbusPersistVfsWrite(supervisor, path, content, snapshot),
|
|
872
|
+
false,
|
|
873
|
+
true,
|
|
874
|
+
);
|
|
875
|
+
try {
|
|
876
|
+
await persist();
|
|
877
|
+
} catch (error) {
|
|
878
|
+
// The authority may have committed before the RPC response was lost.
|
|
879
|
+
// One retry is safe: full writes are idempotent, while appends retain
|
|
880
|
+
// the same module/operation identity and are deduplicated by authority.
|
|
881
|
+
if (error && typeof error.code === "string") throw error;
|
|
882
|
+
await persist();
|
|
883
|
+
}
|
|
884
|
+
}),
|
|
885
|
+
__nimbusDrainVfsMutations(),
|
|
886
|
+
]);
|
|
887
|
+
const failure = outcomes.find((outcome) => outcome.status === "rejected");
|
|
888
|
+
if (failure) throw failure.reason;
|
|
889
|
+
}
|