@nimbus-sh/fabric 0.3.0 → 0.5.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.
@@ -1,3 +1,3 @@
1
- export { adoptCtxExports, composeFabric, getCtxExports, stagedBootAssembler, supervisorEntrypoint, supervisorEntrypointName, } from '@nimbus-sh/platform/composition.js';
2
- export type { CtxExports, EntrypointLoopbackFactory, FabricComposition, StagedBootAssembler, } from '@nimbus-sh/platform/composition.js';
1
+ export * from '@nimbus-sh/platform/composition.js';
2
+ export type * from '@nimbus-sh/platform/composition.js';
3
3
  //# sourceMappingURL=composition.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"composition.d.ts","sourceRoot":"","sources":["../src/composition.ts"],"names":[],"mappings":"AACA,OAAO,EACL,eAAe,EACf,aAAa,EACb,aAAa,EACb,mBAAmB,EACnB,oBAAoB,EACpB,wBAAwB,GACzB,MAAM,oCAAoC,CAAC;AAE5C,YAAY,EACV,UAAU,EACV,yBAAyB,EACzB,iBAAiB,EACjB,mBAAmB,GACpB,MAAM,oCAAoC,CAAC"}
1
+ {"version":3,"file":"composition.d.ts","sourceRoot":"","sources":["../src/composition.ts"],"names":[],"mappings":"AACA,cAAc,oCAAoC,CAAC;AACnD,mBAAmB,oCAAoC,CAAC"}
@@ -1,2 +1,2 @@
1
1
  // Core and fabric share one holder without introducing a dependency cycle.
2
- export { adoptCtxExports, composeFabric, getCtxExports, stagedBootAssembler, supervisorEntrypoint, supervisorEntrypointName, } from '@nimbus-sh/platform/composition.js';
2
+ export * from '@nimbus-sh/platform/composition.js';
@@ -1 +1 @@
1
- {"version":3,"file":"fenced-work.d.ts","sourceRoot":"","sources":["../src/fenced-work.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"fenced-work.d.ts","sourceRoot":"","sources":["../src/fenced-work.ts"],"names":[],"mappings":"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"}
@@ -145,6 +145,11 @@ export class FencedWork {
145
145
  let inflight = this.drives.get(key);
146
146
  if (inflight === undefined) {
147
147
  inflight = (async () => {
148
+ if (record.attempt >= FENCED_WORK_MAX_ATTEMPT) {
149
+ this.host.onAbandoned?.(record);
150
+ await this.supersede(key);
151
+ return true;
152
+ }
148
153
  let failed = false;
149
154
  try {
150
155
  await this.host.redrive(record, record.attempt + 1);
@@ -235,6 +240,7 @@ export class FencedWork {
235
240
  * a previous generation the terminal hook will never fire for.
236
241
  */
237
242
  async supersede(key) {
243
+ this.journalledPids.delete(Number(key.slice(FENCED_WORK_KEY_PREFIX.length)));
238
244
  await this.storage.delete(key);
239
245
  await this.storage.sync();
240
246
  }
@@ -14,6 +14,12 @@
14
14
  * counter, and bumps it once. The embedder reads `generation(ctx)` wherever it
15
15
  * needs the incarnation number — one source of truth instead of mirrored host
16
16
  * fields.
17
+ *
18
+ * This is also the allocator for a workspace's generation: an embedder
19
+ * composing NimbusWorkspace over its own Durable Object calls
20
+ * `adoptGeneration(ctx)` once per incarnation and passes `generation(ctx)`
21
+ * as the workspace's `generation` — the same persisted counter the session
22
+ * derives its pid generation from, so there is no second counter to keep.
17
23
  */
18
24
  /**
19
25
  * Storage key for the isolate-generation counter (cold-start +
@@ -1 +1 @@
1
- {"version":3,"file":"generation.d.ts","sourceRoot":"","sources":["../src/generation.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAIH;;;;;;;GAOG;AACH,eAAO,MAAM,cAAc,mBAAmB,CAAC;AAE/C,2DAA2D;AAC3D,MAAM,WAAW,iBAAiB;IAChC,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IACnC,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACjD;AAED,qEAAqE;AACrE,MAAM,WAAW,iBAAiB;IAChC,OAAO,EAAE,iBAAiB,CAAC;CAC5B;AAsBD,6EAA6E;AAC7E,wBAAgB,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAE9C;AAED,yEAAyE;AACzE,wBAAsB,eAAe,CAAC,GAAG,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC,CA0B3E;AAED;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAIjE;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,GAAG,IAAI,CAE3E;AAED;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAYvD"}
1
+ {"version":3,"file":"generation.d.ts","sourceRoot":"","sources":["../src/generation.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAIH;;;;;;;GAOG;AACH,eAAO,MAAM,cAAc,mBAAmB,CAAC;AAE/C,2DAA2D;AAC3D,MAAM,WAAW,iBAAiB;IAChC,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IACnC,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACjD;AAED,qEAAqE;AACrE,MAAM,WAAW,iBAAiB;IAChC,OAAO,EAAE,iBAAiB,CAAC;CAC5B;AAsBD,6EAA6E;AAC7E,wBAAgB,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAE9C;AAED,yEAAyE;AACzE,wBAAsB,eAAe,CAAC,GAAG,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC,CA0B3E;AAED;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAIjE;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,GAAG,IAAI,CAE3E;AAED;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAYvD"}
@@ -14,6 +14,12 @@
14
14
  * counter, and bumps it once. The embedder reads `generation(ctx)` wherever it
15
15
  * needs the incarnation number — one source of truth instead of mirrored host
16
16
  * fields.
17
+ *
18
+ * This is also the allocator for a workspace's generation: an embedder
19
+ * composing NimbusWorkspace over its own Durable Object calls
20
+ * `adoptGeneration(ctx)` once per incarnation and passes `generation(ctx)`
21
+ * as the workspace's `generation` — the same persisted counter the session
22
+ * derives its pid generation from, so there is no second counter to keep.
17
23
  */
18
24
  import { errorText } from '@nimbus-sh/core/_shared/error-text.js';
19
25
  /**
@@ -92,10 +92,20 @@ export declare class ImageStore {
92
92
  * Materialize generated module sources in the content-addressed image store
93
93
  * and return the module-name → path map naming them.
94
94
  *
95
+ * Takes the images as a SEQUENCE, produced on demand and released as each
96
+ * one's slices land, so exactly one image's text is resident here. The old
97
+ * record-shaped parameter held every source for the whole call, and the
98
+ * caller held its own copy beside it: measured on a real-vite launch, the
99
+ * second image reported not one slice — ~25 MB of module text as UTF-16 in
100
+ * two places, plus the first image's just-freed 10.36 MB encode buffer, on a
101
+ * 128 MiB isolate. Yielding and dropping is what makes the peak one image
102
+ * instead of all of them; it is not a pacing question, and a fresh turn does
103
+ * not shrink a live heap.
104
+ *
95
105
  * Writing the sources here, once, is what lets the session stop holding
96
106
  * them: after this returns, the only thing it keeps is a path.
97
107
  */
98
- materialize(pid: number, modules: Record<string, string>, pacer: TurnBudget): Promise<Record<string, string>>;
108
+ materialize(pid: number, images: AsyncIterable<readonly [string, string]> | Iterable<readonly [string, string]>, pacer: TurnBudget): Promise<Record<string, string>>;
99
109
  /**
100
110
  * Drop every image no running process boots from.
101
111
  *
@@ -1 +1 @@
1
- {"version":3,"file":"image-store.d.ts","sourceRoot":"","sources":["../src/image-store.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAIH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAEnD;;;;;;;;;GASG;AACH,eAAO,MAAM,6BAA6B,QAA0D,CAAC;AAErG;;;;;GAKG;AACH,MAAM,WAAW,cAAc;IAC7B,iEAAiE;IACjE,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,wEAAwE;IACxE,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAC;IACpC,wEAAwE;IACxE,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,UAAU,GAAG,IAAI,CAAC;IACjD,kDAAkD;IAClD,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,UAAU,GAAG,IAAI,CAAC;IAClE,2EAA2E;IAC3E,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;IAC5B,qDAAqD;IACrD,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;CAC5B;AAED;;;;;;;GAOG;AACH,qBAAa,UAAU;IAanB,OAAO,CAAC,QAAQ,CAAC,KAAK;IACtB,OAAO,CAAC,QAAQ,CAAC,MAAM;IAbzB,8EAA8E;IAC9E,OAAO,CAAC,cAAc,CAA+B;IACrD,OAAO,CAAC,QAAQ,CAAS;IAEzB;;;;;;OAMG;gBAEgB,KAAK,EAAE,MAAM,cAAc,EAC3B,MAAM,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO;IAGnD;;;;;;;;;;;;;;;;OAgBG;IACH,SAAS,IAAI,IAAI;IAOjB;;;;;;OAMG;IACG,WAAW,CACf,GAAG,EAAE,MAAM,EACX,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAC/B,KAAK,EAAE,UAAU,GAChB,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAoDlC;;;;;;;;;;OAUG;IACH,OAAO,CAAC,KAAK;CAgBd"}
1
+ {"version":3,"file":"image-store.d.ts","sourceRoot":"","sources":["../src/image-store.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAIH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAEnD;;;;;;;;;GASG;AACH,eAAO,MAAM,6BAA6B,QAA0D,CAAC;AAErG;;;;;GAKG;AACH,MAAM,WAAW,cAAc;IAC7B,iEAAiE;IACjE,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,wEAAwE;IACxE,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAC;IACpC,wEAAwE;IACxE,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,UAAU,GAAG,IAAI,CAAC;IACjD,kDAAkD;IAClD,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,UAAU,GAAG,IAAI,CAAC;IAClE,2EAA2E;IAC3E,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;IAC5B,qDAAqD;IACrD,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;CAC5B;AAED;;;;;;;GAOG;AACH,qBAAa,UAAU;IAanB,OAAO,CAAC,QAAQ,CAAC,KAAK;IACtB,OAAO,CAAC,QAAQ,CAAC,MAAM;IAbzB,8EAA8E;IAC9E,OAAO,CAAC,cAAc,CAA+B;IACrD,OAAO,CAAC,QAAQ,CAAS;IAEzB;;;;;;OAMG;gBAEgB,KAAK,EAAE,MAAM,cAAc,EAC3B,MAAM,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO;IAGnD;;;;;;;;;;;;;;;;OAgBG;IACH,SAAS,IAAI,IAAI;IAOjB;;;;;;;;;;;;;;;;OAgBG;IACG,WAAW,CACf,GAAG,EAAE,MAAM,EACX,MAAM,EAAE,aAAa,CAAC,SAAS,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,GAAG,QAAQ,CAAC,SAAS,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,EACtF,KAAK,EAAE,UAAU,GAChB,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAuDlC;;;;;;;;;;OAUG;IACH,OAAO,CAAC,KAAK;CAgBd"}
@@ -84,32 +84,42 @@ export class ImageStore {
84
84
  * Materialize generated module sources in the content-addressed image store
85
85
  * and return the module-name → path map naming them.
86
86
  *
87
+ * Takes the images as a SEQUENCE, produced on demand and released as each
88
+ * one's slices land, so exactly one image's text is resident here. The old
89
+ * record-shaped parameter held every source for the whole call, and the
90
+ * caller held its own copy beside it: measured on a real-vite launch, the
91
+ * second image reported not one slice — ~25 MB of module text as UTF-16 in
92
+ * two places, plus the first image's just-freed 10.36 MB encode buffer, on a
93
+ * 128 MiB isolate. Yielding and dropping is what makes the peak one image
94
+ * instead of all of them; it is not a pacing question, and a fresh turn does
95
+ * not shrink a live heap.
96
+ *
87
97
  * Writing the sources here, once, is what lets the session stop holding
88
98
  * them: after this returns, the only thing it keeps is a path.
89
99
  */
90
- async materialize(pid, modules, pacer) {
100
+ async materialize(pid, images, pacer) {
91
101
  const fs = this.blobs();
92
- const images = {};
93
- const sources = new Map();
94
- for (const [moduleName, source] of Object.entries(modules)) {
95
- const path = facetImagePath(await facetImageDigest(source));
96
- images[moduleName] = path;
97
- sources.set(path, source);
98
- await pacer.spend(source.length);
99
- }
100
- // Register the WHOLE root set here, in one synchronous step, before any
101
- // byte of it exists on disk. That ordering is the entire protocol between
102
- // a launch and the sweep: an image is rooted from before it is written, so
103
- // a sweep can never observe a file this launch has written but not yet
104
- // claimed. The old loop achieved it by not awaiting at all, which read as
105
- // "the writes must not be interrupted" — they may be. What must not be
106
- // interrupted is the gap between writing and rooting, and rooting first
107
- // closes it for every write that follows, however many turns they span.
108
- this.residentImages.set(pid, [...sources.keys()]);
102
+ const paths = {};
103
+ // The root set is this ARRAY, held by the sweep's map from before the
104
+ // first byte and appended to as each image is named. Rooting an image
105
+ // before its own first byte is the whole of the protocol: a sweep that
106
+ // runs while this launch is suspended sees every image already written as
107
+ // rooted, and an image not yet written is not yet a file. The array
108
+ // identity is what makes an append visible — do not replace it.
109
+ const rooted = [];
110
+ this.residentImages.set(pid, rooted);
109
111
  fs.mkdirp(FACET_IMAGE_DIR);
110
- for (const [path, source] of sources) {
112
+ let count = 0;
113
+ for await (const [moduleName, source] of images) {
114
+ const path = facetImagePath(await facetImageDigest(source));
115
+ paths[moduleName] = path;
116
+ rooted.push(path);
117
+ count++;
111
118
  const stored = path.replace(/^\/+/, '');
112
119
  const bytes = new TextEncoder().encode(source);
120
+ console.log('[image-store] pid=' + pid + ' image ' + count + ' ' + moduleName + ' → '
121
+ + path.slice(-12) + ' ' + bytes.byteLength + ' bytes, slice=' + FACET_IMAGE_WRITE_SLICE_BYTES
122
+ + ' turns=' + pacer.chunks);
113
123
  // An image at its full size is a COMPLETE one: a write only ever grows
114
124
  // the file from offset zero, so a write cut short by a reset leaves a
115
125
  // strictly shorter file and fails this test. Size is enough of a check
@@ -138,7 +148,8 @@ export class ImageStore {
138
148
  } while (offset < bytes.byteLength);
139
149
  }
140
150
  this.sweep(fs);
141
- return images;
151
+ console.log('[image-store] pid=' + pid + ' materialized ' + count + ' image(s) in ' + pacer.chunks + ' turn(s)');
152
+ return paths;
142
153
  }
143
154
  /**
144
155
  * Drop every image no running process boots from.
@@ -235,6 +235,18 @@ export declare class IsolatePool {
235
235
  private readonly defaultRetries;
236
236
  private readonly tag;
237
237
  private readonly slotGenerations;
238
+ /**
239
+ * Per-slot execution ownership: the tail of each slot's in-flight
240
+ * dispatch chain. Two dispatches on the same warm isolate at once
241
+ * interleave on its QueueState — map() callers used to trust the
242
+ * caller's slot round-robin, which could not prevent submit() (slot
243
+ * 0) or a second map() landing on a slot a task still occupied. Every
244
+ * dispatch now waits for the slot's previous execution to settle
245
+ * before touching it.
246
+ */
247
+ private readonly slotTails;
248
+ /** Set by dispose() — queued dispatches reject instead of running. */
249
+ private disposed;
238
250
  private bindings;
239
251
  private readonly preamble;
240
252
  private readonly preambleHash;
@@ -262,6 +274,13 @@ export declare class IsolatePool {
262
274
  * 12 chars is enough entropy for DO ids to collide-free per process.
263
275
  */
264
276
  private readonly doIdShort;
277
+ /**
278
+ * The supervisor identity the minted worker's env.SUPERVISOR binding
279
+ * bakes (doId short-form + pid), folded into the loader cache key. 's-none'
280
+ * when no SUPERVISOR binding was minted, so a supervisor-less pool keeps
281
+ * the old key shape and its warm slots stay shared.
282
+ */
283
+ private readonly supervisorKey;
265
284
  constructor(env: unknown, ctx: DurableObjectState, opts?: IsolatePoolOptions);
266
285
  /** Effective concurrency used when no per-call override is supplied. */
267
286
  get defaultConcurrency(): number;
@@ -1 +1 @@
1
- {"version":3,"file":"isolate-pool.d.ts","sourceRoot":"","sources":["../src/isolate-pool.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"isolate-pool.d.ts","sourceRoot":"","sources":["../src/isolate-pool.ts"],"names":[],"mappings":"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"}
@@ -113,6 +113,18 @@ export class IsolatePool {
113
113
  defaultRetries;
114
114
  tag;
115
115
  slotGenerations = new Map();
116
+ /**
117
+ * Per-slot execution ownership: the tail of each slot's in-flight
118
+ * dispatch chain. Two dispatches on the same warm isolate at once
119
+ * interleave on its QueueState — map() callers used to trust the
120
+ * caller's slot round-robin, which could not prevent submit() (slot
121
+ * 0) or a second map() landing on a slot a task still occupied. Every
122
+ * dispatch now waits for the slot's previous execution to settle
123
+ * before touching it.
124
+ */
125
+ slotTails = new Map();
126
+ /** Set by dispose() — queued dispatches reject instead of running. */
127
+ disposed = false;
116
128
  bindings;
117
129
  preamble;
118
130
  preambleHash;
@@ -140,6 +152,13 @@ export class IsolatePool {
140
152
  * 12 chars is enough entropy for DO ids to collide-free per process.
141
153
  */
142
154
  doIdShort;
155
+ /**
156
+ * The supervisor identity the minted worker's env.SUPERVISOR binding
157
+ * bakes (doId short-form + pid), folded into the loader cache key. 's-none'
158
+ * when no SUPERVISOR binding was minted, so a supervisor-less pool keeps
159
+ * the old key shape and its warm slots stay shared.
160
+ */
161
+ supervisorKey;
143
162
  constructor(env, ctx, opts) {
144
163
  // A host hands its whole env over; the binding is claimed here and the
145
164
  // claim is checked on the next line.
@@ -207,6 +226,7 @@ export class IsolatePool {
207
226
  this.wasmHash = hashSource(fp);
208
227
  }
209
228
  const bindings = { ...(opts?.extraBindings ?? {}) };
229
+ this.supervisorKey = 's-none';
210
230
  if (!opts?.omitSupervisor) {
211
231
  const supervisorRpc = supervisorEntrypoint();
212
232
  if (supervisorRpc) {
@@ -215,9 +235,19 @@ export class IsolatePool {
215
235
  // to the user's session DO, not the peer DO. Default to the
216
236
  // local ctx.id (single-DO callers and the in-DO in-DO fanout path).
217
237
  const supDoId = opts?.supervisorDoIdOverride ?? ctx.id.toString();
238
+ const supPid = opts?.supervisorPid ?? 0;
218
239
  bindings.SUPERVISOR = supervisorRpc({
219
- props: { doId: supDoId, pid: opts?.supervisorPid ?? 0 },
240
+ props: { doId: supDoId, pid: supPid },
220
241
  });
242
+ // Whatever the minted worker's env carries must be in its loader
243
+ // cache key — workerd's loader cache survives a DO hibernation
244
+ // wake while generation-strided pids (1000001 → 2000001) do not:
245
+ // a warm slot keyed without the supervisor identity returns in
246
+ // the new generation still credentialed to the dead pid, and
247
+ // every pid-authorized RPC from it fails "process pid … does
248
+ // not exist". doIdShort alone cannot cover this — it changes
249
+ // across sessions, not across wakes of the same session.
250
+ this.supervisorKey = `s${supDoId.slice(0, 12)}-${supPid}`;
221
251
  }
222
252
  else {
223
253
  // Supervisor entrypoint unavailable — running without ctx.exports
@@ -398,8 +428,35 @@ export class IsolatePool {
398
428
  /**
399
429
  * Dispatch a single task to the slot isolate. `slotIndex` picks which
400
430
  * warm isolate services the call; callers round-robin slots themselves.
431
+ * Serialized per slot: this call waits for the slot's previous
432
+ * execution to settle before touching the warm isolate.
401
433
  */
402
434
  async #dispatchSlot(fnSource, fnHash, slotIndex, args, resilience, perCallWasm) {
435
+ // A warm slot executes one dispatch at a time: queue behind the
436
+ // previous owner, then record this dispatch as the new tail. The
437
+ // tail outlives the caller's outcome: a timeout rejects the
438
+ // Promise.race while runOnce's RPC is still live on the Worker, so
439
+ // release waits for every execution the owned body started to
440
+ // settle — never the caller's settle alone.
441
+ const previous = this.slotTails.get(slotIndex) ?? Promise.resolve();
442
+ let release;
443
+ this.slotTails.set(slotIndex, new Promise((resolve) => { release = resolve; }));
444
+ await previous;
445
+ if (this.disposed) {
446
+ release();
447
+ throw new BindingError(`IsolatePool(${this.tag}) is disposed`);
448
+ }
449
+ const inFlight = [];
450
+ try {
451
+ return await this.#dispatchSlotOwned(fnSource, fnHash, slotIndex, args, resilience, perCallWasm, inFlight);
452
+ }
453
+ finally {
454
+ // Do not delay the caller's own outcome — the tail releases when
455
+ // the RPCs the body launched have actually settled.
456
+ void Promise.allSettled(inFlight).then(() => release());
457
+ }
458
+ }
459
+ async #dispatchSlotOwned(fnSource, fnHash, slotIndex, args, resilience, perCallWasm, inFlight) {
403
460
  // Per-call wasm fingerprint. Mixed into the cache key so two calls
404
461
  // with different bytes hit different slots (no cache poisoning).
405
462
  // For the common case (no per-call wasm) the fingerprint is '0',
@@ -407,11 +464,11 @@ export class IsolatePool {
407
464
  const perCallWasmEntries = this.#materialisePerCallWasm(perCallWasm);
408
465
  const perCallWasmHash = this.#fingerprintWasm(perCallWasmEntries);
409
466
  // Cache key includes the short DO id so warm isolates are scoped to
410
- // ONE session. See the doIdShort field comment for why — without it,
411
- // a later session's pool reuses the warm worker from a previous
412
- // session (which still carries the old session's env.SUPERVISOR
413
- // binding), and writeBatch RPCs land in the wrong DO's VFS.
414
- const buildId = (generation) => `nfp:${this.tag}:${this.doIdShort}:${fnHash}:${this.preambleHash}:${this.wasmHash}:${perCallWasmHash}:slot-${slotIndex}:g${generation}`;
467
+ // ONE session (see the doIdShort field comment), and the supervisor
468
+ // identity so a wake of that same session can never reuse a warm
469
+ // worker whose SUPERVISOR binding still names the dead generation's
470
+ // pid. See the supervisorKey field comment for the failure mode.
471
+ const buildId = (generation) => `nfp:${this.tag}:${this.doIdShort}:${fnHash}:${this.preambleHash}:${this.wasmHash}:${perCallWasmHash}:${this.supervisorKey}:slot-${slotIndex}:g${generation}`;
415
472
  let id = buildId(this.slotGenerations.get(slotIndex) ?? 0);
416
473
  const code = this.#buildCode(fnSource, perCallWasmEntries);
417
474
  // W5 Lever 5: record the dispatch so /api/_diag/memory shows the
@@ -489,8 +546,10 @@ export class IsolatePool {
489
546
  // See plan in close-plan-2026-04-28.
490
547
  let timerId;
491
548
  try {
549
+ const attemptPromise = runOnce();
550
+ inFlight.push(attemptPromise);
492
551
  return await Promise.race([
493
- runOnce(),
552
+ attemptPromise,
494
553
  new Promise((_, reject) => {
495
554
  timerId = setTimeout(() => reject(new TimeoutError(resilience.timeoutMs)), resilience.timeoutMs);
496
555
  }),
@@ -501,7 +560,9 @@ export class IsolatePool {
501
560
  clearTimeout(timerId);
502
561
  }
503
562
  }
504
- return await runOnce();
563
+ const attemptPromise = runOnce();
564
+ inFlight.push(attemptPromise);
565
+ return await attemptPromise;
505
566
  }
506
567
  catch (err) {
507
568
  lastError = err instanceof Error ? err : new Error(String(err));
@@ -655,6 +716,7 @@ export class IsolatePool {
655
716
  * Safe to call more than once; idempotent.
656
717
  */
657
718
  dispose() {
719
+ this.disposed = true;
658
720
  if (!this.bindings)
659
721
  return;
660
722
  for (const key of Object.keys(this.bindings)) {
@@ -58,10 +58,10 @@ export declare const TURN_CHUNK_MAX_BYTES = 2000000;
58
58
  * spent.
59
59
  *
60
60
  * Callers report the work they are about to do or have just done and await
61
- * the result; a pacer that is not yielding returns without suspending, so the
62
- * one-shot exec path — which passes no pacer at all — keeps its exact
63
- * behaviour and cost. Nothing here decides WHAT the launch does, only where it
64
- * is allowed to stop.
61
+ * the result; a pacer that is not yielding returns without suspending, so a
62
+ * build small enough to fit in one chunk — most one-shot execs, every cache
63
+ * hit — keeps its exact behaviour and cost. Nothing here decides WHAT the
64
+ * launch does, only where it is allowed to stop.
65
65
  */
66
66
  export declare class TurnBudget {
67
67
  private readonly scheduler;
@@ -114,7 +114,7 @@ export interface PacedWorkHost {
114
114
  * reason. Without it the pump degrades to a same-context timer — see
115
115
  * {@link PacedWork.nextTurn}.
116
116
  */
117
- requestTurn?: () => void;
117
+ requestTurn?: (notBefore?: number) => void;
118
118
  }
119
119
  /**
120
120
  * The granting side of {@link TurnScheduler}: parks suspended launches
@@ -149,7 +149,7 @@ export declare class PacedWork implements TurnScheduler {
149
149
  * always performed, so a harness or a runtime without alarms loses the
150
150
  * responsiveness but keeps the behaviour.
151
151
  */
152
- nextTurn(chunkEnded: Promise<void>): Promise<void>;
152
+ nextTurn(chunkEnded: Promise<void>, notBefore?: number): Promise<void>;
153
153
  /**
154
154
  * Run one chunk of every launch waiting for a turn.
155
155
  *
@@ -1 +1 @@
1
- {"version":3,"file":"turn-budget.d.ts","sourceRoot":"","sources":["../src/turn-budget.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AAIH,qEAAqE;AACrE,MAAM,WAAW,aAAa;IAC5B;;;;;;;OAOG;IACH,QAAQ,CAAC,UAAU,EAAE,OAAO,CAAC,IAAI,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACpD;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,oBAAoB,UAAY,CAAC;AAE9C;;;;;;;;;GASG;AACH,qBAAa,UAAU;IAkBnB,OAAO,CAAC,QAAQ,CAAC,SAAS;IAC1B,OAAO,CAAC,QAAQ,CAAC,aAAa;IAC9B,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAC;IAnB/B,qEAAqE;IACrE,MAAM,SAAK;IACX,iDAAiD;IACjD,KAAK,SAAK;IAEV,OAAO,CAAC,KAAK,CAAK;IAClB,OAAO,CAAC,UAAU,CAA8D;IAEhF;;;;;;;OAOG;gBAEgB,SAAS,EAAE,aAAa,EACxB,aAAa,GAAE,MAA6B,EAC5C,WAAW,CAAC,GAAE,MAAM,IAAI,aAAA;IAG3C;;;;;;OAMG;IACG,KAAK,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAczC;;;;OAIG;IACH,MAAM,IAAI,IAAI;CAIf;AAED,mEAAmE;AACnE,wBAAgB,aAAa,CAAC,CAAC,GAAG,IAAI,KAAK;IAAE,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC;IAAC,OAAO,EAAE,CAAC,KAAK,EAAE,CAAC,GAAG,WAAW,CAAC,CAAC,CAAC,KAAK,IAAI,CAAA;CAAE,CAI/G;AAED,uEAAuE;AACvE,MAAM,WAAW,aAAa;IAC5B;;;;;;;;;OASG;IACH,WAAW,CAAC,EAAE,MAAM,IAAI,CAAC;CAC1B;AAED;;;GAGG;AACH,qBAAa,SAAU,YAAW,aAAa;IAkB3C,OAAO,CAAC,QAAQ,CAAC,GAAG;IACpB,OAAO,CAAC,QAAQ,CAAC,IAAI;IAlBvB;;;;;;;;;OASG;IACH,OAAO,CAAC,OAAO,CAAgE;IAE/E;;;OAGG;gBAEgB,GAAG,EAAE,MAAM,EACX,IAAI,EAAE,aAAa;IAGtC;;;;;;;;;OASG;IACH,QAAQ,CAAC,UAAU,EAAE,OAAO,CAAC,IAAI,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC;IAWlD;;;;;;;OAOG;IACG,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAa3B,0DAA0D;IAC1D,IAAI,UAAU,IAAI,OAAO,CAExB;CACF;AAED;;;;;;;;;GASG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,OAAO,GAAG,MAAM,CAMtD"}
1
+ {"version":3,"file":"turn-budget.d.ts","sourceRoot":"","sources":["../src/turn-budget.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AAIH,qEAAqE;AACrE,MAAM,WAAW,aAAa;IAC5B;;;;;;;OAOG;IACH,QAAQ,CAAC,UAAU,EAAE,OAAO,CAAC,IAAI,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACpD;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,oBAAoB,UAAY,CAAC;AAE9C;;;;;;;;;GASG;AACH,qBAAa,UAAU;IAkBnB,OAAO,CAAC,QAAQ,CAAC,SAAS;IAC1B,OAAO,CAAC,QAAQ,CAAC,aAAa;IAC9B,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAC;IAnB/B,qEAAqE;IACrE,MAAM,SAAK;IACX,iDAAiD;IACjD,KAAK,SAAK;IAEV,OAAO,CAAC,KAAK,CAAK;IAClB,OAAO,CAAC,UAAU,CAA8D;IAEhF;;;;;;;OAOG;gBAEgB,SAAS,EAAE,aAAa,EACxB,aAAa,GAAE,MAA6B,EAC5C,WAAW,CAAC,GAAE,MAAM,IAAI,aAAA;IAG3C;;;;;;OAMG;IACG,KAAK,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAczC;;;;OAIG;IACH,MAAM,IAAI,IAAI;CAIf;AAED,mEAAmE;AACnE,wBAAgB,aAAa,CAAC,CAAC,GAAG,IAAI,KAAK;IAAE,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC;IAAC,OAAO,EAAE,CAAC,KAAK,EAAE,CAAC,GAAG,WAAW,CAAC,CAAC,CAAC,KAAK,IAAI,CAAA;CAAE,CAI/G;AAED,uEAAuE;AACvE,MAAM,WAAW,aAAa;IAC5B;;;;;;;;;OASG;IACH,WAAW,CAAC,EAAE,CAAC,SAAS,CAAC,EAAE,MAAM,KAAK,IAAI,CAAC;CAC5C;AAED;;;GAGG;AACH,qBAAa,SAAU,YAAW,aAAa;IAkB3C,OAAO,CAAC,QAAQ,CAAC,GAAG;IACpB,OAAO,CAAC,QAAQ,CAAC,IAAI;IAlBvB;;;;;;;;;OASG;IACH,OAAO,CAAC,OAAO,CAAmF;IAElG;;;OAGG;gBAEgB,GAAG,EAAE,MAAM,EACX,IAAI,EAAE,aAAa;IAGtC;;;;;;;;;OASG;IACH,QAAQ,CAAC,UAAU,EAAE,OAAO,CAAC,IAAI,CAAC,EAAE,SAAS,SAAI,GAAG,OAAO,CAAC,IAAI,CAAC;IAWjE;;;;;;;OAOG;IACG,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAe3B,0DAA0D;IAC1D,IAAI,UAAU,IAAI,OAAO,CAExB;CACF;AAED;;;;;;;;;GASG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,OAAO,GAAG,MAAM,CAMtD"}
@@ -47,10 +47,10 @@ export const TURN_CHUNK_MAX_BYTES = 2_000_000;
47
47
  * spent.
48
48
  *
49
49
  * Callers report the work they are about to do or have just done and await
50
- * the result; a pacer that is not yielding returns without suspending, so the
51
- * one-shot exec path — which passes no pacer at all — keeps its exact
52
- * behaviour and cost. Nothing here decides WHAT the launch does, only where it
53
- * is allowed to stop.
50
+ * the result; a pacer that is not yielding returns without suspending, so a
51
+ * build small enough to fit in one chunk — most one-shot execs, every cache
52
+ * hit — keeps its exact behaviour and cost. Nothing here decides WHAT the
53
+ * launch does, only where it is allowed to stop.
54
54
  */
55
55
  export class TurnBudget {
56
56
  scheduler;
@@ -148,14 +148,14 @@ export class PacedWork {
148
148
  * always performed, so a harness or a runtime without alarms loses the
149
149
  * responsiveness but keeps the behaviour.
150
150
  */
151
- nextTurn(chunkEnded) {
151
+ nextTurn(chunkEnded, notBefore = 0) {
152
152
  return new Promise((resume) => {
153
- this.waiters.push({ resume, chunkEnded });
153
+ this.waiters.push({ resume, chunkEnded, notBefore });
154
154
  if (this.host.requestTurn) {
155
- this.host.requestTurn();
155
+ this.host.requestTurn(notBefore);
156
156
  return;
157
157
  }
158
- setTimeout(() => { void this.pump(); }, 0);
158
+ setTimeout(() => { void this.pump(); }, Math.max(0, notBefore - Date.now()));
159
159
  });
160
160
  }
161
161
  /**
@@ -172,10 +172,13 @@ export class PacedWork {
172
172
  // is what an alarm calls, and the first turn after a reset is the
173
173
  // re-delivered alarm of a launch the reset interrupted.
174
174
  await runColdStart(this.ctx);
175
- const waiting = this.waiters;
175
+ const now = Date.now();
176
+ const waiting = this.waiters.filter((waiter) => waiter.notBefore <= now);
177
+ this.waiters = this.waiters.filter((waiter) => waiter.notBefore > now);
178
+ if (this.waiters.length > 0)
179
+ this.host.requestTurn?.(Math.min(...this.waiters.map((waiter) => waiter.notBefore)));
176
180
  if (waiting.length === 0)
177
181
  return;
178
- this.waiters = [];
179
182
  for (const waiter of waiting)
180
183
  waiter.resume();
181
184
  await Promise.all(waiting.map((waiter) => waiter.chunkEnded));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nimbus-sh/fabric",
3
- "version": "0.3.0",
3
+ "version": "0.5.0",
4
4
  "description": "The Cloudflare half of Nimbus — Durable Object facet hosting, dynamic-worker loader pools, the resident-process fabric, and DO alarm/hibernation machinery.",
5
5
  "keywords": [
6
6
  "cloudflare",
@@ -50,8 +50,8 @@
50
50
  "typecheck": "tsc --noEmit"
51
51
  },
52
52
  "dependencies": {
53
- "@nimbus-sh/core": "^0.7.0",
54
- "@nimbus-sh/platform": "^0.2.0",
53
+ "@nimbus-sh/core": "^0.9.0",
54
+ "@nimbus-sh/platform": "^0.4.0",
55
55
  "zod": "^4.4.3"
56
56
  },
57
57
  "devDependencies": {
@@ -1,16 +1,3 @@
1
1
  // Core and fabric share one holder without introducing a dependency cycle.
2
- export {
3
- adoptCtxExports,
4
- composeFabric,
5
- getCtxExports,
6
- stagedBootAssembler,
7
- supervisorEntrypoint,
8
- supervisorEntrypointName,
9
- } from '@nimbus-sh/platform/composition.js';
10
-
11
- export type {
12
- CtxExports,
13
- EntrypointLoopbackFactory,
14
- FabricComposition,
15
- StagedBootAssembler,
16
- } from '@nimbus-sh/platform/composition.js';
2
+ export * from '@nimbus-sh/platform/composition.js';
3
+ export type * from '@nimbus-sh/platform/composition.js';
@@ -225,6 +225,11 @@ export class FencedWork<R extends FencedWorkRecord> {
225
225
  let inflight = this.drives.get(key);
226
226
  if (inflight === undefined) {
227
227
  inflight = (async (): Promise<boolean> => {
228
+ if (record.attempt >= FENCED_WORK_MAX_ATTEMPT) {
229
+ this.host.onAbandoned?.(record);
230
+ await this.supersede(key);
231
+ return true;
232
+ }
228
233
  let failed = false;
229
234
  try {
230
235
  await this.host.redrive(record, record.attempt + 1);
@@ -310,6 +315,7 @@ export class FencedWork<R extends FencedWorkRecord> {
310
315
  * a previous generation the terminal hook will never fire for.
311
316
  */
312
317
  private async supersede(key: string): Promise<void> {
318
+ this.journalledPids.delete(Number(key.slice(FENCED_WORK_KEY_PREFIX.length)));
313
319
  await this.storage.delete(key);
314
320
  await this.storage.sync();
315
321
  }
package/src/generation.ts CHANGED
@@ -14,6 +14,12 @@
14
14
  * counter, and bumps it once. The embedder reads `generation(ctx)` wherever it
15
15
  * needs the incarnation number — one source of truth instead of mirrored host
16
16
  * fields.
17
+ *
18
+ * This is also the allocator for a workspace's generation: an embedder
19
+ * composing NimbusWorkspace over its own Durable Object calls
20
+ * `adoptGeneration(ctx)` once per incarnation and passes `generation(ctx)`
21
+ * as the workspace's `generation` — the same persisted counter the session
22
+ * derives its pid generation from, so there is no second counter to keep.
17
23
  */
18
24
 
19
25
  import { errorText } from '@nimbus-sh/core/_shared/error-text.js';
@@ -107,36 +107,48 @@ export class ImageStore {
107
107
  * Materialize generated module sources in the content-addressed image store
108
108
  * and return the module-name → path map naming them.
109
109
  *
110
+ * Takes the images as a SEQUENCE, produced on demand and released as each
111
+ * one's slices land, so exactly one image's text is resident here. The old
112
+ * record-shaped parameter held every source for the whole call, and the
113
+ * caller held its own copy beside it: measured on a real-vite launch, the
114
+ * second image reported not one slice — ~25 MB of module text as UTF-16 in
115
+ * two places, plus the first image's just-freed 10.36 MB encode buffer, on a
116
+ * 128 MiB isolate. Yielding and dropping is what makes the peak one image
117
+ * instead of all of them; it is not a pacing question, and a fresh turn does
118
+ * not shrink a live heap.
119
+ *
110
120
  * Writing the sources here, once, is what lets the session stop holding
111
121
  * them: after this returns, the only thing it keeps is a path.
112
122
  */
113
123
  async materialize(
114
124
  pid: number,
115
- modules: Record<string, string>,
125
+ images: AsyncIterable<readonly [string, string]> | Iterable<readonly [string, string]>,
116
126
  pacer: TurnBudget,
117
127
  ): Promise<Record<string, string>> {
118
128
  const fs = this.blobs();
119
- const images: Record<string, string> = {};
120
- const sources = new Map<string, string>();
121
- for (const [moduleName, source] of Object.entries(modules)) {
122
- const path = facetImagePath(await facetImageDigest(source));
123
- images[moduleName] = path;
124
- sources.set(path, source);
125
- await pacer.spend(source.length);
126
- }
127
- // Register the WHOLE root set here, in one synchronous step, before any
128
- // byte of it exists on disk. That ordering is the entire protocol between
129
- // a launch and the sweep: an image is rooted from before it is written, so
130
- // a sweep can never observe a file this launch has written but not yet
131
- // claimed. The old loop achieved it by not awaiting at all, which read as
132
- // "the writes must not be interrupted" — they may be. What must not be
133
- // interrupted is the gap between writing and rooting, and rooting first
134
- // closes it for every write that follows, however many turns they span.
135
- this.residentImages.set(pid, [...sources.keys()]);
129
+ const paths: Record<string, string> = {};
130
+ // The root set is this ARRAY, held by the sweep's map from before the
131
+ // first byte and appended to as each image is named. Rooting an image
132
+ // before its own first byte is the whole of the protocol: a sweep that
133
+ // runs while this launch is suspended sees every image already written as
134
+ // rooted, and an image not yet written is not yet a file. The array
135
+ // identity is what makes an append visible — do not replace it.
136
+ const rooted: string[] = [];
137
+ this.residentImages.set(pid, rooted);
136
138
  fs.mkdirp(FACET_IMAGE_DIR);
137
- for (const [path, source] of sources) {
139
+ let count = 0;
140
+ for await (const [moduleName, source] of images) {
141
+ const path = facetImagePath(await facetImageDigest(source));
142
+ paths[moduleName] = path;
143
+ rooted.push(path);
144
+ count++;
138
145
  const stored = path.replace(/^\/+/, '');
139
146
  const bytes = new TextEncoder().encode(source);
147
+ console.log(
148
+ '[image-store] pid=' + pid + ' image ' + count + ' ' + moduleName + ' → '
149
+ + path.slice(-12) + ' ' + bytes.byteLength + ' bytes, slice=' + FACET_IMAGE_WRITE_SLICE_BYTES
150
+ + ' turns=' + pacer.chunks,
151
+ );
140
152
  // An image at its full size is a COMPLETE one: a write only ever grows
141
153
  // the file from offset zero, so a write cut short by a reset leaves a
142
154
  // strictly shorter file and fails this test. Size is enough of a check
@@ -163,7 +175,8 @@ export class ImageStore {
163
175
  } while (offset < bytes.byteLength);
164
176
  }
165
177
  this.sweep(fs);
166
- return images;
178
+ console.log('[image-store] pid=' + pid + ' materialized ' + count + ' image(s) in ' + pacer.chunks + ' turn(s)');
179
+ return paths;
167
180
  }
168
181
 
169
182
  /**
@@ -345,6 +345,18 @@ export class IsolatePool {
345
345
  private readonly defaultRetries: number;
346
346
  private readonly tag: string;
347
347
  private readonly slotGenerations = new Map<number, number>();
348
+ /**
349
+ * Per-slot execution ownership: the tail of each slot's in-flight
350
+ * dispatch chain. Two dispatches on the same warm isolate at once
351
+ * interleave on its QueueState — map() callers used to trust the
352
+ * caller's slot round-robin, which could not prevent submit() (slot
353
+ * 0) or a second map() landing on a slot a task still occupied. Every
354
+ * dispatch now waits for the slot's previous execution to settle
355
+ * before touching it.
356
+ */
357
+ private readonly slotTails = new Map<number, Promise<void>>();
358
+ /** Set by dispose() — queued dispatches reject instead of running. */
359
+ private disposed = false;
348
360
  private bindings: Record<string, unknown> | undefined;
349
361
 
350
362
  private readonly preamble: string | undefined;
@@ -380,6 +392,13 @@ export class IsolatePool {
380
392
  * 12 chars is enough entropy for DO ids to collide-free per process.
381
393
  */
382
394
  private readonly doIdShort: string;
395
+ /**
396
+ * The supervisor identity the minted worker's env.SUPERVISOR binding
397
+ * bakes (doId short-form + pid), folded into the loader cache key. 's-none'
398
+ * when no SUPERVISOR binding was minted, so a supervisor-less pool keeps
399
+ * the old key shape and its warm slots stay shared.
400
+ */
401
+ private readonly supervisorKey: string;
383
402
 
384
403
  constructor(
385
404
  env: unknown,
@@ -459,6 +478,7 @@ export class IsolatePool {
459
478
  }
460
479
 
461
480
  const bindings: Record<string, unknown> = { ...(opts?.extraBindings ?? {}) };
481
+ this.supervisorKey = 's-none';
462
482
  if (!opts?.omitSupervisor) {
463
483
  const supervisorRpc = supervisorEntrypoint();
464
484
  if (supervisorRpc) {
@@ -467,9 +487,19 @@ export class IsolatePool {
467
487
  // to the user's session DO, not the peer DO. Default to the
468
488
  // local ctx.id (single-DO callers and the in-DO in-DO fanout path).
469
489
  const supDoId = opts?.supervisorDoIdOverride ?? ctx.id.toString();
490
+ const supPid = opts?.supervisorPid ?? 0;
470
491
  bindings.SUPERVISOR = supervisorRpc({
471
- props: { doId: supDoId, pid: opts?.supervisorPid ?? 0 },
492
+ props: { doId: supDoId, pid: supPid },
472
493
  });
494
+ // Whatever the minted worker's env carries must be in its loader
495
+ // cache key — workerd's loader cache survives a DO hibernation
496
+ // wake while generation-strided pids (1000001 → 2000001) do not:
497
+ // a warm slot keyed without the supervisor identity returns in
498
+ // the new generation still credentialed to the dead pid, and
499
+ // every pid-authorized RPC from it fails "process pid … does
500
+ // not exist". doIdShort alone cannot cover this — it changes
501
+ // across sessions, not across wakes of the same session.
502
+ this.supervisorKey = `s${supDoId.slice(0, 12)}-${supPid}`;
473
503
  } else {
474
504
  // Supervisor entrypoint unavailable — running without ctx.exports
475
505
  // (e.g. unit-test harness, or LOADER.load contexts where the
@@ -669,6 +699,8 @@ export class IsolatePool {
669
699
  /**
670
700
  * Dispatch a single task to the slot isolate. `slotIndex` picks which
671
701
  * warm isolate services the call; callers round-robin slots themselves.
702
+ * Serialized per slot: this call waits for the slot's previous
703
+ * execution to settle before touching the warm isolate.
672
704
  */
673
705
  async #dispatchSlot(
674
706
  fnSource: string,
@@ -677,6 +709,39 @@ export class IsolatePool {
677
709
  args: unknown[],
678
710
  resilience: ResolvedResilience,
679
711
  perCallWasm?: Record<string, ArrayBuffer>,
712
+ ): Promise<unknown> {
713
+ // A warm slot executes one dispatch at a time: queue behind the
714
+ // previous owner, then record this dispatch as the new tail. The
715
+ // tail outlives the caller's outcome: a timeout rejects the
716
+ // Promise.race while runOnce's RPC is still live on the Worker, so
717
+ // release waits for every execution the owned body started to
718
+ // settle — never the caller's settle alone.
719
+ const previous = this.slotTails.get(slotIndex) ?? Promise.resolve();
720
+ let release: () => void;
721
+ this.slotTails.set(slotIndex, new Promise<void>((resolve) => { release = resolve; }));
722
+ await previous;
723
+ if (this.disposed) {
724
+ release!();
725
+ throw new BindingError(`IsolatePool(${this.tag}) is disposed`);
726
+ }
727
+ const inFlight: Promise<unknown>[] = [];
728
+ try {
729
+ return await this.#dispatchSlotOwned(fnSource, fnHash, slotIndex, args, resilience, perCallWasm, inFlight);
730
+ } finally {
731
+ // Do not delay the caller's own outcome — the tail releases when
732
+ // the RPCs the body launched have actually settled.
733
+ void Promise.allSettled(inFlight).then(() => release!());
734
+ }
735
+ }
736
+
737
+ async #dispatchSlotOwned(
738
+ fnSource: string,
739
+ fnHash: string,
740
+ slotIndex: number,
741
+ args: unknown[],
742
+ resilience: ResolvedResilience,
743
+ perCallWasm: Record<string, ArrayBuffer> | undefined,
744
+ inFlight: Promise<unknown>[],
680
745
  ): Promise<unknown> {
681
746
  // Per-call wasm fingerprint. Mixed into the cache key so two calls
682
747
  // with different bytes hit different slots (no cache poisoning).
@@ -686,12 +751,12 @@ export class IsolatePool {
686
751
  const perCallWasmHash = this.#fingerprintWasm(perCallWasmEntries);
687
752
 
688
753
  // Cache key includes the short DO id so warm isolates are scoped to
689
- // ONE session. See the doIdShort field comment for why — without it,
690
- // a later session's pool reuses the warm worker from a previous
691
- // session (which still carries the old session's env.SUPERVISOR
692
- // binding), and writeBatch RPCs land in the wrong DO's VFS.
754
+ // ONE session (see the doIdShort field comment), and the supervisor
755
+ // identity so a wake of that same session can never reuse a warm
756
+ // worker whose SUPERVISOR binding still names the dead generation's
757
+ // pid. See the supervisorKey field comment for the failure mode.
693
758
  const buildId = (generation: number): string =>
694
- `nfp:${this.tag}:${this.doIdShort}:${fnHash}:${this.preambleHash}:${this.wasmHash}:${perCallWasmHash}:slot-${slotIndex}:g${generation}`;
759
+ `nfp:${this.tag}:${this.doIdShort}:${fnHash}:${this.preambleHash}:${this.wasmHash}:${perCallWasmHash}:${this.supervisorKey}:slot-${slotIndex}:g${generation}`;
695
760
  let id = buildId(this.slotGenerations.get(slotIndex) ?? 0);
696
761
  const code = this.#buildCode(fnSource, perCallWasmEntries);
697
762
 
@@ -767,8 +832,10 @@ export class IsolatePool {
767
832
  // See plan in close-plan-2026-04-28.
768
833
  let timerId: ReturnType<typeof setTimeout> | undefined;
769
834
  try {
835
+ const attemptPromise = runOnce();
836
+ inFlight.push(attemptPromise);
770
837
  return await Promise.race([
771
- runOnce(),
838
+ attemptPromise,
772
839
  new Promise<never>((_, reject) => {
773
840
  timerId = setTimeout(
774
841
  () => reject(new TimeoutError(resilience.timeoutMs)),
@@ -780,7 +847,9 @@ export class IsolatePool {
780
847
  if (timerId !== undefined) clearTimeout(timerId);
781
848
  }
782
849
  }
783
- return await runOnce();
850
+ const attemptPromise = runOnce();
851
+ inFlight.push(attemptPromise);
852
+ return await attemptPromise;
784
853
  } catch (err) {
785
854
  lastError = err instanceof Error ? err : new Error(String(err));
786
855
  const cause = classifyError(lastError);
@@ -974,6 +1043,7 @@ export class IsolatePool {
974
1043
  * Safe to call more than once; idempotent.
975
1044
  */
976
1045
  dispose(): void {
1046
+ this.disposed = true;
977
1047
  if (!this.bindings) return;
978
1048
  for (const key of Object.keys(this.bindings)) {
979
1049
  disposeRpcResource(this.bindings[key]);
@@ -63,10 +63,10 @@ export const TURN_CHUNK_MAX_BYTES = 2_000_000;
63
63
  * spent.
64
64
  *
65
65
  * Callers report the work they are about to do or have just done and await
66
- * the result; a pacer that is not yielding returns without suspending, so the
67
- * one-shot exec path — which passes no pacer at all — keeps its exact
68
- * behaviour and cost. Nothing here decides WHAT the launch does, only where it
69
- * is allowed to stop.
66
+ * the result; a pacer that is not yielding returns without suspending, so a
67
+ * build small enough to fit in one chunk — most one-shot execs, every cache
68
+ * hit — keeps its exact behaviour and cost. Nothing here decides WHAT the
69
+ * launch does, only where it is allowed to stop.
70
70
  */
71
71
  export class TurnBudget {
72
72
  /** Turn handoffs this launch has taken. Reported with the launch. */
@@ -142,7 +142,7 @@ export interface PacedWorkHost {
142
142
  * reason. Without it the pump degrades to a same-context timer — see
143
143
  * {@link PacedWork.nextTurn}.
144
144
  */
145
- requestTurn?: () => void;
145
+ requestTurn?: (notBefore?: number) => void;
146
146
  }
147
147
 
148
148
  /**
@@ -160,7 +160,7 @@ export class PacedWork implements TurnScheduler {
160
160
  * half-built work for pids that no longer exist, where re-driving a launch
161
161
  * from its inputs is the same idempotent work again.
162
162
  */
163
- private waiters: Array<{ resume: () => void; chunkEnded: Promise<void> }> = [];
163
+ private waiters: Array<{ resume: () => void; chunkEnded: Promise<void>; notBefore: number }> = [];
164
164
 
165
165
  /**
166
166
  * `ctx` keys the cold-start queue the pump drains first on every turn it
@@ -181,14 +181,14 @@ export class PacedWork implements TurnScheduler {
181
181
  * always performed, so a harness or a runtime without alarms loses the
182
182
  * responsiveness but keeps the behaviour.
183
183
  */
184
- nextTurn(chunkEnded: Promise<void>): Promise<void> {
184
+ nextTurn(chunkEnded: Promise<void>, notBefore = 0): Promise<void> {
185
185
  return new Promise<void>((resume) => {
186
- this.waiters.push({ resume, chunkEnded });
186
+ this.waiters.push({ resume, chunkEnded, notBefore });
187
187
  if (this.host.requestTurn) {
188
- this.host.requestTurn();
188
+ this.host.requestTurn(notBefore);
189
189
  return;
190
190
  }
191
- setTimeout(() => { void this.pump(); }, 0);
191
+ setTimeout(() => { void this.pump(); }, Math.max(0, notBefore - Date.now()));
192
192
  });
193
193
  }
194
194
 
@@ -206,9 +206,11 @@ export class PacedWork implements TurnScheduler {
206
206
  // is what an alarm calls, and the first turn after a reset is the
207
207
  // re-delivered alarm of a launch the reset interrupted.
208
208
  await runColdStart(this.ctx);
209
- const waiting = this.waiters;
209
+ const now = Date.now();
210
+ const waiting = this.waiters.filter((waiter) => waiter.notBefore <= now);
211
+ this.waiters = this.waiters.filter((waiter) => waiter.notBefore > now);
212
+ if (this.waiters.length > 0) this.host.requestTurn?.(Math.min(...this.waiters.map((waiter) => waiter.notBefore)));
210
213
  if (waiting.length === 0) return;
211
- this.waiters = [];
212
214
  for (const waiter of waiting) waiter.resume();
213
215
  await Promise.all(waiting.map((waiter) => waiter.chunkEnded));
214
216
  }