@nimbus-sh/fabric 0.8.0 → 0.10.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 +98 -11
- package/dist/bindings.d.ts +31 -33
- package/dist/bindings.d.ts.map +1 -1
- package/dist/bindings.js +108 -97
- package/dist/budgets.d.ts +102 -29
- package/dist/budgets.d.ts.map +1 -1
- package/dist/budgets.js +266 -44
- package/dist/do-calls.d.ts +20 -0
- package/dist/do-calls.d.ts.map +1 -1
- package/dist/do-calls.js +24 -11
- package/dist/fanout.d.ts +40 -32
- package/dist/fanout.d.ts.map +1 -1
- package/dist/fanout.js +48 -51
- package/dist/fenced-work.d.ts +3 -3
- package/dist/fenced-work.js +3 -3
- package/dist/host-wasm.d.ts +29 -0
- package/dist/host-wasm.d.ts.map +1 -0
- package/dist/host-wasm.js +31 -0
- package/dist/image-store.d.ts +1 -1
- package/dist/image-store.d.ts.map +1 -1
- package/dist/image-store.js +33 -1
- package/dist/inner-do-env.d.ts +83 -0
- package/dist/inner-do-env.d.ts.map +1 -0
- package/dist/inner-do-env.js +181 -0
- package/dist/isolate-pool.d.ts +40 -23
- package/dist/isolate-pool.d.ts.map +1 -1
- package/dist/isolate-pool.js +105 -55
- package/dist/process-fabric.d.ts +26 -11
- package/dist/process-fabric.d.ts.map +1 -1
- package/dist/process-fabric.js +44 -0
- package/dist/timers.d.ts +12 -0
- package/dist/timers.d.ts.map +1 -1
- package/dist/timers.js +44 -9
- package/dist/vendor/types.d.ts +11 -5
- package/dist/vendor/types.d.ts.map +1 -1
- package/dist/workerd-facet-host.d.ts.map +1 -1
- package/dist/workerd-facet-host.js +35 -30
- package/package.json +4 -4
- package/src/bindings.ts +121 -98
- package/src/budgets.ts +311 -53
- package/src/do-calls.ts +45 -11
- package/src/fanout.ts +62 -53
- package/src/fenced-work.ts +3 -3
- package/src/host-wasm.ts +41 -0
- package/src/image-store.ts +29 -2
- package/src/inner-do-env.ts +213 -0
- package/src/isolate-pool.ts +145 -75
- package/src/process-fabric.ts +55 -13
- package/src/timers.ts +45 -9
- package/src/vendor/types.ts +11 -5
- package/src/workerd-facet-host.ts +36 -31
package/dist/timers.js
CHANGED
|
@@ -103,6 +103,7 @@ export class Timers {
|
|
|
103
103
|
arms.push({ reason, whenMs });
|
|
104
104
|
return Promise.resolve(true);
|
|
105
105
|
}
|
|
106
|
+
const epoch = host._timerEpoch ?? 0;
|
|
106
107
|
// Serialize every read-modify-write of the reasons map through one
|
|
107
108
|
// per-instance chain: two schedulers firing back-to-back from one activity
|
|
108
109
|
// hook would otherwise interleave their get→put cycles and silently drop
|
|
@@ -113,15 +114,21 @@ export class Timers {
|
|
|
113
114
|
if (typeof setAlarmFn !== 'function')
|
|
114
115
|
return false;
|
|
115
116
|
const existing = (await ctx.storage.get(TIMER_REASONS_KEY));
|
|
117
|
+
// Requested before a reset: it belongs to the timers the reset voided.
|
|
118
|
+
if ((host._timerEpoch ?? 0) !== epoch)
|
|
119
|
+
return false;
|
|
116
120
|
const map = { ...(existing || {}) };
|
|
117
121
|
// Earliest-deadline-first: only update if new request is sooner or
|
|
118
122
|
// this reason has no pending entry.
|
|
123
|
+
let written;
|
|
119
124
|
if (!(reason in map) || whenMs < map[reason]) {
|
|
120
125
|
map[reason] = whenMs;
|
|
121
|
-
|
|
126
|
+
written = ctx.storage.put(TIMER_REASONS_KEY, map);
|
|
122
127
|
}
|
|
123
|
-
|
|
124
|
-
|
|
128
|
+
// Issued in the turn the epoch was checked in: a reset after this
|
|
129
|
+
// point wipes and disarms after these, never before.
|
|
130
|
+
setAlarmFn.call(ctx.storage, Math.min(...Object.values(map)));
|
|
131
|
+
await written;
|
|
125
132
|
return true;
|
|
126
133
|
}
|
|
127
134
|
catch (e) {
|
|
@@ -133,6 +140,17 @@ export class Timers {
|
|
|
133
140
|
host._timerChain = chained;
|
|
134
141
|
return chained;
|
|
135
142
|
}
|
|
143
|
+
/**
|
|
144
|
+
* Void every timer of this instance: a schedule or dispatch already
|
|
145
|
+
* requested — still queued on the chain, or a dispatch whose handlers are
|
|
146
|
+
* running — writes no reason and arms no alarm from here on. For a
|
|
147
|
+
* deliberate end of the actor's state (a session destroy): call it before
|
|
148
|
+
* wiping storage and deleting the alarm, so nothing in flight writes the
|
|
149
|
+
* map back or re-arms after the wipe. Requests made after it proceed.
|
|
150
|
+
*/
|
|
151
|
+
reset() {
|
|
152
|
+
this.host._timerEpoch = (this.host._timerEpoch ?? 0) + 1;
|
|
153
|
+
}
|
|
136
154
|
/**
|
|
137
155
|
* Multi-reason timer dispatcher. Called from the DO's `alarm()` handler
|
|
138
156
|
* with the embedder's handler map.
|
|
@@ -155,14 +173,22 @@ export class Timers {
|
|
|
155
173
|
*/
|
|
156
174
|
dispatch(handlers, onLegacyAlarm, alarmInfo) {
|
|
157
175
|
const { host, ctx } = this;
|
|
176
|
+
const epoch = host._timerEpoch ?? 0;
|
|
177
|
+
const current = () => (host._timerEpoch ?? 0) === epoch;
|
|
158
178
|
// Same serialization as schedule: the dispatcher's read→handlers→write
|
|
159
179
|
// cycle must not interleave with an activity-hook schedule.
|
|
160
|
-
const chained = (host._timerChain ?? Promise.resolve()).then(() => dispatchBody(ctx, handlers, onLegacyAlarm, alarmInfo), () => dispatchBody(ctx, handlers, onLegacyAlarm, alarmInfo));
|
|
180
|
+
const chained = (host._timerChain ?? Promise.resolve()).then(() => dispatchBody(ctx, current, handlers, onLegacyAlarm, alarmInfo), () => dispatchBody(ctx, current, handlers, onLegacyAlarm, alarmInfo));
|
|
161
181
|
host._timerChain = chained;
|
|
162
182
|
return chained;
|
|
163
183
|
}
|
|
164
184
|
}
|
|
165
|
-
|
|
185
|
+
/**
|
|
186
|
+
* One dispatch. `current` answers whether the epoch it was requested in still
|
|
187
|
+
* stands: once a reset voids it, no further handler runs and nothing is
|
|
188
|
+
* written or armed — the reset's caller is wiping this state, and a write
|
|
189
|
+
* after the wipe would outlive it.
|
|
190
|
+
*/
|
|
191
|
+
async function dispatchBody(ctx, current, handlers, onLegacyAlarm, alarmInfo) {
|
|
166
192
|
// Collect schedule requests made inside handler context (see
|
|
167
193
|
// dispatchArms) and fold them into the map below, so an in-dispatch arm
|
|
168
194
|
// neither deadlocks on the chain nor races the write.
|
|
@@ -170,6 +196,8 @@ async function dispatchBody(ctx, handlers, onLegacyAlarm, alarmInfo) {
|
|
|
170
196
|
try {
|
|
171
197
|
const now = Date.now();
|
|
172
198
|
const existing = (await ctx?.storage?.get?.(TIMER_REASONS_KEY));
|
|
199
|
+
if (!current())
|
|
200
|
+
return;
|
|
173
201
|
const map = { ...(existing || {}) };
|
|
174
202
|
const hadMap = Object.keys(map).length > 0;
|
|
175
203
|
if (!hadMap) {
|
|
@@ -185,6 +213,8 @@ async function dispatchBody(ctx, handlers, onLegacyAlarm, alarmInfo) {
|
|
|
185
213
|
fired.push(reason);
|
|
186
214
|
}
|
|
187
215
|
for (const reason of fired) {
|
|
216
|
+
if (!current())
|
|
217
|
+
return;
|
|
188
218
|
delete map[reason];
|
|
189
219
|
const handler = handlers[reason];
|
|
190
220
|
// Unknown reasons silently dropped (forward-compat).
|
|
@@ -201,20 +231,25 @@ async function dispatchBody(ctx, handlers, onLegacyAlarm, alarmInfo) {
|
|
|
201
231
|
}
|
|
202
232
|
}
|
|
203
233
|
}
|
|
234
|
+
// A reset while the handlers ran: the map read above is gone, and
|
|
235
|
+
// writing it back — or arming for it — would revive what was ended.
|
|
236
|
+
if (!current())
|
|
237
|
+
return;
|
|
204
238
|
// Fold the in-dispatch arms, earliest-deadline-first per reason.
|
|
205
239
|
for (const arm of arms) {
|
|
206
240
|
if (!(arm.reason in map) || arm.whenMs < map[arm.reason]) {
|
|
207
241
|
map[arm.reason] = arm.whenMs;
|
|
208
242
|
}
|
|
209
243
|
}
|
|
210
|
-
// Re-arm or clear
|
|
244
|
+
// Re-arm or clear, issued in the turn the epoch was checked in: a reset
|
|
245
|
+
// after this point wipes and disarms after these, never before.
|
|
211
246
|
const setAlarmFn = ctx?.storage?.setAlarm;
|
|
212
247
|
if (Object.keys(map).length > 0) {
|
|
213
|
-
|
|
214
|
-
const earliest = Math.min(...Object.values(map));
|
|
248
|
+
const written = ctx.storage.put(TIMER_REASONS_KEY, map);
|
|
215
249
|
if (typeof setAlarmFn === 'function') {
|
|
216
|
-
setAlarmFn.call(ctx.storage,
|
|
250
|
+
setAlarmFn.call(ctx.storage, Math.min(...Object.values(map)));
|
|
217
251
|
}
|
|
252
|
+
await written;
|
|
218
253
|
}
|
|
219
254
|
else if (hadMap) {
|
|
220
255
|
try {
|
package/dist/vendor/types.d.ts
CHANGED
|
@@ -17,13 +17,19 @@ export interface JsonModule {
|
|
|
17
17
|
json: unknown;
|
|
18
18
|
}
|
|
19
19
|
/**
|
|
20
|
-
* A
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
20
|
+
* A WebAssembly module, importable from a facet by module name. Nimbus ships
|
|
21
|
+
* these on every path — sql.js for node:sqlite, the interpreter images for
|
|
22
|
+
* python and ruby — so the omission here was the type lagging the API, not a
|
|
23
|
+
* kind the loader lacks. The loader takes either the bytes, which it
|
|
24
|
+
* compiles, or a module the caller holds compiled already, whose compiled
|
|
25
|
+
* code the dynamic worker then shares (workerd src/workerd/api/
|
|
26
|
+
* worker-loader.c++, extractWasmModuleContent). A module handed over this
|
|
27
|
+
* way should be described with describeHostWasm (host-wasm.ts): the
|
|
28
|
+
* code-size budget and the loader cache key cannot read a Module's size or
|
|
29
|
+
* identity from JS.
|
|
24
30
|
*/
|
|
25
31
|
export interface WasmModule {
|
|
26
|
-
wasm: ArrayBuffer;
|
|
32
|
+
wasm: ArrayBuffer | WebAssembly.Module;
|
|
27
33
|
}
|
|
28
34
|
/** Plain string = type inferred from file extension (.js or .py). */
|
|
29
35
|
export type ModuleContent = string | JsModule | CjsModule | PyModule | TextModule | DataModule | JsonModule | WasmModule;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/vendor/types.ts"],"names":[],"mappings":"AAIA,MAAM,WAAW,QAAQ;IAAG,EAAE,EAAE,MAAM,CAAA;CAAE;AACxC,MAAM,WAAW,SAAS;IAAG,GAAG,EAAE,MAAM,CAAA;CAAE;AAC1C,MAAM,WAAW,QAAQ;IAAG,EAAE,EAAE,MAAM,CAAA;CAAE;AACxC,MAAM,WAAW,UAAU;IAAG,IAAI,EAAE,MAAM,CAAA;CAAE;AAC5C,MAAM,WAAW,UAAU;IAAG,IAAI,EAAE,WAAW,CAAA;CAAE;AACjD,MAAM,WAAW,UAAU;IAAG,IAAI,EAAE,OAAO,CAAA;CAAE;AAC7C
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/vendor/types.ts"],"names":[],"mappings":"AAIA,MAAM,WAAW,QAAQ;IAAG,EAAE,EAAE,MAAM,CAAA;CAAE;AACxC,MAAM,WAAW,SAAS;IAAG,GAAG,EAAE,MAAM,CAAA;CAAE;AAC1C,MAAM,WAAW,QAAQ;IAAG,EAAE,EAAE,MAAM,CAAA;CAAE;AACxC,MAAM,WAAW,UAAU;IAAG,IAAI,EAAE,MAAM,CAAA;CAAE;AAC5C,MAAM,WAAW,UAAU;IAAG,IAAI,EAAE,WAAW,CAAA;CAAE;AACjD,MAAM,WAAW,UAAU;IAAG,IAAI,EAAE,OAAO,CAAA;CAAE;AAC7C;;;;;;;;;;;GAWG;AACH,MAAM,WAAW,UAAU;IAAG,IAAI,EAAE,WAAW,GAAG,WAAW,CAAC,MAAM,CAAA;CAAE;AAEtE,qEAAqE;AACrE,MAAM,MAAM,aAAa,GACrB,MAAM,GACN,QAAQ,GACR,SAAS,GACT,QAAQ,GACR,UAAU,GACV,UAAU,GACV,UAAU,GACV,UAAU,CAAC;AAEf,MAAM,WAAW,UAAU;IACzB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,kBAAkB,CAAC,EAAE,MAAM,EAAE,CAAC;IAC9B,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,aAAa,CAAC,CAAC;IACvC,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAE9B;;;;;OAKG;IACH,cAAc,CAAC,EAAE,WAAW,GAAG,IAAI,CAAC;IACpC,KAAK,CAAC,EAAE,WAAW,EAAE,CAAC;CACvB;AAED,MAAM,WAAW,WAAW;IAC1B,KAAK,CAAC,KAAK,EAAE,WAAW,EAAE,IAAI,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;CAClE;AAED,MAAM,WAAW,iBAAiB;IAChC,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACjC;AAED,MAAM,WAAW,UAAU;IACzB,aAAa,IAAI,cAAc,CAAC;IAChC,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,iBAAiB,GAAG,cAAc,CAAC;CACvE;AAED,MAAM,WAAW,cAAc;IAC7B,KAAK,CAAC,KAAK,EAAE,WAAW,EAAE,IAAI,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;IACjE,OAAO,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CAC/C;AAED,MAAM,MAAM,eAAe,GAAG,MAAM,OAAO,CAAC,UAAU,CAAC,CAAC;AAExD,MAAM,WAAW,YAAY;IAC3B;;;;OAIG;IACH,GAAG,CAAC,EAAE,EAAE,MAAM,EAAE,eAAe,EAAE,eAAe,GAAG,UAAU,CAAC;CAC/D"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"workerd-facet-host.d.ts","sourceRoot":"","sources":["../src/workerd-facet-host.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;
|
|
1
|
+
{"version":3,"file":"workerd-facet-host.d.ts","sourceRoot":"","sources":["../src/workerd-facet-host.ts"],"names":[],"mappings":"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"}
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
import { disposeRpcResource } from '@nimbus-sh/platform/rpc-dispose.js';
|
|
16
16
|
import { StorageLedger, forgetFacetStorage } from '@nimbus-sh/core/runtime/storage-ledger.js';
|
|
17
17
|
import { getCtxExports, stagedBootAssembler, supervisorEntrypoint, supervisorEntrypointName, } from './composition.js';
|
|
18
|
-
import { assertModuleMapWithinCodeLimit, beginLoaderFetch, facetNameCount, facetNameCountDurable, recordFacetNameMinted,
|
|
18
|
+
import { assertModuleMapWithinCodeLimit, beginLoaderFetch, facetNameCount, facetNameCountDurable, recordFacetNameMinted, withDynamicWorkerCapNamed, withFacetBudgetNamed, } from './budgets.js';
|
|
19
19
|
import { RESIDENT_PROCESS_CLASS, residentLoaderConfig, } from './process-fabric.js';
|
|
20
20
|
import { supervisorLoaderKey } from './supervisor-props.js';
|
|
21
21
|
export function getNimbusCtxExports() {
|
|
@@ -87,7 +87,7 @@ export async function cloneStorage(ctx, clone) {
|
|
|
87
87
|
const facets = facetContainer(ctx);
|
|
88
88
|
if (typeof facets.clone !== 'function') {
|
|
89
89
|
throw new Error('Nimbus: ctx.facets.clone is unavailable in this runtime; the reflink image '
|
|
90
|
-
+ 'path needs
|
|
90
|
+
+ 'path needs workerd 1.20260926.1 or later, or deployed Cloudflare workerd');
|
|
91
91
|
}
|
|
92
92
|
const { src, dst } = clone;
|
|
93
93
|
if (!(await clone.populated(src))) {
|
|
@@ -292,10 +292,13 @@ function spawnResident(ctx, env, disk, supervisor, params) {
|
|
|
292
292
|
+ 'it is not restarted');
|
|
293
293
|
}
|
|
294
294
|
evaluated = true;
|
|
295
|
-
return { class: residentProcessClass(
|
|
295
|
+
return { class: residentProcessClass(env, disk, supervisor, params, loaderKey) };
|
|
296
296
|
};
|
|
297
297
|
const book = slotBook(ctx);
|
|
298
298
|
const ledger = sessionLedger(ctx);
|
|
299
|
+
// A warm worker keeps the SUPERVISOR binding it was built with, and the
|
|
300
|
+
// loader outlives this instance.
|
|
301
|
+
const loaderKey = supervisorLoaderKey(params.workerKey, supervisor);
|
|
299
302
|
let facet;
|
|
300
303
|
try {
|
|
301
304
|
// N18: the fill is admitted, and recorded under the facet's name, before
|
|
@@ -315,6 +318,12 @@ function spawnResident(ctx, env, disk, supervisor, params) {
|
|
|
315
318
|
}
|
|
316
319
|
if (explicit)
|
|
317
320
|
book.live.add(name);
|
|
321
|
+
// The facet's worker is one Dynamic Worker in flight for as long as the
|
|
322
|
+
// process is resident, not only while a call is open: its WebSockets and
|
|
323
|
+
// streamed responses outlive the calls the ledger could bracket, and a
|
|
324
|
+
// request can reach it at any moment. Held from here to `release`, so no
|
|
325
|
+
// fan-out spends the slot a running process needs.
|
|
326
|
+
const endResidency = beginLoaderFetch(ctx, loaderKey);
|
|
318
327
|
facetOfPid(ctx).set(params.pid, name);
|
|
319
328
|
let disposed = false;
|
|
320
329
|
const release = async () => {
|
|
@@ -323,6 +332,7 @@ function spawnResident(ctx, env, disk, supervisor, params) {
|
|
|
323
332
|
disposed = true;
|
|
324
333
|
released = true;
|
|
325
334
|
facetOfPid(ctx).delete(params.pid);
|
|
335
|
+
endResidency();
|
|
326
336
|
try {
|
|
327
337
|
facets.abort(name, new Error('Nimbus: resident process released'));
|
|
328
338
|
}
|
|
@@ -396,25 +406,15 @@ function spawnResident(ctx, env, disk, supervisor, params) {
|
|
|
396
406
|
* assembles its module map at most once and the bytes never stay resident in
|
|
397
407
|
* the hosting DO's heap.
|
|
398
408
|
*/
|
|
399
|
-
function residentProcessClass(
|
|
409
|
+
function residentProcessClass(env, disk, supervisor, params, loaderKey) {
|
|
400
410
|
const loader = env.LOADER;
|
|
401
411
|
if (!loader || typeof loader.get !== 'function') {
|
|
402
412
|
throw new Error('Nimbus: env.LOADER binding missing or invalid. Resident processes require '
|
|
403
413
|
+ 'the Worker Loader binding; add it via worker_loaders in wrangler.jsonc.');
|
|
404
414
|
}
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
try {
|
|
409
|
-
const worker = loader
|
|
410
|
-
.get(loaderKey, () => residentWorkerConfig(env, disk, supervisor, params.boot))
|
|
411
|
-
.getDurableObjectClass(RESIDENT_PROCESS_CLASS);
|
|
412
|
-
recordLoaderId(ctx, loaderKey);
|
|
413
|
-
return worker;
|
|
414
|
-
}
|
|
415
|
-
catch (error) {
|
|
416
|
-
throw withDynamicWorkerCapNamed(ctx, error);
|
|
417
|
-
}
|
|
415
|
+
return loader
|
|
416
|
+
.get(loaderKey, () => residentWorkerConfig(env, disk, supervisor, params.boot))
|
|
417
|
+
.getDurableObjectClass(RESIDENT_PROCESS_CLASS);
|
|
418
418
|
}
|
|
419
419
|
async function runOneShot(ctx, env, supervisor, params, consume) {
|
|
420
420
|
const loader = env.LOADER;
|
|
@@ -455,23 +455,28 @@ async function runOneShot(ctx, env, supervisor, params, consume) {
|
|
|
455
455
|
throw new Error('Nimbus: one-shot runtime entrypoint has no fetch method');
|
|
456
456
|
}
|
|
457
457
|
params.onLoaded?.();
|
|
458
|
-
// The unkeyed worker is
|
|
459
|
-
//
|
|
460
|
-
// never wrapped: see beginLoaderFetch for the
|
|
461
|
-
// hazard, and the pipelined-`fetch.call` note above
|
|
462
|
-
|
|
463
|
-
|
|
458
|
+
// The unkeyed worker is one distinct dynamic worker in flight until its
|
|
459
|
+
// response is consumed (the body streams from it), keyed by this run's
|
|
460
|
+
// writer id — bracketed, never wrapped: see beginLoaderFetch for the
|
|
461
|
+
// measured DO-poisoning hazard, and the pipelined-`fetch.call` note above
|
|
462
|
+
// for its sibling.
|
|
463
|
+
const endFetch = beginLoaderFetch(ctx, `one-shot:${params.writerId}`);
|
|
464
464
|
try {
|
|
465
|
-
response = await ep.fetch(params.request);
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
465
|
+
const response = await ep.fetch(params.request);
|
|
466
|
+
try {
|
|
467
|
+
return await consume(response);
|
|
468
|
+
}
|
|
469
|
+
finally {
|
|
470
|
+
disposeRpcResource(response);
|
|
471
|
+
}
|
|
469
472
|
}
|
|
470
|
-
|
|
471
|
-
|
|
473
|
+
catch (error) {
|
|
474
|
+
// A limit refusal pauses the ledger's admissions (beginLoaderFetchWhenFree).
|
|
475
|
+
endFetch(error);
|
|
476
|
+
throw error;
|
|
472
477
|
}
|
|
473
478
|
finally {
|
|
474
|
-
|
|
479
|
+
endFetch();
|
|
475
480
|
}
|
|
476
481
|
}
|
|
477
482
|
catch (error) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nimbus-sh/fabric",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "The Cloudflare half of Nimbus \u2014 Durable Object facet hosting, dynamic-worker loader pools, the resident-process fabric, and DO alarm/hibernation machinery.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cloudflare",
|
|
@@ -50,12 +50,12 @@
|
|
|
50
50
|
"typecheck": "tsc --noEmit"
|
|
51
51
|
},
|
|
52
52
|
"dependencies": {
|
|
53
|
-
"@nimbus-sh/core": "^0.
|
|
54
|
-
"@nimbus-sh/platform": "^0.
|
|
53
|
+
"@nimbus-sh/core": "^0.15.0",
|
|
54
|
+
"@nimbus-sh/platform": "^0.7.0",
|
|
55
55
|
"zod": "^4.4.3"
|
|
56
56
|
},
|
|
57
57
|
"devDependencies": {
|
|
58
|
-
"@cloudflare/workers-types": "^
|
|
58
|
+
"@cloudflare/workers-types": "^5.20260928.1",
|
|
59
59
|
"typescript": "^5.7.0"
|
|
60
60
|
},
|
|
61
61
|
"publishConfig": {
|
package/src/bindings.ts
CHANGED
|
@@ -29,6 +29,7 @@ import { disposeRpcResource, useRpcResource } from '@nimbus-sh/platform/rpc-disp
|
|
|
29
29
|
import { hostNamespace, supervisorEntrypoint, supervisorEntrypointName, stagedBootAssembler } from './composition.js';
|
|
30
30
|
import type { HostRoute } from './composition.js';
|
|
31
31
|
import { hostNamespaceBinding, hostOpDispatch, type HostOpDispatch } from './host-dispatch.js';
|
|
32
|
+
import { innerDoIdFromName } from './inner-do-env.js';
|
|
32
33
|
import { assertModuleMapWithinCodeLimit } from './budgets.js';
|
|
33
34
|
import type { EntrypointLoopbackFactory } from './composition.js';
|
|
34
35
|
import type { WorkerCode } from './vendor/types.js';
|
|
@@ -708,46 +709,23 @@ interface NimbusDoNamespaceProps {
|
|
|
708
709
|
}
|
|
709
710
|
|
|
710
711
|
/**
|
|
711
|
-
* `env.MY_DO`
|
|
712
|
+
* The binding the loader passes for `env.MY_DO` (a WorkerEntrypoint of the
|
|
713
|
+
* session's isolate). The inner Worker does not use it as its namespace: a
|
|
714
|
+
* DurableObjectNamespace's API is synchronous, and this one's answers are
|
|
715
|
+
* RpcPromises, which cannot travel as arguments. The inner Worker replaces it
|
|
716
|
+
* with a local namespace (inner-do-env.ts, innerWorkerModules) that makes ids
|
|
717
|
+
* and stubs itself, and relays each member a stub's caller reaches, its
|
|
718
|
+
* fetch included, to `callOn` or `getOn`.
|
|
712
719
|
*
|
|
713
|
-
*
|
|
714
|
-
*
|
|
715
|
-
*
|
|
716
|
-
*
|
|
717
|
-
*
|
|
718
|
-
* IMPORTANT: unlike the real DurableObjectNamespace, idFromName /
|
|
719
|
-
* newUniqueId / idFromString here return **Promises**, because they're
|
|
720
|
-
* RPC-backed WorkerEntrypoint methods. The inner caller MUST `await`
|
|
721
|
-
* them before passing the result to `.get()`. Workers RPC pipelining
|
|
722
|
-
* does not currently allow passing an RpcPromise as a method argument
|
|
723
|
-
* — the no-await form fails with:
|
|
724
|
-
* "Could not serialize object of type \"RpcPromise\"."
|
|
725
|
-
*
|
|
726
|
-
* Typical real-Worker code written for Cloudflare's synchronous
|
|
727
|
-
* DurableObjectNamespace needs a one-word change (add `await`).
|
|
728
|
-
*
|
|
729
|
-
* idFromName produces prefix `name:` (deterministic FNV-style hash);
|
|
730
|
-
* newUniqueId uses `uniq:` (random). The prefixes keep the two id
|
|
731
|
-
* spaces distinct so a name-derived id can't collide with a random
|
|
732
|
-
* one.
|
|
720
|
+
* `idFromName`, `newUniqueId`, `idFromString` and `get` answer as before, for
|
|
721
|
+
* a caller that awaits them. idFromName produces prefix `name:` (a
|
|
722
|
+
* deterministic hash, innerDoIdFromName); newUniqueId uses `uniq:`; the
|
|
723
|
+
* prefixes keep the two id spaces distinct.
|
|
733
724
|
*/
|
|
734
|
-
export class NimbusDurableObjectNamespace extends WorkerEntrypoint<
|
|
725
|
+
export class NimbusDurableObjectNamespace extends WorkerEntrypoint<object, NimbusDoNamespaceProps> {
|
|
735
726
|
/** Stable string id derived from a name. Hash is deterministic. */
|
|
736
727
|
idFromName(name: string): string {
|
|
737
|
-
|
|
738
|
-
// distinct names → distinct strings; same name → same string.
|
|
739
|
-
let h1 = 0xdeadbeef ^ name.length;
|
|
740
|
-
let h2 = 0x41c6ce57 ^ name.length;
|
|
741
|
-
for (let i = 0; i < name.length; i++) {
|
|
742
|
-
const ch = name.charCodeAt(i);
|
|
743
|
-
h1 = Math.imul(h1 ^ ch, 2654435761);
|
|
744
|
-
h2 = Math.imul(h2 ^ ch, 1597334677);
|
|
745
|
-
}
|
|
746
|
-
h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507) ^ Math.imul(h2 ^ (h2 >>> 13), 3266489909);
|
|
747
|
-
h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507) ^ Math.imul(h1 ^ (h1 >>> 13), 3266489909);
|
|
748
|
-
const high = (h1 >>> 0).toString(16).padStart(8, '0');
|
|
749
|
-
const low = (h2 >>> 0).toString(16).padStart(8, '0');
|
|
750
|
-
return 'name:' + high + low;
|
|
728
|
+
return innerDoIdFromName(name);
|
|
751
729
|
}
|
|
752
730
|
|
|
753
731
|
/** Fresh random id (matches DurableObjectNamespace.newUniqueId()). */
|
|
@@ -774,6 +752,19 @@ export class NimbusDurableObjectNamespace extends WorkerEntrypoint<unknown, Nimb
|
|
|
774
752
|
},
|
|
775
753
|
});
|
|
776
754
|
}
|
|
755
|
+
|
|
756
|
+
/**
|
|
757
|
+
* The member of object `id` at `path` (names from the object down), called
|
|
758
|
+
* with `args`: its answer, or what it throws, as the object gave it.
|
|
759
|
+
*/
|
|
760
|
+
callOn(id: string, path: string[], args: unknown[]): Promise<unknown> {
|
|
761
|
+
return innerDoMember(this.env, { ...(this.ctx.props || {}), id: String(id) }, path, args);
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
/** The member of object `id` at `path`, read. */
|
|
765
|
+
getOn(id: string, path: string[]): Promise<unknown> {
|
|
766
|
+
return innerDoMember(this.env, { ...(this.ctx.props || {}), id: String(id) }, path, null);
|
|
767
|
+
}
|
|
777
768
|
}
|
|
778
769
|
|
|
779
770
|
/** Props the DO stub carries: which binding, which supervisor, which id. */
|
|
@@ -782,70 +773,102 @@ interface NimbusDoStubProps extends NimbusDoNamespaceProps {
|
|
|
782
773
|
}
|
|
783
774
|
|
|
784
775
|
/**
|
|
785
|
-
*
|
|
786
|
-
*
|
|
787
|
-
*
|
|
788
|
-
* inner DO class via getInnerDoClass() (./inner-do-registry.js) and
|
|
789
|
-
* spins up / attaches to a facet via the supervisor's ctx.facets in
|
|
790
|
-
* the SAME outer request context — never reusing stubs across requests.
|
|
776
|
+
* The session Durable Object, through the composed host namespace, and its
|
|
777
|
+
* one supervisorOp entrypoint: a host forwards envelopes, not private _rpc*
|
|
778
|
+
* methods. Throws when the binding cannot reach it; `release` drops the stub.
|
|
791
779
|
*/
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
const
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
780
|
+
function supervisorOf(env: object | undefined, props: NimbusDoStubProps): { dispatch: HostOpDispatch; release(): void } {
|
|
781
|
+
const supervisorDoId = String(props.supervisorDoId || '');
|
|
782
|
+
if (!supervisorDoId) throw new Error('supervisorDoId missing');
|
|
783
|
+
let stub: DurableObjectStub | null = null;
|
|
784
|
+
try {
|
|
785
|
+
const ns = hostNamespaceBinding(env ?? {}, 'NimbusDOStub', props.route);
|
|
786
|
+
stub = ns.get(ns.idFromString(supervisorDoId));
|
|
787
|
+
const held = stub;
|
|
788
|
+
return { dispatch: hostOpDispatch(stub, 'NimbusDOStub', props.route), release: () => disposeRpcResource(held) };
|
|
789
|
+
} catch (e) {
|
|
790
|
+
disposeRpcResource(stub);
|
|
791
|
+
throw e;
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
/** What the session's innerDoFetch answers: the object's response, as fields. */
|
|
796
|
+
export interface InnerDoFetchAnswer {
|
|
797
|
+
status: number;
|
|
798
|
+
statusText: string;
|
|
799
|
+
headers: [string, string][];
|
|
800
|
+
body: ArrayBuffer | null;
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
function isInnerDoFetchAnswer(value: unknown): value is InnerDoFetchAnswer {
|
|
804
|
+
return value !== null && typeof value === 'object'
|
|
805
|
+
&& 'status' in value && typeof value.status === 'number'
|
|
806
|
+
&& 'statusText' in value && typeof value.statusText === 'string'
|
|
807
|
+
&& 'headers' in value && Array.isArray(value.headers)
|
|
808
|
+
&& 'body' in value && (value.body === null || value.body instanceof ArrayBuffer);
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
/**
|
|
812
|
+
* The inner object's fetch: the request forwarded whole (method, body,
|
|
813
|
+
* headers) as fields the session reconstructs, and its response rebuilt here
|
|
814
|
+
* from the fields the session answers. A binding that cannot reach the
|
|
815
|
+
* session answers 500.
|
|
816
|
+
*/
|
|
817
|
+
async function innerDoFetch(env: object | undefined, props: NimbusDoStubProps, request: Request): Promise<Response> {
|
|
818
|
+
let supervisor: ReturnType<typeof supervisorOf>;
|
|
819
|
+
try {
|
|
820
|
+
supervisor = supervisorOf(env, props);
|
|
821
|
+
} catch (e) {
|
|
822
|
+
return new Response(`Nimbus: ${e instanceof Error ? e.message : String(e)}`, { status: 500 });
|
|
823
|
+
}
|
|
824
|
+
try {
|
|
825
|
+
const body = request.method !== 'GET' && request.method !== 'HEAD' ? await request.arrayBuffer() : null;
|
|
821
826
|
const headerList: [string, string][] = [];
|
|
822
827
|
request.headers.forEach((v, k) => { headerList.push([k, v]); });
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
828
|
+
return await useRpcResource(
|
|
829
|
+
supervisor.dispatch({
|
|
830
|
+
op: 'innerDoFetch',
|
|
831
|
+
args: [{ bindingName: String(props.bindingName || ''), id: String(props.id || ''), method: request.method, url: request.url, headers: headerList, body }],
|
|
832
|
+
}),
|
|
833
|
+
(res) => {
|
|
834
|
+
if (!isInnerDoFetchAnswer(res)) {
|
|
835
|
+
return new Response('Nimbus: innerDoFetch returned an invalid result', { status: 502 });
|
|
836
|
+
}
|
|
837
|
+
return new Response(res.body, { status: res.status, statusText: res.statusText, headers: res.headers });
|
|
838
|
+
},
|
|
839
|
+
);
|
|
840
|
+
} finally {
|
|
841
|
+
supervisor.release();
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
/**
|
|
846
|
+
* A member of the inner object, called with `args` or read (`args` null): the
|
|
847
|
+
* session reaches it on the object's facet and answers what it answered (a
|
|
848
|
+
* stub, function or stream travels as one) or rejects with what it threw,
|
|
849
|
+
* type and message kept.
|
|
850
|
+
*/
|
|
851
|
+
async function innerDoMember(env: object | undefined, props: NimbusDoStubProps, path: string[], args: unknown[] | null): Promise<unknown> {
|
|
852
|
+
const supervisor = supervisorOf(env, props);
|
|
853
|
+
try {
|
|
854
|
+
return await supervisor.dispatch({
|
|
855
|
+
op: 'innerDoCall',
|
|
856
|
+
args: [{ bindingName: String(props.bindingName || ''), id: String(props.id || ''), path, args }],
|
|
857
|
+
});
|
|
858
|
+
} finally {
|
|
859
|
+
supervisor.release();
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
/**
|
|
864
|
+
* A Durable-Object-namespace-stub for a specific id, for a caller of the
|
|
865
|
+
* binding's own `get`: its fetch. The important invariant: EVERY call
|
|
866
|
+
* resolves the inner DO class via getInnerDoClass() (./inner-do-registry.js)
|
|
867
|
+
* and spins up / attaches to a facet via the supervisor's ctx.facets in the
|
|
868
|
+
* SAME outer request context — never reusing stubs across requests.
|
|
869
|
+
*/
|
|
870
|
+
export class NimbusDOStub extends WorkerEntrypoint<object, NimbusDoStubProps> {
|
|
871
|
+
async fetch(request: Request): Promise<Response> {
|
|
872
|
+
return innerDoFetch(this.env, this.ctx.props || {}, request);
|
|
850
873
|
}
|
|
851
874
|
}
|