@nimbus-sh/fabric 0.1.0 → 0.3.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 +208 -293
- 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 +3 -0
- package/dist/composition.d.ts.map +1 -0
- package/dist/composition.js +2 -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} +58 -17
- package/dist/fenced-work.d.ts.map +1 -0
- package/dist/fenced-work.js +241 -0
- 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 +33 -15
- package/dist/process-fabric.d.ts.map +1 -1
- package/dist/process-fabric.js +25 -15
- package/dist/process-host.d.ts +1 -1
- package/dist/process-host.d.ts.map +1 -1
- package/dist/process-host.js +19 -11
- 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} +24 -21
- package/dist/turn-budget.d.ts.map +1 -0
- package/dist/{launch-pacer.js → turn-budget.js} +24 -12
- package/dist/workerd-facet-host.d.ts +67 -70
- package/dist/workerd-facet-host.d.ts.map +1 -1
- package/dist/workerd-facet-host.js +129 -181
- 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 +16 -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} +129 -42
- 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 +43 -34
- package/src/process-host.ts +22 -20
- package/src/sealed.ts +150 -0
- package/src/timers.ts +294 -0
- package/src/{launch-pacer.ts → turn-budget.ts} +34 -27
- package/src/workerd-facet-host.ts +159 -208
- 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-journal.js +0 -154
- 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
|
}
|
|
@@ -196,14 +136,19 @@ interface LoadedWorkerStub {
|
|
|
196
136
|
* Durable Object class, so a resident process can be re-entered; `load` is
|
|
197
137
|
* unkeyed and yields a stateless entrypoint, which is all a program that ends
|
|
198
138
|
* with its call can ever need.
|
|
139
|
+
*
|
|
140
|
+
* `get` stays wide on purpose: the platform passes a null id for the unkeyed
|
|
141
|
+
* call and answers the callback with the code object or a promise of it, so a
|
|
142
|
+
* narrower declaration would refuse the real binding. The fabric itself always
|
|
143
|
+
* passes a string id and a promise callback.
|
|
199
144
|
*/
|
|
200
145
|
interface WorkerLoaderBinding {
|
|
201
|
-
get(id: string, code: () =>
|
|
146
|
+
get(id: string | null, code: () => unknown): { getDurableObjectClass(name: string): unknown };
|
|
202
147
|
load(code: unknown): LoadedWorkerStub;
|
|
203
148
|
}
|
|
204
149
|
|
|
205
150
|
/**
|
|
206
|
-
* The bindings `
|
|
151
|
+
* The bindings `processes` needs off whichever DO is hosting. A
|
|
207
152
|
* staged boot's assembler may read more off the same env (Nimbus's reads
|
|
208
153
|
* ASSETS); the env travels to it whole, so nothing further is named here.
|
|
209
154
|
*/
|
|
@@ -250,7 +195,7 @@ function facetContainer(ctx: DurableObjectState): FacetContainer {
|
|
|
250
195
|
* storage budget grants no copy-on-write credit — crossing it resets the
|
|
251
196
|
* object rather than raising an error.
|
|
252
197
|
*/
|
|
253
|
-
export async function
|
|
198
|
+
export async function cloneStorage(
|
|
254
199
|
ctx: DurableObjectState,
|
|
255
200
|
clone: {
|
|
256
201
|
src: string;
|
|
@@ -283,7 +228,7 @@ export async function cloneFacetStorage(
|
|
|
283
228
|
}
|
|
284
229
|
|
|
285
230
|
/**
|
|
286
|
-
* The facet name for
|
|
231
|
+
* The facet name for an ephemeral slot. Reused, and that is the entire point.
|
|
287
232
|
*
|
|
288
233
|
* A Durable Object admits 65,536 facets over its LIFETIME: the IDs are
|
|
289
234
|
* append-only and are never reclaimed, so the bound is on facets ever CREATED,
|
|
@@ -295,21 +240,19 @@ export async function cloneFacetStorage(
|
|
|
295
240
|
* Reusing a NAME costs no new ID. So the name comes from a free list and the
|
|
296
241
|
* pid stays what it always was: the process identity in the ProcessTable. The
|
|
297
242
|
* two were only ever conflated because one of them happened to be handy.
|
|
243
|
+
*
|
|
244
|
+
* The book shares the facet-ID space with one other namespace: durable
|
|
245
|
+
* applications, which mint `app-slot-<n>` names of their own (one ID per app,
|
|
246
|
+
* ever). The prefixes are disjoint BY CONSTRUCTION, and that disjointness is
|
|
247
|
+
* load-bearing — a proc-slot name reissued onto a durable app's retained
|
|
248
|
+
* storage would boot the wrong process into someone else's disk.
|
|
298
249
|
*/
|
|
299
250
|
export function residentFacetName(slot: number): string {
|
|
300
251
|
return `proc-slot-${slot}`;
|
|
301
252
|
}
|
|
302
253
|
|
|
303
|
-
/**
|
|
304
|
-
|
|
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';
|
|
254
|
+
/** The prefix every durable application's facet name carries. */
|
|
255
|
+
export const DURABLE_FACET_NAME_PREFIX = 'app-slot-';
|
|
313
256
|
|
|
314
257
|
/** One hosting actor's slot book. */
|
|
315
258
|
interface SlotBook {
|
|
@@ -319,16 +262,6 @@ interface SlotBook {
|
|
|
319
262
|
next: number;
|
|
320
263
|
/** Slot held by each live pid, so release can find it. */
|
|
321
264
|
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
265
|
}
|
|
333
266
|
|
|
334
267
|
/**
|
|
@@ -345,52 +278,23 @@ interface SlotBook {
|
|
|
345
278
|
* VFS epoch, in which case `invalidatedSince` can only answer poison and the
|
|
346
279
|
* whole store is dropped. A process therefore cannot boot onto a previous
|
|
347
280
|
* tenant's filesystem even when release never ran.
|
|
281
|
+
*
|
|
282
|
+
* The book names only the `proc-slot-` space. Durable `app-slot-` names are
|
|
283
|
+
* allocated against DO storage instead (their owner survives a reset), so a
|
|
284
|
+
* fresh incarnation's `next` starting at 0 can never collide with them even
|
|
285
|
+
* before the durable ledger is adopted.
|
|
348
286
|
*/
|
|
349
287
|
const slotBooks = new WeakMap<DurableObjectState, SlotBook>();
|
|
350
288
|
|
|
351
289
|
function slotBook(ctx: DurableObjectState): SlotBook {
|
|
352
290
|
let book = slotBooks.get(ctx);
|
|
353
291
|
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;
|
|
292
|
+
book = { free: [], next: 0, held: new Map() };
|
|
369
293
|
slotBooks.set(ctx, book);
|
|
370
294
|
}
|
|
371
295
|
return book;
|
|
372
296
|
}
|
|
373
297
|
|
|
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
298
|
/** Take a slot for `pid`, reusing a returned one before minting a new name. */
|
|
395
299
|
function acquireSlot(ctx: DurableObjectState, pid: number): number {
|
|
396
300
|
const book = slotBook(ctx);
|
|
@@ -399,49 +303,12 @@ function acquireSlot(ctx: DurableObjectState, pid: number): number {
|
|
|
399
303
|
const reused = book.free.length > 0;
|
|
400
304
|
const slot = reused ? book.free.shift()! : book.next++;
|
|
401
305
|
book.held.set(pid, slot);
|
|
402
|
-
|
|
306
|
+
// A fresh name is a permanently consumed facet ID; the durable count lives
|
|
307
|
+
// in the budgets ledger (see budgets.ts).
|
|
308
|
+
if (!reused) recordFacetNameMinted(ctx, book.next);
|
|
403
309
|
return slot;
|
|
404
310
|
}
|
|
405
311
|
|
|
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
312
|
/** Return `pid`'s slot to the free list. */
|
|
446
313
|
function releaseSlot(ctx: DurableObjectState, pid: number): void {
|
|
447
314
|
const book = slotBook(ctx);
|
|
@@ -452,29 +319,83 @@ function releaseSlot(ctx: DurableObjectState, pid: number): void {
|
|
|
452
319
|
book.free.sort((a, b) => a - b);
|
|
453
320
|
}
|
|
454
321
|
|
|
322
|
+
/**
|
|
323
|
+
* Drop one facet's SQLite by name — the ONLY call site that may delete facet
|
|
324
|
+
* storage. `spawnResident` releases ephemeral processes with abort+delete
|
|
325
|
+
* (storage is slot-reuse hygiene) and durable ones with abort alone (the
|
|
326
|
+
* storage IS the durable application's state); explicit removal arrives here
|
|
327
|
+
* through the coordinator's durable-slot book, owner-checked.
|
|
328
|
+
*/
|
|
329
|
+
export function deleteFacetStorage(ctx: DurableObjectState, name: string): void {
|
|
330
|
+
facetContainer(ctx).delete(name);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
|
|
455
334
|
|
|
456
335
|
/**
|
|
457
|
-
* What `
|
|
336
|
+
* What `processes(ctx, env).spawn` hands back: a running process, minus its placement.
|
|
458
337
|
*
|
|
459
|
-
* `
|
|
460
|
-
*
|
|
461
|
-
*
|
|
462
|
-
*
|
|
338
|
+
* `name` is the facet's real name and `slot` its ephemeral book entry (absent
|
|
339
|
+
* for a durable spawn, whose name its coordinator allocated out of storage).
|
|
340
|
+
* Both ride along because the caller's `describe` needs them and neither is
|
|
341
|
+
* derivable from the pid — reading a slot back out of the book would race the
|
|
342
|
+
* release that empties it.
|
|
463
343
|
*/
|
|
464
|
-
export type ResidentFacet = Omit<HostedProcess, 'describe'> & {
|
|
344
|
+
export type ResidentFacet = Omit<HostedProcess, 'describe'> & { name: string; slot?: number };
|
|
465
345
|
|
|
466
346
|
/**
|
|
467
|
-
*
|
|
468
|
-
*
|
|
347
|
+
* The process surface of one hosting actor: how a resident process comes
|
|
348
|
+
* into existence on workerd, and how a one-shot program runs to completion.
|
|
469
349
|
*
|
|
470
|
-
*
|
|
350
|
+
* `spawn` is the ONE way a resident process comes into existence, and every
|
|
471
351
|
* substrate goes through it: the facet host calls it with the coordinator's
|
|
472
352
|
* own `ctx`, the peer host calls it — over one RPC — with a sibling session
|
|
473
353
|
* DO's. Everything a substrate could plausibly want to special-case is a
|
|
474
354
|
* PARAMETER here rather than a branch: which actor hosts the child, and how
|
|
475
355
|
* the boot spec's by-path members are read.
|
|
476
356
|
*/
|
|
477
|
-
export function
|
|
357
|
+
export function processes(ctx: DurableObjectState, env: ResidentFacetEnv): Processes {
|
|
358
|
+
return new Processes(ctx, env);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
export class Processes {
|
|
362
|
+
constructor(
|
|
363
|
+
private readonly ctx: DurableObjectState,
|
|
364
|
+
private readonly env: ResidentFacetEnv,
|
|
365
|
+
) {}
|
|
366
|
+
|
|
367
|
+
/** Open a resident process as a facet of this actor, and start its runner. */
|
|
368
|
+
spawn(
|
|
369
|
+
disk: () => ResidentDiskReader,
|
|
370
|
+
supervisor: ResidentSupervisorProps,
|
|
371
|
+
params: ProcessHostParams,
|
|
372
|
+
): ResidentFacet {
|
|
373
|
+
return spawnResident(this.ctx, this.env, disk, supervisor, params);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Run one program to completion as an UNKEYED dynamic worker.
|
|
378
|
+
*
|
|
379
|
+
* Unkeyed is the whole difference from `spawn`: nothing can re-resolve this
|
|
380
|
+
* worker into a later request's context, so it can never be a routeable
|
|
381
|
+
* target and never has to be released by name. It exists for the duration
|
|
382
|
+
* of one call and its stubs are dropped as that call unwinds.
|
|
383
|
+
*
|
|
384
|
+
* Shared by both substrates on purpose. `peer` places processes that have a
|
|
385
|
+
* residency to place; a one-shot has none, and shipping its fully-inline
|
|
386
|
+
* map across a sibling hop would meet the 32 MiB RPC ceiling that by-path
|
|
387
|
+
* boot specs exist to avoid — for a run that gains nothing by moving.
|
|
388
|
+
*/
|
|
389
|
+
run<T>(
|
|
390
|
+
supervisor: ResidentSupervisorProps,
|
|
391
|
+
params: OneShotParams,
|
|
392
|
+
consume: (response: Response) => Promise<T>,
|
|
393
|
+
): Promise<T> {
|
|
394
|
+
return runOneShot(this.ctx, this.env, supervisor, params, consume);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
function spawnResident(
|
|
478
399
|
ctx: DurableObjectState,
|
|
479
400
|
env: ResidentFacetEnv,
|
|
480
401
|
disk: () => ResidentDiskReader,
|
|
@@ -482,9 +403,21 @@ export function openResidentFacet(
|
|
|
482
403
|
params: ProcessHostParams,
|
|
483
404
|
): ResidentFacet {
|
|
484
405
|
const facets = facetContainer(ctx);
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
406
|
+
// An explicit name is the durable path: the caller allocated an
|
|
407
|
+
// `app-slot-<n>` identity out of DO storage and this facet keeps its SQLite
|
|
408
|
+
// across aborts. Anything else takes the in-memory book — and that book's
|
|
409
|
+
// `proc-slot-` names must never be minted for it, or an ephemeral release's
|
|
410
|
+
// delete would wipe the app's storage and a reused slot would land a new
|
|
411
|
+
// process on someone else's disk.
|
|
412
|
+
const explicit = params.facet;
|
|
413
|
+
if (explicit && !explicit.name.startsWith(DURABLE_FACET_NAME_PREFIX)) {
|
|
414
|
+
throw new Error(
|
|
415
|
+
`Nimbus: an explicit facet name must carry the '${DURABLE_FACET_NAME_PREFIX}' `
|
|
416
|
+
+ `prefix, got '${explicit.name}'`,
|
|
417
|
+
);
|
|
418
|
+
}
|
|
419
|
+
const slot = explicit ? undefined : acquireSlot(ctx, params.pid);
|
|
420
|
+
const name = explicit ? explicit.name : residentFacetName(slot!);
|
|
488
421
|
// The start callback is the ONLY way this facet is ever created, and it
|
|
489
422
|
// fires AT MOST ONCE. Every later use goes through the stub below, so the
|
|
490
423
|
// callback running a second time means the facet was released or died —
|
|
@@ -510,8 +443,8 @@ export function openResidentFacet(
|
|
|
510
443
|
try {
|
|
511
444
|
facet = facets.get(name, start);
|
|
512
445
|
} catch (error) {
|
|
513
|
-
releaseSlot(ctx, params.pid);
|
|
514
|
-
throw withFacetBudgetNamed(
|
|
446
|
+
if (slot !== undefined) releaseSlot(ctx, params.pid);
|
|
447
|
+
throw withFacetBudgetNamed(facetNameCount(ctx), error);
|
|
515
448
|
}
|
|
516
449
|
|
|
517
450
|
let disposed = false;
|
|
@@ -520,10 +453,17 @@ export function openResidentFacet(
|
|
|
520
453
|
disposed = true;
|
|
521
454
|
released = true;
|
|
522
455
|
try { facets.abort(name, new Error('Nimbus: resident process released')); } catch { /* already gone */ }
|
|
523
|
-
|
|
456
|
+
// The two release classes: an ephemeral facet's SQLite is slot-reuse
|
|
457
|
+
// hygiene — the name is handed out again, so the store must not be — and
|
|
458
|
+
// a durable one's is the application itself: abort ends the process, the
|
|
459
|
+
// data stays for the next boot, and only removeDurableApp's explicit
|
|
460
|
+
// deleteFacetStorage call ever drops it.
|
|
461
|
+
if (!explicit?.durable) {
|
|
462
|
+
try { facets.delete(name); } catch { /* already gone */ }
|
|
463
|
+
}
|
|
524
464
|
// Only after the facet is gone. A slot handed out while its previous
|
|
525
465
|
// tenant were still being torn down would have two processes on one name.
|
|
526
|
-
releaseSlot(ctx, params.pid);
|
|
466
|
+
if (slot !== undefined) releaseSlot(ctx, params.pid);
|
|
527
467
|
};
|
|
528
468
|
|
|
529
469
|
let started: Promise<unknown>;
|
|
@@ -531,15 +471,14 @@ export function openResidentFacet(
|
|
|
531
471
|
started = facet.startProcess(params.startArgs);
|
|
532
472
|
} catch (error) {
|
|
533
473
|
void release();
|
|
534
|
-
throw withFacetBudgetNamed(
|
|
474
|
+
throw withFacetBudgetNamed(facetNameCount(ctx), error);
|
|
535
475
|
}
|
|
536
476
|
// The rejection that carries the platform's failure at ID exhaustion is
|
|
537
477
|
// this one, and it is annotated AFTER awaiting the ledger — the first
|
|
538
478
|
// failure of a fresh incarnation must compare against the persisted count,
|
|
539
479
|
// not the zero its adoption read has not yet replaced.
|
|
540
480
|
started = started.catch(async (error) => {
|
|
541
|
-
|
|
542
|
-
throw withFacetBudgetNamed(Math.max(durable, book.next), error);
|
|
481
|
+
throw withFacetBudgetNamed(await facetNameCountDurable(ctx), error);
|
|
543
482
|
});
|
|
544
483
|
// A caller reads whichever of `started` and the lifecycle it needs, so keep
|
|
545
484
|
// the runtime from reporting the other as an unhandled rejection.
|
|
@@ -552,6 +491,7 @@ export function openResidentFacet(
|
|
|
552
491
|
handleHttpRequest: (request: Request) => facet.handleHttpRequest(request),
|
|
553
492
|
handleWebSocketRequest: (request: Request) => facet.fetch(request),
|
|
554
493
|
release,
|
|
494
|
+
name,
|
|
555
495
|
slot,
|
|
556
496
|
};
|
|
557
497
|
}
|
|
@@ -587,20 +527,7 @@ function residentProcessClass(
|
|
|
587
527
|
}
|
|
588
528
|
}
|
|
589
529
|
|
|
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>(
|
|
530
|
+
async function runOneShot<T>(
|
|
604
531
|
ctx: DurableObjectState,
|
|
605
532
|
env: ResidentFacetEnv,
|
|
606
533
|
supervisor: ResidentSupervisorProps,
|
|
@@ -672,18 +599,33 @@ export async function runOneShotWorker<T>(
|
|
|
672
599
|
}
|
|
673
600
|
}
|
|
674
601
|
|
|
675
|
-
|
|
602
|
+
/**
|
|
603
|
+
* The WorkerCode the loader callback returns for one resident boot: the
|
|
604
|
+
* module map from {@link residentLoaderConfig} (or the staged assembler),
|
|
605
|
+
* plus the isolate's env and network posture.
|
|
606
|
+
*
|
|
607
|
+
* A `code` boot with an explicit `env` — defined, even as `{}` — is the
|
|
608
|
+
* embedder's whole statement about the isolate: the env rides through
|
|
609
|
+
* exactly as minted (loopback stubs by reference), and the composed supervisor
|
|
610
|
+
* entrypoint is not consulted at all, so no SUPERVISOR binding appears.
|
|
611
|
+
* Without one, the default holds: inherited network plus a SUPERVISOR
|
|
612
|
+
* minted from the composed entrypoint for the coordinator's identity.
|
|
613
|
+
*/
|
|
614
|
+
export async function residentWorkerConfig(
|
|
676
615
|
env: ResidentFacetEnv,
|
|
677
616
|
disk: () => ResidentDiskReader,
|
|
678
617
|
supervisor: ResidentSupervisorProps,
|
|
679
618
|
boot: ResidentBootSpec,
|
|
680
619
|
): Promise<Record<string, unknown>> {
|
|
620
|
+
if (boot.kind === 'code' && boot.code.env !== undefined) {
|
|
621
|
+
const isolated = await residentLoaderConfig(boot.code, disk());
|
|
622
|
+
assertModuleMapWithinCodeLimit(configModules(isolated));
|
|
623
|
+
return isolated;
|
|
624
|
+
}
|
|
681
625
|
const config = boot.kind === 'staged'
|
|
682
|
-
? await
|
|
626
|
+
? await stagedBootAssembler()(env, boot.stage)
|
|
683
627
|
: await residentLoaderConfig(boot.code, disk());
|
|
684
|
-
assertModuleMapWithinCodeLimit(
|
|
685
|
-
(config as { modules?: Record<string, unknown> }).modules ?? {},
|
|
686
|
-
);
|
|
628
|
+
assertModuleMapWithinCodeLimit(configModules(config));
|
|
687
629
|
const supervisorRpc = supervisorEntrypoint();
|
|
688
630
|
if (!supervisorRpc) {
|
|
689
631
|
throw new Error(
|
|
@@ -692,3 +634,12 @@ async function residentWorkerConfig(
|
|
|
692
634
|
}
|
|
693
635
|
return { ...config, env: { SUPERVISOR: supervisorRpc({ props: supervisor }) } };
|
|
694
636
|
}
|
|
637
|
+
|
|
638
|
+
/** The module map a loader config assembled, or empty when it named none. */
|
|
639
|
+
function configModules(config: object): Record<string, unknown> {
|
|
640
|
+
const modules: unknown = 'modules' in config ? config.modules : undefined;
|
|
641
|
+
if (typeof modules !== 'object' || modules === null) return {};
|
|
642
|
+
// Loader configs only ever carry a module map under this key.
|
|
643
|
+
const map: Record<string, unknown> = modules as Record<string, unknown>;
|
|
644
|
+
return map;
|
|
645
|
+
}
|