@nimbus-sh/worker 0.2.3 → 0.4.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.
Files changed (146) hide show
  1. package/dist/_shared/session-router.d.ts +8 -0
  2. package/dist/_shared/session-router.d.ts.map +1 -1
  3. package/dist/_shared/session-router.js +8 -0
  4. package/dist/esbuild-wasm-bundle.generated.d.ts +1 -1
  5. package/dist/esbuild-wasm-bundle.generated.js +1 -1
  6. package/dist/facets/cirrus-real.js +1 -1
  7. package/dist/facets/manager.d.ts +43 -129
  8. package/dist/facets/manager.d.ts.map +1 -1
  9. package/dist/facets/manager.js +107 -318
  10. package/dist/facets/opencode-staging.d.ts +1 -1
  11. package/dist/facets/opencode-staging.d.ts.map +1 -1
  12. package/dist/facets/opencode-staging.js +3 -0
  13. package/dist/facets/real-vite-hmr.js +1 -1
  14. package/dist/facets/vite-dev-server.d.ts +4 -4
  15. package/dist/facets/vite-dev-server.js +5 -5
  16. package/dist/git/commands.d.ts +8 -0
  17. package/dist/git/commands.d.ts.map +1 -1
  18. package/dist/git/commands.js +41 -9
  19. package/dist/git/network-facet.d.ts +1 -1
  20. package/dist/git/network-facet.d.ts.map +1 -1
  21. package/dist/git/network-facet.js +39 -15
  22. package/dist/git-bundle.generated.d.ts +2 -2
  23. package/dist/git-bundle.generated.d.ts.map +1 -1
  24. package/dist/git-bundle.generated.js +3 -3
  25. package/dist/index.d.ts.map +1 -1
  26. package/dist/index.js +9 -1
  27. package/dist/loaders/child-process/spawn-facet.js +1 -1
  28. package/dist/loaders/child-process/spawn-pool.d.ts.map +1 -1
  29. package/dist/loaders/child-process/spawn-pool.js +2 -2
  30. package/dist/loaders/generated-workers.d.ts +2 -2
  31. package/dist/loaders/generated-workers.d.ts.map +1 -1
  32. package/dist/loaders/generated-workers.js +3 -3
  33. package/dist/loaders/npm-resolve-preamble.d.ts +3 -3
  34. package/dist/loaders/npm-resolve-preamble.js +3 -3
  35. package/dist/loaders/pre-bundle-preamble.d.ts +5 -5
  36. package/dist/loaders/pre-bundle-preamble.js +6 -6
  37. package/dist/loaders/process-host.d.ts +6 -107
  38. package/dist/loaders/process-host.d.ts.map +1 -1
  39. package/dist/loaders/process-host.js +6 -405
  40. package/dist/npm/install-batch-facet.d.ts +1 -1
  41. package/dist/npm/install-batch-facet.js +1 -1
  42. package/dist/npm/installer.d.ts +6 -6
  43. package/dist/npm/installer.d.ts.map +1 -1
  44. package/dist/npm/installer.js +28 -27
  45. package/dist/npm/pre-bundle-facet.d.ts +2 -2
  46. package/dist/npm/pre-bundle-facet.js +5 -5
  47. package/dist/npm/r2-cache.d.ts +1 -1
  48. package/dist/npm/resolve-facet.d.ts +1 -1
  49. package/dist/npm/resolve-facet.js +1 -1
  50. package/dist/npm/resolve-one-facet.d.ts +5 -5
  51. package/dist/npm/resolve-one-facet.js +5 -5
  52. package/dist/router/index.js +2 -2
  53. package/dist/router/remote-api.js +9 -1
  54. package/dist/runtime/bun-repl.d.ts +1 -1
  55. package/dist/runtime/bun-repl.js +3 -3
  56. package/dist/runtime/esbuild-wasm-bytes.js +1 -1
  57. package/dist/runtime/facet-loader-host.d.ts +5 -8
  58. package/dist/runtime/facet-loader-host.d.ts.map +1 -1
  59. package/dist/runtime/facet-loader-host.js +7 -11
  60. package/dist/runtime/node-repl.js +2 -2
  61. package/dist/runtime/node-shims-artifact.js +1 -1
  62. package/dist/runtime/node-shims.js +2 -1
  63. package/dist/runtime/opencode-artifact.js +1 -1
  64. package/dist/runtime/opentui-wasm-bytes.js +1 -1
  65. package/dist/runtime/python-repl.js +3 -3
  66. package/dist/runtime/ruby-repl.js +2 -2
  67. package/dist/runtime/ruby-resident.js +1 -1
  68. package/dist/runtime/sqlite-wasm-bytes.js +1 -1
  69. package/dist/session/diag.js +1 -1
  70. package/dist/session/hibernation.d.ts +20 -53
  71. package/dist/session/hibernation.d.ts.map +1 -1
  72. package/dist/session/hibernation.js +57 -194
  73. package/dist/session/init-phases.d.ts +2 -3
  74. package/dist/session/init-phases.d.ts.map +1 -1
  75. package/dist/session/init-phases.js +3 -2
  76. package/dist/session/init.d.ts.map +1 -1
  77. package/dist/session/init.js +8 -4
  78. package/dist/session/keys.d.ts +14 -37
  79. package/dist/session/keys.d.ts.map +1 -1
  80. package/dist/session/keys.js +18 -37
  81. package/dist/session/nimbus-session.d.ts +10 -15
  82. package/dist/session/nimbus-session.d.ts.map +1 -1
  83. package/dist/session/nimbus-session.js +28 -31
  84. package/dist/session/port-capability.d.ts +43 -0
  85. package/dist/session/port-capability.d.ts.map +1 -0
  86. package/dist/session/port-capability.js +49 -0
  87. package/dist/session/programmatic.d.ts +39 -5
  88. package/dist/session/programmatic.d.ts.map +1 -1
  89. package/dist/session/programmatic.js +150 -19
  90. package/dist/session/routes.d.ts +2 -0
  91. package/dist/session/routes.d.ts.map +1 -1
  92. package/dist/session/routes.js +96 -20
  93. package/dist/session/rpc.d.ts +36 -9
  94. package/dist/session/rpc.d.ts.map +1 -1
  95. package/dist/session/rpc.js +84 -24
  96. package/dist/session/start-real-vite.d.ts.map +1 -1
  97. package/dist/session/start-real-vite.js +3 -1
  98. package/dist/session/state-store.d.ts +3 -2
  99. package/dist/session/state-store.d.ts.map +1 -1
  100. package/dist/session/state-store.js +3 -2
  101. package/dist/session/supervisor-rpc.d.ts.map +1 -1
  102. package/dist/session/supervisor-rpc.js +7 -4
  103. package/dist/session/ws.d.ts +1 -3
  104. package/dist/session/ws.d.ts.map +1 -1
  105. package/dist/session/ws.js +4 -3
  106. package/dist/wrangler/nimbus-wrangler.js +1 -1
  107. package/package.json +4 -2
  108. package/scripts/bundle-esbuild-wasm.mjs +2 -2
  109. package/scripts/bundle-facet-workers.mjs +5 -7
  110. package/scripts/bundle-sqlite-wasm.mjs +1 -1
  111. package/dist/facets/inner-do-registry.d.ts +0 -41
  112. package/dist/facets/inner-do-registry.d.ts.map +0 -1
  113. package/dist/facets/inner-do-registry.js +0 -51
  114. package/dist/facets/launch-pacer.d.ts +0 -111
  115. package/dist/facets/launch-pacer.d.ts.map +0 -1
  116. package/dist/facets/launch-pacer.js +0 -130
  117. package/dist/loaders/fanout-pool.d.ts +0 -207
  118. package/dist/loaders/fanout-pool.d.ts.map +0 -1
  119. package/dist/loaders/fanout-pool.js +0 -363
  120. package/dist/loaders/loader-pool.d.ts +0 -294
  121. package/dist/loaders/loader-pool.d.ts.map +0 -1
  122. package/dist/loaders/loader-pool.js +0 -642
  123. package/dist/loaders/process-fabric.d.ts +0 -509
  124. package/dist/loaders/process-fabric.d.ts.map +0 -1
  125. package/dist/loaders/process-fabric.js +0 -360
  126. package/dist/loaders/vendor/errors.d.ts +0 -24
  127. package/dist/loaders/vendor/errors.d.ts.map +0 -1
  128. package/dist/loaders/vendor/errors.js +0 -46
  129. package/dist/loaders/vendor/serialize.d.ts +0 -3
  130. package/dist/loaders/vendor/serialize.d.ts.map +0 -1
  131. package/dist/loaders/vendor/serialize.js +0 -25
  132. package/dist/loaders/vendor/types.d.ts +0 -69
  133. package/dist/loaders/vendor/types.d.ts.map +0 -1
  134. package/dist/loaders/vendor/types.js +0 -4
  135. package/dist/loaders/workerd-facet-host.d.ts +0 -141
  136. package/dist/loaders/workerd-facet-host.d.ts.map +0 -1
  137. package/dist/loaders/workerd-facet-host.js +0 -284
  138. package/dist/session/bindings.d.ts +0 -209
  139. package/dist/session/bindings.d.ts.map +0 -1
  140. package/dist/session/bindings.js +0 -680
  141. package/dist/session/ctx-exports.d.ts +0 -16
  142. package/dist/session/ctx-exports.d.ts.map +0 -1
  143. package/dist/session/ctx-exports.js +0 -22
  144. package/dist/session/ws-hibernation-config.d.ts +0 -64
  145. package/dist/session/ws-hibernation-config.d.ts.map +0 -1
  146. package/dist/session/ws-hibernation-config.js +0 -89
@@ -1,363 +0,0 @@
1
- /**
2
- * Two-tier fan-out primitive for work that must execute in Worker Loader
3
- * facets without tripping workerd's per-DO dynamic-worker ceiling.
4
- *
5
- * A single Durable Object method can drive at most four concurrent
6
- * Worker Loader fetches before extra dispatches serialize or fail. Small
7
- * batches therefore run in the coordinator DO through NimbusLoaderPool.
8
- * Wider batches are sharded across sibling NimbusSession DOs, each of
9
- * which owns its own four-loader budget.
10
- *
11
- * Routing is deterministic: each task has a stable key, and the key maps
12
- * to a sibling DO shard. There is no silent fallback to width-1 execution;
13
- * missing LOADER or NIMBUS_SESSION bindings fail loudly so install and
14
- * runtime operations do not appear successful after partial dispatch.
15
- */
16
- import { serializeFunction } from './vendor/serialize.js';
17
- import { BindingError } from './vendor/errors.js';
18
- import { NimbusLoaderPool } from './loader-pool.js';
19
- import { disposeRpcResource } from '@nimbus-sh/core/_shared/rpc-dispose.js';
20
- import { describeError, isDoOverloaded, isTransientDoReset } from '@nimbus-sh/core/observability/oom-classify.js';
21
- /**
22
- * Threshold at which routing switches from coordinator-local loaders to
23
- * sibling Durable Objects.
24
- *
25
- * Set to **5** so the in-DO path stays below the V8 4-loaders-per-method
26
- * cap by construction. width < 5 stays local; width >= 5 uses sibling DOs.
27
- */
28
- export const IN_DO_THRESHOLD = 5;
29
- /**
30
- * Hard cap on concurrent peer DOs per single submitMany call. Throughput stays
31
- * flat through this width while keeping per-request scheduler pressure bounded.
32
- */
33
- export const MAX_PEER_FANOUT = 32;
34
- /**
35
- * Bounded retries for a peer-DO shard dispatch that rejects with a
36
- * transient platform reset (code roll-over, storage cold-start hiccup).
37
- * Sibling DOs are addressed by stable name, so the retry re-dispatches
38
- * the SAME shard to the re-provisioning object; the fanned-out work
39
- * (packument resolution, tarball materialisation) is idempotent, so
40
- * re-running a shard is safe. Budget mirrors the resolve-facet's own
41
- * per-fetch retry policy so a single flaky cold start no longer fails a
42
- * whole install. The same budget covers an overloaded peer, on the longer
43
- * schedule below. Non-transient rejections (OOM, count mismatch, genuine
44
- * task throw) are NOT retried — they propagate on the first hit.
45
- */
46
- export const PEER_TRANSIENT_RESET_RETRIES = 3;
47
- export const PEER_RETRY_BACKOFF_MS = [250, 750, 1500];
48
- /**
49
- * Backoff for a shard whose peer DO was shed as overloaded. The object is
50
- * alive and the shard never ran; what it needs is time for the input-gate
51
- * queue to drain, so the schedule is an order of magnitude longer than the
52
- * reset schedule. A whole-batch abort here used to fail an entire install.
53
- */
54
- export const PEER_OVERLOAD_BACKOFF_MS = [1000, 3000, 6000];
55
- /**
56
- * Peer shards dispatched per phase. Each phase is a barrier that costs its
57
- * slowest member, so a wide fan-out pays ⌈shards / FANOUT_PHASE_SIZE⌉ serial
58
- * round-trips; the size trades that serialization against simultaneous cold
59
- * sibling DO starts.
60
- *
61
- * The six-barrier profile once measured on a 123-package install (21 shards of
62
- * ~6 packages, 10.6/6.8/21.8/7.9/34.6/4.8 s) came from the shard count, not
63
- * from this width. Capping install shards at INSTALL_PEER_CAP fixed it at the
64
- * source and that install now clears in two phases. Widening to 8 on top of
65
- * that bought one further barrier and doubled the simultaneous cold sibling-DO
66
- * starts, which is the account-level pressure the phasing exists for: twelve
67
- * concurrent Markflow installs went from 48 simultaneous peer starts to 96 and
68
- * began timing out. Phasing does not change how many peers start, only how
69
- * many start at once, so this width is set by the burst the scheduler
70
- * tolerates rather than by the barrier count.
71
- */
72
- export const FANOUT_PHASE_SIZE = 4;
73
- function isNimbusFanoutPeerStub(value) {
74
- if ((typeof value !== 'object' && typeof value !== 'function') || value === null) {
75
- return false;
76
- }
77
- const execute = Reflect.get(value, '_rpcFanoutExecute');
78
- return typeof execute === 'function';
79
- }
80
- function fanoutPeerStub(value) {
81
- if ((typeof value !== 'object' && typeof value !== 'function') || value === null) {
82
- throw new BindingError('NimbusFanoutPool: NIMBUS_SESSION.get() did not return a peer stub.');
83
- }
84
- if (!isNimbusFanoutPeerStub(value)) {
85
- throw new BindingError('NimbusFanoutPool: peer stub does not expose _rpcFanoutExecute().');
86
- }
87
- return value;
88
- }
89
- /**
90
- * Two-tier fan-out pool. Constructed by the supervisor DO; routes
91
- * each `submitMany` call automatically based on width.
92
- *
93
- * Lifetime: cheap to construct (no async init). Multiple submitMany
94
- * calls share NO state — each is dispatched fresh. The class
95
- * exists primarily as a clean API surface; per-call dispatch state
96
- * lives only inside submitMany's promise.
97
- */
98
- export class NimbusFanoutPool {
99
- env;
100
- ctx;
101
- opts;
102
- coordDoId;
103
- coordDoIdShort;
104
- constructor(env, ctx, opts) {
105
- // Hard-fail on missing LOADER. NimbusLoaderPool also enforces this,
106
- // but we check up front so the diagnostic points at the fanout-pool
107
- // construction site rather than the deferred loader-pool one.
108
- if (!env?.LOADER || typeof env.LOADER.get !== 'function') {
109
- throw new BindingError('NimbusFanoutPool: env.LOADER binding missing or invalid. ' +
110
- 'Add a [[worker_loaders]] entry to wrangler.jsonc.');
111
- }
112
- this.env = env;
113
- this.ctx = ctx;
114
- this.opts = opts;
115
- this.coordDoId = ctx.id.toString();
116
- this.coordDoIdShort = this.coordDoId.slice(0, 12);
117
- }
118
- /**
119
- * Dispatch `tasks` across the appropriate topology and return
120
- * results in input order.
121
- *
122
- * Routing:
123
- * tasks.length < 5 -> coordinator-local NimbusLoaderPool
124
- * tasks.length >= 5 -> sibling NimbusSession DOs
125
- *
126
- * Backpressure: if `tasks.length > MAX_PEER_FANOUT (32)`, tasks
127
- * are sharded modulo `MAX_PEER_FANOUT` and each shard's bucket
128
- * runs serially inside its assigned peer DO via the in-peer
129
- * NimbusLoaderPool's concurrency (capped at 4 there too). A
130
- * single submitMany call returns when ALL tasks complete (or any
131
- * throws).
132
- *
133
- * `fn` is the user function executed per task. It runs INSIDE a
134
- * Worker Loader isolate (in the in-DO path) or inside a peer DO's
135
- * Worker Loader isolate (in the peer-DO path); same trust posture
136
- * as NimbusLoaderPool.submit. The function is serialized via
137
- * the vendored serializeFunction (same as NimbusLoaderPool#prepare).
138
- */
139
- async submitMany(tasks, fn) {
140
- if (tasks.length === 0)
141
- return [];
142
- if (tasks.length < IN_DO_THRESHOLD) {
143
- return this._dispatchInDo(tasks, fn);
144
- }
145
- return this._dispatchPeerDo(tasks, fn);
146
- }
147
- /** Report which topology a task count uses without dispatching. */
148
- topologyFor(taskCount) {
149
- if (taskCount === 0)
150
- return 'empty';
151
- return taskCount < IN_DO_THRESHOLD ? 'in-do' : 'peer-do';
152
- }
153
- /**
154
- * Compute the deterministic peer-DO id for a task key and peer count.
155
- *
156
- * Shape: `nbf:${tag}:${coordDoIdShort}:${shard}` where
157
- * `shard = hash(key) mod peerCount`. Peer count is
158
- * `min(tasks.length, MAX_PEER_FANOUT)`.
159
- */
160
- peerSiblingId(key, peerCount) {
161
- const shard = hashKeyToShard(key, peerCount);
162
- return `nbf:${this.opts.tag}:${this.coordDoIdShort}:${shard}`;
163
- }
164
- // ── Private: in-DO dispatch (in-DO fanout) ──────────────────────────────
165
- async _dispatchInDo(tasks, fn) {
166
- // Use the existing NimbusLoaderPool. Concurrency = task count
167
- // (capped at 4 by constructor — tasks.length is already < 5
168
- // here, so the cap won't bite). Each task = one pool.submit;
169
- // pool.map runs them with stable-slot reuse.
170
- const concurrency = Math.min(tasks.length, IN_DO_THRESHOLD - 1);
171
- const pool = new NimbusLoaderPool(this.env, this.ctx, {
172
- concurrency,
173
- timeoutMs: this.opts.timeoutMs,
174
- tag: this.opts.tag,
175
- preamble: this.opts.preamble,
176
- wasmModules: this.opts.wasmModules,
177
- extraBindings: this.opts.extraBindings,
178
- omitSupervisor: this.opts.omitSupervisor,
179
- supervisorPid: this.opts.supervisorPid,
180
- });
181
- try {
182
- // pool.map runs the function over `items` with concurrency-bounded
183
- // slot reuse. Each slot is one warm loader isolate; we get exactly
184
- // `concurrency` loader isolates total — well under the 4-cap.
185
- const items = tasks.map((t) => t.args);
186
- const results = await pool.map(fn, items);
187
- // pool.map returns Array<R | null> (null on per-item failure with
188
- // onError='null'/'skip'). Default onError='throw' rejects on
189
- // first failure, so successful settle here implies all R values.
190
- return results;
191
- }
192
- finally {
193
- try {
194
- pool.dispose();
195
- }
196
- catch { /* best-effort */ }
197
- }
198
- }
199
- // ── Private: peer-DO dispatch (peer-DO fanout) ────────────────────────────
200
- async _dispatchPeerDo(tasks, fn) {
201
- const ns = this.env?.NIMBUS_SESSION;
202
- if (!ns || typeof ns.idFromName !== 'function' || typeof ns.get !== 'function') {
203
- throw new BindingError('NimbusFanoutPool: env.NIMBUS_SESSION binding missing or invalid. ' +
204
- 'The peer-DO topology requires it. ' +
205
- 'Add the binding via durable_objects.bindings in wrangler.jsonc.');
206
- }
207
- // Serialize the user function ONCE here on the supervisor side.
208
- // Each peer DO receives the same fnSource string; warm peer
209
- // loader isolates (keyed on fnHash) reuse across calls with
210
- // identical fns.
211
- const fnSource = serializeFunction(fn);
212
- // Cap peer count at MAX_PEER_FANOUT. Tasks beyond N=32 are
213
- // bucketed into existing shards — each shard's peer DO then
214
- // runs its bucket through its in-DO NimbusLoaderPool.map
215
- // (concurrency capped at 4 there).
216
- const peerCount = Math.min(tasks.length, this.opts.maxPeers ?? MAX_PEER_FANOUT);
217
- // Group tasks by deterministic shard. Same key → same shard, so
218
- // tests can predict which peer handles which task.
219
- const shards = new Map();
220
- for (const t of tasks) {
221
- const shard = hashKeyToShard(t.key, peerCount);
222
- let bucket = shards.get(shard);
223
- if (!bucket) {
224
- bucket = [];
225
- shards.set(shard, bucket);
226
- }
227
- bucket.push(t);
228
- }
229
- // Dispatch each shard to its peer DO. Build a map from
230
- // task → its place in the original tasks array so we can
231
- // reassemble results in input order.
232
- const taskIndex = new Map();
233
- tasks.forEach((t, i) => taskIndex.set(t, i));
234
- const results = new Array(tasks.length);
235
- // Build one async dispatcher per shard (closure capturing siblingName,
236
- // bucket). NOT eagerly-started — wrapped in a thunk so we can stagger
237
- // dispatch via Promise chains without forcing all shards to start
238
- // simultaneously.
239
- const dispatchers = [];
240
- for (const [shard, bucket] of shards) {
241
- const siblingName = `nbf:${this.opts.tag}:${this.coordDoIdShort}:${shard}`;
242
- const id = ns.idFromName(siblingName);
243
- const peerArgs = bucket.map((t) => t.args);
244
- dispatchers.push(async () => {
245
- for (let attempt = 0;; attempt++) {
246
- // Fresh stub per attempt: after a transient reset the previous
247
- // stub points at a torn-down object, so a retry re-resolves the
248
- // sibling by its stable id.
249
- const peerStub = ns.get(id);
250
- const stub = fanoutPeerStub(peerStub);
251
- try {
252
- // Each peer DO RPC call uses ONE LOADER worker on its side.
253
- // Supervisor → peer DO is a stub.fetch / RPC method call,
254
- // NOT an env.LOADER.get(); that's the cap-sidestep that
255
- // makes peer-DO fanout work.
256
- const rpcResp = await stub._rpcFanoutExecute(fnSource, peerArgs, {
257
- tag: this.opts.tag,
258
- timeoutMs: this.opts.timeoutMs,
259
- preamble: this.opts.preamble,
260
- wasmModules: this.opts.wasmModules,
261
- extraBindings: this.opts.extraBindings,
262
- omitSupervisor: this.opts.omitSupervisor,
263
- // INSTALL-HONESTY: forward the COORDINATOR's full doId so
264
- // the peer's NimbusLoaderPool can mint a SUPERVISOR
265
- // binding that routes back HERE (the user's session DO),
266
- // not to the peer DO itself. Without this, peer DOs'
267
- // env.SUPERVISOR.writeBatch / writeBatchStream / stdout /
268
- // ... write into the peer's own VFS — invisible to the
269
- // user. See INSTALL-HONESTY-retro.md.
270
- coordinatorDoId: this.coordDoId,
271
- // Credential source for peer-side writeBatchStream — the
272
- // invoking process pid, so package writes are authorized
273
- // as the user (not rejected as pid:0).
274
- supervisorPid: this.opts.supervisorPid,
275
- });
276
- try {
277
- const peerResults = rpcResp.results ?? [];
278
- if (peerResults.length !== bucket.length) {
279
- throw new Error(`peer DO returned ${peerResults.length} results for ${bucket.length} tasks ` +
280
- `(siblingName=${siblingName})`);
281
- }
282
- // Place each result back into its original input slot.
283
- for (let i = 0; i < bucket.length; i++) {
284
- const origIdx = taskIndex.get(bucket[i]);
285
- if (origIdx === undefined) {
286
- throw new Error(`peer DO result had no original task index (siblingName=${siblingName})`);
287
- }
288
- results[origIdx] = peerResults[i];
289
- }
290
- }
291
- finally {
292
- disposeRpcResource(rpcResp);
293
- }
294
- return;
295
- }
296
- catch (err) {
297
- const schedule = isTransientDoReset(err) ? PEER_RETRY_BACKOFF_MS
298
- : isDoOverloaded(err) ? PEER_OVERLOAD_BACKOFF_MS
299
- : null;
300
- if (schedule && attempt < PEER_TRANSIENT_RESET_RETRIES) {
301
- const backoff = schedule[Math.min(attempt, schedule.length - 1)];
302
- await new Promise((r) => setTimeout(r, backoff));
303
- continue;
304
- }
305
- // Re-throwing bare loses everything only this frame knows: which
306
- // sibling ran the shard, how wide it was, and how many attempts
307
- // it already cost. Callers report the message, so a rejection the
308
- // platform words as `internal error` arrived at the user with no
309
- // way to tell a one-off peer from a shard that had exhausted its
310
- // retries. `cause` keeps the original for anything that inspects
311
- // errors rather than reads them.
312
- throw new Error(`peer shard ${siblingName} (${bucket.length} task${bucket.length === 1 ? '' : 's'}) `
313
- + `failed after ${attempt + 1} attempt${attempt === 0 ? '' : 's'}: ${describeError(err)}`, { cause: err });
314
- }
315
- finally {
316
- disposeRpcResource(peerStub);
317
- }
318
- }
319
- });
320
- }
321
- // Dispatch peer shards in bounded phases. A single Promise.all across all
322
- // shards can create too many simultaneous cold sibling DO starts under
323
- // concurrent installs. Promise-chain phasing limits scheduler pressure
324
- // without sleeps, timers, or idle gaps between phases.
325
- //
326
- // A phase is a hard barrier: no shard in phase N+1 starts until every
327
- // shard in phase N returns, so `width@ms` per phase is what separates
328
- // "the shards are slow" from "there are too many barriers".
329
- for (let i = 0; i < dispatchers.length; i += FANOUT_PHASE_SIZE) {
330
- const phase = dispatchers.slice(i, i + FANOUT_PHASE_SIZE);
331
- const phaseStartedAt = Date.now();
332
- await Promise.all(phase.map((d) => d()));
333
- this.opts.onDispatchPhase?.(phase.length, Date.now() - phaseStartedAt);
334
- }
335
- return results;
336
- }
337
- }
338
- /**
339
- * Stable hash → shard. Uses a fresh djb2 over the key (NOT
340
- * hashSource) and modulos by peerCount.
341
- *
342
- * Why not reuse hashSource: hashSource returns a base-36 string,
343
- * NOT hex — its alphabet is `[0-9a-z]`. parseInt(str, 16) on a
344
- * base-36 string aborts at the first non-hex char (any of g-z),
345
- * which produces extremely poor distribution: keys with the same
346
- * leading-hex-prefix collide regardless of their suffix. (Seen in
347
- * the wild: `task-0 .. task-7` all collided onto shard 4.)
348
- *
349
- * Deterministic: same key + same peerCount → same shard, every run.
350
- * Tests use this to predict placement.
351
- */
352
- export function hashKeyToShard(key, peerCount) {
353
- if (peerCount <= 1)
354
- return 0;
355
- // djb2, returning an unsigned 32-bit integer — full 2^32 range,
356
- // no string-format conversion gotchas. peerCount <= MAX_PEER_FANOUT
357
- // (32) << 2^32, so the modulo distributes uniformly for any input.
358
- let h = 5381;
359
- for (let i = 0; i < key.length; i++) {
360
- h = ((h << 5) + h + key.charCodeAt(i)) | 0;
361
- }
362
- return (h >>> 0) % peerCount;
363
- }
@@ -1,294 +0,0 @@
1
- /**
2
- * loader-pool.ts — Nimbus loader-isolate pool based on cloudflare-parallel.
3
- *
4
- * Adds Nimbus-specific behavior to the upstream pool design:
5
- * 1. **Stable-slot isolate reuse**. Upstream's #counter++ gives every
6
- * dispatch a fresh isolate — fine for one-off AI calls, terrible for
7
- * running 67 npm tarball extractions (cold-start dominates). We pin
8
- * each job to `slot = cursor % concurrency` and use stable loader
9
- * IDs `nfp:${fnHash}:slot-${i}:g${generation}`, so a pool of
10
- * concurrency=4 keeps at most 4 warm isolates rather than N fresh ones.
11
- * 2. **Nimbus defaults**: compatibilityDate = CF_COMPAT_DATE (matches
12
- * the supervisor worker), compatibilityFlags = ['nodejs_compat'],
13
- * globalOutbound = undefined (inherit parent network so the facet can
14
- * reach https://registry.npmjs.org without a proxy binding).
15
- * 3. **SupervisorRPC autoinjection**. The pool grabs a SupervisorRPC
16
- * stub from `getCtxExports()` and forwards it as `env.SUPERVISOR` to
17
- * every facet, same pattern as git-network-facet.ts. Callers can add
18
- * more bindings via `extraBindings`.
19
- * 4. **Fail-loud defaults**: timeout 60s, retries 0, onError 'throw'.
20
- * Caller opts in to leniency.
21
- *
22
- * The vendored directory contains only the upstream serialization, error,
23
- * and binding types used by this implementation.
24
- */
25
- /** Options handed to NimbusLoaderPool's constructor. */
26
- export interface NimbusLoaderPoolOptions {
27
- /** Maximum concurrent in-flight facets. Default 4. */
28
- concurrency?: number;
29
- /** Per-task timeout in ms. Default 60_000. */
30
- timeoutMs?: number;
31
- /**
32
- * Per-task retry attempts AFTER the initial failure. Default 0.
33
- * Set to a small number only if transient RPC errors are common.
34
- */
35
- retries?: number;
36
- /**
37
- * Additional bindings forwarded to each facet. These merge on top of the
38
- * default `{ SUPERVISOR: SupervisorRPC({ doId, pid:0 }) }`. Use this to
39
- * give facets access to KV, R2, AI, or additional supervisor-level APIs.
40
- */
41
- extraBindings?: Record<string, unknown>;
42
- /**
43
- * Optional tag used in loader IDs for debugging (e.g. "npm-install").
44
- * Does NOT affect isolate identity — same fn + same tag = same slot.
45
- */
46
- tag?: string;
47
- /**
48
- * If true, omit the default SupervisorRPC binding. Use this for pools
49
- * that don't need DO callbacks (e.g. a pure CPU compute pool).
50
- */
51
- omitSupervisor?: boolean;
52
- /**
53
- * Loader cache scope. Defaults to `session`, which bakes the owning DO id
54
- * into the loader key so stateful facets cannot leak bindings or globals
55
- * across sessions. Use `global` only for stateless compute modules that do
56
- * not receive a Supervisor binding and do not retain user state.
57
- */
58
- cacheScope?: 'session' | 'global';
59
- /**
60
- * Override the `doId` baked into the auto-injected SUPERVISOR binding.
61
- * Default: `ctx.id.toString()` (the DO that constructs the pool).
62
- *
63
- * Used by NimbusFanoutPool's peer-DO branch (peer-DO fanout): peer DOs
64
- * construct their per-task NimbusLoaderPool from inside
65
- * `_rpcFanoutExecute`, where `ctx` is the PEER DO's ctx. Without this
66
- * override the peer's auto-injected SUPERVISOR routes back to the
67
- * peer DO itself — so writes (e.g. install-batch-facet's
68
- * writeBatchStream) land in the peer's VFS instead of the
69
- * COORDINATOR's. The user's terminal session is on the coordinator;
70
- * writes-to-peer are invisible. See INSTALL-HONESTY-retro.md.
71
- *
72
- * When set, the auto-injected SupervisorRPC uses this string as the
73
- * `props.doId`, routing all SUPERVISOR.* calls back to the
74
- * coordinator. Effective only when `omitSupervisor !== true`.
75
- */
76
- supervisorDoIdOverride?: string;
77
- /**
78
- * Process pid baked into the auto-injected SUPERVISOR binding's props.
79
- * The supervisor derives the write credential from this pid
80
- * (`SupervisorRPC._pid()` → `processes.cred(pid)`), so any facet that
81
- * calls a filesystem RPC (`writeBatchStream`, `writeFile`, …) must be
82
- * dispatched with the invoking process's real pid — otherwise the RPC
83
- * throws "missing or invalid process pid in props". npm install threads
84
- * the shell command's `ctx.pid` here so package files land as the user.
85
- * Left 0 (default) for pools whose facets touch only cache/registry RPCs
86
- * (npm resolve, pre-bundle), which never call `_pid()`.
87
- */
88
- supervisorPid?: number;
89
- /**
90
- * Raw JavaScript source prepended to every generated worker module.
91
- * Lets callers inject bundled helpers, such as a tar parser. The user
92
- * function can reference top-level names declared in the preamble as if
93
- * they were in lexical scope.
94
- *
95
- * Example: `preamble: 'export const parse = ...; const helper = ...;'`
96
- * — the preamble runs at module-load time; any side effects happen
97
- * inside the facet isolate.
98
- *
99
- * Preamble text is bytes-stable for a given pool — it's part of the
100
- * loader-cache key (fnHash), so changing the preamble invalidates all
101
- * warm slots.
102
- */
103
- preamble?: string;
104
- /**
105
- * WebAssembly modules to ship into the facet via the LOADER's
106
- * `modules` map. Map keys are module specifier paths (e.g.
107
- * `'esbuild.wasm'`); values are the raw bytes.
108
- *
109
- * Workerd registers each entry as `{ wasm: ArrayBuffer }` in the
110
- * worker's modules map. The pool prepends a static
111
- * `import __NIMBUS_WASM_<id> from './<key>';` to the generated
112
- * worker.js so workerd compiles each at module-load (startup phase,
113
- * where wasm code generation is permitted). The compiled Modules
114
- * are exposed via `globalThis.__NIMBUS_WASM[<key>]` for the user
115
- * function to read at request time.
116
- *
117
- * Why this works when other paths don't:
118
- * - request-time `WebAssembly.compile()` — disallowed by workerd
119
- * in this deploy.
120
- * - request-time RPC of a pre-compiled Module — workerd
121
- * structured-clone refuses ("Unable to deserialize cloned data").
122
- * - inlining bytes in the preamble — 16 MiB string per dispatch
123
- * OOMs the supervisor at module-source allocation time.
124
- * - LOADER modules-map (this) — bytes ride INSIDE the worker code
125
- * blob; workerd compiles wasm during its own startup pipeline,
126
- * never crossing structured-clone, never executing JS eval.
127
- *
128
- * The bytes ARE part of the loader-cache key (workerd hashes the
129
- * whole WorkerCode), so changing the wasm bytes invalidates warm
130
- * slots — desirable when the bundled wasm version changes.
131
- */
132
- wasmModules?: Record<string, ArrayBuffer>;
133
- }
134
- /** Per-call override (merged with pool defaults). */
135
- export interface NimbusLoaderCallOptions {
136
- timeoutMs?: number;
137
- retries?: number;
138
- /**
139
- * Per-call WebAssembly modules. Merged with the pool's
140
- * constructor-time `wasmModules` at dispatch time and shipped via
141
- * the LOADER's modules map (same `{ wasm: ArrayBuffer }` shape).
142
- *
143
- * Shipping path validated empirically against prod (see
144
- * `WebAssembly.instantiate(bytes)` is blocked at request-time but
145
- * the LOADER-modules path compiles bytes during the inner
146
- * worker's module-load phase, where wasm code generation IS
147
- * permitted. The bytes ride INSIDE the worker code blob; workerd
148
- * never crosses structured-clone, never executes user-eval.
149
- *
150
- * Cache key impact: per-call bytes are fingerprinted (length +
151
- * first/last byte per module) and folded into the loader cache
152
- * key. Identical bytes on the same slot → warm reuse; different
153
- * bytes → fresh isolate. The pool's existing `wasmHash` field
154
- * captures CONSTRUCTOR-time bytes only; per-call bytes get an
155
- * independent fingerprint mixed into the slot id at dispatch.
156
- *
157
- * Naming collision rule: a per-call key MUST NOT collide with a
158
- * constructor-time key (after identifier sanitisation). The
159
- * dispatch path throws BindingError if it does — silently
160
- * shadowing the constructor's wasm would break the cache-key
161
- * invariant downstream callers rely on.
162
- *
163
- * Used by the `wasm-runner` shell command in src/runtime/
164
- * wasm-runner.ts to ship user-supplied .wasm bytes from VFS into
165
- * a fresh facet isolate per invocation.
166
- */
167
- wasmModules?: Record<string, ArrayBuffer>;
168
- }
169
- /** Per-map override. Adds onError strategy for partial failures. */
170
- export interface NimbusLoaderMapOptions extends NimbusLoaderCallOptions {
171
- /** Concurrency override for this call. Defaults to pool's concurrency. */
172
- concurrency?: number;
173
- /**
174
- * What to do when an individual item fails:
175
- * - 'throw' (default): reject whole map on first failure.
176
- * - 'null': replace failed items with null in the result array.
177
- * - 'skip': omit failed items from the result array.
178
- * We default to 'throw' — install-time failures are not silently ignored.
179
- */
180
- onError?: 'throw' | 'null' | 'skip';
181
- }
182
- export interface LoaderWorkerModuleSourceOptions {
183
- fnSource: string;
184
- preamble?: string;
185
- wasmEntries?: ReadonlyArray<{
186
- name: string;
187
- id: string;
188
- }>;
189
- hasBindings: boolean;
190
- }
191
- /** Assemble the exact JavaScript module parsed by a dynamic loader worker. */
192
- export declare function assembleLoaderWorkerModuleSource(options: LoaderWorkerModuleSourceOptions): string;
193
- /**
194
- * Nimbus-scoped parallel dispatch over `env.LOADER`. Tasks are pure
195
- * functions whose last argument is an `env` object containing the
196
- * forwarded bindings (default: `{ SUPERVISOR }`).
197
- *
198
- * Typical use:
199
- *
200
- * const pool = new NimbusLoaderPool(env, ctx, {
201
- * concurrency: 4,
202
- * tag: 'npm-install',
203
- * });
204
- * const results = await pool.map(
205
- * async (pkg, env) => env.SUPERVISOR.writeBatch(buildPayload(pkg)),
206
- * toFetch,
207
- * );
208
- */
209
- export declare class NimbusLoaderPool {
210
- #private;
211
- private readonly loader;
212
- private readonly concurrency;
213
- private readonly defaultTimeoutMs;
214
- private readonly defaultRetries;
215
- private readonly tag;
216
- private readonly slotGenerations;
217
- private bindings;
218
- private readonly preamble;
219
- private readonly preambleHash;
220
- /**
221
- * WASM modules to ship in the LOADER `modules` map. See
222
- * NimbusLoaderPoolOptions.wasmModules for the rationale. Stored in
223
- * insertion order so the per-import preamble we generate matches
224
- * across pool dispatches (cache-key stability).
225
- */
226
- private readonly wasmModules;
227
- /** Hash of (name + byte length + first/last bytes) of every wasm
228
- * module, folded into the loader cache key so changes invalidate
229
- * warm slots. Hashing the FULL bytes would be O(20+ MiB) per dispatch
230
- * and is unnecessary — wasm bytes are pinned at deploy time, the
231
- * length+endpoints are a strong-enough fingerprint. */
232
- private readonly wasmHash;
233
- /**
234
- * Short prefix of the owning DO's id, baked into the loader.get()
235
- * cache key so warm isolates are scoped to ONE session. Without this,
236
- * session A's pool and session B's pool (same `tag` + `fnHash`) share
237
- * an isolate — which means B's writeBatch RPCs routed through A's
238
- * env.SUPERVISOR binding (minted with A's doId at construction
239
- * time). B's install reports success but the writes land in A's VFS,
240
- * leaving B with only the git-clone seed files (~119 instead of ~1491).
241
- * 12 chars is enough entropy for DO ids to collide-free per process.
242
- */
243
- private readonly doIdShort;
244
- constructor(env: any, ctx: DurableObjectState, opts?: NimbusLoaderPoolOptions);
245
- /** Effective concurrency used when no per-call override is supplied. */
246
- get defaultConcurrency(): number;
247
- /**
248
- * Run `fn` once with `arg` on a slot isolate. Returns the result or
249
- * throws TimeoutError / RetryExhaustedError / ExecutionError.
250
- */
251
- submit<T, R>(fn: (arg: T, env: any) => R | Promise<R>, arg: T, opts?: NimbusLoaderCallOptions): Promise<Awaited<R>>;
252
- /**
253
- * Run `fn` on every item in `items`, at most `concurrency` at a time,
254
- * pinned to stable slots so warm isolates are reused.
255
- *
256
- * Results are returned in input order. Failure handling per `onError`.
257
- */
258
- map<T, R>(fn: (item: T, env: any) => R | Promise<R>, items: T[], opts?: NimbusLoaderMapOptions): Promise<Array<Awaited<R> | null>>;
259
- /**
260
- * Same shape as `map`, but accepts a pre-serialized function source
261
- * string instead of a live function reference. Used by
262
- * `NimbusFanoutPool`'s peer-DO leg, where the function was already
263
- * serialized on the coordinator side and forwarded over RPC.
264
- *
265
- * The fnSource MUST be the output of `serializeFunction(fn)`
266
- * (typically forwarded directly from a coordinator RPC). Bytes-
267
- * stable invariants:
268
- * - `fnHash = hashSource(fnSource)` must be deterministic so
269
- * warm slots are correctly keyed.
270
- * - `fnSource` must NOT reference `this` — same rule as
271
- * `serializeFunction`.
272
- *
273
- * No fn-validation runs here (it already ran on the coordinator);
274
- * the peer trusts the caller to forward a valid serialization.
275
- */
276
- mapSource<T, R>(fnSource: string, items: T[], opts?: NimbusLoaderMapOptions): Promise<Array<Awaited<R> | null>>;
277
- /**
278
- * Release any RPC stubs held by the pool. Call this once the caller
279
- * is done with the pool (post-`map`/`submit`) so the underlying
280
- * stubs don't linger in workerd's deferred-destruction queue.
281
- *
282
- * Primary target: the SUPERVISOR binding stub we minted at
283
- * construction time (via `ctxExports.SupervisorRPC({props})`). It's
284
- * a cross-isolate RPC stub — without explicit disposal it stays
285
- * referenced until the parent isolate's event-handler context
286
- * finishes, which during npm install means "until the whole install
287
- * completes" — long enough to accumulate alongside other leaked
288
- * stubs and trip the QueueState::ACTIVE fatal.
289
- *
290
- * Safe to call more than once; idempotent.
291
- */
292
- dispose(): void;
293
- }
294
- //# sourceMappingURL=loader-pool.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"loader-pool.d.ts","sourceRoot":"","sources":["../../src/loaders/loader-pool.ts"],"names":[],"mappings":"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"}