@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/src/timers.ts
CHANGED
|
@@ -58,9 +58,12 @@ export const TIMER_REASONS_KEY = 'w1_next_alarm_reasons';
|
|
|
58
58
|
* The host instance carrying the per-instance timer chain. The field lives on
|
|
59
59
|
* the embedder's DO instance so one chain serializes every timer-map
|
|
60
60
|
* read-modify-write for that instance (see {@link Timers.schedule}).
|
|
61
|
+
* `_timerEpoch` counts {@link Timers.reset}s: a schedule or dispatch writes
|
|
62
|
+
* only while the epoch it was requested in is still current.
|
|
61
63
|
*/
|
|
62
64
|
export interface TimerHost {
|
|
63
65
|
_timerChain?: Promise<unknown>;
|
|
66
|
+
_timerEpoch?: number;
|
|
64
67
|
}
|
|
65
68
|
|
|
66
69
|
/**
|
|
@@ -159,6 +162,7 @@ export class Timers {
|
|
|
159
162
|
arms.push({ reason, whenMs });
|
|
160
163
|
return Promise.resolve(true);
|
|
161
164
|
}
|
|
165
|
+
const epoch = host._timerEpoch ?? 0;
|
|
162
166
|
// Serialize every read-modify-write of the reasons map through one
|
|
163
167
|
// per-instance chain: two schedulers firing back-to-back from one activity
|
|
164
168
|
// hook would otherwise interleave their get→put cycles and silently drop
|
|
@@ -170,15 +174,20 @@ export class Timers {
|
|
|
170
174
|
const existing = (await ctx.storage.get(TIMER_REASONS_KEY)) as
|
|
171
175
|
| Record<string, number>
|
|
172
176
|
| undefined;
|
|
177
|
+
// Requested before a reset: it belongs to the timers the reset voided.
|
|
178
|
+
if ((host._timerEpoch ?? 0) !== epoch) return false;
|
|
173
179
|
const map: Record<string, number> = { ...(existing || {}) };
|
|
174
180
|
// Earliest-deadline-first: only update if new request is sooner or
|
|
175
181
|
// this reason has no pending entry.
|
|
182
|
+
let written: Promise<unknown> | undefined;
|
|
176
183
|
if (!(reason in map) || whenMs < map[reason]) {
|
|
177
184
|
map[reason] = whenMs;
|
|
178
|
-
|
|
185
|
+
written = ctx.storage.put(TIMER_REASONS_KEY, map);
|
|
179
186
|
}
|
|
180
|
-
|
|
181
|
-
|
|
187
|
+
// Issued in the turn the epoch was checked in: a reset after this
|
|
188
|
+
// point wipes and disarms after these, never before.
|
|
189
|
+
setAlarmFn.call(ctx.storage, Math.min(...Object.values(map)));
|
|
190
|
+
await written;
|
|
182
191
|
return true;
|
|
183
192
|
} catch (e) {
|
|
184
193
|
console.warn('[nimbus/W1] timers.schedule threw:', errorText(e));
|
|
@@ -190,6 +199,18 @@ export class Timers {
|
|
|
190
199
|
return chained;
|
|
191
200
|
}
|
|
192
201
|
|
|
202
|
+
/**
|
|
203
|
+
* Void every timer of this instance: a schedule or dispatch already
|
|
204
|
+
* requested — still queued on the chain, or a dispatch whose handlers are
|
|
205
|
+
* running — writes no reason and arms no alarm from here on. For a
|
|
206
|
+
* deliberate end of the actor's state (a session destroy): call it before
|
|
207
|
+
* wiping storage and deleting the alarm, so nothing in flight writes the
|
|
208
|
+
* map back or re-arms after the wipe. Requests made after it proceed.
|
|
209
|
+
*/
|
|
210
|
+
reset(): void {
|
|
211
|
+
this.host._timerEpoch = (this.host._timerEpoch ?? 0) + 1;
|
|
212
|
+
}
|
|
213
|
+
|
|
193
214
|
/**
|
|
194
215
|
* Multi-reason timer dispatcher. Called from the DO's `alarm()` handler
|
|
195
216
|
* with the embedder's handler map.
|
|
@@ -216,19 +237,28 @@ export class Timers {
|
|
|
216
237
|
alarmInfo?: TimerAlarmInfo,
|
|
217
238
|
): Promise<void> {
|
|
218
239
|
const { host, ctx } = this;
|
|
240
|
+
const epoch = host._timerEpoch ?? 0;
|
|
241
|
+
const current = (): boolean => (host._timerEpoch ?? 0) === epoch;
|
|
219
242
|
// Same serialization as schedule: the dispatcher's read→handlers→write
|
|
220
243
|
// cycle must not interleave with an activity-hook schedule.
|
|
221
244
|
const chained = (host._timerChain ?? Promise.resolve()).then(
|
|
222
|
-
() => dispatchBody(ctx, handlers, onLegacyAlarm, alarmInfo),
|
|
223
|
-
() => dispatchBody(ctx, handlers, onLegacyAlarm, alarmInfo),
|
|
245
|
+
() => dispatchBody(ctx, current, handlers, onLegacyAlarm, alarmInfo),
|
|
246
|
+
() => dispatchBody(ctx, current, handlers, onLegacyAlarm, alarmInfo),
|
|
224
247
|
);
|
|
225
248
|
host._timerChain = chained;
|
|
226
249
|
return chained;
|
|
227
250
|
}
|
|
228
251
|
}
|
|
229
252
|
|
|
253
|
+
/**
|
|
254
|
+
* One dispatch. `current` answers whether the epoch it was requested in still
|
|
255
|
+
* stands: once a reset voids it, no further handler runs and nothing is
|
|
256
|
+
* written or armed — the reset's caller is wiping this state, and a write
|
|
257
|
+
* after the wipe would outlive it.
|
|
258
|
+
*/
|
|
230
259
|
async function dispatchBody(
|
|
231
260
|
ctx: TimerContext,
|
|
261
|
+
current: () => boolean,
|
|
232
262
|
handlers: TimerHandlers,
|
|
233
263
|
onLegacyAlarm?: () => void,
|
|
234
264
|
alarmInfo?: TimerAlarmInfo,
|
|
@@ -242,6 +272,7 @@ async function dispatchBody(
|
|
|
242
272
|
const existing = (await ctx?.storage?.get?.(TIMER_REASONS_KEY)) as
|
|
243
273
|
| Record<string, number>
|
|
244
274
|
| undefined;
|
|
275
|
+
if (!current()) return;
|
|
245
276
|
const map: Record<string, number> = { ...(existing || {}) };
|
|
246
277
|
const hadMap = Object.keys(map).length > 0;
|
|
247
278
|
if (!hadMap) {
|
|
@@ -255,6 +286,7 @@ async function dispatchBody(
|
|
|
255
286
|
if (when <= now) fired.push(reason);
|
|
256
287
|
}
|
|
257
288
|
for (const reason of fired) {
|
|
289
|
+
if (!current()) return;
|
|
258
290
|
delete map[reason];
|
|
259
291
|
const handler = handlers[reason];
|
|
260
292
|
// Unknown reasons silently dropped (forward-compat).
|
|
@@ -269,20 +301,24 @@ async function dispatchBody(
|
|
|
269
301
|
}
|
|
270
302
|
}
|
|
271
303
|
}
|
|
304
|
+
// A reset while the handlers ran: the map read above is gone, and
|
|
305
|
+
// writing it back — or arming for it — would revive what was ended.
|
|
306
|
+
if (!current()) return;
|
|
272
307
|
// Fold the in-dispatch arms, earliest-deadline-first per reason.
|
|
273
308
|
for (const arm of arms) {
|
|
274
309
|
if (!(arm.reason in map) || arm.whenMs < map[arm.reason]) {
|
|
275
310
|
map[arm.reason] = arm.whenMs;
|
|
276
311
|
}
|
|
277
312
|
}
|
|
278
|
-
// Re-arm or clear
|
|
313
|
+
// Re-arm or clear, issued in the turn the epoch was checked in: a reset
|
|
314
|
+
// after this point wipes and disarms after these, never before.
|
|
279
315
|
const setAlarmFn = ctx?.storage?.setAlarm;
|
|
280
316
|
if (Object.keys(map).length > 0) {
|
|
281
|
-
|
|
282
|
-
const earliest = Math.min(...Object.values(map));
|
|
317
|
+
const written = ctx.storage.put(TIMER_REASONS_KEY, map);
|
|
283
318
|
if (typeof setAlarmFn === 'function') {
|
|
284
|
-
setAlarmFn.call(ctx.storage,
|
|
319
|
+
setAlarmFn.call(ctx.storage, Math.min(...Object.values(map)));
|
|
285
320
|
}
|
|
321
|
+
await written;
|
|
286
322
|
} else if (hadMap) {
|
|
287
323
|
try { await ctx.storage.delete(TIMER_REASONS_KEY); } catch {}
|
|
288
324
|
// No remaining reasons → no setAlarm call → DO becomes
|
package/src/vendor/types.ts
CHANGED
|
@@ -9,12 +9,18 @@ export interface TextModule { text: string }
|
|
|
9
9
|
export interface DataModule { data: ArrayBuffer }
|
|
10
10
|
export interface JsonModule { json: unknown }
|
|
11
11
|
/**
|
|
12
|
-
* A
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
12
|
+
* A WebAssembly module, importable from a facet by module name. Nimbus ships
|
|
13
|
+
* these on every path — sql.js for node:sqlite, the interpreter images for
|
|
14
|
+
* python and ruby — so the omission here was the type lagging the API, not a
|
|
15
|
+
* kind the loader lacks. The loader takes either the bytes, which it
|
|
16
|
+
* compiles, or a module the caller holds compiled already, whose compiled
|
|
17
|
+
* code the dynamic worker then shares (workerd src/workerd/api/
|
|
18
|
+
* worker-loader.c++, extractWasmModuleContent). A module handed over this
|
|
19
|
+
* way should be described with describeHostWasm (host-wasm.ts): the
|
|
20
|
+
* code-size budget and the loader cache key cannot read a Module's size or
|
|
21
|
+
* identity from JS.
|
|
16
22
|
*/
|
|
17
|
-
export interface WasmModule { wasm: ArrayBuffer }
|
|
23
|
+
export interface WasmModule { wasm: ArrayBuffer | WebAssembly.Module }
|
|
18
24
|
|
|
19
25
|
/** Plain string = type inferred from file extension (.js or .py). */
|
|
20
26
|
export type ModuleContent =
|
|
@@ -28,7 +28,6 @@ import {
|
|
|
28
28
|
facetNameCount,
|
|
29
29
|
facetNameCountDurable,
|
|
30
30
|
recordFacetNameMinted,
|
|
31
|
-
recordLoaderId,
|
|
32
31
|
withDynamicWorkerCapNamed,
|
|
33
32
|
withFacetBudgetNamed,
|
|
34
33
|
} from './budgets.js';
|
|
@@ -121,9 +120,10 @@ interface FacetContainer {
|
|
|
121
120
|
abort(name: string, reason?: unknown): void;
|
|
122
121
|
delete(name: string): void;
|
|
123
122
|
/**
|
|
124
|
-
*
|
|
125
|
-
*
|
|
126
|
-
* {@link cloneStorage}, the one way the
|
|
123
|
+
* Declared by `@cloudflare/workers-types` 5 and present in workerd
|
|
124
|
+
* ≥ 1.20260926.1 and in production; an embedder's older local workerd
|
|
125
|
+
* (≤ 1.20260603.1) lacks it — see {@link cloneStorage}, the one way the
|
|
126
|
+
* fabric calls it.
|
|
127
127
|
*/
|
|
128
128
|
clone?(src: string, dst: string): void;
|
|
129
129
|
}
|
|
@@ -212,7 +212,7 @@ export async function cloneStorage(
|
|
|
212
212
|
if (typeof facets.clone !== 'function') {
|
|
213
213
|
throw new Error(
|
|
214
214
|
'Nimbus: ctx.facets.clone is unavailable in this runtime; the reflink image '
|
|
215
|
-
+ 'path needs
|
|
215
|
+
+ 'path needs workerd 1.20260926.1 or later, or deployed Cloudflare workerd',
|
|
216
216
|
);
|
|
217
217
|
}
|
|
218
218
|
const { src, dst } = clone;
|
|
@@ -471,10 +471,13 @@ function spawnResident(
|
|
|
471
471
|
);
|
|
472
472
|
}
|
|
473
473
|
evaluated = true;
|
|
474
|
-
return { class: residentProcessClass(
|
|
474
|
+
return { class: residentProcessClass(env, disk, supervisor, params, loaderKey) };
|
|
475
475
|
};
|
|
476
476
|
const book = slotBook(ctx);
|
|
477
477
|
const ledger = sessionLedger(ctx);
|
|
478
|
+
// A warm worker keeps the SUPERVISOR binding it was built with, and the
|
|
479
|
+
// loader outlives this instance.
|
|
480
|
+
const loaderKey = supervisorLoaderKey(params.workerKey, supervisor);
|
|
478
481
|
let facet: ResidentFacetStub;
|
|
479
482
|
try {
|
|
480
483
|
// N18: the fill is admitted, and recorded under the facet's name, before
|
|
@@ -490,6 +493,12 @@ function spawnResident(
|
|
|
490
493
|
throw withFacetBudgetNamed(facetNameCount(ctx), error);
|
|
491
494
|
}
|
|
492
495
|
if (explicit) book.live.add(name);
|
|
496
|
+
// The facet's worker is one Dynamic Worker in flight for as long as the
|
|
497
|
+
// process is resident, not only while a call is open: its WebSockets and
|
|
498
|
+
// streamed responses outlive the calls the ledger could bracket, and a
|
|
499
|
+
// request can reach it at any moment. Held from here to `release`, so no
|
|
500
|
+
// fan-out spends the slot a running process needs.
|
|
501
|
+
const endResidency = beginLoaderFetch(ctx, loaderKey);
|
|
493
502
|
facetOfPid(ctx).set(params.pid, name);
|
|
494
503
|
|
|
495
504
|
let disposed = false;
|
|
@@ -498,6 +507,7 @@ function spawnResident(
|
|
|
498
507
|
disposed = true;
|
|
499
508
|
released = true;
|
|
500
509
|
facetOfPid(ctx).delete(params.pid);
|
|
510
|
+
endResidency();
|
|
501
511
|
try { facets.abort(name, new Error('Nimbus: resident process released')); } catch { /* already gone */ }
|
|
502
512
|
if (explicit) book.live.delete(name);
|
|
503
513
|
// The two release classes: an ephemeral facet's SQLite is slot-reuse
|
|
@@ -564,11 +574,11 @@ function spawnResident(
|
|
|
564
574
|
* the hosting DO's heap.
|
|
565
575
|
*/
|
|
566
576
|
function residentProcessClass(
|
|
567
|
-
ctx: DurableObjectState,
|
|
568
577
|
env: ResidentFacetEnv,
|
|
569
578
|
disk: () => ResidentDiskReader,
|
|
570
579
|
supervisor: ResidentSupervisorProps,
|
|
571
580
|
params: ProcessHostParams,
|
|
581
|
+
loaderKey: string,
|
|
572
582
|
): unknown {
|
|
573
583
|
const loader = env.LOADER;
|
|
574
584
|
if (!loader || typeof loader.get !== 'function') {
|
|
@@ -577,18 +587,9 @@ function residentProcessClass(
|
|
|
577
587
|
+ 'the Worker Loader binding; add it via worker_loaders in wrangler.jsonc.',
|
|
578
588
|
);
|
|
579
589
|
}
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
try {
|
|
584
|
-
const worker = loader
|
|
585
|
-
.get(loaderKey, () => residentWorkerConfig(env, disk, supervisor, params.boot))
|
|
586
|
-
.getDurableObjectClass(RESIDENT_PROCESS_CLASS);
|
|
587
|
-
recordLoaderId(ctx, loaderKey);
|
|
588
|
-
return worker;
|
|
589
|
-
} catch (error) {
|
|
590
|
-
throw withDynamicWorkerCapNamed(ctx, error);
|
|
591
|
-
}
|
|
590
|
+
return loader
|
|
591
|
+
.get(loaderKey, () => residentWorkerConfig(env, disk, supervisor, params.boot))
|
|
592
|
+
.getDurableObjectClass(RESIDENT_PROCESS_CLASS);
|
|
592
593
|
}
|
|
593
594
|
|
|
594
595
|
async function runOneShot<T>(
|
|
@@ -638,22 +639,26 @@ async function runOneShot<T>(
|
|
|
638
639
|
throw new Error('Nimbus: one-shot runtime entrypoint has no fetch method');
|
|
639
640
|
}
|
|
640
641
|
params.onLoaded?.();
|
|
641
|
-
// The unkeyed worker is
|
|
642
|
-
//
|
|
643
|
-
// never wrapped: see beginLoaderFetch for the
|
|
644
|
-
// hazard, and the pipelined-`fetch.call` note above
|
|
645
|
-
|
|
646
|
-
|
|
642
|
+
// The unkeyed worker is one distinct dynamic worker in flight until its
|
|
643
|
+
// response is consumed (the body streams from it), keyed by this run's
|
|
644
|
+
// writer id — bracketed, never wrapped: see beginLoaderFetch for the
|
|
645
|
+
// measured DO-poisoning hazard, and the pipelined-`fetch.call` note above
|
|
646
|
+
// for its sibling.
|
|
647
|
+
const endFetch = beginLoaderFetch(ctx, `one-shot:${params.writerId}`);
|
|
647
648
|
try {
|
|
648
|
-
response = await ep.fetch(params.request);
|
|
649
|
+
const response = await ep.fetch(params.request);
|
|
650
|
+
try {
|
|
651
|
+
return await consume(response);
|
|
652
|
+
} finally {
|
|
653
|
+
disposeRpcResource(response);
|
|
654
|
+
}
|
|
655
|
+
} catch (error) {
|
|
656
|
+
// A limit refusal pauses the ledger's admissions (beginLoaderFetchWhenFree).
|
|
657
|
+
endFetch(error);
|
|
658
|
+
throw error;
|
|
649
659
|
} finally {
|
|
650
660
|
endFetch();
|
|
651
661
|
}
|
|
652
|
-
try {
|
|
653
|
-
return await consume(response);
|
|
654
|
-
} finally {
|
|
655
|
-
disposeRpcResource(response);
|
|
656
|
-
}
|
|
657
662
|
} catch (error) {
|
|
658
663
|
throw withDynamicWorkerCapNamed(ctx, error);
|
|
659
664
|
} finally {
|