@nimbus-sh/fabric 0.2.0 → 0.4.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 +169 -283
- package/dist/composition.d.ts +2 -86
- package/dist/composition.d.ts.map +1 -1
- package/dist/composition.js +2 -76
- package/dist/fenced-work.d.ts +33 -4
- package/dist/fenced-work.d.ts.map +1 -1
- package/dist/fenced-work.js +87 -28
- package/dist/process-fabric.d.ts +31 -1
- package/dist/process-fabric.d.ts.map +1 -1
- package/dist/process-fabric.js +19 -0
- package/dist/process-host.d.ts.map +1 -1
- package/dist/process-host.js +11 -4
- package/dist/turn-budget.d.ts +7 -2
- package/dist/turn-budget.d.ts.map +1 -1
- package/dist/turn-budget.js +11 -7
- package/dist/workerd-facet-host.d.ts +44 -8
- package/dist/workerd-facet-host.d.ts.map +1 -1
- package/dist/workerd-facet-host.js +80 -10
- package/package.json +3 -3
- package/src/composition.ts +3 -127
- package/src/fenced-work.ts +87 -30
- package/src/process-fabric.ts +37 -1
- package/src/process-host.ts +13 -6
- package/src/turn-budget.ts +14 -11
- package/src/workerd-facet-host.ts +93 -16
package/README.md
CHANGED
|
@@ -4,44 +4,35 @@
|
|
|
4
4
|
> cloud OS. This README is edited and maintained with Claude (AI) and
|
|
5
5
|
> presented as-is.
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
The root export pulls `cloudflare:workers`, so `import ... from
|
|
38
|
-
'@nimbus-sh/fabric'` resolves only inside a Worker. Outside workerd (unit
|
|
39
|
-
tests, tooling) import the subpath modules directly —
|
|
40
|
-
`@nimbus-sh/fabric/timers.js`, `@nimbus-sh/fabric/fenced-work.js`, and so
|
|
41
|
-
on. Most of the package is structurally typed against plain objects precisely
|
|
42
|
-
so it can be tested in bun or node.
|
|
43
|
-
|
|
44
|
-
An embedder states its composition once, in its composition root:
|
|
7
|
+
Run long-lived programs on Cloudflare Durable Objects.
|
|
8
|
+
|
|
9
|
+
A Durable Object gives you one alarm, one 128 MiB isolate, a 30-second CPU
|
|
10
|
+
turn, and storage that can reset under you. This package turns those into
|
|
11
|
+
things you can build on: many timers on the one alarm, work that survives a
|
|
12
|
+
reset, CPU work that spans turns, and real processes in their own isolates.
|
|
13
|
+
|
|
14
|
+
Use it if you host something that outlives a request. A dev server, a build,
|
|
15
|
+
an agent, a terminal session.
|
|
16
|
+
|
|
17
|
+
## Install
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npm install @nimbus-sh/fabric
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Requirements
|
|
24
|
+
|
|
25
|
+
Set `compatibility_flags: ["nodejs_compat"]` in your Worker. The timer
|
|
26
|
+
dispatcher needs `AsyncLocalStorage`, which workerd ships only under that
|
|
27
|
+
flag. Without it the module fails to load at deploy time.
|
|
28
|
+
|
|
29
|
+
Import the root inside a Worker. Outside workerd, import subpaths such as
|
|
30
|
+
`@nimbus-sh/fabric/timers.js`, which are typed against plain objects and run
|
|
31
|
+
in bun or node.
|
|
32
|
+
|
|
33
|
+
## Setup
|
|
34
|
+
|
|
35
|
+
Declare your composition once, in your Worker's entry module.
|
|
45
36
|
|
|
46
37
|
```ts
|
|
47
38
|
import { composeFabric, adoptCtxExports } from '@nimbus-sh/fabric';
|
|
@@ -60,21 +51,12 @@ composeFabric({
|
|
|
60
51
|
adoptCtxExports(ctx.exports);
|
|
61
52
|
```
|
|
62
53
|
|
|
63
|
-
Both calls are
|
|
64
|
-
|
|
65
|
-
Before a release reaches the registry, consumers link it by packed tarball:
|
|
66
|
-
`npm pack` here, a `file:` path there. One bun behavior to know when you do:
|
|
67
|
-
bun pins a `file:` tarball by the integrity hash in its lockfile and keeps
|
|
68
|
-
serving the extraction it already has, so repacking the tarball at the same
|
|
69
|
-
path changes nothing at the consumer. After a repack, bump the version you
|
|
70
|
-
pack or delete the tarball's lockfile entry; a plain `bun install` is not
|
|
71
|
-
enough.
|
|
54
|
+
Both calls take the first value they are given.
|
|
72
55
|
|
|
73
|
-
##
|
|
56
|
+
## Timers
|
|
74
57
|
|
|
75
|
-
A Durable Object has
|
|
76
|
-
|
|
77
|
-
reason→deadline map in storage, with one dispatcher:
|
|
58
|
+
A Durable Object has one alarm, and a second `setAlarm()` overwrites the
|
|
59
|
+
first. Route every timer through one dispatcher instead.
|
|
78
60
|
|
|
79
61
|
```ts
|
|
80
62
|
import { DurableObject } from 'cloudflare:workers';
|
|
@@ -99,23 +81,16 @@ export class MySession extends DurableObject {
|
|
|
99
81
|
}
|
|
100
82
|
```
|
|
101
83
|
|
|
102
|
-
`schedule`
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
from a deploy that added reasons must not wedge the alarm), and when no
|
|
107
|
-
reasons remain it deletes the map and does not re-arm — which is what lets the
|
|
108
|
-
object hibernate.
|
|
84
|
+
`schedule` stores a deadline per reason and arms the alarm at the earliest
|
|
85
|
+
one. `dispatch` runs the reasons that are due, ignores reasons it does not
|
|
86
|
+
know, and stops re-arming when none are left, which lets the object
|
|
87
|
+
hibernate. A handler re-arms itself by returning `{ rearmAt }`.
|
|
109
88
|
|
|
110
|
-
##
|
|
89
|
+
## Generations and reset detection
|
|
111
90
|
|
|
112
|
-
Workerd
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
process IDs derive from it (`PID_GEN_STRIDE` = 1,000,000 in core's process
|
|
116
|
-
table), which yields the one reset predicate everything else builds on: **a
|
|
117
|
-
pid at or below the current generation's base was allocated by a previous
|
|
118
|
-
incarnation.**
|
|
91
|
+
Workerd hands you a fresh isolate on cold starts, hibernation wakes, and
|
|
92
|
+
resets. The generation counter tells you which incarnation you are in, and
|
|
93
|
+
process IDs derive from it.
|
|
119
94
|
|
|
120
95
|
```ts
|
|
121
96
|
import { adoptGeneration, generation } from '@nimbus-sh/fabric';
|
|
@@ -128,20 +103,18 @@ export class MySession extends DurableObject {
|
|
|
128
103
|
}
|
|
129
104
|
```
|
|
130
105
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
boot and re-issued — two instances sharing one generation is exactly the pid
|
|
134
|
-
aliasing the counter exists to prevent. Note that `await put()` returning is
|
|
135
|
-
not durability; the output gate is what keeps a pid from generation N from
|
|
136
|
-
escaping before N is on disk.
|
|
106
|
+
This gives you one reliable test for stale state: **an ID at or below the
|
|
107
|
+
current generation's base came from a previous incarnation.**
|
|
137
108
|
|
|
138
|
-
|
|
109
|
+
Adopt the persisted value before bumping it. `await put()` returning is not
|
|
110
|
+
durability, so a bump that has not landed can be re-issued to the next boot,
|
|
111
|
+
and two instances would share a generation.
|
|
139
112
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
113
|
+
## Fenced work
|
|
114
|
+
|
|
115
|
+
A reset destroys whatever the current turn had in flight, including a
|
|
116
|
+
half-built process. Journal the work first, and a later instance can finish
|
|
117
|
+
it.
|
|
145
118
|
|
|
146
119
|
```ts
|
|
147
120
|
import { FencedWork, type FencedWorkRecord } from '@nimbus-sh/fabric';
|
|
@@ -164,33 +137,18 @@ await journal.release(pid);
|
|
|
164
137
|
await journal.recoverInterrupted();
|
|
165
138
|
```
|
|
166
139
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
went looking.
|
|
180
|
-
|
|
181
|
-
Recovery applies the generation predicate (`pid <= generationBase()`), deletes
|
|
182
|
-
each stale row, and re-drives once per record (`FENCED_WORK_MAX_ATTEMPT` =
|
|
183
|
-
1) — a reset that recurs is not the transient kind.
|
|
184
|
-
|
|
185
|
-
## Pacing big work across turns
|
|
186
|
-
|
|
187
|
-
One DO turn has a CPU budget of about 30 s (we were killed with `exceededCpu`
|
|
188
|
-
at 31.8 s and 32.5 s), and yielding inside an invocation buys nothing — CPU
|
|
189
|
-
accrues to the invocation, and only genuinely re-entering the object resets
|
|
190
|
-
it. Worse, a long turn pins the actor's only thread, so the terminal WebSocket
|
|
191
|
-
dies even when the work succeeds. And progress cannot be measured in
|
|
192
|
-
milliseconds, because the in-DO clock does not advance without I/O (0 ms
|
|
193
|
-
across 200,000 consecutive reads). So the pacer accounts **bytes**:
|
|
140
|
+
Write the row before the work starts and release it when the process ends,
|
|
141
|
+
not when the launch ends. Resets usually arrive after a launch settles, so a
|
|
142
|
+
launch-scoped row is already gone when recovery looks for it.
|
|
143
|
+
|
|
144
|
+
The journal writes through `ctx.storage.sync()`, because `await put()`
|
|
145
|
+
resolves before the write is durable. Recovery re-drives each stale row once.
|
|
146
|
+
|
|
147
|
+
## Turn pacing
|
|
148
|
+
|
|
149
|
+
One turn gets about 30 seconds of CPU. Yielding inside a turn does not help,
|
|
150
|
+
because CPU accrues to the invocation. Only re-entering the object resets the
|
|
151
|
+
budget, and a long turn also blocks the actor's thread and drops WebSockets.
|
|
194
152
|
|
|
195
153
|
```ts
|
|
196
154
|
import { TurnBudget, PacedWork, onColdStart, timers } from '@nimbus-sh/fabric';
|
|
@@ -208,20 +166,13 @@ await budget.spend(bytesJustProcessed); // suspends every TURN_CHUNK_MAX_BYTES
|
|
|
208
166
|
'launch-turn': () => pump.pump(),
|
|
209
167
|
```
|
|
210
168
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
free, which makes `schedule(..., Date.now())` a genuine "re-enter now"
|
|
215
|
-
primitive. Without an alarm-capable host the pump degrades to a same-context
|
|
216
|
-
timer: the single-turn behaviour this path always had, minus the
|
|
217
|
-
responsiveness.
|
|
169
|
+
Account in bytes, not milliseconds: the in-DO clock does not advance without
|
|
170
|
+
I/O. `spend()` suspends every 2 MB and resumes on a fresh turn. Scheduling a
|
|
171
|
+
past deadline re-enters the object immediately.
|
|
218
172
|
|
|
219
|
-
##
|
|
173
|
+
## Isolate pool
|
|
220
174
|
|
|
221
|
-
|
|
222
|
-
`env.LOADER`. Functions are serialized with `fn.toString()`, so they must be
|
|
223
|
-
self-contained: no captured variables, no `this` (rejected at dispatch), and
|
|
224
|
-
their last parameter receives the forwarded bindings.
|
|
175
|
+
Run plain functions in warm dynamic-worker isolates.
|
|
225
176
|
|
|
226
177
|
```ts
|
|
227
178
|
import { IsolatePool } from '@nimbus-sh/fabric';
|
|
@@ -241,46 +192,32 @@ try {
|
|
|
241
192
|
}
|
|
242
193
|
```
|
|
243
194
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
a keyed `loader.get(id)` is never released — plus live and peak concurrent
|
|
271
|
-
Loader fetches, read via `loaderLedgerStats(ctx)`. A "Too many concurrent
|
|
272
|
-
dynamic workers" refusal classifies as `dynamic_worker_cap` and is annotated
|
|
273
|
-
with the ids actually holding slots. Measurement and honest failure naming
|
|
274
|
-
only — no admission control, because the cap is the platform's and
|
|
275
|
-
approximate, and a gate on an approximate number would refuse work the
|
|
276
|
-
platform would have run.
|
|
277
|
-
|
|
278
|
-
## Running processes: the resident fabric
|
|
279
|
-
|
|
280
|
-
A resident process — a dev server, a socket runner, an attached TUI — is a DO
|
|
281
|
-
facet whose class comes from a dynamic worker. `processes(ctx, env).spawn` is
|
|
282
|
-
the one way such a process comes into existence; `ProcessFabric` is the
|
|
283
|
-
lifecycle around it:
|
|
195
|
+
Functions are serialized with `fn.toString()`, so they must be
|
|
196
|
+
self-contained: no captured variables, no `this`. Bindings arrive as the last
|
|
197
|
+
parameter. Slots are stable, so a batch of 67 tasks reuses 4 warm isolates
|
|
198
|
+
instead of paying 67 cold starts.
|
|
199
|
+
|
|
200
|
+
Ship wasm through the loader's modules map as `{ wasm: ArrayBuffer }`. It is
|
|
201
|
+
the only path that works. Request-time `WebAssembly.compile` is blocked by
|
|
202
|
+
CSP, structured clone refuses a compiled `Module`, and inlining bytes into
|
|
203
|
+
the source exhausts the supervisor's memory.
|
|
204
|
+
|
|
205
|
+
Warm isolates are scoped to one session. A pool may opt into
|
|
206
|
+
`cacheScope: 'global'` only if it takes no supervisor binding and keeps no
|
|
207
|
+
user state.
|
|
208
|
+
|
|
209
|
+
`Fanout` handles wider batches. One DO method can drive at most 4 concurrent
|
|
210
|
+
loader fetches, so batches under 5 run in the coordinator and larger ones
|
|
211
|
+
shard across up to 32 sibling objects, 4 at a time.
|
|
212
|
+
|
|
213
|
+
Each keyed `loader.get(id)` permanently holds one of roughly 5–6
|
|
214
|
+
dynamic-worker slots. `loaderLedgerStats(ctx)` reports what you have
|
|
215
|
+
consumed, and a cap refusal names the IDs holding slots.
|
|
216
|
+
|
|
217
|
+
## Process fabric
|
|
218
|
+
|
|
219
|
+
A resident process is a Durable Object facet running a class from a dynamic
|
|
220
|
+
worker.
|
|
284
221
|
|
|
285
222
|
```ts
|
|
286
223
|
import { ProcessFabric, createProcessHost } from '@nimbus-sh/fabric';
|
|
@@ -305,130 +242,81 @@ handle.kill();
|
|
|
305
242
|
await handle.done;
|
|
306
243
|
```
|
|
307
244
|
|
|
308
|
-
The
|
|
309
|
-
(`
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
opaque message. Exhaustion is permanent for the object, so it is the one
|
|
328
|
-
failure worth naming precisely.
|
|
329
|
-
- **At-most-once start.** The facet's start callback re-running would
|
|
330
|
-
re-execute the user's program, answering a request from a process the user
|
|
331
|
-
never started. Both re-entry cases (released, lost) throw instead.
|
|
332
|
-
- **Boot specs name large members by VFS path.** A whole structured-clone RPC
|
|
333
|
-
value caps at 32 MiB, and one node snapshot alone serialized to 44,252,709
|
|
334
|
-
bytes. `vfsWasmModules` and `vfsTextModules` send paths; the hosting actor
|
|
335
|
-
reads the bytes through the `ResidentDiskReader` it was given, inside the
|
|
336
|
-
loader's cache-miss callback, so they exist only for the duration of the
|
|
337
|
-
load. Text images are verified against the digest their own path claims —
|
|
338
|
-
a truncated image would otherwise boot as silently-wrong code.
|
|
339
|
-
- **The substrate is one deployment-wide value** (`createProcessHost`'s mode,
|
|
340
|
-
`'facet'` or `'peer'`), never per-spawn. No program name, mode, or payload
|
|
341
|
-
size reaches the choice.
|
|
342
|
-
|
|
343
|
-
What each substrate costs, measured on the production shape:
|
|
245
|
+
The worker exports a Durable Object class named `NimbusProcess` with
|
|
246
|
+
`startProcess(args)` and `handleHttpRequest(request)`. `startProcess`
|
|
247
|
+
declares one of two contracts. Use `'lifetime'` when the call should stay
|
|
248
|
+
open for the process's life, as an attached terminal does. Use `'boot'` when
|
|
249
|
+
it should return once the process is up and leave the facet resident, as a
|
|
250
|
+
server does.
|
|
251
|
+
|
|
252
|
+
**Facet names come from a free list.** A Durable Object allows 65,536 facets
|
|
253
|
+
over its lifetime, and IDs are never reclaimed, so the limit counts facets
|
|
254
|
+
ever created. Reusing a name costs no new ID. `facetIdBudget(ctx)` reports
|
|
255
|
+
`{ consumed, budget }`.
|
|
256
|
+
|
|
257
|
+
**Large boot members travel as VFS paths.** A structured-clone RPC value caps
|
|
258
|
+
at 32 MiB. Use `vfsWasmModules` and `vfsTextModules`; the host reads the
|
|
259
|
+
bytes during the load and verifies each image against the digest its path
|
|
260
|
+
claims.
|
|
261
|
+
|
|
262
|
+
**The substrate is one deployment-wide setting**, `'facet'` or `'peer'`,
|
|
263
|
+
never a per-spawn choice.
|
|
344
264
|
|
|
345
265
|
| | spawn | memory | CPU | SQLite |
|
|
346
266
|
|---|---|---|---|---|
|
|
347
|
-
| facet | 8–16 ms | independent (~208 MiB each) |
|
|
267
|
+
| facet | 8–16 ms | independent (~208 MiB each) | shared | own |
|
|
348
268
|
| peer | 242–359 ms | independent | independent | own |
|
|
349
269
|
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
- **Root before the first byte.** The whole root set is registered
|
|
382
|
-
synchronously before any byte lands, so the sweep can never observe a
|
|
383
|
-
written-but-unclaimed image, however many turns the write spans.
|
|
384
|
-
- **Sliced writes.** One transaction takes `FACET_IMAGE_WRITE_SLICE_BYTES`
|
|
385
|
-
(a whole number of VFS chunks under the 1 MiB transaction bound — a slice
|
|
386
|
-
ending mid-chunk forces a read-back, and an oversize write falls back to
|
|
387
|
-
copy-on-write, which is quadratic). A 22.9 MB map written in one turn took
|
|
388
|
-
the session down with it about 25% of the time; sliced and paced, it
|
|
389
|
-
doesn't.
|
|
390
|
-
- **Size equality is completeness.** A write only ever grows the file from
|
|
391
|
-
offset zero, so an interrupted write leaves a strictly shorter file; the
|
|
392
|
-
reader verifies the digest before the loader sees the bytes.
|
|
393
|
-
- **The sweep roots off the process table.** An image is live for exactly as
|
|
394
|
-
long as a process boots from it. No TTL, no eviction heuristic; after a
|
|
395
|
-
reset the table is empty and every orphan goes.
|
|
396
|
-
|
|
397
|
-
## Binding shims for inner workers
|
|
270
|
+
Facets are separate isolates in one actor thread, so they scale memory but
|
|
271
|
+
not CPU. Awaiting I/O yields the thread; a 9,956 ms CPU burn stalled a
|
|
272
|
+
sibling for 9,966 ms. A peer costs about 20× the spawn time and buys real CPU
|
|
273
|
+
isolation, and it verifies it did not co-locate.
|
|
274
|
+
|
|
275
|
+
A facet shares its session's object, so it can copy the session's store by
|
|
276
|
+
reflink (`ctx.facets.clone`, 18–31 ms for 45.73 MB, 34–54 ms for 1 GB) and
|
|
277
|
+
shares the session's ~10 GiB budget. A peer brings its own storage and cannot
|
|
278
|
+
reflink.
|
|
279
|
+
|
|
280
|
+
Call clone through `cloneStorage`. An unresolvable `src` empties the
|
|
281
|
+
destination and reports success, so the wrapper checks the source before the
|
|
282
|
+
call and the destination after. An emptied facet still shows a 4,096-byte
|
|
283
|
+
database, so check for your own data rather than a non-zero size.
|
|
284
|
+
|
|
285
|
+
## Image store
|
|
286
|
+
|
|
287
|
+
`ImageStore` writes generated boot images into a content-addressed store at
|
|
288
|
+
`var/lib/nimbus/facet-images/<sha256>.js`, through an `ImageBlobStore` port
|
|
289
|
+
you implement.
|
|
290
|
+
|
|
291
|
+
It registers the whole root set before writing any byte, so a sweep never
|
|
292
|
+
sees an unclaimed image. Writes are sliced to stay inside the 1 MiB
|
|
293
|
+
transaction bound; a 22.9 MB map written in one turn reset the session about
|
|
294
|
+
a quarter of the time. Because a write only grows the file from offset zero,
|
|
295
|
+
matching size means a complete write, and the reader verifies the digest
|
|
296
|
+
before the loader sees the bytes. Images stay live while a process boots from
|
|
297
|
+
them, rooted in the process table, with no TTL.
|
|
298
|
+
|
|
299
|
+
## Binding shims
|
|
398
300
|
|
|
399
301
|
`NimbusLoaderRPC`, `NimbusLoadedWorker`, `NimbusLoadedEntrypoint`,
|
|
400
302
|
`NimbusAssetsRPC`, `NimbusDurableObjectNamespace`, and `NimbusDOStub` give a
|
|
401
|
-
dynamically-loaded inner Worker working `env` bindings.
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
Nimbus-in-Nimbus is fine, five levels is a runaway.
|
|
419
|
-
|
|
420
|
-
## The platform, measured
|
|
421
|
-
|
|
422
|
-
These tables are the part of this package I most wanted to publish. They are
|
|
423
|
-
enforced by the code above where code can enforce them; the rest is here so
|
|
424
|
-
the next person does not have to measure them again. All figures are from
|
|
425
|
-
production workerd, June–August 2026.
|
|
426
|
-
|
|
427
|
-
The tables are the short form. [PLATFORM.md](PLATFORM.md) is the full
|
|
428
|
-
catalog: the same invariants merged with a sibling project's independent
|
|
429
|
-
measurements, every entry graded by evidence (probe / source / production /
|
|
430
|
-
documented), dated, and marked for whether this library enforces it or you
|
|
431
|
-
handle it yourself.
|
|
303
|
+
dynamically-loaded inner Worker working `env` bindings.
|
|
304
|
+
|
|
305
|
+
Three platform rules shape them. A `WorkerStub` does not serialize, so each
|
|
306
|
+
hop is its own `WorkerEntrypoint` class. A stub belongs to the request that
|
|
307
|
+
minted it, so the shims store code rather than stubs and re-resolve through
|
|
308
|
+
`LOADER.get(id, cb)`; workerd caches by ID, and the code map is capped at 32
|
|
309
|
+
entries. An RPC method is a wildcard property, so call `ep.method(request)`
|
|
310
|
+
and never `method.call(ep, request)`, which workerd refuses.
|
|
311
|
+
|
|
312
|
+
Nesting is capped at depth 4. Raise it with `NIMBUS_INNER_LOADER_DEPTH`.
|
|
313
|
+
|
|
314
|
+
## Measured platform limits
|
|
315
|
+
|
|
316
|
+
Figures below come from production workerd, June to August 2026. The code
|
|
317
|
+
above enforces them where it can. [PLATFORM.md](PLATFORM.md) is the full
|
|
318
|
+
catalog: every entry dated, graded by evidence, and marked as enforced here
|
|
319
|
+
or left to you.
|
|
432
320
|
|
|
433
321
|
### Durable Object storage
|
|
434
322
|
|
|
@@ -498,18 +386,16 @@ handle it yourself.
|
|
|
498
386
|
| `exceededMemory` and `exceededCpu` are both uncatchable inside the dying isolate, observable only across an RPC boundary | absence of the error is not evidence of its absence |
|
|
499
387
|
| `process.memoryUsage()` returns 0 in DO context | any heap estimate is a lower bound; say so |
|
|
500
388
|
|
|
501
|
-
##
|
|
389
|
+
## Related packages
|
|
502
390
|
|
|
503
|
-
`@nimbus-sh/core` is the
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
taxonomy, RPC disposal, and the supervisor
|
|
391
|
+
[`@nimbus-sh/core`](https://www.npmjs.com/package/@nimbus-sh/core) is the
|
|
392
|
+
backend-agnostic OS: filesystem, shell, process contracts.
|
|
393
|
+
[`@nimbus-sh/platform`](https://www.npmjs.com/package/@nimbus-sh/platform)
|
|
394
|
+
holds the limits tables, the error taxonomy, RPC disposal, and the supervisor
|
|
395
|
+
budget machinery.
|
|
507
396
|
[`@nimbus-sh/worker`](https://www.npmjs.com/package/@nimbus-sh/worker) is the
|
|
508
|
-
|
|
509
|
-
the
|
|
510
|
-
product shape, start from `npx create-nimbus-app`; if you are building your
|
|
511
|
-
own thing on Durable Objects, this package and its doc comments are the part
|
|
512
|
-
of Nimbus you can take without taking Nimbus.
|
|
397
|
+
reference embedder, with the supervisor entrypoint and the session protocol.
|
|
398
|
+
For the full hosted product, start from `npx create-nimbus-app`.
|
|
513
399
|
|
|
514
400
|
## License
|
|
515
401
|
|