@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/README.md
CHANGED
|
@@ -22,9 +22,19 @@ npm install @nimbus-sh/fabric
|
|
|
22
22
|
|
|
23
23
|
## Requirements
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
dispatcher needs `AsyncLocalStorage`, which
|
|
27
|
-
|
|
25
|
+
Use a compatibility date of 2026-08-04 or later, or list `nodejs_compat` in
|
|
26
|
+
`compatibility_flags`. The timer dispatcher needs `AsyncLocalStorage`, which
|
|
27
|
+
workerd ships only under `nodejs_compat`, on by date from 2026-08-04. Without
|
|
28
|
+
it the module fails to load at deploy time.
|
|
29
|
+
|
|
30
|
+
`composeFabric` also needs `enhanced_error_serialization`: a compatibility
|
|
31
|
+
date of 2026-04-21 or later, or the flag in `compatibility_flags` on an older
|
|
32
|
+
date. A program's filesystem errors reach it across workerd RPC, and only
|
|
33
|
+
that flag carries their `code`. On workerd without it `composeFabric` throws,
|
|
34
|
+
naming both fixes, rather than every program seeing `EIO`. It throws where
|
|
35
|
+
it is called. At module scope the Worker fails at startup. A library host
|
|
36
|
+
that composes through `NimbusWorkspace.create({ fabric })` deploys and
|
|
37
|
+
starts, and its first create throws.
|
|
28
38
|
|
|
29
39
|
Import the root inside a Worker. Outside workerd, import subpaths such as
|
|
30
40
|
`@nimbus-sh/fabric/timers.js`, which are typed against plain objects and run
|
|
@@ -57,6 +67,15 @@ Worker that imports Nimbus's own entry (`@nimbus-sh/sdk/worker`) inherits
|
|
|
57
67
|
that entry's composition, so a host that names its own namespace composes in
|
|
58
68
|
a Worker that does not import it.
|
|
59
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
|
+
|
|
60
79
|
The route back to the host (namespace, dispatch method, supervisor
|
|
61
80
|
entrypoint) is minted into every binding the fabric hands a program, in the
|
|
62
81
|
host's isolate, and the entrypoints that answer those bindings read it from
|
|
@@ -224,13 +243,55 @@ Warm isolates are scoped to one session. A pool may opt into
|
|
|
224
243
|
`cacheScope: 'global'` only if it takes no supervisor binding and keeps no
|
|
225
244
|
user state.
|
|
226
245
|
|
|
227
|
-
`Fanout` handles wider batches.
|
|
228
|
-
|
|
229
|
-
|
|
246
|
+
`Fanout` handles wider batches. A Durable Object may have 10 distinct Dynamic
|
|
247
|
+
Workers with in-flight requests at once (`DO_DYNAMIC_WORKER_LIMIT`), shared
|
|
248
|
+
across every concurrent request to it; repeated requests to one Dynamic
|
|
249
|
+
Worker count once. A batch that fits the coordinator's remaining headroom
|
|
250
|
+
runs there, one Dynamic Worker per task; a wider one shards across up to 32
|
|
251
|
+
sibling objects, 4 at a time, each spending its own headroom.
|
|
252
|
+
|
|
253
|
+
The loader ledger counts what is in flight per DO: pool and one-shot calls,
|
|
254
|
+
esbuild facet calls, git network ops, and every resident process for as long
|
|
255
|
+
as it lives. `dynamicWorkerHeadroom(ctx)` is what is left,
|
|
256
|
+
`claimDynamicWorkers(ctx, n)` reserves a width, `loaderLedgerStats(ctx)`
|
|
257
|
+
reports it all, and a limit refusal names the workers in flight.
|
|
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:
|
|
230
262
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
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.
|
|
234
295
|
|
|
235
296
|
## Process fabric
|
|
236
297
|
|
|
@@ -329,6 +390,32 @@ and never `method.call(ep, request)`, which workerd refuses.
|
|
|
329
390
|
|
|
330
391
|
Nesting is capped at depth 4. Raise it with `NIMBUS_INNER_LOADER_DEPTH`.
|
|
331
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
|
+
|
|
332
419
|
## Measured platform limits
|
|
333
420
|
|
|
334
421
|
Figures below come from production workerd, June to August 2026. The code
|
|
@@ -370,8 +457,8 @@ or left to you.
|
|
|
370
457
|
| Request-time `WebAssembly.compile`/`instantiate` CSP-blocked; wasm rides the loader modules map as `{ wasm: ArrayBuffer }`, compiled at module load | RPC of a compiled `Module` refused by structured clone; inlined bytes OOMed the supervisor |
|
|
371
458
|
| Module scope bans I/O; `new Function` succeeds at module scope and throws at request time | code reaches a facet through the module map or not at all |
|
|
372
459
|
| The facet start callback fires at most once | re-running it would re-execute the user's program |
|
|
373
|
-
|
|
|
374
|
-
| `ctx.facets.clone` is same-object only
|
|
460
|
+
| 10 distinct Dynamic Workers with in-flight requests per DO, shared across its concurrent requests; repeated requests to one Dynamic Worker count once ([changelog, 2026-08-28](https://developers.cloudflare.com/changelog/post/2026-08-28-durable-objects-dynamic-workers-limit/)) | `DO_DYNAMIC_WORKER_LIMIT`; `Fanout` sizes in-DO batches to the live headroom, and a refusal names the workers in flight |
|
|
461
|
+
| `ctx.facets.clone` is same-object only; declared by `@cloudflare/workers-types` 5 and present in workerd ≥ 1.20260926.1 and in production | 18–31 ms / 45.7 MB, 34–54 ms / 1 GB; an unresolvable `src` silently EMPTIES the destination and reports success — `cloneStorage` enforces the both-ends validation |
|
|
375
462
|
| A DO dies at ~200 MiB of live wasm linear memory; reserved and written pages die at the same ceiling | lazy growth buys nothing; bound guest memory by rewriting the memory section |
|
|
376
463
|
| A wasm stack suspended (JSPI) in one request cannot resume in another | 3 in-context resumes took 6 ms; the first cross-context one hit a 30 s timeout |
|
|
377
464
|
|
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
|
}
|