@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.
- package/README.md +84 -55
- package/dist/bindings.js +5 -5
- package/dist/budgets.d.ts +132 -0
- package/dist/budgets.d.ts.map +1 -0
- package/dist/budgets.js +248 -0
- package/dist/composition.d.ts +87 -0
- package/dist/composition.d.ts.map +1 -0
- package/dist/composition.js +76 -0
- package/dist/connections.d.ts +81 -0
- package/dist/connections.d.ts.map +1 -0
- package/dist/connections.js +114 -0
- package/dist/derived.d.ts +65 -0
- package/dist/derived.d.ts.map +1 -0
- package/dist/derived.js +95 -0
- package/dist/do-calls.d.ts +94 -0
- package/dist/do-calls.d.ts.map +1 -0
- package/dist/do-calls.js +111 -0
- package/dist/facet-pool.d.ts +90 -0
- package/dist/facet-pool.d.ts.map +1 -0
- package/dist/facet-pool.js +113 -0
- package/dist/{fanout-pool.d.ts → fanout.d.ts} +20 -20
- package/dist/fanout.d.ts.map +1 -0
- package/dist/{fanout-pool.js → fanout.js} +20 -20
- package/dist/{launch-journal.d.ts → fenced-work.d.ts} +25 -13
- package/dist/fenced-work.d.ts.map +1 -0
- package/dist/{launch-journal.js → fenced-work.js} +47 -13
- package/dist/generation.d.ts +69 -0
- package/dist/generation.d.ts.map +1 -0
- package/dist/generation.js +118 -0
- package/dist/{facet-image-store.d.ts → image-store.d.ts} +8 -8
- package/dist/image-store.d.ts.map +1 -0
- package/dist/{facet-image-store.js → image-store.js} +4 -4
- package/dist/index.d.ts +16 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +16 -8
- package/dist/{loader-pool.d.ts → isolate-pool.d.ts} +19 -19
- package/dist/isolate-pool.d.ts.map +1 -0
- package/dist/{loader-pool.js → isolate-pool.js} +20 -20
- package/dist/journal.d.ts +111 -0
- package/dist/journal.d.ts.map +1 -0
- package/dist/journal.js +177 -0
- package/dist/outbox.d.ts +249 -0
- package/dist/outbox.d.ts.map +1 -0
- package/dist/outbox.js +355 -0
- package/dist/process-fabric.d.ts +2 -14
- package/dist/process-fabric.d.ts.map +1 -1
- package/dist/process-fabric.js +6 -15
- package/dist/process-host.d.ts +1 -1
- package/dist/process-host.d.ts.map +1 -1
- package/dist/process-host.js +10 -9
- package/dist/sealed.d.ts +78 -0
- package/dist/sealed.d.ts.map +1 -0
- package/dist/sealed.js +145 -0
- package/dist/timers.d.ts +138 -0
- package/dist/timers.d.ts.map +1 -0
- package/dist/timers.js +231 -0
- package/dist/{launch-pacer.d.ts → turn-budget.d.ts} +19 -21
- package/dist/turn-budget.d.ts.map +1 -0
- package/dist/{launch-pacer.js → turn-budget.js} +22 -11
- package/dist/workerd-facet-host.d.ts +28 -67
- package/dist/workerd-facet-host.d.ts.map +1 -1
- package/dist/workerd-facet-host.js +49 -171
- package/examples/agent-core-adapter.ts +191 -0
- package/package.json +4 -2
- package/src/bindings.ts +6 -6
- package/src/budgets.ts +308 -0
- package/src/composition.ts +127 -0
- package/src/connections.ts +140 -0
- package/src/derived.ts +135 -0
- package/src/do-calls.ts +156 -0
- package/src/facet-pool.ts +157 -0
- package/src/{fanout-pool.ts → fanout.ts} +35 -35
- package/src/{launch-journal.ts → fenced-work.ts} +58 -22
- package/src/generation.ts +144 -0
- package/src/{facet-image-store.ts → image-store.ts} +9 -9
- package/src/index.ts +16 -8
- package/src/{loader-pool.ts → isolate-pool.ts} +34 -34
- package/src/journal.ts +242 -0
- package/src/node-async-hooks.d.ts +14 -0
- package/src/outbox.ts +520 -0
- package/src/process-fabric.ts +6 -33
- package/src/process-host.ts +10 -15
- package/src/sealed.ts +150 -0
- package/src/timers.ts +294 -0
- package/src/{launch-pacer.ts → turn-budget.ts} +30 -24
- package/src/workerd-facet-host.ts +67 -193
- package/dist/alarms.d.ts +0 -134
- package/dist/alarms.d.ts.map +0 -1
- package/dist/alarms.js +0 -214
- package/dist/ctx-exports.d.ts +0 -47
- package/dist/ctx-exports.d.ts.map +0 -1
- package/dist/ctx-exports.js +0 -54
- package/dist/facet-image-store.d.ts.map +0 -1
- package/dist/fanout-pool.d.ts.map +0 -1
- package/dist/launch-journal.d.ts.map +0 -1
- package/dist/launch-pacer.d.ts.map +0 -1
- package/dist/loader-ledger.d.ts +0 -57
- package/dist/loader-ledger.d.ts.map +0 -1
- package/dist/loader-ledger.js +0 -91
- package/dist/loader-pool.d.ts.map +0 -1
- package/src/alarms.ts +0 -275
- package/src/ctx-exports.ts +0 -77
- 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/
|
|
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 './
|
|
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
|
-
|
|
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
|
|
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 `
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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 `
|
|
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
|
-
*
|
|
468
|
-
*
|
|
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
|
-
*
|
|
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
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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
|
|
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
|
package/dist/alarms.d.ts.map
DELETED
|
@@ -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"}
|