@nimbus-sh/fabric 0.9.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 +72 -0
- 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 +61 -9
- package/dist/budgets.d.ts.map +1 -1
- package/dist/budgets.js +205 -31
- package/dist/fanout.d.ts.map +1 -1
- package/dist/fanout.js +4 -3
- package/dist/fenced-work.d.ts +3 -3
- package/dist/fenced-work.js +3 -3
- 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 +9 -0
- package/dist/isolate-pool.d.ts.map +1 -1
- package/dist/isolate-pool.js +55 -27
- package/dist/workerd-facet-host.d.ts.map +1 -1
- package/dist/workerd-facet-host.js +5 -0
- package/package.json +2 -2
- package/src/bindings.ts +121 -98
- package/src/budgets.ts +244 -33
- package/src/fanout.ts +5 -3
- package/src/fenced-work.ts +3 -3
- package/src/inner-do-env.ts +213 -0
- package/src/isolate-pool.ts +65 -27
- package/src/workerd-facet-host.ts +4 -0
package/README.md
CHANGED
|
@@ -67,6 +67,15 @@ Worker that imports Nimbus's own entry (`@nimbus-sh/sdk/worker`) inherits
|
|
|
67
67
|
that entry's composition, so a host that names its own namespace composes in
|
|
68
68
|
a Worker that does not import it.
|
|
69
69
|
|
|
70
|
+
A program's filesystem calls reach the supervisor entrypoint through its
|
|
71
|
+
`answer(method, args)`, which resolves `{ value }`, or `{ refusal }` for an
|
|
72
|
+
error with a `code` (ENOENT, ENOTDIR, EEXIST), and the program's client
|
|
73
|
+
rethrows the refusal as the error a throw would have delivered. A refusal
|
|
74
|
+
thrown from an entrypoint is recorded by the platform as an exception
|
|
75
|
+
("canceled ... your Worker's code had hung") although its caller was
|
|
76
|
+
answered. `SupervisorRPC` implements `answer`, so an entrypoint that extends
|
|
77
|
+
it has it; one written from scratch must implement it too.
|
|
78
|
+
|
|
70
79
|
The route back to the host (namespace, dispatch method, supervisor
|
|
71
80
|
entrypoint) is minted into every binding the fabric hands a program, in the
|
|
72
81
|
host's isolate, and the entrypoints that answer those bindings read it from
|
|
@@ -247,6 +256,43 @@ as it lives. `dynamicWorkerHeadroom(ctx)` is what is left,
|
|
|
247
256
|
`claimDynamicWorkers(ctx, n)` reserves a width, `loaderLedgerStats(ctx)`
|
|
248
257
|
reports it all, and a limit refusal names the workers in flight.
|
|
249
258
|
|
|
259
|
+
`beginLoaderFetch(ctx, key)` holds a worker and returns the function that
|
|
260
|
+
ends the hold. To wait for room rather than be refused, take the hold with
|
|
261
|
+
`beginLoaderFetchWhenFree` instead:
|
|
262
|
+
|
|
263
|
+
```ts
|
|
264
|
+
import { beginLoaderFetchWhenFree } from '@nimbus-sh/fabric';
|
|
265
|
+
|
|
266
|
+
const end = await beginLoaderFetchWhenFree(ctx, key, { signal: AbortSignal.timeout(15_000) });
|
|
267
|
+
try {
|
|
268
|
+
return await worker.getEntrypoint().run();
|
|
269
|
+
} catch (error) {
|
|
270
|
+
end(error); // a limit refusal pauses the ledger; anything else just ends the hold
|
|
271
|
+
throw error;
|
|
272
|
+
} finally {
|
|
273
|
+
end();
|
|
274
|
+
}
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
It resolves, holding `key`, once `key` is already in flight (requests to
|
|
278
|
+
one worker count once) or one more distinct worker fits. Waits are let in in
|
|
279
|
+
the order they asked, one per freed slot, by whoever makes the room: any
|
|
280
|
+
hold's end, a claim's release, or the end of a refusal's pause, so a wait
|
|
281
|
+
sees Nimbus's own releases as well as the caller's. The hold is taken as the
|
|
282
|
+
wait is let in, so no other caller can take the slot first, and it is the
|
|
283
|
+
caller's to end. An aborted `signal` rejects the wait with its reason, and
|
|
284
|
+
the wait holds nothing. A resident process holds its worker for as long as
|
|
285
|
+
it runs, so bound a wait that room may never reach.
|
|
286
|
+
|
|
287
|
+
The platform counts a worker for a moment after its call returns, which no
|
|
288
|
+
release can show. So a call refused with "Dynamic worker concurrency limit
|
|
289
|
+
exceeded" hands the refusal to its end function, and the ledger then admits
|
|
290
|
+
no new worker for 50 ms, doubling to 2 s while refusals continue
|
|
291
|
+
(`loaderLedgerStats(ctx).pauseMs`; the headroom reads 0 meanwhile).
|
|
292
|
+
`IsolatePool` retries a refused call this way, for 15 s in all. Holds taken
|
|
293
|
+
under a claim (`beginLoaderFetch(ctx, key, claim)`, or `IsolatePool`'s
|
|
294
|
+
`claim` option, which `Fanout` passes) count inside its width.
|
|
295
|
+
|
|
250
296
|
## Process fabric
|
|
251
297
|
|
|
252
298
|
A resident process is a Durable Object facet running a class from a dynamic
|
|
@@ -344,6 +390,32 @@ and never `method.call(ep, request)`, which workerd refuses.
|
|
|
344
390
|
|
|
345
391
|
Nesting is capped at depth 4. Raise it with `NIMBUS_INNER_LOADER_DEPTH`.
|
|
346
392
|
|
|
393
|
+
A classic Durable Object binding is a local namespace inside the inner
|
|
394
|
+
Worker (`inner-do-env.ts`), because a namespace's API is synchronous and an
|
|
395
|
+
RpcPromise cannot travel as an argument. `innerWorkerModules` makes an adapter
|
|
396
|
+
the bundle's first import; it replaces `env.MY_DO` in the env every handler,
|
|
397
|
+
entrypoint and object of the isolate sees, so
|
|
398
|
+
`env.MY_DO.get(env.MY_DO.idFromName('x'))` needs no `await`. A stub is
|
|
399
|
+
`new RpcStub(target)`, with a prototype shaped as a Durable Object stub's:
|
|
400
|
+
an entrypoint of a dynamically-loaded Worker is not transferable, an RPC stub
|
|
401
|
+
is. The target relays each member the caller reaches (a call, a read, or a
|
|
402
|
+
path through both) to `NimbusDurableObjectNamespace.callOn` or `getOn`, and
|
|
403
|
+
the session reaches it on the object's facet. The adapter also exports the
|
|
404
|
+
build's class check, under a name the bundle never spells.
|
|
405
|
+
tests/unit/wrangler-dev-do-rpc-workerd.mjs checks the whole stub surface
|
|
406
|
+
against plain workerd.
|
|
407
|
+
|
|
408
|
+
Limits, where a stub differs from Cloudflare's:
|
|
409
|
+
|
|
410
|
+
- `typeof stub` is `'function'`, not `'object'`: the runtime makes every RPC
|
|
411
|
+
stub callable.
|
|
412
|
+
- A Worker Loader env cannot carry a stub. `env.LOADER.load({ env: { S: stub } })`
|
|
413
|
+
throws "RpcStub cannot be serialized in this context because it is not a
|
|
414
|
+
persistent stub", where Cloudflare passes it. The loader shim keeps a
|
|
415
|
+
child's code and loads it again in each later request, so the child's env
|
|
416
|
+
can carry nothing made in one request; a namespace is refused there on
|
|
417
|
+
Cloudflare too.
|
|
418
|
+
|
|
347
419
|
## Measured platform limits
|
|
348
420
|
|
|
349
421
|
Figures below come from production workerd, June to August 2026. The code
|
package/dist/bindings.d.ts
CHANGED
|
@@ -226,30 +226,20 @@ interface NimbusDoNamespaceProps {
|
|
|
226
226
|
route?: HostRoute;
|
|
227
227
|
}
|
|
228
228
|
/**
|
|
229
|
-
* `env.MY_DO`
|
|
229
|
+
* The binding the loader passes for `env.MY_DO` (a WorkerEntrypoint of the
|
|
230
|
+
* session's isolate). The inner Worker does not use it as its namespace: a
|
|
231
|
+
* DurableObjectNamespace's API is synchronous, and this one's answers are
|
|
232
|
+
* RpcPromises, which cannot travel as arguments. The inner Worker replaces it
|
|
233
|
+
* with a local namespace (inner-do-env.ts, innerWorkerModules) that makes ids
|
|
234
|
+
* and stubs itself, and relays each member a stub's caller reaches, its
|
|
235
|
+
* fetch included, to `callOn` or `getOn`.
|
|
230
236
|
*
|
|
231
|
-
*
|
|
232
|
-
*
|
|
233
|
-
*
|
|
234
|
-
*
|
|
235
|
-
*
|
|
236
|
-
* IMPORTANT: unlike the real DurableObjectNamespace, idFromName /
|
|
237
|
-
* newUniqueId / idFromString here return **Promises**, because they're
|
|
238
|
-
* RPC-backed WorkerEntrypoint methods. The inner caller MUST `await`
|
|
239
|
-
* them before passing the result to `.get()`. Workers RPC pipelining
|
|
240
|
-
* does not currently allow passing an RpcPromise as a method argument
|
|
241
|
-
* — the no-await form fails with:
|
|
242
|
-
* "Could not serialize object of type \"RpcPromise\"."
|
|
243
|
-
*
|
|
244
|
-
* Typical real-Worker code written for Cloudflare's synchronous
|
|
245
|
-
* DurableObjectNamespace needs a one-word change (add `await`).
|
|
246
|
-
*
|
|
247
|
-
* idFromName produces prefix `name:` (deterministic FNV-style hash);
|
|
248
|
-
* newUniqueId uses `uniq:` (random). The prefixes keep the two id
|
|
249
|
-
* spaces distinct so a name-derived id can't collide with a random
|
|
250
|
-
* one.
|
|
237
|
+
* `idFromName`, `newUniqueId`, `idFromString` and `get` answer as before, for
|
|
238
|
+
* a caller that awaits them. idFromName produces prefix `name:` (a
|
|
239
|
+
* deterministic hash, innerDoIdFromName); newUniqueId uses `uniq:`; the
|
|
240
|
+
* prefixes keep the two id spaces distinct.
|
|
251
241
|
*/
|
|
252
|
-
export declare class NimbusDurableObjectNamespace extends WorkerEntrypoint<
|
|
242
|
+
export declare class NimbusDurableObjectNamespace extends WorkerEntrypoint<object, NimbusDoNamespaceProps> {
|
|
253
243
|
/** Stable string id derived from a name. Hash is deterministic. */
|
|
254
244
|
idFromName(name: string): string;
|
|
255
245
|
/** Fresh random id (matches DurableObjectNamespace.newUniqueId()). */
|
|
@@ -258,25 +248,33 @@ export declare class NimbusDurableObjectNamespace extends WorkerEntrypoint<unkno
|
|
|
258
248
|
idFromString(s: string): string;
|
|
259
249
|
/** Return a stub bound to the given id. */
|
|
260
250
|
get(id: string): unknown;
|
|
251
|
+
/**
|
|
252
|
+
* The member of object `id` at `path` (names from the object down), called
|
|
253
|
+
* with `args`: its answer, or what it throws, as the object gave it.
|
|
254
|
+
*/
|
|
255
|
+
callOn(id: string, path: string[], args: unknown[]): Promise<unknown>;
|
|
256
|
+
/** The member of object `id` at `path`, read. */
|
|
257
|
+
getOn(id: string, path: string[]): Promise<unknown>;
|
|
261
258
|
}
|
|
262
259
|
/** Props the DO stub carries: which binding, which supervisor, which id. */
|
|
263
260
|
interface NimbusDoStubProps extends NimbusDoNamespaceProps {
|
|
264
261
|
id?: string;
|
|
265
262
|
}
|
|
263
|
+
/** What the session's innerDoFetch answers: the object's response, as fields. */
|
|
264
|
+
export interface InnerDoFetchAnswer {
|
|
265
|
+
status: number;
|
|
266
|
+
statusText: string;
|
|
267
|
+
headers: [string, string][];
|
|
268
|
+
body: ArrayBuffer | null;
|
|
269
|
+
}
|
|
266
270
|
/**
|
|
267
|
-
* A Durable-Object-namespace-stub for a specific id
|
|
268
|
-
*
|
|
269
|
-
*
|
|
270
|
-
*
|
|
271
|
-
*
|
|
272
|
-
* the SAME outer request context — never reusing stubs across requests.
|
|
271
|
+
* A Durable-Object-namespace-stub for a specific id, for a caller of the
|
|
272
|
+
* binding's own `get`: its fetch. The important invariant: EVERY call
|
|
273
|
+
* resolves the inner DO class via getInnerDoClass() (./inner-do-registry.js)
|
|
274
|
+
* and spins up / attaches to a facet via the supervisor's ctx.facets in the
|
|
275
|
+
* SAME outer request context — never reusing stubs across requests.
|
|
273
276
|
*/
|
|
274
277
|
export declare class NimbusDOStub extends WorkerEntrypoint<object, NimbusDoStubProps> {
|
|
275
|
-
/**
|
|
276
|
-
* Resolve the supervisor DO through the composed host namespace and
|
|
277
|
-
* dispatch the innerDoFetch op through its one supervisorOp entrypoint —
|
|
278
|
-
* a host forwards envelopes, not private _rpc* methods.
|
|
279
|
-
*/
|
|
280
278
|
fetch(request: Request): Promise<Response>;
|
|
281
279
|
}
|
|
282
280
|
export {};
|
package/dist/bindings.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bindings.d.ts","sourceRoot":"","sources":["../src/bindings.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,OAAO,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AACtD,OAAO,EAAE,CAAC,EAAE,MAAM,QAAQ,CAAC;AAG3B,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;
|
|
1
|
+
{"version":3,"file":"bindings.d.ts","sourceRoot":"","sources":["../src/bindings.ts"],"names":[],"mappings":"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"}
|
package/dist/bindings.js
CHANGED
|
@@ -27,6 +27,7 @@ import { z } from 'zod/v4';
|
|
|
27
27
|
import { disposeRpcResource, useRpcResource } from '@nimbus-sh/platform/rpc-dispose.js';
|
|
28
28
|
import { supervisorEntrypoint, supervisorEntrypointName, stagedBootAssembler } from './composition.js';
|
|
29
29
|
import { hostNamespaceBinding, hostOpDispatch } from './host-dispatch.js';
|
|
30
|
+
import { innerDoIdFromName } from './inner-do-env.js';
|
|
30
31
|
import { assertModuleMapWithinCodeLimit } from './budgets.js';
|
|
31
32
|
/**
|
|
32
33
|
* `ctx.exports` — workerd's loopback bag, which the installed
|
|
@@ -586,46 +587,23 @@ export class NimbusLoadedEntrypoint extends WorkerEntrypoint {
|
|
|
586
587
|
}
|
|
587
588
|
}
|
|
588
589
|
/**
|
|
589
|
-
* `env.MY_DO`
|
|
590
|
+
* The binding the loader passes for `env.MY_DO` (a WorkerEntrypoint of the
|
|
591
|
+
* session's isolate). The inner Worker does not use it as its namespace: a
|
|
592
|
+
* DurableObjectNamespace's API is synchronous, and this one's answers are
|
|
593
|
+
* RpcPromises, which cannot travel as arguments. The inner Worker replaces it
|
|
594
|
+
* with a local namespace (inner-do-env.ts, innerWorkerModules) that makes ids
|
|
595
|
+
* and stubs itself, and relays each member a stub's caller reaches, its
|
|
596
|
+
* fetch included, to `callOn` or `getOn`.
|
|
590
597
|
*
|
|
591
|
-
*
|
|
592
|
-
*
|
|
593
|
-
*
|
|
594
|
-
*
|
|
595
|
-
*
|
|
596
|
-
* IMPORTANT: unlike the real DurableObjectNamespace, idFromName /
|
|
597
|
-
* newUniqueId / idFromString here return **Promises**, because they're
|
|
598
|
-
* RPC-backed WorkerEntrypoint methods. The inner caller MUST `await`
|
|
599
|
-
* them before passing the result to `.get()`. Workers RPC pipelining
|
|
600
|
-
* does not currently allow passing an RpcPromise as a method argument
|
|
601
|
-
* — the no-await form fails with:
|
|
602
|
-
* "Could not serialize object of type \"RpcPromise\"."
|
|
603
|
-
*
|
|
604
|
-
* Typical real-Worker code written for Cloudflare's synchronous
|
|
605
|
-
* DurableObjectNamespace needs a one-word change (add `await`).
|
|
606
|
-
*
|
|
607
|
-
* idFromName produces prefix `name:` (deterministic FNV-style hash);
|
|
608
|
-
* newUniqueId uses `uniq:` (random). The prefixes keep the two id
|
|
609
|
-
* spaces distinct so a name-derived id can't collide with a random
|
|
610
|
-
* one.
|
|
598
|
+
* `idFromName`, `newUniqueId`, `idFromString` and `get` answer as before, for
|
|
599
|
+
* a caller that awaits them. idFromName produces prefix `name:` (a
|
|
600
|
+
* deterministic hash, innerDoIdFromName); newUniqueId uses `uniq:`; the
|
|
601
|
+
* prefixes keep the two id spaces distinct.
|
|
611
602
|
*/
|
|
612
603
|
export class NimbusDurableObjectNamespace extends WorkerEntrypoint {
|
|
613
604
|
/** Stable string id derived from a name. Hash is deterministic. */
|
|
614
605
|
idFromName(name) {
|
|
615
|
-
|
|
616
|
-
// distinct names → distinct strings; same name → same string.
|
|
617
|
-
let h1 = 0xdeadbeef ^ name.length;
|
|
618
|
-
let h2 = 0x41c6ce57 ^ name.length;
|
|
619
|
-
for (let i = 0; i < name.length; i++) {
|
|
620
|
-
const ch = name.charCodeAt(i);
|
|
621
|
-
h1 = Math.imul(h1 ^ ch, 2654435761);
|
|
622
|
-
h2 = Math.imul(h2 ^ ch, 1597334677);
|
|
623
|
-
}
|
|
624
|
-
h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507) ^ Math.imul(h2 ^ (h2 >>> 13), 3266489909);
|
|
625
|
-
h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507) ^ Math.imul(h1 ^ (h1 >>> 13), 3266489909);
|
|
626
|
-
const high = (h1 >>> 0).toString(16).padStart(8, '0');
|
|
627
|
-
const low = (h2 >>> 0).toString(16).padStart(8, '0');
|
|
628
|
-
return 'name:' + high + low;
|
|
606
|
+
return innerDoIdFromName(name);
|
|
629
607
|
}
|
|
630
608
|
/** Fresh random id (matches DurableObjectNamespace.newUniqueId()). */
|
|
631
609
|
newUniqueId() {
|
|
@@ -650,72 +628,105 @@ export class NimbusDurableObjectNamespace extends WorkerEntrypoint {
|
|
|
650
628
|
},
|
|
651
629
|
});
|
|
652
630
|
}
|
|
631
|
+
/**
|
|
632
|
+
* The member of object `id` at `path` (names from the object down), called
|
|
633
|
+
* with `args`: its answer, or what it throws, as the object gave it.
|
|
634
|
+
*/
|
|
635
|
+
callOn(id, path, args) {
|
|
636
|
+
return innerDoMember(this.env, { ...(this.ctx.props || {}), id: String(id) }, path, args);
|
|
637
|
+
}
|
|
638
|
+
/** The member of object `id` at `path`, read. */
|
|
639
|
+
getOn(id, path) {
|
|
640
|
+
return innerDoMember(this.env, { ...(this.ctx.props || {}), id: String(id) }, path, null);
|
|
641
|
+
}
|
|
653
642
|
}
|
|
654
643
|
/**
|
|
655
|
-
*
|
|
656
|
-
*
|
|
657
|
-
*
|
|
658
|
-
* inner DO class via getInnerDoClass() (./inner-do-registry.js) and
|
|
659
|
-
* spins up / attaches to a facet via the supervisor's ctx.facets in
|
|
660
|
-
* the SAME outer request context — never reusing stubs across requests.
|
|
644
|
+
* The session Durable Object, through the composed host namespace, and its
|
|
645
|
+
* one supervisorOp entrypoint: a host forwards envelopes, not private _rpc*
|
|
646
|
+
* methods. Throws when the binding cannot reach it; `release` drops the stub.
|
|
661
647
|
*/
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
const
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
648
|
+
function supervisorOf(env, props) {
|
|
649
|
+
const supervisorDoId = String(props.supervisorDoId || '');
|
|
650
|
+
if (!supervisorDoId)
|
|
651
|
+
throw new Error('supervisorDoId missing');
|
|
652
|
+
let stub = null;
|
|
653
|
+
try {
|
|
654
|
+
const ns = hostNamespaceBinding(env ?? {}, 'NimbusDOStub', props.route);
|
|
655
|
+
stub = ns.get(ns.idFromString(supervisorDoId));
|
|
656
|
+
const held = stub;
|
|
657
|
+
return { dispatch: hostOpDispatch(stub, 'NimbusDOStub', props.route), release: () => disposeRpcResource(held) };
|
|
658
|
+
}
|
|
659
|
+
catch (e) {
|
|
660
|
+
disposeRpcResource(stub);
|
|
661
|
+
throw e;
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
function isInnerDoFetchAnswer(value) {
|
|
665
|
+
return value !== null && typeof value === 'object'
|
|
666
|
+
&& 'status' in value && typeof value.status === 'number'
|
|
667
|
+
&& 'statusText' in value && typeof value.statusText === 'string'
|
|
668
|
+
&& 'headers' in value && Array.isArray(value.headers)
|
|
669
|
+
&& 'body' in value && (value.body === null || value.body instanceof ArrayBuffer);
|
|
670
|
+
}
|
|
671
|
+
/**
|
|
672
|
+
* The inner object's fetch: the request forwarded whole (method, body,
|
|
673
|
+
* headers) as fields the session reconstructs, and its response rebuilt here
|
|
674
|
+
* from the fields the session answers. A binding that cannot reach the
|
|
675
|
+
* session answers 500.
|
|
676
|
+
*/
|
|
677
|
+
async function innerDoFetch(env, props, request) {
|
|
678
|
+
let supervisor;
|
|
679
|
+
try {
|
|
680
|
+
supervisor = supervisorOf(env, props);
|
|
681
|
+
}
|
|
682
|
+
catch (e) {
|
|
683
|
+
return new Response(`Nimbus: ${e instanceof Error ? e.message : String(e)}`, { status: 500 });
|
|
684
|
+
}
|
|
685
|
+
try {
|
|
686
|
+
const body = request.method !== 'GET' && request.method !== 'HEAD' ? await request.arrayBuffer() : null;
|
|
693
687
|
const headerList = [];
|
|
694
688
|
request.headers.forEach((v, k) => { headerList.push([k, v]); });
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
689
|
+
return await useRpcResource(supervisor.dispatch({
|
|
690
|
+
op: 'innerDoFetch',
|
|
691
|
+
args: [{ bindingName: String(props.bindingName || ''), id: String(props.id || ''), method: request.method, url: request.url, headers: headerList, body }],
|
|
692
|
+
}), (res) => {
|
|
693
|
+
if (!isInnerDoFetchAnswer(res)) {
|
|
694
|
+
return new Response('Nimbus: innerDoFetch returned an invalid result', { status: 502 });
|
|
695
|
+
}
|
|
696
|
+
return new Response(res.body, { status: res.status, statusText: res.statusText, headers: res.headers });
|
|
697
|
+
});
|
|
698
|
+
}
|
|
699
|
+
finally {
|
|
700
|
+
supervisor.release();
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
/**
|
|
704
|
+
* A member of the inner object, called with `args` or read (`args` null): the
|
|
705
|
+
* session reaches it on the object's facet and answers what it answered (a
|
|
706
|
+
* stub, function or stream travels as one) or rejects with what it threw,
|
|
707
|
+
* type and message kept.
|
|
708
|
+
*/
|
|
709
|
+
async function innerDoMember(env, props, path, args) {
|
|
710
|
+
const supervisor = supervisorOf(env, props);
|
|
711
|
+
try {
|
|
712
|
+
return await supervisor.dispatch({
|
|
713
|
+
op: 'innerDoCall',
|
|
714
|
+
args: [{ bindingName: String(props.bindingName || ''), id: String(props.id || ''), path, args }],
|
|
715
|
+
});
|
|
716
|
+
}
|
|
717
|
+
finally {
|
|
718
|
+
supervisor.release();
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
/**
|
|
722
|
+
* A Durable-Object-namespace-stub for a specific id, for a caller of the
|
|
723
|
+
* binding's own `get`: its fetch. The important invariant: EVERY call
|
|
724
|
+
* resolves the inner DO class via getInnerDoClass() (./inner-do-registry.js)
|
|
725
|
+
* and spins up / attaches to a facet via the supervisor's ctx.facets in the
|
|
726
|
+
* SAME outer request context — never reusing stubs across requests.
|
|
727
|
+
*/
|
|
728
|
+
export class NimbusDOStub extends WorkerEntrypoint {
|
|
729
|
+
async fetch(request) {
|
|
730
|
+
return innerDoFetch(this.env, this.ctx.props || {}, request);
|
|
720
731
|
}
|
|
721
732
|
}
|
package/dist/budgets.d.ts
CHANGED
|
@@ -15,7 +15,9 @@
|
|
|
15
15
|
* the width fan-outs have claimed and not yet released. A fan-out spends only
|
|
16
16
|
* the {@link dynamicWorkerHeadroom} that leaves, so work a Durable Object
|
|
17
17
|
* already has in flight — a resident process, the esbuild facet, a git
|
|
18
|
-
* network op, another fan-out — keeps its slots.
|
|
18
|
+
* network op, another fan-out — keeps its slots. Work that would rather wait
|
|
19
|
+
* than be refused waits on the ledger ({@link beginLoaderFetchWhenFree}) and
|
|
20
|
+
* is let in, in the order it asked, by whichever release makes room.
|
|
19
21
|
*
|
|
20
22
|
* Keyed weakly off the hosting actor's `ctx`, like the facet slot books: the
|
|
21
23
|
* limit is per Durable Object, and dynamic workers die with the isolate that
|
|
@@ -29,11 +31,29 @@
|
|
|
29
31
|
* https://developers.cloudflare.com/changelog/post/2026-08-28-durable-objects-dynamic-workers-limit/
|
|
30
32
|
*/
|
|
31
33
|
export declare const DO_DYNAMIC_WORKER_LIMIT = 10;
|
|
34
|
+
/**
|
|
35
|
+
* Ends one hold, idempotently. Pass the error the call failed with, if it
|
|
36
|
+
* did: a "Dynamic worker concurrency limit exceeded" refusal pauses the
|
|
37
|
+
* ledger's admissions (see {@link beginLoaderFetchWhenFree}); anything else,
|
|
38
|
+
* or nothing, just ends the hold.
|
|
39
|
+
*/
|
|
40
|
+
export type EndLoaderFetch = (failure?: unknown) => void;
|
|
41
|
+
/**
|
|
42
|
+
* A width one fan-out reserved with {@link claimDynamicWorkers}. Holds taken
|
|
43
|
+
* under it (`beginLoaderFetch(ctx, key, claim)`) count inside that width, not
|
|
44
|
+
* on top of it, until `release` (idempotent).
|
|
45
|
+
*/
|
|
46
|
+
export interface DynamicWorkerClaim {
|
|
47
|
+
release(): void;
|
|
48
|
+
}
|
|
32
49
|
/**
|
|
33
50
|
* Hold the Dynamic Worker `workerKey` in flight on this actor's ledger; the
|
|
34
51
|
* returned function ends the hold (idempotently), from the caller's own
|
|
35
52
|
* `finally`. Holds on one key nest: the worker counts once until the last
|
|
36
|
-
* one ends, as the platform counts it.
|
|
53
|
+
* one ends, as the platform counts it. Under a `claim`, the hold counts
|
|
54
|
+
* inside the claim's width. This never waits: it is for work the actor
|
|
55
|
+
* starts regardless (a resident process); {@link beginLoaderFetchWhenFree}
|
|
56
|
+
* waits for room.
|
|
37
57
|
*
|
|
38
58
|
* A begin/end pair rather than a wrapper on purpose, and the shape is
|
|
39
59
|
* load-bearing: wrapping the stub call in a ledger-owned async frame
|
|
@@ -46,22 +66,50 @@ export declare const DO_DYNAMIC_WORKER_LIMIT = 10;
|
|
|
46
66
|
* workers: an RPC stub call must stay a direct property call awaited by the
|
|
47
67
|
* frame that made it, so the ledger only brackets it.
|
|
48
68
|
*/
|
|
49
|
-
export declare function beginLoaderFetch(ctx: object, workerKey: string):
|
|
69
|
+
export declare function beginLoaderFetch(ctx: object, workerKey: string, claim?: DynamicWorkerClaim): EndLoaderFetch;
|
|
70
|
+
/**
|
|
71
|
+
* {@link beginLoaderFetch} once the ledger has room: resolves, holding
|
|
72
|
+
* `workerKey`, as soon as that worker is already in flight (holds on it
|
|
73
|
+
* count once) or a distinct worker more fits — within the `claim`'s width,
|
|
74
|
+
* or the actor's headroom. Waits are let in in the order they asked, by
|
|
75
|
+
* whoever's release makes the room: a hold's end, a claim's release, a
|
|
76
|
+
* pause's end. The hold is taken as the wait is let in, so a freed slot
|
|
77
|
+
* wakes one waiter and no other caller can take it first; once resolved, it
|
|
78
|
+
* is the caller's to end.
|
|
79
|
+
*
|
|
80
|
+
* A call refused with "Dynamic worker concurrency limit exceeded" ends its
|
|
81
|
+
* hold with the refusal (`end(error)`) and waits again: the refusal pauses
|
|
82
|
+
* admission (50 ms, doubling to 2 s while refusals continue), because the
|
|
83
|
+
* platform counts a worker for a moment after its call returns and no
|
|
84
|
+
* release can show that.
|
|
85
|
+
*
|
|
86
|
+
* `signal` abandons the wait: it rejects with the signal's reason and holds
|
|
87
|
+
* nothing. A wait outlives nothing on its own: bound it with a signal when
|
|
88
|
+
* room may never come (a resident process holds its worker for as long as
|
|
89
|
+
* it runs).
|
|
90
|
+
*
|
|
91
|
+
* const end = await beginLoaderFetchWhenFree(ctx, key, { signal });
|
|
92
|
+
* try { return await worker.getEntrypoint().run(); }
|
|
93
|
+
* catch (error) { end(error); throw error; }
|
|
94
|
+
* finally { end(); }
|
|
95
|
+
*/
|
|
96
|
+
export declare function beginLoaderFetchWhenFree(ctx: object, workerKey: string, options?: {
|
|
97
|
+
signal?: AbortSignal;
|
|
98
|
+
claim?: DynamicWorkerClaim;
|
|
99
|
+
}): Promise<EndLoaderFetch>;
|
|
50
100
|
/**
|
|
51
101
|
* Distinct Dynamic Workers this actor may still put in flight: the limit
|
|
52
|
-
* less what is held and claimed right now
|
|
102
|
+
* less what is held and claimed right now, and none while a limit refusal's
|
|
103
|
+
* pause lasts. Never negative.
|
|
53
104
|
*/
|
|
54
105
|
export declare function dynamicWorkerHeadroom(ctx: object): number;
|
|
55
106
|
/**
|
|
56
107
|
* Claim `width` distinct Dynamic Workers for one fan-out, or null when the
|
|
57
108
|
* headroom cannot hold it. The claim counts until `release` (idempotent), so
|
|
58
109
|
* a second fan-out sizing itself meanwhile sees it; the claimant's own
|
|
59
|
-
* dispatches
|
|
60
|
-
* toward sending that second fan-out elsewhere.
|
|
110
|
+
* dispatches, held under the claim, count inside it.
|
|
61
111
|
*/
|
|
62
|
-
export declare function claimDynamicWorkers(ctx: object, width: number):
|
|
63
|
-
release(): void;
|
|
64
|
-
} | null;
|
|
112
|
+
export declare function claimDynamicWorkers(ctx: object, width: number): DynamicWorkerClaim | null;
|
|
65
113
|
/** Snapshot for the diag surface. Pure read; no I/O. */
|
|
66
114
|
export declare function loaderLedgerStats(ctx: object): {
|
|
67
115
|
limit: number;
|
|
@@ -69,6 +117,10 @@ export declare function loaderLedgerStats(ctx: object): {
|
|
|
69
117
|
claimed: number;
|
|
70
118
|
headroom: number;
|
|
71
119
|
peak: number;
|
|
120
|
+
/** Waits not yet admitted. */
|
|
121
|
+
waiting: number;
|
|
122
|
+
/** Length of the pause a limit refusal started, while it lasts; 0 when admitting. */
|
|
123
|
+
pauseMs: number;
|
|
72
124
|
};
|
|
73
125
|
/**
|
|
74
126
|
* Name the per-DO accounting on a "Dynamic worker concurrency limit exceeded"
|
package/dist/budgets.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"budgets.d.ts","sourceRoot":"","sources":["../src/budgets.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"budgets.d.ts","sourceRoot":"","sources":["../src/budgets.ts"],"names":[],"mappings":"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"}
|