@nimbus-sh/fabric 0.1.0 → 0.2.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 (103) hide show
  1. package/README.md +84 -55
  2. package/dist/bindings.js +5 -5
  3. package/dist/budgets.d.ts +132 -0
  4. package/dist/budgets.d.ts.map +1 -0
  5. package/dist/budgets.js +248 -0
  6. package/dist/composition.d.ts +87 -0
  7. package/dist/composition.d.ts.map +1 -0
  8. package/dist/composition.js +76 -0
  9. package/dist/connections.d.ts +81 -0
  10. package/dist/connections.d.ts.map +1 -0
  11. package/dist/connections.js +114 -0
  12. package/dist/derived.d.ts +65 -0
  13. package/dist/derived.d.ts.map +1 -0
  14. package/dist/derived.js +95 -0
  15. package/dist/do-calls.d.ts +94 -0
  16. package/dist/do-calls.d.ts.map +1 -0
  17. package/dist/do-calls.js +111 -0
  18. package/dist/facet-pool.d.ts +90 -0
  19. package/dist/facet-pool.d.ts.map +1 -0
  20. package/dist/facet-pool.js +113 -0
  21. package/dist/{fanout-pool.d.ts → fanout.d.ts} +20 -20
  22. package/dist/fanout.d.ts.map +1 -0
  23. package/dist/{fanout-pool.js → fanout.js} +20 -20
  24. package/dist/{launch-journal.d.ts → fenced-work.d.ts} +25 -13
  25. package/dist/fenced-work.d.ts.map +1 -0
  26. package/dist/{launch-journal.js → fenced-work.js} +47 -13
  27. package/dist/generation.d.ts +69 -0
  28. package/dist/generation.d.ts.map +1 -0
  29. package/dist/generation.js +118 -0
  30. package/dist/{facet-image-store.d.ts → image-store.d.ts} +8 -8
  31. package/dist/image-store.d.ts.map +1 -0
  32. package/dist/{facet-image-store.js → image-store.js} +4 -4
  33. package/dist/index.d.ts +16 -8
  34. package/dist/index.d.ts.map +1 -1
  35. package/dist/index.js +16 -8
  36. package/dist/{loader-pool.d.ts → isolate-pool.d.ts} +19 -19
  37. package/dist/isolate-pool.d.ts.map +1 -0
  38. package/dist/{loader-pool.js → isolate-pool.js} +20 -20
  39. package/dist/journal.d.ts +111 -0
  40. package/dist/journal.d.ts.map +1 -0
  41. package/dist/journal.js +177 -0
  42. package/dist/outbox.d.ts +249 -0
  43. package/dist/outbox.d.ts.map +1 -0
  44. package/dist/outbox.js +355 -0
  45. package/dist/process-fabric.d.ts +2 -14
  46. package/dist/process-fabric.d.ts.map +1 -1
  47. package/dist/process-fabric.js +6 -15
  48. package/dist/process-host.d.ts +1 -1
  49. package/dist/process-host.d.ts.map +1 -1
  50. package/dist/process-host.js +10 -9
  51. package/dist/sealed.d.ts +78 -0
  52. package/dist/sealed.d.ts.map +1 -0
  53. package/dist/sealed.js +145 -0
  54. package/dist/timers.d.ts +138 -0
  55. package/dist/timers.d.ts.map +1 -0
  56. package/dist/timers.js +231 -0
  57. package/dist/{launch-pacer.d.ts → turn-budget.d.ts} +19 -21
  58. package/dist/turn-budget.d.ts.map +1 -0
  59. package/dist/{launch-pacer.js → turn-budget.js} +22 -11
  60. package/dist/workerd-facet-host.d.ts +28 -67
  61. package/dist/workerd-facet-host.d.ts.map +1 -1
  62. package/dist/workerd-facet-host.js +49 -171
  63. package/examples/agent-core-adapter.ts +191 -0
  64. package/package.json +4 -2
  65. package/src/bindings.ts +6 -6
  66. package/src/budgets.ts +308 -0
  67. package/src/composition.ts +127 -0
  68. package/src/connections.ts +140 -0
  69. package/src/derived.ts +135 -0
  70. package/src/do-calls.ts +156 -0
  71. package/src/facet-pool.ts +157 -0
  72. package/src/{fanout-pool.ts → fanout.ts} +35 -35
  73. package/src/{launch-journal.ts → fenced-work.ts} +58 -22
  74. package/src/generation.ts +144 -0
  75. package/src/{facet-image-store.ts → image-store.ts} +9 -9
  76. package/src/index.ts +16 -8
  77. package/src/{loader-pool.ts → isolate-pool.ts} +34 -34
  78. package/src/journal.ts +242 -0
  79. package/src/node-async-hooks.d.ts +14 -0
  80. package/src/outbox.ts +520 -0
  81. package/src/process-fabric.ts +6 -33
  82. package/src/process-host.ts +10 -15
  83. package/src/sealed.ts +150 -0
  84. package/src/timers.ts +294 -0
  85. package/src/{launch-pacer.ts → turn-budget.ts} +30 -24
  86. package/src/workerd-facet-host.ts +67 -193
  87. package/dist/alarms.d.ts +0 -134
  88. package/dist/alarms.d.ts.map +0 -1
  89. package/dist/alarms.js +0 -214
  90. package/dist/ctx-exports.d.ts +0 -47
  91. package/dist/ctx-exports.d.ts.map +0 -1
  92. package/dist/ctx-exports.js +0 -54
  93. package/dist/facet-image-store.d.ts.map +0 -1
  94. package/dist/fanout-pool.d.ts.map +0 -1
  95. package/dist/launch-journal.d.ts.map +0 -1
  96. package/dist/launch-pacer.d.ts.map +0 -1
  97. package/dist/loader-ledger.d.ts +0 -57
  98. package/dist/loader-ledger.d.ts.map +0 -1
  99. package/dist/loader-ledger.js +0 -91
  100. package/dist/loader-pool.d.ts.map +0 -1
  101. package/src/alarms.ts +0 -275
  102. package/src/ctx-exports.ts +0 -77
  103. package/src/loader-ledger.ts +0 -112
@@ -13,20 +13,25 @@
13
13
  * `HostedProcess` and never imports this file.
14
14
  */
15
15
 
16
- import { disposeRpcResource } from '@nimbus-sh/core/_shared/rpc-dispose.js';
16
+ import { disposeRpcResource } from '@nimbus-sh/platform/rpc-dispose.js';
17
17
  import {
18
18
  getCtxExports,
19
+ stagedBootAssembler,
19
20
  supervisorEntrypoint,
20
21
  supervisorEntrypointName,
21
- } from './ctx-exports.js';
22
+ } from './composition.js';
22
23
  import {
24
+ assertModuleMapWithinCodeLimit,
23
25
  beginLoaderFetch,
26
+ facetNameCount,
27
+ facetNameCountDurable,
28
+ recordFacetNameMinted,
24
29
  recordLoaderId,
25
30
  withDynamicWorkerCapNamed,
26
- } from './loader-ledger.js';
31
+ withFacetBudgetNamed,
32
+ } from './budgets.js';
27
33
  import {
28
34
  RESIDENT_PROCESS_CLASS,
29
- requireStagedBootAssembler,
30
35
  residentLoaderConfig,
31
36
  type HostedProcess,
32
37
  type OneShotCodeSpec,
@@ -91,71 +96,6 @@ export async function createLoadedWorkerEntrypoint(
91
96
  });
92
97
  }
93
98
 
94
- /**
95
- * Total bytes a dynamic Worker's module map may carry, across every member of
96
- * it. A hard platform limit, not a policy knob: 62 MiB lands and 64 MiB is
97
- * refused with "Dynamic Worker code size (N bytes) exceeds the maximum allowed
98
- * size of 67108864 bytes", confirmed at five sizes with two trials each. The
99
- * budget is shared, so a ruby process is already 34.3 MiB down before its disk
100
- * is counted.
101
- */
102
- export const DYNAMIC_WORKER_CODE_LIMIT_BYTES = 67_108_864;
103
-
104
- /**
105
- * Refuse a module map over {@link DYNAMIC_WORKER_CODE_LIMIT_BYTES}, naming
106
- * the largest members. The platform's own refusal reports one number for a
107
- * budget shared across every member of the map, which tells the operator
108
- * nothing about WHAT to shrink — so every fabric seam that assembles a map
109
- * runs this before the loader sees it.
110
- *
111
- * Costed to its two paths. Under the ceiling: one length read per member —
112
- * UTF-16 code units for text, which equal UTF-8 bytes for the ASCII module
113
- * text the generators emit and undercount otherwise; the platform's own
114
- * refusal still backstops the exotic case, because this check exists to name
115
- * members, not to be the ceiling. Over it: exact UTF-8 sizes, computed only
116
- * then, sorted so the biggest lever is first.
117
- */
118
- export function assertModuleMapWithinCodeLimit(modules: Record<string, unknown>): void {
119
- let estimate = 0;
120
- for (const content of Object.values(modules)) {
121
- estimate += memberBytes(content, null);
122
- }
123
- if (estimate <= DYNAMIC_WORKER_CODE_LIMIT_BYTES) return;
124
-
125
- const encoder = new TextEncoder();
126
- const sized = Object.entries(modules)
127
- .map(([name, content]) => ({ name, bytes: memberBytes(content, encoder) }))
128
- .sort((a, b) => b.bytes - a.bytes);
129
- const total = sized.reduce((sum, member) => sum + member.bytes, 0);
130
- const top = sized.slice(0, 5)
131
- .map(({ name, bytes }) => `'${name}' (${bytes.toLocaleString('en-US')} bytes)`)
132
- .join(', ');
133
- throw new Error(
134
- `Nimbus: dynamic-worker module map is ${total.toLocaleString('en-US')} bytes, over the `
135
- + `${DYNAMIC_WORKER_CODE_LIMIT_BYTES.toLocaleString('en-US')}-byte platform ceiling shared by `
136
- + `every member. Largest members: ${top}`,
137
- );
138
- }
139
-
140
- /**
141
- * Bytes one module-map member carries, across the loader's content kinds
142
- * (plain string, `{ js | cjs | py | text }`, `{ wasm | data }`). With an
143
- * encoder, text is measured exactly; without one, by code-unit length.
144
- */
145
- function memberBytes(content: unknown, encoder: TextEncoder | null): number {
146
- const textBytes = (text: string): number =>
147
- encoder ? encoder.encode(text).byteLength : text.length;
148
- if (typeof content === 'string') return textBytes(content);
149
- if (content !== null && typeof content === 'object') {
150
- for (const value of Object.values(content)) {
151
- if (typeof value === 'string') return textBytes(value);
152
- if (value instanceof ArrayBuffer) return value.byteLength;
153
- if (ArrayBuffer.isView(value)) return value.byteLength;
154
- }
155
- }
156
- return 0;
157
- }
158
-
159
99
  // ── Facet plumbing ──────────────────────────────────────────────────────────
160
100
 
161
101
  /** The subset of a facet stub a resident process exposes to whoever opened it. */
@@ -178,7 +118,7 @@ interface FacetContainer {
178
118
  /**
179
119
  * Present on deployed Cloudflare workerd, absent from the pinned
180
120
  * `@cloudflare/workers-types` and from local workerd ≤ 1.20260603.1 — see
181
- * {@link cloneFacetStorage}, the one way the fabric calls it.
121
+ * {@link cloneStorage}, the one way the fabric calls it.
182
122
  */
183
123
  clone?(src: string, dst: string): void;
184
124
  }
@@ -203,7 +143,7 @@ interface WorkerLoaderBinding {
203
143
  }
204
144
 
205
145
  /**
206
- * The bindings `openResidentFacet` needs off whichever DO is hosting. A
146
+ * The bindings `processes` needs off whichever DO is hosting. A
207
147
  * staged boot's assembler may read more off the same env (Nimbus's reads
208
148
  * ASSETS); the env travels to it whole, so nothing further is named here.
209
149
  */
@@ -250,7 +190,7 @@ function facetContainer(ctx: DurableObjectState): FacetContainer {
250
190
  * storage budget grants no copy-on-write credit — crossing it resets the
251
191
  * object rather than raising an error.
252
192
  */
253
- export async function cloneFacetStorage(
193
+ export async function cloneStorage(
254
194
  ctx: DurableObjectState,
255
195
  clone: {
256
196
  src: string;
@@ -300,17 +240,6 @@ export function residentFacetName(slot: number): string {
300
240
  return `proc-slot-${slot}`;
301
241
  }
302
242
 
303
- /**
304
- * Facet IDs a Durable Object is granted over its LIFETIME. Append-only and
305
- * never reclaimed, so crossing it is unrecoverable for the object — which is
306
- * why the ledger below counts consumption durably instead of leaving the
307
- * bound as prose the slot book merely respects.
308
- */
309
- export const FACET_ID_LIFETIME_BUDGET = 65_536;
310
-
311
- /** Where the ledger persists the count of facet names ever minted. */
312
- export const FACET_NAME_HIGH_WATER_KEY = 'fabric_facet_name_high_water';
313
-
314
243
  /** One hosting actor's slot book. */
315
244
  interface SlotBook {
316
245
  /** Returned slots, lowest reused first so the high-water mark stays low. */
@@ -319,16 +248,6 @@ interface SlotBook {
319
248
  next: number;
320
249
  /** Slot held by each live pid, so release can find it. */
321
250
  held: Map<number, number>;
322
- /**
323
- * The durable high-water of names ever minted, as an adopt-then-advance
324
- * chain. It starts as the read of {@link FACET_NAME_HIGH_WATER_KEY} and
325
- * every later link writes only a LARGER count — a fresh incarnation restarts
326
- * `next` at zero, and a write that had not adopted first would clobber the
327
- * lifetime count down to this incarnation's. The chain never rejects.
328
- */
329
- ledger: Promise<number>;
330
- /** The largest count the chain has adopted or written, for sync reads. */
331
- ledgerKnown: number;
332
251
  }
333
252
 
334
253
  /**
@@ -351,46 +270,12 @@ const slotBooks = new WeakMap<DurableObjectState, SlotBook>();
351
270
  function slotBook(ctx: DurableObjectState): SlotBook {
352
271
  let book = slotBooks.get(ctx);
353
272
  if (!book) {
354
- const created: SlotBook = {
355
- free: [],
356
- next: 0,
357
- held: new Map(),
358
- ledger: Promise.resolve(0),
359
- ledgerKnown: 0,
360
- };
361
- created.ledger = Promise.resolve(ctx.storage.get(FACET_NAME_HIGH_WATER_KEY))
362
- .then((value) => (typeof value === 'number' ? value : 0))
363
- .catch(() => 0)
364
- .then((adopted) => {
365
- created.ledgerKnown = Math.max(created.ledgerKnown, adopted);
366
- return adopted;
367
- });
368
- book = created;
273
+ book = { free: [], next: 0, held: new Map() };
369
274
  slotBooks.set(ctx, book);
370
275
  }
371
276
  return book;
372
277
  }
373
278
 
374
- /**
375
- * The lifetime facet-ID ledger: how many facet names this fabric has ever
376
- * minted on the Durable Object, against the 65,536 the platform will ever
377
- * grant it. `consumed` only ever counts FIRST uses — a reused name, in this
378
- * incarnation or any earlier one, cost no new ID, which is the slot book's
379
- * whole reason to exist. Surfaced so an operator can see proximity to a wall
380
- * whose crossing is unrecoverable, instead of discovering it from the
381
- * platform's opaque failure.
382
- */
383
- export async function facetIdBudget(
384
- ctx: DurableObjectState,
385
- ): Promise<{ consumed: number; budget: number }> {
386
- const book = slotBook(ctx);
387
- const durable = await book.ledger;
388
- return {
389
- consumed: Math.max(durable, book.next),
390
- budget: FACET_ID_LIFETIME_BUDGET,
391
- };
392
- }
393
-
394
279
  /** Take a slot for `pid`, reusing a returned one before minting a new name. */
395
280
  function acquireSlot(ctx: DurableObjectState, pid: number): number {
396
281
  const book = slotBook(ctx);
@@ -399,49 +284,12 @@ function acquireSlot(ctx: DurableObjectState, pid: number): number {
399
284
  const reused = book.free.length > 0;
400
285
  const slot = reused ? book.free.shift()! : book.next++;
401
286
  book.held.set(pid, slot);
402
- if (!reused) recordNameMinted(ctx, book);
287
+ // A fresh name is a permanently consumed facet ID; the durable count lives
288
+ // in the budgets ledger (see budgets.ts).
289
+ if (!reused) recordFacetNameMinted(ctx, book.next);
403
290
  return slot;
404
291
  }
405
292
 
406
- /**
407
- * Advance the durable ledger to this incarnation's name count, if it is a new
408
- * lifetime high. Chained behind adoption so the comparison is always against
409
- * the real persisted value; a failed write leaves the old link's count and the
410
- * next mint tries again — the ledger may transiently undercount, never over.
411
- */
412
- function recordNameMinted(ctx: DurableObjectState, book: SlotBook): void {
413
- const count = book.next;
414
- book.ledger = book.ledger.then(async (durable) => {
415
- if (count <= durable) return durable;
416
- try {
417
- await ctx.storage.put(FACET_NAME_HIGH_WATER_KEY, count);
418
- } catch {
419
- return durable;
420
- }
421
- book.ledgerKnown = Math.max(book.ledgerKnown, count);
422
- return count;
423
- });
424
- }
425
-
426
- /**
427
- * Name the facet-ID budget on a creation failure at the wall; below it, hand
428
- * the error back untouched. Exhaustion is the one failure here the platform
429
- * reports opaquely AND that no teardown, retry or reset can undo, so the
430
- * ledger — the only witness to the real cause — does the naming. Not a
431
- * threshold: the comparison is against the budget itself.
432
- */
433
- function withFacetBudgetNamed(consumed: number, error: unknown): unknown {
434
- if (consumed < FACET_ID_LIFETIME_BUDGET) return error;
435
- const platform = error instanceof Error ? error.message : String(error);
436
- return new Error(
437
- `Nimbus: facet creation failed with this Durable Object's `
438
- + `${FACET_ID_LIFETIME_BUDGET.toLocaleString('en-US')} facet-ID lifetime budget consumed `
439
- + `(${consumed} facet names ever created). Facet IDs are append-only and never reclaimed, `
440
- + `so this failure is permanent for the object: ${platform}`,
441
- { cause: error },
442
- );
443
- }
444
-
445
293
  /** Return `pid`'s slot to the free list. */
446
294
  function releaseSlot(ctx: DurableObjectState, pid: number): void {
447
295
  const book = slotBook(ctx);
@@ -454,7 +302,7 @@ function releaseSlot(ctx: DurableObjectState, pid: number): void {
454
302
 
455
303
 
456
304
  /**
457
- * What `openResidentFacet` hands back: a running process, minus its placement.
305
+ * What `processes(ctx, env).spawn` hands back: a running process, minus its placement.
458
306
  *
459
307
  * `slot` rides along because the caller's `describe` needs the facet's real
460
308
  * name and the slot is not derivable from the pid — that indirection is the
@@ -464,17 +312,58 @@ function releaseSlot(ctx: DurableObjectState, pid: number): void {
464
312
  export type ResidentFacet = Omit<HostedProcess, 'describe'> & { slot: number };
465
313
 
466
314
  /**
467
- * Open a resident process as a facet of the actor whose `ctx` and `env` are
468
- * given, and start its runner.
315
+ * The process surface of one hosting actor: how a resident process comes
316
+ * into existence on workerd, and how a one-shot program runs to completion.
469
317
  *
470
- * This is the ONE way a resident process comes into existence, and every
318
+ * `spawn` is the ONE way a resident process comes into existence, and every
471
319
  * substrate goes through it: the facet host calls it with the coordinator's
472
320
  * own `ctx`, the peer host calls it — over one RPC — with a sibling session
473
321
  * DO's. Everything a substrate could plausibly want to special-case is a
474
322
  * PARAMETER here rather than a branch: which actor hosts the child, and how
475
323
  * the boot spec's by-path members are read.
476
324
  */
477
- export function openResidentFacet(
325
+ export function processes(ctx: DurableObjectState, env: ResidentFacetEnv): Processes {
326
+ return new Processes(ctx, env);
327
+ }
328
+
329
+ export class Processes {
330
+ constructor(
331
+ private readonly ctx: DurableObjectState,
332
+ private readonly env: ResidentFacetEnv,
333
+ ) {}
334
+
335
+ /** Open a resident process as a facet of this actor, and start its runner. */
336
+ spawn(
337
+ disk: () => ResidentDiskReader,
338
+ supervisor: ResidentSupervisorProps,
339
+ params: ProcessHostParams,
340
+ ): ResidentFacet {
341
+ return spawnResident(this.ctx, this.env, disk, supervisor, params);
342
+ }
343
+
344
+ /**
345
+ * Run one program to completion as an UNKEYED dynamic worker.
346
+ *
347
+ * Unkeyed is the whole difference from `spawn`: nothing can re-resolve this
348
+ * worker into a later request's context, so it can never be a routeable
349
+ * target and never has to be released by name. It exists for the duration
350
+ * of one call and its stubs are dropped as that call unwinds.
351
+ *
352
+ * Shared by both substrates on purpose. `peer` places processes that have a
353
+ * residency to place; a one-shot has none, and shipping its fully-inline
354
+ * map across a sibling hop would meet the 32 MiB RPC ceiling that by-path
355
+ * boot specs exist to avoid — for a run that gains nothing by moving.
356
+ */
357
+ run<T>(
358
+ supervisor: ResidentSupervisorProps,
359
+ params: OneShotParams,
360
+ consume: (response: Response) => Promise<T>,
361
+ ): Promise<T> {
362
+ return runOneShot(this.ctx, this.env, supervisor, params, consume);
363
+ }
364
+ }
365
+
366
+ function spawnResident(
478
367
  ctx: DurableObjectState,
479
368
  env: ResidentFacetEnv,
480
369
  disk: () => ResidentDiskReader,
@@ -482,7 +371,6 @@ export function openResidentFacet(
482
371
  params: ProcessHostParams,
483
372
  ): ResidentFacet {
484
373
  const facets = facetContainer(ctx);
485
- const book = slotBook(ctx);
486
374
  const slot = acquireSlot(ctx, params.pid);
487
375
  const name = residentFacetName(slot);
488
376
  // The start callback is the ONLY way this facet is ever created, and it
@@ -511,7 +399,7 @@ export function openResidentFacet(
511
399
  facet = facets.get(name, start);
512
400
  } catch (error) {
513
401
  releaseSlot(ctx, params.pid);
514
- throw withFacetBudgetNamed(Math.max(book.ledgerKnown, book.next), error);
402
+ throw withFacetBudgetNamed(facetNameCount(ctx), error);
515
403
  }
516
404
 
517
405
  let disposed = false;
@@ -531,15 +419,14 @@ export function openResidentFacet(
531
419
  started = facet.startProcess(params.startArgs);
532
420
  } catch (error) {
533
421
  void release();
534
- throw withFacetBudgetNamed(Math.max(book.ledgerKnown, book.next), error);
422
+ throw withFacetBudgetNamed(facetNameCount(ctx), error);
535
423
  }
536
424
  // The rejection that carries the platform's failure at ID exhaustion is
537
425
  // this one, and it is annotated AFTER awaiting the ledger — the first
538
426
  // failure of a fresh incarnation must compare against the persisted count,
539
427
  // not the zero its adoption read has not yet replaced.
540
428
  started = started.catch(async (error) => {
541
- const durable = await book.ledger;
542
- throw withFacetBudgetNamed(Math.max(durable, book.next), error);
429
+ throw withFacetBudgetNamed(await facetNameCountDurable(ctx), error);
543
430
  });
544
431
  // A caller reads whichever of `started` and the lifecycle it needs, so keep
545
432
  // the runtime from reporting the other as an unhandled rejection.
@@ -587,20 +474,7 @@ function residentProcessClass(
587
474
  }
588
475
  }
589
476
 
590
- /**
591
- * Run one program to completion as an UNKEYED dynamic worker.
592
- *
593
- * Unkeyed is the whole difference from `openResidentFacet`: nothing can
594
- * re-resolve this worker into a later request's context, so it can never be a
595
- * routeable target and never has to be released by name. It exists for the
596
- * duration of one call and its stubs are dropped as that call unwinds.
597
- *
598
- * Shared by both substrates on purpose. `peer` places processes that have a
599
- * residency to place; a one-shot has none, and shipping its fully-inline map
600
- * across a sibling hop would meet the 32 MiB RPC ceiling that by-path boot
601
- * specs exist to avoid — for a run that gains nothing by moving.
602
- */
603
- export async function runOneShotWorker<T>(
477
+ async function runOneShot<T>(
604
478
  ctx: DurableObjectState,
605
479
  env: ResidentFacetEnv,
606
480
  supervisor: ResidentSupervisorProps,
@@ -679,7 +553,7 @@ async function residentWorkerConfig(
679
553
  boot: ResidentBootSpec,
680
554
  ): Promise<Record<string, unknown>> {
681
555
  const config = boot.kind === 'staged'
682
- ? await requireStagedBootAssembler()(env, boot.stage)
556
+ ? await stagedBootAssembler()(env, boot.stage)
683
557
  : await residentLoaderConfig(boot.code, disk());
684
558
  assertModuleMapWithinCodeLimit(
685
559
  (config as { modules?: Record<string, unknown> }).modules ?? {},
package/dist/alarms.d.ts DELETED
@@ -1,134 +0,0 @@
1
- /**
2
- * alarms.ts — Durable Object alarm multiplexing + isolate-generation
3
- * machinery, persisted across hibernation.
4
- *
5
- * Workerd hibernates Durable Objects between requests to free memory. On
6
- * wake, the new isolate must rebuild its in-memory state from SQL — but it
7
- * also needs to know "is this the same lifecycle as before, or did workerd
8
- * recycle me?" That distinction matters for recovery (warmJoin vs cold init)
9
- * and is captured by the isolate generation, a counter persisted across
10
- * hibernations.
11
- *
12
- * A Durable Object has ONE alarm, and a second `setAlarm()` silently
13
- * overwrites the first — so every alarm-driven subsystem coordinates through
14
- * a single reason→deadline map and one dispatcher. Reasons are plain strings
15
- * registered by the embedder: `scheduleAlarm` arms one, and `dispatchAlarm`
16
- * runs the embedder-supplied handler for every reason whose deadline has
17
- * passed.
18
- */
19
- /**
20
- * The storage the alarm map lives in. `setAlarm` is optional because
21
- * `wrangler dev` serves a storage without it, which is the whole reason
22
- * scheduling degrades to a no-op instead of throwing.
23
- */
24
- export interface AlarmStorage {
25
- get(key: string): Promise<unknown>;
26
- put(key: string, value: unknown): Promise<void>;
27
- delete(key: string): Promise<boolean>;
28
- setAlarm?(scheduledTime: number): Promise<void>;
29
- }
30
- /** The hosting actor's context, as the alarm coordination reads it. */
31
- export interface AlarmContext {
32
- storage: AlarmStorage;
33
- }
34
- /**
35
- * Multi-reason alarm coordination map.
36
- *
37
- * JSON-serialised `Record<reason, deadlineMsEpoch>` where keys are the
38
- * embedder's canonical reason strings (e.g. 'w9-flush', 'log-janitor'). The
39
- * alarm() dispatcher reads this on fire, dispatches every reason whose
40
- * deadline has passed, and re-arms `ctx.storage.setAlarm` at the earliest
41
- * remaining deadline.
42
- *
43
- * Why a map (not a single nextAlarmAt + reason): two subsystems can have
44
- * distinct deadlines. Without the map, the later setAlarm() call would
45
- * overwrite the earlier reason silently, breaking whichever subsystem
46
- * expected its deadline.
47
- *
48
- * Forward-compat: the dispatcher silently drops unknown reasons so a
49
- * rollback from a future deploy that added new reasons doesn't leave the
50
- * alarm stuck.
51
- *
52
- * The VALUE is live production DO storage ('w1_next_alarm_reasons', from the
53
- * workstream that introduced it) and must never change — renaming a storage
54
- * key is a migration, and orphaned rows are the least of what it breaks.
55
- */
56
- export declare const ALARM_REASONS_KEY = "w1_next_alarm_reasons";
57
- /**
58
- * Storage key for the isolate-generation counter (cold-start +
59
- * post-hibernation wake; one increment per fresh isolate).
60
- *
61
- * The VALUE is live production DO storage ('w9_isolate_gen') and must never
62
- * change, same contract as {@link ALARM_REASONS_KEY}.
63
- */
64
- export declare const ISOLATE_GEN_KEY = "w9_isolate_gen";
65
- /**
66
- * The host instance carrying the per-instance alarm chain. The field lives on
67
- * the embedder's DO instance so one chain serializes every alarm-map
68
- * read-modify-write for that instance (see {@link scheduleAlarm}).
69
- */
70
- export interface AlarmHost {
71
- _alarmChain?: Promise<unknown>;
72
- }
73
- /** The host instance carrying the isolate-generation state. */
74
- export interface IsolateGenHost {
75
- _isolateGen: number;
76
- _isolateGenPersisted: boolean;
77
- }
78
- /**
79
- * Schedule (or re-schedule) an alarm reason. Coordinated via a single map in
80
- * DO storage so multiple subsystems don't clobber each other's `setAlarm()`
81
- * calls.
82
- *
83
- * Semantics:
84
- * - Reads the existing reasons map.
85
- * - Sets `map[reason] = whenMs` IF `whenMs` is sooner than the
86
- * currently-pending deadline for that reason (or no entry exists).
87
- * Later-than-pending requests are silently ignored — the existing
88
- * alarm will fire and re-arm anyway.
89
- * - Writes the map back and calls `ctx.storage.setAlarm(min(deadlines))`.
90
- *
91
- * Cost: 1 storage read + 1 storage write + 1 setAlarm per call. setAlarm
92
- * itself is billed as 1 row written per DO pricing. At a 60s janitor
93
- * cadence, this is ~$0.05/mo/session at scale — dwarfed by the
94
- * hibernation duration savings.
95
- *
96
- * Fail-soft: any throw is swallowed with a warn. On older runtimes /
97
- * wrangler-dev where setAlarm is unavailable, this is a no-op (the
98
- * subsystem's in-isolate setTimeout fallback continues to work).
99
- */
100
- export declare function scheduleAlarm(host: AlarmHost, ctx: AlarmContext, reason: string, whenMs: number): Promise<boolean>;
101
- /**
102
- * What one alarm handler may return: nothing, or a deadline this reason
103
- * re-arms itself at. Re-arming through the return value keeps the map's
104
- * read-modify-write inside the dispatcher, where it is serialized.
105
- */
106
- export type AlarmHandlerResult = void | {
107
- rearmAt: number;
108
- };
109
- /** The embedder's reasons, each with the handler that answers it. */
110
- export type AlarmHandlers = Record<string, (now: number) => AlarmHandlerResult | Promise<AlarmHandlerResult>>;
111
- /**
112
- * Multi-reason alarm dispatcher. Called from the DO's `alarm()` handler with
113
- * the embedder's handler map.
114
- *
115
- * For each pending reason whose deadline has passed, run its handler.
116
- * Handlers are awaited in place: the alarm invocation is the fresh turn a
117
- * re-entering subsystem asked for, and it has to stay the one paying for the
118
- * work it just released.
119
- *
120
- * After running fireable reasons, re-arms `ctx.storage.setAlarm` at the
121
- * earliest remaining deadline. If no reasons remain, deletes the map key and
122
- * does NOT call setAlarm — the DO becomes hibernation-eligible after the 10s
123
- * idle window.
124
- *
125
- * Forward/back-compat: unknown reasons silently dropped. `onLegacyAlarm`
126
- * covers an alarm that fires with no map at all — a deploy from before the
127
- * map existed left a bare `setAlarm` behind, and the embedder decides what
128
- * that one-time fire means (one dispatch later the map is populated by the
129
- * next scheduleAlarm call).
130
- */
131
- export declare function dispatchAlarm(host: AlarmHost, ctx: AlarmContext, handlers: AlarmHandlers, onLegacyAlarm?: () => void): Promise<void>;
132
- /** Increment + persist the isolate-gen counter once per fresh isolate. */
133
- export declare function maybeBumpIsolateGen(host: IsolateGenHost, ctx: AlarmContext): Promise<void>;
134
- //# sourceMappingURL=alarms.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"alarms.d.ts","sourceRoot":"","sources":["../src/alarms.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAIH;;;;GAIG;AACH,MAAM,WAAW,YAAY;IAC3B,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;IAChD,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IACtC,QAAQ,CAAC,CAAC,aAAa,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACjD;AAED,uEAAuE;AACvE,MAAM,WAAW,YAAY;IAC3B,OAAO,EAAE,YAAY,CAAC;CACvB;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,eAAO,MAAM,iBAAiB,0BAA0B,CAAC;AAEzD;;;;;;GAMG;AACH,eAAO,MAAM,eAAe,mBAAmB,CAAC;AAEhD;;;;GAIG;AACH,MAAM,WAAW,SAAS;IACxB,WAAW,CAAC,EAAE,OAAO,CAAC,OAAO,CAAC,CAAC;CAChC;AAED,+DAA+D;AAC/D,MAAM,WAAW,cAAc;IAC7B,WAAW,EAAE,MAAM,CAAC;IACpB,oBAAoB,EAAE,OAAO,CAAC;CAC/B;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,aAAa,CAC3B,IAAI,EAAE,SAAS,EACf,GAAG,EAAE,YAAY,EACjB,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,OAAO,CAAC,CA8BlB;AAED;;;;GAIG;AACH,MAAM,MAAM,kBAAkB,GAAG,IAAI,GAAG;IAAE,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC;AAE5D,qEAAqE;AACrE,MAAM,MAAM,aAAa,GAAG,MAAM,CAChC,MAAM,EACN,CAAC,GAAG,EAAE,MAAM,KAAK,kBAAkB,GAAG,OAAO,CAAC,kBAAkB,CAAC,CAClE,CAAC;AAEF;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,aAAa,CAC3B,IAAI,EAAE,SAAS,EACf,GAAG,EAAE,YAAY,EACjB,QAAQ,EAAE,aAAa,EACvB,aAAa,CAAC,EAAE,MAAM,IAAI,GACzB,OAAO,CAAC,IAAI,CAAC,CASf;AAwDD,0EAA0E;AAC1E,wBAAsB,mBAAmB,CAAC,IAAI,EAAE,cAAc,EAAE,GAAG,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAyBhG"}