@nimbus-sh/fabric 0.1.0 → 0.2.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 +84 -55
- package/dist/bindings.js +5 -5
- package/dist/budgets.d.ts +132 -0
- package/dist/budgets.d.ts.map +1 -0
- package/dist/budgets.js +248 -0
- package/dist/composition.d.ts +87 -0
- package/dist/composition.d.ts.map +1 -0
- package/dist/composition.js +76 -0
- package/dist/connections.d.ts +81 -0
- package/dist/connections.d.ts.map +1 -0
- package/dist/connections.js +114 -0
- package/dist/derived.d.ts +65 -0
- package/dist/derived.d.ts.map +1 -0
- package/dist/derived.js +95 -0
- package/dist/do-calls.d.ts +94 -0
- package/dist/do-calls.d.ts.map +1 -0
- package/dist/do-calls.js +111 -0
- package/dist/facet-pool.d.ts +90 -0
- package/dist/facet-pool.d.ts.map +1 -0
- package/dist/facet-pool.js +113 -0
- package/dist/{fanout-pool.d.ts → fanout.d.ts} +20 -20
- package/dist/fanout.d.ts.map +1 -0
- package/dist/{fanout-pool.js → fanout.js} +20 -20
- package/dist/{launch-journal.d.ts → fenced-work.d.ts} +25 -13
- package/dist/fenced-work.d.ts.map +1 -0
- package/dist/{launch-journal.js → fenced-work.js} +47 -13
- package/dist/generation.d.ts +69 -0
- package/dist/generation.d.ts.map +1 -0
- package/dist/generation.js +118 -0
- package/dist/{facet-image-store.d.ts → image-store.d.ts} +8 -8
- package/dist/image-store.d.ts.map +1 -0
- package/dist/{facet-image-store.js → image-store.js} +4 -4
- package/dist/index.d.ts +16 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +16 -8
- package/dist/{loader-pool.d.ts → isolate-pool.d.ts} +19 -19
- package/dist/isolate-pool.d.ts.map +1 -0
- package/dist/{loader-pool.js → isolate-pool.js} +20 -20
- package/dist/journal.d.ts +111 -0
- package/dist/journal.d.ts.map +1 -0
- package/dist/journal.js +177 -0
- package/dist/outbox.d.ts +249 -0
- package/dist/outbox.d.ts.map +1 -0
- package/dist/outbox.js +355 -0
- package/dist/process-fabric.d.ts +2 -14
- package/dist/process-fabric.d.ts.map +1 -1
- package/dist/process-fabric.js +6 -15
- package/dist/process-host.d.ts +1 -1
- package/dist/process-host.d.ts.map +1 -1
- package/dist/process-host.js +10 -9
- package/dist/sealed.d.ts +78 -0
- package/dist/sealed.d.ts.map +1 -0
- package/dist/sealed.js +145 -0
- package/dist/timers.d.ts +138 -0
- package/dist/timers.d.ts.map +1 -0
- package/dist/timers.js +231 -0
- package/dist/{launch-pacer.d.ts → turn-budget.d.ts} +19 -21
- package/dist/turn-budget.d.ts.map +1 -0
- package/dist/{launch-pacer.js → turn-budget.js} +22 -11
- package/dist/workerd-facet-host.d.ts +28 -67
- package/dist/workerd-facet-host.d.ts.map +1 -1
- package/dist/workerd-facet-host.js +49 -171
- package/examples/agent-core-adapter.ts +191 -0
- package/package.json +4 -2
- package/src/bindings.ts +6 -6
- package/src/budgets.ts +308 -0
- package/src/composition.ts +127 -0
- package/src/connections.ts +140 -0
- package/src/derived.ts +135 -0
- package/src/do-calls.ts +156 -0
- package/src/facet-pool.ts +157 -0
- package/src/{fanout-pool.ts → fanout.ts} +35 -35
- package/src/{launch-journal.ts → fenced-work.ts} +58 -22
- package/src/generation.ts +144 -0
- package/src/{facet-image-store.ts → image-store.ts} +9 -9
- package/src/index.ts +16 -8
- package/src/{loader-pool.ts → isolate-pool.ts} +34 -34
- package/src/journal.ts +242 -0
- package/src/node-async-hooks.d.ts +14 -0
- package/src/outbox.ts +520 -0
- package/src/process-fabric.ts +6 -33
- package/src/process-host.ts +10 -15
- package/src/sealed.ts +150 -0
- package/src/timers.ts +294 -0
- package/src/{launch-pacer.ts → turn-budget.ts} +30 -24
- package/src/workerd-facet-host.ts +67 -193
- package/dist/alarms.d.ts +0 -134
- package/dist/alarms.d.ts.map +0 -1
- package/dist/alarms.js +0 -214
- package/dist/ctx-exports.d.ts +0 -47
- package/dist/ctx-exports.d.ts.map +0 -1
- package/dist/ctx-exports.js +0 -54
- package/dist/facet-image-store.d.ts.map +0 -1
- package/dist/fanout-pool.d.ts.map +0 -1
- package/dist/launch-journal.d.ts.map +0 -1
- package/dist/launch-pacer.d.ts.map +0 -1
- package/dist/loader-ledger.d.ts +0 -57
- package/dist/loader-ledger.d.ts.map +0 -1
- package/dist/loader-ledger.js +0 -91
- package/dist/loader-pool.d.ts.map +0 -1
- package/src/alarms.ts +0 -275
- package/src/ctx-exports.ts +0 -77
- package/src/loader-ledger.ts +0 -112
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* turn-budget.ts — spreading a resident launch across Durable Object turns.
|
|
3
3
|
*
|
|
4
4
|
* Building a resident process is the largest single span of computation this
|
|
5
5
|
* session performs: for pi it walks a 17 MB source tree through eight
|
|
@@ -32,6 +32,7 @@
|
|
|
32
32
|
* `git/network-facet.ts` bounds its checkout chunks by entries and decoded
|
|
33
33
|
* bytes and treats its wall guard as coarse.
|
|
34
34
|
*/
|
|
35
|
+
import { runColdStart } from './generation.js';
|
|
35
36
|
/**
|
|
36
37
|
* Bytes of launch work one turn may perform before it must yield.
|
|
37
38
|
*
|
|
@@ -40,7 +41,7 @@
|
|
|
40
41
|
* each an alarm round trip — small enough not to dominate a launch. pi's
|
|
41
42
|
* 22.9 MB map crosses this about a dozen times per phase that handles it.
|
|
42
43
|
*/
|
|
43
|
-
export const
|
|
44
|
+
export const TURN_CHUNK_MAX_BYTES = 2_000_000;
|
|
44
45
|
/**
|
|
45
46
|
* Accounts launch progress and ends the turn when a chunk's worth has been
|
|
46
47
|
* spent.
|
|
@@ -51,7 +52,7 @@ export const LAUNCH_CHUNK_MAX_BYTES = 2_000_000;
|
|
|
51
52
|
* behaviour and cost. Nothing here decides WHAT the launch does, only where it
|
|
52
53
|
* is allowed to stop.
|
|
53
54
|
*/
|
|
54
|
-
export class
|
|
55
|
+
export class TurnBudget {
|
|
55
56
|
scheduler;
|
|
56
57
|
maxChunkBytes;
|
|
57
58
|
stillWanted;
|
|
@@ -69,7 +70,7 @@ export class LaunchPacer {
|
|
|
69
70
|
* place a launch can be interrupted, rather than at whichever phases
|
|
70
71
|
* remembered to ask.
|
|
71
72
|
*/
|
|
72
|
-
constructor(scheduler, maxChunkBytes =
|
|
73
|
+
constructor(scheduler, maxChunkBytes = TURN_CHUNK_MAX_BYTES, stillWanted) {
|
|
73
74
|
this.scheduler = scheduler;
|
|
74
75
|
this.maxChunkBytes = maxChunkBytes;
|
|
75
76
|
this.stillWanted = stillWanted;
|
|
@@ -111,10 +112,11 @@ function withResolvers() {
|
|
|
111
112
|
return { promise, resolve };
|
|
112
113
|
}
|
|
113
114
|
/**
|
|
114
|
-
* The granting side of {@link
|
|
115
|
+
* The granting side of {@link TurnScheduler}: parks suspended launches
|
|
115
116
|
* and resumes every one of them when the host grants a fresh turn.
|
|
116
117
|
*/
|
|
117
|
-
export class
|
|
118
|
+
export class PacedWork {
|
|
119
|
+
ctx;
|
|
118
120
|
host;
|
|
119
121
|
/**
|
|
120
122
|
* Launches suspended between chunks, waiting for a turn of their own.
|
|
@@ -127,7 +129,12 @@ export class LaunchTurnPump {
|
|
|
127
129
|
* from its inputs is the same idempotent work again.
|
|
128
130
|
*/
|
|
129
131
|
waiters = [];
|
|
130
|
-
|
|
132
|
+
/**
|
|
133
|
+
* `ctx` keys the cold-start queue the pump drains first on every turn it
|
|
134
|
+
* grants — see {@link pump}.
|
|
135
|
+
*/
|
|
136
|
+
constructor(ctx, host) {
|
|
137
|
+
this.ctx = ctx;
|
|
131
138
|
this.host = host;
|
|
132
139
|
}
|
|
133
140
|
/**
|
|
@@ -159,7 +166,11 @@ export class LaunchTurnPump {
|
|
|
159
166
|
* the runtime may tear the context down mid-chunk.
|
|
160
167
|
*/
|
|
161
168
|
async pump() {
|
|
162
|
-
|
|
169
|
+
// Deferred reconciliation runs first, before any waiter resumes. Where
|
|
170
|
+
// the resident-launch journal's recovery sits (`onColdStart`): the pump
|
|
171
|
+
// is what an alarm calls, and the first turn after a reset is the
|
|
172
|
+
// re-delivered alarm of a launch the reset interrupted.
|
|
173
|
+
await runColdStart(this.ctx);
|
|
163
174
|
const waiting = this.waiters;
|
|
164
175
|
if (waiting.length === 0)
|
|
165
176
|
return;
|
|
@@ -183,11 +194,11 @@ export class LaunchTurnPump {
|
|
|
183
194
|
* `git/commands.ts` carries `NIMBUS_GIT_CHECKOUT_CHUNK_ENTRIES`. Unset in
|
|
184
195
|
* production, where the default applies.
|
|
185
196
|
*/
|
|
186
|
-
export function
|
|
197
|
+
export function turnChunkMaxBytes(env) {
|
|
187
198
|
const raw = env
|
|
188
199
|
?.NIMBUS_LAUNCH_CHUNK_BYTES;
|
|
189
200
|
if (!raw)
|
|
190
|
-
return
|
|
201
|
+
return TURN_CHUNK_MAX_BYTES;
|
|
191
202
|
const parsed = Number(raw);
|
|
192
|
-
return Number.isFinite(parsed) && parsed > 0 ? parsed :
|
|
203
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : TURN_CHUNK_MAX_BYTES;
|
|
193
204
|
}
|
|
@@ -45,30 +45,6 @@ export declare function createLoadedWorkerEntrypoint(ctxExports: NimbusCtxExport
|
|
|
45
45
|
pid: number;
|
|
46
46
|
writerId: string;
|
|
47
47
|
}, stage: unknown, name?: string | null): Promise<LoadedWorkerEntrypointStub>;
|
|
48
|
-
/**
|
|
49
|
-
* Total bytes a dynamic Worker's module map may carry, across every member of
|
|
50
|
-
* it. A hard platform limit, not a policy knob: 62 MiB lands and 64 MiB is
|
|
51
|
-
* refused with "Dynamic Worker code size (N bytes) exceeds the maximum allowed
|
|
52
|
-
* size of 67108864 bytes", confirmed at five sizes with two trials each. The
|
|
53
|
-
* budget is shared, so a ruby process is already 34.3 MiB down before its disk
|
|
54
|
-
* is counted.
|
|
55
|
-
*/
|
|
56
|
-
export declare const DYNAMIC_WORKER_CODE_LIMIT_BYTES = 67108864;
|
|
57
|
-
/**
|
|
58
|
-
* Refuse a module map over {@link DYNAMIC_WORKER_CODE_LIMIT_BYTES}, naming
|
|
59
|
-
* the largest members. The platform's own refusal reports one number for a
|
|
60
|
-
* budget shared across every member of the map, which tells the operator
|
|
61
|
-
* nothing about WHAT to shrink — so every fabric seam that assembles a map
|
|
62
|
-
* runs this before the loader sees it.
|
|
63
|
-
*
|
|
64
|
-
* Costed to its two paths. Under the ceiling: one length read per member —
|
|
65
|
-
* UTF-16 code units for text, which equal UTF-8 bytes for the ASCII module
|
|
66
|
-
* text the generators emit and undercount otherwise; the platform's own
|
|
67
|
-
* refusal still backstops the exotic case, because this check exists to name
|
|
68
|
-
* members, not to be the ceiling. Over it: exact UTF-8 sizes, computed only
|
|
69
|
-
* then, sorted so the biggest lever is first.
|
|
70
|
-
*/
|
|
71
|
-
export declare function assertModuleMapWithinCodeLimit(modules: Record<string, unknown>): void;
|
|
72
48
|
/** What an unkeyed `LOADER.load` hands back. */
|
|
73
49
|
interface LoadedWorkerStub {
|
|
74
50
|
getEntrypoint(): LoadedWorkerEntrypointStub;
|
|
@@ -89,7 +65,7 @@ interface WorkerLoaderBinding {
|
|
|
89
65
|
load(code: unknown): LoadedWorkerStub;
|
|
90
66
|
}
|
|
91
67
|
/**
|
|
92
|
-
* The bindings `
|
|
68
|
+
* The bindings `processes` needs off whichever DO is hosting. A
|
|
93
69
|
* staged boot's assembler may read more off the same env (Nimbus's reads
|
|
94
70
|
* ASSETS); the env travels to it whole, so nothing further is named here.
|
|
95
71
|
*/
|
|
@@ -124,7 +100,7 @@ export interface ResidentFacetEnv {
|
|
|
124
100
|
* storage budget grants no copy-on-write credit — crossing it resets the
|
|
125
101
|
* object rather than raising an error.
|
|
126
102
|
*/
|
|
127
|
-
export declare function
|
|
103
|
+
export declare function cloneStorage(ctx: DurableObjectState, clone: {
|
|
128
104
|
src: string;
|
|
129
105
|
dst: string;
|
|
130
106
|
populated(name: string): boolean | Promise<boolean>;
|
|
@@ -145,29 +121,7 @@ export declare function cloneFacetStorage(ctx: DurableObjectState, clone: {
|
|
|
145
121
|
*/
|
|
146
122
|
export declare function residentFacetName(slot: number): string;
|
|
147
123
|
/**
|
|
148
|
-
*
|
|
149
|
-
* never reclaimed, so crossing it is unrecoverable for the object — which is
|
|
150
|
-
* why the ledger below counts consumption durably instead of leaving the
|
|
151
|
-
* bound as prose the slot book merely respects.
|
|
152
|
-
*/
|
|
153
|
-
export declare const FACET_ID_LIFETIME_BUDGET = 65536;
|
|
154
|
-
/** Where the ledger persists the count of facet names ever minted. */
|
|
155
|
-
export declare const FACET_NAME_HIGH_WATER_KEY = "fabric_facet_name_high_water";
|
|
156
|
-
/**
|
|
157
|
-
* The lifetime facet-ID ledger: how many facet names this fabric has ever
|
|
158
|
-
* minted on the Durable Object, against the 65,536 the platform will ever
|
|
159
|
-
* grant it. `consumed` only ever counts FIRST uses — a reused name, in this
|
|
160
|
-
* incarnation or any earlier one, cost no new ID, which is the slot book's
|
|
161
|
-
* whole reason to exist. Surfaced so an operator can see proximity to a wall
|
|
162
|
-
* whose crossing is unrecoverable, instead of discovering it from the
|
|
163
|
-
* platform's opaque failure.
|
|
164
|
-
*/
|
|
165
|
-
export declare function facetIdBudget(ctx: DurableObjectState): Promise<{
|
|
166
|
-
consumed: number;
|
|
167
|
-
budget: number;
|
|
168
|
-
}>;
|
|
169
|
-
/**
|
|
170
|
-
* What `openResidentFacet` hands back: a running process, minus its placement.
|
|
124
|
+
* What `processes(ctx, env).spawn` hands back: a running process, minus its placement.
|
|
171
125
|
*
|
|
172
126
|
* `slot` rides along because the caller's `describe` needs the facet's real
|
|
173
127
|
* name and the slot is not derivable from the pid — that indirection is the
|
|
@@ -178,30 +132,37 @@ export type ResidentFacet = Omit<HostedProcess, 'describe'> & {
|
|
|
178
132
|
slot: number;
|
|
179
133
|
};
|
|
180
134
|
/**
|
|
181
|
-
*
|
|
182
|
-
*
|
|
135
|
+
* The process surface of one hosting actor: how a resident process comes
|
|
136
|
+
* into existence on workerd, and how a one-shot program runs to completion.
|
|
183
137
|
*
|
|
184
|
-
*
|
|
138
|
+
* `spawn` is the ONE way a resident process comes into existence, and every
|
|
185
139
|
* substrate goes through it: the facet host calls it with the coordinator's
|
|
186
140
|
* own `ctx`, the peer host calls it — over one RPC — with a sibling session
|
|
187
141
|
* DO's. Everything a substrate could plausibly want to special-case is a
|
|
188
142
|
* PARAMETER here rather than a branch: which actor hosts the child, and how
|
|
189
143
|
* the boot spec's by-path members are read.
|
|
190
144
|
*/
|
|
191
|
-
export declare function
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
145
|
+
export declare function processes(ctx: DurableObjectState, env: ResidentFacetEnv): Processes;
|
|
146
|
+
export declare class Processes {
|
|
147
|
+
private readonly ctx;
|
|
148
|
+
private readonly env;
|
|
149
|
+
constructor(ctx: DurableObjectState, env: ResidentFacetEnv);
|
|
150
|
+
/** Open a resident process as a facet of this actor, and start its runner. */
|
|
151
|
+
spawn(disk: () => ResidentDiskReader, supervisor: ResidentSupervisorProps, params: ProcessHostParams): ResidentFacet;
|
|
152
|
+
/**
|
|
153
|
+
* Run one program to completion as an UNKEYED dynamic worker.
|
|
154
|
+
*
|
|
155
|
+
* Unkeyed is the whole difference from `spawn`: nothing can re-resolve this
|
|
156
|
+
* worker into a later request's context, so it can never be a routeable
|
|
157
|
+
* target and never has to be released by name. It exists for the duration
|
|
158
|
+
* of one call and its stubs are dropped as that call unwinds.
|
|
159
|
+
*
|
|
160
|
+
* Shared by both substrates on purpose. `peer` places processes that have a
|
|
161
|
+
* residency to place; a one-shot has none, and shipping its fully-inline
|
|
162
|
+
* map across a sibling hop would meet the 32 MiB RPC ceiling that by-path
|
|
163
|
+
* boot specs exist to avoid — for a run that gains nothing by moving.
|
|
164
|
+
*/
|
|
165
|
+
run<T>(supervisor: ResidentSupervisorProps, params: OneShotParams, consume: (response: Response) => Promise<T>): Promise<T>;
|
|
166
|
+
}
|
|
206
167
|
export {};
|
|
207
168
|
//# sourceMappingURL=workerd-facet-host.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"workerd-facet-host.d.ts","sourceRoot":"","sources":["../src/workerd-facet-host.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;
|
|
1
|
+
{"version":3,"file":"workerd-facet-host.d.ts","sourceRoot":"","sources":["../src/workerd-facet-host.ts"],"names":[],"mappings":"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"}
|
|
@@ -12,10 +12,10 @@
|
|
|
12
12
|
* that is not a Durable Object implements `ProcessHost` against the same
|
|
13
13
|
* `HostedProcess` and never imports this file.
|
|
14
14
|
*/
|
|
15
|
-
import { disposeRpcResource } from '@nimbus-sh/
|
|
16
|
-
import { getCtxExports, supervisorEntrypoint, supervisorEntrypointName, } from './
|
|
17
|
-
import { beginLoaderFetch, recordLoaderId, withDynamicWorkerCapNamed, } from './
|
|
18
|
-
import { RESIDENT_PROCESS_CLASS,
|
|
15
|
+
import { disposeRpcResource } from '@nimbus-sh/platform/rpc-dispose.js';
|
|
16
|
+
import { getCtxExports, stagedBootAssembler, supervisorEntrypoint, supervisorEntrypointName, } from './composition.js';
|
|
17
|
+
import { assertModuleMapWithinCodeLimit, beginLoaderFetch, facetNameCount, facetNameCountDurable, recordFacetNameMinted, recordLoaderId, withDynamicWorkerCapNamed, withFacetBudgetNamed, } from './budgets.js';
|
|
18
|
+
import { RESIDENT_PROCESS_CLASS, residentLoaderConfig, } from './process-fabric.js';
|
|
19
19
|
export function getNimbusCtxExports() {
|
|
20
20
|
const ctxExports = getCtxExports();
|
|
21
21
|
if (!ctxExports || typeof ctxExports !== 'object') {
|
|
@@ -43,69 +43,6 @@ export async function createLoadedWorkerEntrypoint(ctxExports, supervisor, stage
|
|
|
43
43
|
},
|
|
44
44
|
});
|
|
45
45
|
}
|
|
46
|
-
/**
|
|
47
|
-
* Total bytes a dynamic Worker's module map may carry, across every member of
|
|
48
|
-
* it. A hard platform limit, not a policy knob: 62 MiB lands and 64 MiB is
|
|
49
|
-
* refused with "Dynamic Worker code size (N bytes) exceeds the maximum allowed
|
|
50
|
-
* size of 67108864 bytes", confirmed at five sizes with two trials each. The
|
|
51
|
-
* budget is shared, so a ruby process is already 34.3 MiB down before its disk
|
|
52
|
-
* is counted.
|
|
53
|
-
*/
|
|
54
|
-
export const DYNAMIC_WORKER_CODE_LIMIT_BYTES = 67_108_864;
|
|
55
|
-
/**
|
|
56
|
-
* Refuse a module map over {@link DYNAMIC_WORKER_CODE_LIMIT_BYTES}, naming
|
|
57
|
-
* the largest members. The platform's own refusal reports one number for a
|
|
58
|
-
* budget shared across every member of the map, which tells the operator
|
|
59
|
-
* nothing about WHAT to shrink — so every fabric seam that assembles a map
|
|
60
|
-
* runs this before the loader sees it.
|
|
61
|
-
*
|
|
62
|
-
* Costed to its two paths. Under the ceiling: one length read per member —
|
|
63
|
-
* UTF-16 code units for text, which equal UTF-8 bytes for the ASCII module
|
|
64
|
-
* text the generators emit and undercount otherwise; the platform's own
|
|
65
|
-
* refusal still backstops the exotic case, because this check exists to name
|
|
66
|
-
* members, not to be the ceiling. Over it: exact UTF-8 sizes, computed only
|
|
67
|
-
* then, sorted so the biggest lever is first.
|
|
68
|
-
*/
|
|
69
|
-
export function assertModuleMapWithinCodeLimit(modules) {
|
|
70
|
-
let estimate = 0;
|
|
71
|
-
for (const content of Object.values(modules)) {
|
|
72
|
-
estimate += memberBytes(content, null);
|
|
73
|
-
}
|
|
74
|
-
if (estimate <= DYNAMIC_WORKER_CODE_LIMIT_BYTES)
|
|
75
|
-
return;
|
|
76
|
-
const encoder = new TextEncoder();
|
|
77
|
-
const sized = Object.entries(modules)
|
|
78
|
-
.map(([name, content]) => ({ name, bytes: memberBytes(content, encoder) }))
|
|
79
|
-
.sort((a, b) => b.bytes - a.bytes);
|
|
80
|
-
const total = sized.reduce((sum, member) => sum + member.bytes, 0);
|
|
81
|
-
const top = sized.slice(0, 5)
|
|
82
|
-
.map(({ name, bytes }) => `'${name}' (${bytes.toLocaleString('en-US')} bytes)`)
|
|
83
|
-
.join(', ');
|
|
84
|
-
throw new Error(`Nimbus: dynamic-worker module map is ${total.toLocaleString('en-US')} bytes, over the `
|
|
85
|
-
+ `${DYNAMIC_WORKER_CODE_LIMIT_BYTES.toLocaleString('en-US')}-byte platform ceiling shared by `
|
|
86
|
-
+ `every member. Largest members: ${top}`);
|
|
87
|
-
}
|
|
88
|
-
/**
|
|
89
|
-
* Bytes one module-map member carries, across the loader's content kinds
|
|
90
|
-
* (plain string, `{ js | cjs | py | text }`, `{ wasm | data }`). With an
|
|
91
|
-
* encoder, text is measured exactly; without one, by code-unit length.
|
|
92
|
-
*/
|
|
93
|
-
function memberBytes(content, encoder) {
|
|
94
|
-
const textBytes = (text) => encoder ? encoder.encode(text).byteLength : text.length;
|
|
95
|
-
if (typeof content === 'string')
|
|
96
|
-
return textBytes(content);
|
|
97
|
-
if (content !== null && typeof content === 'object') {
|
|
98
|
-
for (const value of Object.values(content)) {
|
|
99
|
-
if (typeof value === 'string')
|
|
100
|
-
return textBytes(value);
|
|
101
|
-
if (value instanceof ArrayBuffer)
|
|
102
|
-
return value.byteLength;
|
|
103
|
-
if (ArrayBuffer.isView(value))
|
|
104
|
-
return value.byteLength;
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
return 0;
|
|
108
|
-
}
|
|
109
46
|
function facetContainer(ctx) {
|
|
110
47
|
const facets = ctx.facets;
|
|
111
48
|
if (!facets || typeof facets.get !== 'function') {
|
|
@@ -142,7 +79,7 @@ function facetContainer(ctx) {
|
|
|
142
79
|
* storage budget grants no copy-on-write credit — crossing it resets the
|
|
143
80
|
* object rather than raising an error.
|
|
144
81
|
*/
|
|
145
|
-
export async function
|
|
82
|
+
export async function cloneStorage(ctx, clone) {
|
|
146
83
|
const facets = facetContainer(ctx);
|
|
147
84
|
if (typeof facets.clone !== 'function') {
|
|
148
85
|
throw new Error('Nimbus: ctx.facets.clone is unavailable in this runtime; the reflink image '
|
|
@@ -177,15 +114,6 @@ export async function cloneFacetStorage(ctx, clone) {
|
|
|
177
114
|
export function residentFacetName(slot) {
|
|
178
115
|
return `proc-slot-${slot}`;
|
|
179
116
|
}
|
|
180
|
-
/**
|
|
181
|
-
* Facet IDs a Durable Object is granted over its LIFETIME. Append-only and
|
|
182
|
-
* never reclaimed, so crossing it is unrecoverable for the object — which is
|
|
183
|
-
* why the ledger below counts consumption durably instead of leaving the
|
|
184
|
-
* bound as prose the slot book merely respects.
|
|
185
|
-
*/
|
|
186
|
-
export const FACET_ID_LIFETIME_BUDGET = 65_536;
|
|
187
|
-
/** Where the ledger persists the count of facet names ever minted. */
|
|
188
|
-
export const FACET_NAME_HIGH_WATER_KEY = 'fabric_facet_name_high_water';
|
|
189
117
|
/**
|
|
190
118
|
* Slot books, per hosting actor, because the facet index is per Durable
|
|
191
119
|
* Object.
|
|
@@ -205,42 +133,11 @@ const slotBooks = new WeakMap();
|
|
|
205
133
|
function slotBook(ctx) {
|
|
206
134
|
let book = slotBooks.get(ctx);
|
|
207
135
|
if (!book) {
|
|
208
|
-
|
|
209
|
-
free: [],
|
|
210
|
-
next: 0,
|
|
211
|
-
held: new Map(),
|
|
212
|
-
ledger: Promise.resolve(0),
|
|
213
|
-
ledgerKnown: 0,
|
|
214
|
-
};
|
|
215
|
-
created.ledger = Promise.resolve(ctx.storage.get(FACET_NAME_HIGH_WATER_KEY))
|
|
216
|
-
.then((value) => (typeof value === 'number' ? value : 0))
|
|
217
|
-
.catch(() => 0)
|
|
218
|
-
.then((adopted) => {
|
|
219
|
-
created.ledgerKnown = Math.max(created.ledgerKnown, adopted);
|
|
220
|
-
return adopted;
|
|
221
|
-
});
|
|
222
|
-
book = created;
|
|
136
|
+
book = { free: [], next: 0, held: new Map() };
|
|
223
137
|
slotBooks.set(ctx, book);
|
|
224
138
|
}
|
|
225
139
|
return book;
|
|
226
140
|
}
|
|
227
|
-
/**
|
|
228
|
-
* The lifetime facet-ID ledger: how many facet names this fabric has ever
|
|
229
|
-
* minted on the Durable Object, against the 65,536 the platform will ever
|
|
230
|
-
* grant it. `consumed` only ever counts FIRST uses — a reused name, in this
|
|
231
|
-
* incarnation or any earlier one, cost no new ID, which is the slot book's
|
|
232
|
-
* whole reason to exist. Surfaced so an operator can see proximity to a wall
|
|
233
|
-
* whose crossing is unrecoverable, instead of discovering it from the
|
|
234
|
-
* platform's opaque failure.
|
|
235
|
-
*/
|
|
236
|
-
export async function facetIdBudget(ctx) {
|
|
237
|
-
const book = slotBook(ctx);
|
|
238
|
-
const durable = await book.ledger;
|
|
239
|
-
return {
|
|
240
|
-
consumed: Math.max(durable, book.next),
|
|
241
|
-
budget: FACET_ID_LIFETIME_BUDGET,
|
|
242
|
-
};
|
|
243
|
-
}
|
|
244
141
|
/** Take a slot for `pid`, reusing a returned one before minting a new name. */
|
|
245
142
|
function acquireSlot(ctx, pid) {
|
|
246
143
|
const book = slotBook(ctx);
|
|
@@ -250,47 +147,12 @@ function acquireSlot(ctx, pid) {
|
|
|
250
147
|
const reused = book.free.length > 0;
|
|
251
148
|
const slot = reused ? book.free.shift() : book.next++;
|
|
252
149
|
book.held.set(pid, slot);
|
|
150
|
+
// A fresh name is a permanently consumed facet ID; the durable count lives
|
|
151
|
+
// in the budgets ledger (see budgets.ts).
|
|
253
152
|
if (!reused)
|
|
254
|
-
|
|
153
|
+
recordFacetNameMinted(ctx, book.next);
|
|
255
154
|
return slot;
|
|
256
155
|
}
|
|
257
|
-
/**
|
|
258
|
-
* Advance the durable ledger to this incarnation's name count, if it is a new
|
|
259
|
-
* lifetime high. Chained behind adoption so the comparison is always against
|
|
260
|
-
* the real persisted value; a failed write leaves the old link's count and the
|
|
261
|
-
* next mint tries again — the ledger may transiently undercount, never over.
|
|
262
|
-
*/
|
|
263
|
-
function recordNameMinted(ctx, book) {
|
|
264
|
-
const count = book.next;
|
|
265
|
-
book.ledger = book.ledger.then(async (durable) => {
|
|
266
|
-
if (count <= durable)
|
|
267
|
-
return durable;
|
|
268
|
-
try {
|
|
269
|
-
await ctx.storage.put(FACET_NAME_HIGH_WATER_KEY, count);
|
|
270
|
-
}
|
|
271
|
-
catch {
|
|
272
|
-
return durable;
|
|
273
|
-
}
|
|
274
|
-
book.ledgerKnown = Math.max(book.ledgerKnown, count);
|
|
275
|
-
return count;
|
|
276
|
-
});
|
|
277
|
-
}
|
|
278
|
-
/**
|
|
279
|
-
* Name the facet-ID budget on a creation failure at the wall; below it, hand
|
|
280
|
-
* the error back untouched. Exhaustion is the one failure here the platform
|
|
281
|
-
* reports opaquely AND that no teardown, retry or reset can undo, so the
|
|
282
|
-
* ledger — the only witness to the real cause — does the naming. Not a
|
|
283
|
-
* threshold: the comparison is against the budget itself.
|
|
284
|
-
*/
|
|
285
|
-
function withFacetBudgetNamed(consumed, error) {
|
|
286
|
-
if (consumed < FACET_ID_LIFETIME_BUDGET)
|
|
287
|
-
return error;
|
|
288
|
-
const platform = error instanceof Error ? error.message : String(error);
|
|
289
|
-
return new Error(`Nimbus: facet creation failed with this Durable Object's `
|
|
290
|
-
+ `${FACET_ID_LIFETIME_BUDGET.toLocaleString('en-US')} facet-ID lifetime budget consumed `
|
|
291
|
-
+ `(${consumed} facet names ever created). Facet IDs are append-only and never reclaimed, `
|
|
292
|
-
+ `so this failure is permanent for the object: ${platform}`, { cause: error });
|
|
293
|
-
}
|
|
294
156
|
/** Return `pid`'s slot to the free list. */
|
|
295
157
|
function releaseSlot(ctx, pid) {
|
|
296
158
|
const book = slotBook(ctx);
|
|
@@ -302,19 +164,49 @@ function releaseSlot(ctx, pid) {
|
|
|
302
164
|
book.free.sort((a, b) => a - b);
|
|
303
165
|
}
|
|
304
166
|
/**
|
|
305
|
-
*
|
|
306
|
-
*
|
|
167
|
+
* The process surface of one hosting actor: how a resident process comes
|
|
168
|
+
* into existence on workerd, and how a one-shot program runs to completion.
|
|
307
169
|
*
|
|
308
|
-
*
|
|
170
|
+
* `spawn` is the ONE way a resident process comes into existence, and every
|
|
309
171
|
* substrate goes through it: the facet host calls it with the coordinator's
|
|
310
172
|
* own `ctx`, the peer host calls it — over one RPC — with a sibling session
|
|
311
173
|
* DO's. Everything a substrate could plausibly want to special-case is a
|
|
312
174
|
* PARAMETER here rather than a branch: which actor hosts the child, and how
|
|
313
175
|
* the boot spec's by-path members are read.
|
|
314
176
|
*/
|
|
315
|
-
export function
|
|
177
|
+
export function processes(ctx, env) {
|
|
178
|
+
return new Processes(ctx, env);
|
|
179
|
+
}
|
|
180
|
+
export class Processes {
|
|
181
|
+
ctx;
|
|
182
|
+
env;
|
|
183
|
+
constructor(ctx, env) {
|
|
184
|
+
this.ctx = ctx;
|
|
185
|
+
this.env = env;
|
|
186
|
+
}
|
|
187
|
+
/** Open a resident process as a facet of this actor, and start its runner. */
|
|
188
|
+
spawn(disk, supervisor, params) {
|
|
189
|
+
return spawnResident(this.ctx, this.env, disk, supervisor, params);
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Run one program to completion as an UNKEYED dynamic worker.
|
|
193
|
+
*
|
|
194
|
+
* Unkeyed is the whole difference from `spawn`: nothing can re-resolve this
|
|
195
|
+
* worker into a later request's context, so it can never be a routeable
|
|
196
|
+
* target and never has to be released by name. It exists for the duration
|
|
197
|
+
* of one call and its stubs are dropped as that call unwinds.
|
|
198
|
+
*
|
|
199
|
+
* Shared by both substrates on purpose. `peer` places processes that have a
|
|
200
|
+
* residency to place; a one-shot has none, and shipping its fully-inline
|
|
201
|
+
* map across a sibling hop would meet the 32 MiB RPC ceiling that by-path
|
|
202
|
+
* boot specs exist to avoid — for a run that gains nothing by moving.
|
|
203
|
+
*/
|
|
204
|
+
run(supervisor, params, consume) {
|
|
205
|
+
return runOneShot(this.ctx, this.env, supervisor, params, consume);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
function spawnResident(ctx, env, disk, supervisor, params) {
|
|
316
209
|
const facets = facetContainer(ctx);
|
|
317
|
-
const book = slotBook(ctx);
|
|
318
210
|
const slot = acquireSlot(ctx, params.pid);
|
|
319
211
|
const name = residentFacetName(slot);
|
|
320
212
|
// The start callback is the ONLY way this facet is ever created, and it
|
|
@@ -342,7 +234,7 @@ export function openResidentFacet(ctx, env, disk, supervisor, params) {
|
|
|
342
234
|
}
|
|
343
235
|
catch (error) {
|
|
344
236
|
releaseSlot(ctx, params.pid);
|
|
345
|
-
throw withFacetBudgetNamed(
|
|
237
|
+
throw withFacetBudgetNamed(facetNameCount(ctx), error);
|
|
346
238
|
}
|
|
347
239
|
let disposed = false;
|
|
348
240
|
const release = async () => {
|
|
@@ -368,15 +260,14 @@ export function openResidentFacet(ctx, env, disk, supervisor, params) {
|
|
|
368
260
|
}
|
|
369
261
|
catch (error) {
|
|
370
262
|
void release();
|
|
371
|
-
throw withFacetBudgetNamed(
|
|
263
|
+
throw withFacetBudgetNamed(facetNameCount(ctx), error);
|
|
372
264
|
}
|
|
373
265
|
// The rejection that carries the platform's failure at ID exhaustion is
|
|
374
266
|
// this one, and it is annotated AFTER awaiting the ledger — the first
|
|
375
267
|
// failure of a fresh incarnation must compare against the persisted count,
|
|
376
268
|
// not the zero its adoption read has not yet replaced.
|
|
377
269
|
started = started.catch(async (error) => {
|
|
378
|
-
|
|
379
|
-
throw withFacetBudgetNamed(Math.max(durable, book.next), error);
|
|
270
|
+
throw withFacetBudgetNamed(await facetNameCountDurable(ctx), error);
|
|
380
271
|
});
|
|
381
272
|
// A caller reads whichever of `started` and the lifecycle it needs, so keep
|
|
382
273
|
// the runtime from reporting the other as an unhandled rejection.
|
|
@@ -415,20 +306,7 @@ function residentProcessClass(ctx, env, disk, supervisor, params) {
|
|
|
415
306
|
throw withDynamicWorkerCapNamed(ctx, error);
|
|
416
307
|
}
|
|
417
308
|
}
|
|
418
|
-
|
|
419
|
-
* Run one program to completion as an UNKEYED dynamic worker.
|
|
420
|
-
*
|
|
421
|
-
* Unkeyed is the whole difference from `openResidentFacet`: nothing can
|
|
422
|
-
* re-resolve this worker into a later request's context, so it can never be a
|
|
423
|
-
* routeable target and never has to be released by name. It exists for the
|
|
424
|
-
* duration of one call and its stubs are dropped as that call unwinds.
|
|
425
|
-
*
|
|
426
|
-
* Shared by both substrates on purpose. `peer` places processes that have a
|
|
427
|
-
* residency to place; a one-shot has none, and shipping its fully-inline map
|
|
428
|
-
* across a sibling hop would meet the 32 MiB RPC ceiling that by-path boot
|
|
429
|
-
* specs exist to avoid — for a run that gains nothing by moving.
|
|
430
|
-
*/
|
|
431
|
-
export async function runOneShotWorker(ctx, env, supervisor, params, consume) {
|
|
309
|
+
async function runOneShot(ctx, env, supervisor, params, consume) {
|
|
432
310
|
const loader = env.LOADER;
|
|
433
311
|
if (!loader || typeof loader.load !== 'function') {
|
|
434
312
|
throw new Error('Nimbus: env.LOADER binding missing or invalid. Running a program requires '
|
|
@@ -497,7 +375,7 @@ export async function runOneShotWorker(ctx, env, supervisor, params, consume) {
|
|
|
497
375
|
}
|
|
498
376
|
async function residentWorkerConfig(env, disk, supervisor, boot) {
|
|
499
377
|
const config = boot.kind === 'staged'
|
|
500
|
-
? await
|
|
378
|
+
? await stagedBootAssembler()(env, boot.stage)
|
|
501
379
|
: await residentLoaderConfig(boot.code, disk());
|
|
502
380
|
assertModuleMapWithinCodeLimit(config.modules ?? {});
|
|
503
381
|
const supervisorRpc = supervisorEntrypoint();
|