@nimbus-sh/worker 0.2.2 → 0.3.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 +5 -2
- package/dist/_shared/session-router.d.ts +8 -0
- package/dist/_shared/session-router.d.ts.map +1 -1
- package/dist/_shared/session-router.js +8 -0
- package/dist/facets/cirrus-real.js +1 -1
- package/dist/facets/manager.d.ts +42 -104
- package/dist/facets/manager.d.ts.map +1 -1
- package/dist/facets/manager.js +126 -283
- package/dist/facets/opencode-staging.d.ts +1 -1
- package/dist/facets/opencode-staging.d.ts.map +1 -1
- package/dist/facets/opencode-staging.js +4 -0
- package/dist/facets/vite-dev-server.d.ts +2 -2
- package/dist/facets/vite-dev-server.js +4 -4
- package/dist/git/commands.d.ts +8 -0
- package/dist/git/commands.d.ts.map +1 -1
- package/dist/git/commands.js +41 -9
- package/dist/git/network-facet.d.ts +1 -1
- package/dist/git/network-facet.d.ts.map +1 -1
- package/dist/git/network-facet.js +35 -12
- package/dist/git-bundle.generated.d.ts +2 -2
- package/dist/git-bundle.generated.d.ts.map +1 -1
- package/dist/git-bundle.generated.js +3 -3
- package/dist/index.js +1 -1
- package/dist/loaders/child-process/spawn-facet.js +1 -1
- package/dist/loaders/child-process/spawn-pool.d.ts.map +1 -1
- package/dist/loaders/child-process/spawn-pool.js +2 -2
- package/dist/loaders/generated-workers.d.ts +1 -1
- package/dist/loaders/generated-workers.d.ts.map +1 -1
- package/dist/loaders/generated-workers.js +2 -2
- package/dist/loaders/npm-resolve-preamble.d.ts +3 -3
- package/dist/loaders/npm-resolve-preamble.js +3 -3
- package/dist/loaders/pre-bundle-preamble.d.ts +5 -5
- package/dist/loaders/pre-bundle-preamble.js +6 -6
- package/dist/loaders/process-host.d.ts +6 -107
- package/dist/loaders/process-host.d.ts.map +1 -1
- package/dist/loaders/process-host.js +6 -405
- package/dist/npm/install-batch-facet.js +1 -1
- package/dist/npm/installer.d.ts +4 -4
- package/dist/npm/installer.js +18 -18
- package/dist/npm/pre-bundle-facet.d.ts +2 -2
- package/dist/npm/pre-bundle-facet.js +5 -5
- package/dist/npm/resolve-facet.d.ts +1 -1
- package/dist/npm/resolve-facet.js +1 -1
- package/dist/npm/resolve-one-facet.d.ts +5 -5
- package/dist/npm/resolve-one-facet.js +5 -5
- package/dist/router/index.js +1 -1
- package/dist/router/remote-api.js +8 -0
- package/dist/runtime/bun-repl.d.ts +1 -1
- package/dist/runtime/bun-repl.js +3 -3
- package/dist/runtime/facet-loader-host.d.ts +5 -8
- package/dist/runtime/facet-loader-host.d.ts.map +1 -1
- package/dist/runtime/facet-loader-host.js +7 -11
- package/dist/runtime/node-repl.js +2 -2
- package/dist/runtime/python-repl.js +3 -3
- package/dist/runtime/ruby-repl.js +2 -2
- package/dist/runtime/ruby-resident.js +1 -1
- package/dist/session/hibernation.d.ts +18 -51
- package/dist/session/hibernation.d.ts.map +1 -1
- package/dist/session/hibernation.js +49 -186
- package/dist/session/init-phases.d.ts +2 -2
- package/dist/session/init-phases.d.ts.map +1 -1
- package/dist/session/init-phases.js +1 -1
- package/dist/session/init.d.ts.map +1 -1
- package/dist/session/init.js +16 -2
- package/dist/session/keys.d.ts +14 -35
- package/dist/session/keys.d.ts.map +1 -1
- package/dist/session/keys.js +17 -35
- package/dist/session/nimbus-session.d.ts +10 -4
- package/dist/session/nimbus-session.d.ts.map +1 -1
- package/dist/session/nimbus-session.js +20 -11
- package/dist/session/port-capability.d.ts +43 -0
- package/dist/session/port-capability.d.ts.map +1 -0
- package/dist/session/port-capability.js +49 -0
- package/dist/session/programmatic.d.ts +41 -5
- package/dist/session/programmatic.d.ts.map +1 -1
- package/dist/session/programmatic.js +147 -15
- package/dist/session/routes.d.ts +2 -0
- package/dist/session/routes.d.ts.map +1 -1
- package/dist/session/routes.js +91 -16
- package/dist/session/rpc.d.ts +35 -8
- package/dist/session/rpc.d.ts.map +1 -1
- package/dist/session/rpc.js +73 -14
- package/dist/session/start-real-vite.d.ts.map +1 -1
- package/dist/session/start-real-vite.js +2 -0
- package/dist/session/state-store.d.ts +3 -2
- package/dist/session/state-store.d.ts.map +1 -1
- package/dist/session/state-store.js +3 -2
- package/dist/session/supervisor-rpc.d.ts.map +1 -1
- package/dist/session/supervisor-rpc.js +5 -0
- package/dist/session/ws.d.ts +2 -2
- package/dist/session/ws.d.ts.map +1 -1
- package/dist/session/ws.js +2 -2
- package/dist/wrangler/nimbus-wrangler.js +1 -1
- package/package.json +3 -2
- package/scripts/bundle-runtime.mjs +5 -2
- package/dist/facets/inner-do-registry.d.ts +0 -41
- package/dist/facets/inner-do-registry.d.ts.map +0 -1
- package/dist/facets/inner-do-registry.js +0 -51
- package/dist/facets/launch-pacer.d.ts +0 -111
- package/dist/facets/launch-pacer.d.ts.map +0 -1
- package/dist/facets/launch-pacer.js +0 -130
- package/dist/loaders/fanout-pool.d.ts +0 -207
- package/dist/loaders/fanout-pool.d.ts.map +0 -1
- package/dist/loaders/fanout-pool.js +0 -363
- package/dist/loaders/loader-pool.d.ts +0 -294
- package/dist/loaders/loader-pool.d.ts.map +0 -1
- package/dist/loaders/loader-pool.js +0 -642
- package/dist/loaders/process-fabric.d.ts +0 -509
- package/dist/loaders/process-fabric.d.ts.map +0 -1
- package/dist/loaders/process-fabric.js +0 -360
- package/dist/loaders/vendor/errors.d.ts +0 -24
- package/dist/loaders/vendor/errors.d.ts.map +0 -1
- package/dist/loaders/vendor/errors.js +0 -46
- package/dist/loaders/vendor/serialize.d.ts +0 -3
- package/dist/loaders/vendor/serialize.d.ts.map +0 -1
- package/dist/loaders/vendor/serialize.js +0 -25
- package/dist/loaders/vendor/types.d.ts +0 -69
- package/dist/loaders/vendor/types.d.ts.map +0 -1
- package/dist/loaders/vendor/types.js +0 -4
- package/dist/loaders/workerd-facet-host.d.ts +0 -141
- package/dist/loaders/workerd-facet-host.d.ts.map +0 -1
- package/dist/loaders/workerd-facet-host.js +0 -284
- package/dist/session/bindings.d.ts +0 -209
- package/dist/session/bindings.d.ts.map +0 -1
- package/dist/session/bindings.js +0 -680
- package/dist/session/ctx-exports.d.ts +0 -16
- package/dist/session/ctx-exports.d.ts.map +0 -1
- package/dist/session/ctx-exports.js +0 -22
- package/dist/session/ws-hibernation-config.d.ts +0 -64
- package/dist/session/ws-hibernation-config.d.ts.map +0 -1
- package/dist/session/ws-hibernation-config.js +0 -89
|
@@ -1,69 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* process-host.ts —
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* `loaders/process-fabric.ts` owns what a resident process IS. This module
|
|
6
|
-
* owns only where it lives, behind `ProcessHost`:
|
|
7
|
-
*
|
|
8
|
-
* facet — the process is a named child actor of the user's own session DO.
|
|
9
|
-
* peer — the process is a named child actor of a SIBLING session DO, and
|
|
10
|
-
* the coordinator reaches it over one held-open RPC.
|
|
11
|
-
*
|
|
12
|
-
* Both call the same `openResidentFacet`. The peer leg is not a second process
|
|
13
|
-
* implementation; it is the same call made on a different actor, which is why
|
|
14
|
-
* the runner, the boot spec, the class name, the writer handshake, the start
|
|
15
|
-
* contract and the lifecycle are shared code rather than parallel paths.
|
|
16
|
-
*
|
|
17
|
-
* Choosing between them
|
|
18
|
-
* ─────────────────────
|
|
19
|
-
* `NIMBUS_PROCESS_HOST`, one var for the whole deployment. No spawn site
|
|
20
|
-
* chooses; no program name, mode or payload size reaches the choice. If a
|
|
21
|
-
* decision about a particular process ever appears here, the heavy/light
|
|
22
|
-
* classifier has grown back and should be deleted again.
|
|
23
|
-
*
|
|
24
|
-
* | | spawn | memory | CPU | SQLite |
|
|
25
|
-
* |----------|------------|-------------|------------|--------|
|
|
26
|
-
* | facet | 8-16 ms | independent | SHARED | own |
|
|
27
|
-
* | peer | 242-359 ms | independent | independent| own |
|
|
28
|
-
*
|
|
29
|
-
* Facet CPU is shared because facets are separate isolates inside ONE actor
|
|
30
|
-
* thread: awaiting I/O yields it completely (measured 0 ms of sibling impact),
|
|
31
|
-
* but a non-yielding loop stalls every sibling for its full duration
|
|
32
|
-
* (measured 6,852 ms). A peer pays ~20x the spawn cost to buy that back.
|
|
33
|
-
*
|
|
34
|
-
* What the peer leg has to do differently, and why none of it reaches the
|
|
35
|
-
* process
|
|
36
|
-
* ────────────────────────────────────────────────────────────────────────
|
|
37
|
-
* payloads — a whole structured-clone RPC value is capped at 32 MiB, and
|
|
38
|
-
* pi's node snapshot alone serializes to 44,252,709 bytes. Boot
|
|
39
|
-
* specs name their large members BY PATH, so what crosses is a
|
|
40
|
-
* path and the host reads the bytes off the coordinator's disk in
|
|
41
|
-
* 4 MiB ranges through the supervisor. Nothing large is ever an
|
|
42
|
-
* RPC argument, so nothing has to be streamed or replayed.
|
|
43
|
-
* requests — workerd refuses to transfer an object owned by a
|
|
44
|
-
* dynamically-loaded worker across a sibling-DO hop, so a request
|
|
45
|
-
* travels to the peer as PARTS and the response comes back as
|
|
46
|
-
* parts, both with plain `ReadableStream` bodies that RPC carries
|
|
47
|
-
* with flow control. A live SSE body still streams; nothing is
|
|
48
|
-
* buffered.
|
|
49
|
-
* liveness — the host leg is held open for the process's whole life, which
|
|
50
|
-
* is also what keeps the hosting DO resident. A peer therefore
|
|
51
|
-
* never outlives its coordinator: the coordinator dying cancels
|
|
52
|
-
* the inbound call and the facet dies with it. Nothing in this
|
|
53
|
-
* fabric arms an alarm, on either substrate.
|
|
54
|
-
*
|
|
55
|
-
* WebSockets are NOT in that list, because a resident process never receives
|
|
56
|
-
* one. `RouteableFacetTarget` is `handleHttpRequest(Request): Promise<Response>`
|
|
57
|
-
* and nothing else; no runner returns a 101 with a `webSocket` member; every
|
|
58
|
-
* inbound socket Nimbus serves — the shell terminal, `/api/logs/<pid>`, vite
|
|
59
|
-
* HMR — terminates on the session DO and reaches the process, if at all, as
|
|
60
|
-
* events on a supervisor poll. That is substrate-independent by construction.
|
|
2
|
+
* process-host.ts — which substrate this deployment runs resident processes
|
|
3
|
+
* on. The substrates themselves live in `@nimbus-sh/fabric/process-host.js`;
|
|
4
|
+
* this module owns only the deployment config that picks between them.
|
|
61
5
|
*/
|
|
62
|
-
import {
|
|
63
|
-
import { isTransientDoReset } from '@nimbus-sh/core/observability/oom-classify.js';
|
|
64
|
-
import { PEER_RETRY_BACKOFF_MS, PEER_TRANSIENT_RESET_RETRIES } from './fanout-pool.js';
|
|
65
|
-
import { DYNAMIC_WORKER_CODE_LIMIT_BYTES, openResidentFacet, residentFacetName, runOneShotWorker, } from './workerd-facet-host.js';
|
|
66
|
-
import { BindingError } from './vendor/errors.js';
|
|
6
|
+
import { createProcessHost, } from '@nimbus-sh/fabric/process-host.js';
|
|
67
7
|
/**
|
|
68
8
|
* The var that picks the substrate, and the only place its name appears.
|
|
69
9
|
* Unset means `facet`; an unrecognized value is refused rather than defaulted,
|
|
@@ -80,346 +20,7 @@ export function processHostMode(env) {
|
|
|
80
20
|
return 'peer';
|
|
81
21
|
throw new Error(`Nimbus: NIMBUS_PROCESS_HOST must be 'facet' or 'peer' (got '${String(raw)}')`);
|
|
82
22
|
}
|
|
83
|
-
/**
|
|
84
|
-
* The substrate for this deployment, resolved once. `disk` is the
|
|
85
|
-
* coordinator's own filesystem reader; the peer host does not take it, because
|
|
86
|
-
* a peer reads the same disk through the supervisor instead.
|
|
87
|
-
*/
|
|
23
|
+
/** The substrate for this deployment, resolved once. */
|
|
88
24
|
export function processHostFor(ctx, env, disk) {
|
|
89
|
-
return processHostMode(env)
|
|
90
|
-
? new PeerProcessHost(ctx, env)
|
|
91
|
-
: new FacetProcessHost(ctx, env, disk);
|
|
92
|
-
}
|
|
93
|
-
// ── facet: the process is a child of the user's own session DO ──────────────
|
|
94
|
-
class FacetProcessHost {
|
|
95
|
-
ctx;
|
|
96
|
-
disk;
|
|
97
|
-
/**
|
|
98
|
-
* The process shares its session's Durable Object, so the session's own
|
|
99
|
-
* store is reachable by copy-on-write — and its storage budget is the same
|
|
100
|
-
* budget. Both halves of that follow from the one fact, and neither is
|
|
101
|
-
* optional.
|
|
102
|
-
*/
|
|
103
|
-
imageDelivery = {
|
|
104
|
-
reflink: 'same-object',
|
|
105
|
-
moduleCeilingBytes: DYNAMIC_WORKER_CODE_LIMIT_BYTES,
|
|
106
|
-
storageSharedWithSession: true,
|
|
107
|
-
};
|
|
108
|
-
env;
|
|
109
|
-
coordDoId;
|
|
110
|
-
constructor(ctx, env, disk) {
|
|
111
|
-
this.ctx = ctx;
|
|
112
|
-
this.disk = disk;
|
|
113
|
-
this.env = (env ?? {});
|
|
114
|
-
this.coordDoId = ctx.id.toString();
|
|
115
|
-
}
|
|
116
|
-
runOnce(params, consume) {
|
|
117
|
-
return runOneShotWorker(this.env, { doId: this.coordDoId, pid: params.pid, writerId: params.writerId }, params, consume);
|
|
118
|
-
}
|
|
119
|
-
async open(params) {
|
|
120
|
-
const supervisor = {
|
|
121
|
-
doId: this.coordDoId,
|
|
122
|
-
pid: params.pid,
|
|
123
|
-
writerId: params.writerId,
|
|
124
|
-
};
|
|
125
|
-
const { slot, ...facet } = openResidentFacet(this.ctx, this.env, this.disk, supervisor, params);
|
|
126
|
-
return {
|
|
127
|
-
...facet,
|
|
128
|
-
describe: () => `facet '${residentFacetName(slot)}' (pid ${params.pid})`
|
|
129
|
-
+ ` of session ${this.coordDoId.slice(-12)}`
|
|
130
|
-
+ `; ${describeImageDelivery(this.imageDelivery)}`,
|
|
131
|
-
};
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
/**
|
|
135
|
-
* The operator-facing half of the substrate difference, on the one line an
|
|
136
|
-
* operator actually reads. A comment in this file would not have told anyone
|
|
137
|
-
* who flipped the config that the image path changed under them.
|
|
138
|
-
*/
|
|
139
|
-
function describeImageDelivery(delivery) {
|
|
140
|
-
return `fs image: reflink ${delivery.reflink}, module map ≤ ${delivery.moduleCeilingBytes}B, `
|
|
141
|
-
+ `storage ${delivery.storageSharedWithSession ? 'shared with the session' : 'its own'}`;
|
|
142
|
-
}
|
|
143
|
-
// ── peer: the process is a child of a sibling session DO ────────────────────
|
|
144
|
-
/**
|
|
145
|
-
* How many sibling names to try before accepting one that co-located with a
|
|
146
|
-
* process already running. Measured: 1 shared pair in 24 fresh peers.
|
|
147
|
-
*/
|
|
148
|
-
const PEER_PLACEMENT_MAX_ATTEMPTS = 4;
|
|
149
|
-
/**
|
|
150
|
-
* This workerd process's identity. Module scope, so two Durable Objects
|
|
151
|
-
* reporting the same token are in the same process — which is exactly the CPU
|
|
152
|
-
* sharing a peer exists to avoid, and the only way to detect it.
|
|
153
|
-
*/
|
|
154
|
-
let _isolateToken = null;
|
|
155
|
-
export function isolateToken() {
|
|
156
|
-
if (!_isolateToken)
|
|
157
|
-
_isolateToken = crypto.randomUUID();
|
|
158
|
-
return _isolateToken;
|
|
159
|
-
}
|
|
160
|
-
/**
|
|
161
|
-
* Every method a hosting sibling must answer. Checked as a set at first
|
|
162
|
-
* contact: a stub that has the probe but not the router is a deployment skew,
|
|
163
|
-
* and finding that out mid-process is finding it out too late.
|
|
164
|
-
*/
|
|
165
|
-
const PROCESS_PEER_METHODS = [
|
|
166
|
-
'_rpcProcessHostProbe',
|
|
167
|
-
'_rpcHostProcess',
|
|
168
|
-
'_rpcAwaitHostedOpen',
|
|
169
|
-
'_rpcAwaitHostedBoot',
|
|
170
|
-
'_rpcRouteHostedHttp',
|
|
171
|
-
'_rpcCancelHostProcess',
|
|
172
|
-
];
|
|
173
|
-
function peerNamespace(env) {
|
|
174
|
-
const ns = (typeof env === 'object' || typeof env === 'function') && env !== null
|
|
175
|
-
? Reflect.get(env, 'NIMBUS_SESSION')
|
|
176
|
-
: undefined;
|
|
177
|
-
if ((typeof ns !== 'object' && typeof ns !== 'function') || ns === null
|
|
178
|
-
|| typeof Reflect.get(ns, 'idFromName') !== 'function'
|
|
179
|
-
|| typeof Reflect.get(ns, 'get') !== 'function') {
|
|
180
|
-
throw new BindingError('ProcessFabric: env.NIMBUS_SESSION binding missing or invalid. '
|
|
181
|
-
+ "NIMBUS_PROCESS_HOST='peer' hosts every resident process on a sibling "
|
|
182
|
-
+ 'Durable Object; add the binding via durable_objects.bindings in wrangler.jsonc.');
|
|
183
|
-
}
|
|
184
|
-
return ns;
|
|
185
|
-
}
|
|
186
|
-
function processPeerStub(value, peerName) {
|
|
187
|
-
if ((typeof value !== 'object' && typeof value !== 'function') || value === null) {
|
|
188
|
-
throw new BindingError(`ProcessFabric: NIMBUS_SESSION.get() returned no stub for peer '${peerName}'.`);
|
|
189
|
-
}
|
|
190
|
-
for (const method of PROCESS_PEER_METHODS) {
|
|
191
|
-
if (typeof Reflect.get(value, method) !== 'function') {
|
|
192
|
-
throw new BindingError(`ProcessFabric: peer '${peerName}' does not expose ${method}(). `
|
|
193
|
-
+ 'A sibling that answers some of the host-process surface and not the rest '
|
|
194
|
-
+ 'would fail somewhere inside a running process instead of here.');
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
return value;
|
|
198
|
-
}
|
|
199
|
-
class PeerProcessHost {
|
|
200
|
-
ctx;
|
|
201
|
-
/**
|
|
202
|
-
* A peer buys independent CPU and its own storage budget, and pays for both
|
|
203
|
-
* with the image path: nothing crosses a Durable Object boundary by
|
|
204
|
-
* reference, so the whole session filesystem has to travel as bytes.
|
|
205
|
-
*/
|
|
206
|
-
imageDelivery = {
|
|
207
|
-
reflink: 'impossible',
|
|
208
|
-
moduleCeilingBytes: DYNAMIC_WORKER_CODE_LIMIT_BYTES,
|
|
209
|
-
storageSharedWithSession: false,
|
|
210
|
-
};
|
|
211
|
-
ns;
|
|
212
|
-
env;
|
|
213
|
-
coordDoId;
|
|
214
|
-
/** pid → the isolate token of the peer currently hosting that process. */
|
|
215
|
-
tokensInUse = new Map();
|
|
216
|
-
constructor(ctx, env) {
|
|
217
|
-
this.ctx = ctx;
|
|
218
|
-
this.ns = peerNamespace(env);
|
|
219
|
-
this.env = (env ?? {});
|
|
220
|
-
this.coordDoId = ctx.id.toString();
|
|
221
|
-
}
|
|
222
|
-
/**
|
|
223
|
-
* Not placed on a sibling, and that is not a gap in this substrate.
|
|
224
|
-
* `peer` exists to buy a resident process independent CPU; a program that
|
|
225
|
-
* ends with the call it was started by has no residency to place, and its
|
|
226
|
-
* map is fully inline — so a hop would meet the RPC ceiling that by-path
|
|
227
|
-
* boot specs exist to avoid while buying nothing. It runs as a dynamic
|
|
228
|
-
* worker of the coordinator here exactly as it does on `facet`.
|
|
229
|
-
*/
|
|
230
|
-
runOnce(params, consume) {
|
|
231
|
-
return runOneShotWorker(this.env, { doId: this.coordDoId, pid: params.pid, writerId: params.writerId }, params, consume);
|
|
232
|
-
}
|
|
233
|
-
async open(params) {
|
|
234
|
-
const placement = await this._place(params.pid);
|
|
235
|
-
this.tokensInUse.set(params.pid, placement.isolateToken);
|
|
236
|
-
// Held open for the process's whole life: it is the lifecycle, and it is
|
|
237
|
-
// also what keeps the hosting DO resident. It settles when a `lifetime`
|
|
238
|
-
// runner exits or when a `boot` runner's host is cancelled, and rejects if
|
|
239
|
-
// the peer dies under either.
|
|
240
|
-
const hostLeg = placement.stub._rpcHostProcess(params.boot, {
|
|
241
|
-
coordinatorDoId: this.coordDoId,
|
|
242
|
-
pid: params.pid,
|
|
243
|
-
writerId: params.writerId,
|
|
244
|
-
workerKey: params.workerKey,
|
|
245
|
-
startArgs: params.startArgs,
|
|
246
|
-
});
|
|
247
|
-
hostLeg.catch(() => { });
|
|
248
|
-
// The peer starts the runner as part of hosting it; this reads back that
|
|
249
|
-
// one boot payload without re-running anything, so `started` means exactly
|
|
250
|
-
// what it means on a facet. Racing the host leg is what turns a peer that
|
|
251
|
-
// died before the boot landed into a rejection rather than a hang — the
|
|
252
|
-
// peer that would have answered is the thing that is gone.
|
|
253
|
-
const booted = placement.stub._rpcAwaitHostedBoot(params.workerKey).then((r) => r.payload);
|
|
254
|
-
booted.catch(() => { });
|
|
255
|
-
const started = Promise.race([booted, hostLeg.then(() => booted)]);
|
|
256
|
-
started.catch(() => { });
|
|
257
|
-
// Do not return a handle for a process that was never opened. Opening a
|
|
258
|
-
// facet of one's own DO either throws or does not, before any handle
|
|
259
|
-
// exists; this is that same moment, one hop away.
|
|
260
|
-
try {
|
|
261
|
-
await Promise.race([
|
|
262
|
-
placement.stub._rpcAwaitHostedOpen(params.workerKey),
|
|
263
|
-
hostLeg.then(() => ({ ok: true })),
|
|
264
|
-
]);
|
|
265
|
-
}
|
|
266
|
-
catch (error) {
|
|
267
|
-
this.tokensInUse.delete(params.pid);
|
|
268
|
-
// Awaited, not fired off: a spawn that rejects must mean nothing was
|
|
269
|
-
// left running, which is what a throw from `openResidentFacet` means on
|
|
270
|
-
// the other substrate.
|
|
271
|
-
try {
|
|
272
|
-
await this._cancel(params.workerKey, placement.peerName);
|
|
273
|
-
}
|
|
274
|
-
finally {
|
|
275
|
-
disposeRpcResource(placement.stub);
|
|
276
|
-
}
|
|
277
|
-
throw error;
|
|
278
|
-
}
|
|
279
|
-
let released = false;
|
|
280
|
-
return {
|
|
281
|
-
started,
|
|
282
|
-
// The held leg IS the process's residency here. It settles cleanly when
|
|
283
|
-
// the coordinator releases; anything else is the host going away under a
|
|
284
|
-
// process that was up, and the fabric ends the process on it.
|
|
285
|
-
lost: hostLeg.then(() => new Promise(() => { })),
|
|
286
|
-
handleHttpRequest: (request) => routeThroughPeer(placement.stub, params.workerKey, request),
|
|
287
|
-
release: async () => {
|
|
288
|
-
if (released)
|
|
289
|
-
return;
|
|
290
|
-
released = true;
|
|
291
|
-
this.tokensInUse.delete(params.pid);
|
|
292
|
-
try {
|
|
293
|
-
await this._cancel(params.workerKey, placement.peerName);
|
|
294
|
-
}
|
|
295
|
-
finally {
|
|
296
|
-
disposeRpcResource(placement.stub);
|
|
297
|
-
}
|
|
298
|
-
},
|
|
299
|
-
describe: () => `peer '${placement.peerName}' (isolate ${placement.isolateToken.slice(0, 8)})`
|
|
300
|
-
+ `; ${describeImageDelivery(this.imageDelivery)}`,
|
|
301
|
-
};
|
|
302
|
-
}
|
|
303
|
-
/**
|
|
304
|
-
* Probe successive sibling names until one reports an isolate token distinct
|
|
305
|
-
* from this coordinator's and from every peer already hosting a process. A
|
|
306
|
-
* peer that co-located bought nothing — it shares the CPU it was chosen to
|
|
307
|
-
* escape — so placement verifies rather than assumes.
|
|
308
|
-
*/
|
|
309
|
-
async _place(pid) {
|
|
310
|
-
const denied = new Set([isolateToken(), ...this.tokensInUse.values()]);
|
|
311
|
-
for (let attempt = 0; attempt < PEER_PLACEMENT_MAX_ATTEMPTS - 1; attempt++) {
|
|
312
|
-
const candidate = await this._probePlacement(pid, attempt);
|
|
313
|
-
if (!denied.has(candidate.isolateToken))
|
|
314
|
-
return candidate;
|
|
315
|
-
disposeRpcResource(candidate.stub);
|
|
316
|
-
}
|
|
317
|
-
// Every attempt co-located, which happens in single-process topologies. Use
|
|
318
|
-
// the last one anyway: it runs the process correctly, it just shares the
|
|
319
|
-
// CPU it was chosen to escape, and the placement line names the isolate it
|
|
320
|
-
// landed in so that is visible rather than assumed.
|
|
321
|
-
return this._probePlacement(pid, PEER_PLACEMENT_MAX_ATTEMPTS - 1);
|
|
322
|
-
}
|
|
323
|
-
/** One sibling name, resolved and probed. Leaks nothing on failure. */
|
|
324
|
-
async _probePlacement(pid, attempt) {
|
|
325
|
-
const peerName = `${this.coordDoId}:proc:${pid}:${attempt}`;
|
|
326
|
-
let resource;
|
|
327
|
-
try {
|
|
328
|
-
resource = this.ns.get(this.ns.idFromName(peerName));
|
|
329
|
-
const stub = processPeerStub(resource, peerName);
|
|
330
|
-
const probe = await this._probe(stub, peerName);
|
|
331
|
-
return { stub, peerName, isolateToken: probe.isolateToken };
|
|
332
|
-
}
|
|
333
|
-
catch (error) {
|
|
334
|
-
disposeRpcResource(resource);
|
|
335
|
-
throw error;
|
|
336
|
-
}
|
|
337
|
-
}
|
|
338
|
-
/**
|
|
339
|
-
* First contact with a possibly-cold sibling DO: retry transient platform
|
|
340
|
-
* resets with the same bounded policy the fanout peers ship. Non-transient
|
|
341
|
-
* failures propagate on the first hit.
|
|
342
|
-
*/
|
|
343
|
-
async _probe(stub, peerName) {
|
|
344
|
-
for (let attempt = 0;; attempt++) {
|
|
345
|
-
try {
|
|
346
|
-
const probe = await stub._rpcProcessHostProbe();
|
|
347
|
-
if (!probe || typeof probe.isolateToken !== 'string' || probe.isolateToken.length === 0) {
|
|
348
|
-
throw new Error(`ProcessFabric: peer '${peerName}' returned no isolate token`);
|
|
349
|
-
}
|
|
350
|
-
return probe;
|
|
351
|
-
}
|
|
352
|
-
catch (err) {
|
|
353
|
-
if (attempt < PEER_TRANSIENT_RESET_RETRIES && isTransientDoReset(err)) {
|
|
354
|
-
await new Promise((r) => setTimeout(r, PEER_RETRY_BACKOFF_MS[Math.min(attempt, PEER_RETRY_BACKOFF_MS.length - 1)]));
|
|
355
|
-
continue;
|
|
356
|
-
}
|
|
357
|
-
throw err;
|
|
358
|
-
}
|
|
359
|
-
}
|
|
360
|
-
}
|
|
361
|
-
/**
|
|
362
|
-
* A fresh stub to the hosting peer fires `_rpcCancelHostProcess`, which
|
|
363
|
-
* releases the peer's facet — the exact teardown a local facet gets — and
|
|
364
|
-
* only answers once it has. Awaiting it is what makes "released" mean the
|
|
365
|
-
* same thing on both substrates, so the writer identity is retired at the
|
|
366
|
-
* same point in the process's life either way. Also handed to `waitUntil`,
|
|
367
|
-
* so a kill nobody awaited still completes.
|
|
368
|
-
*/
|
|
369
|
-
_cancel(workerKey, peerName) {
|
|
370
|
-
let stub;
|
|
371
|
-
try {
|
|
372
|
-
stub = processPeerStub(this.ns.get(this.ns.idFromName(peerName)), peerName);
|
|
373
|
-
}
|
|
374
|
-
catch {
|
|
375
|
-
return Promise.resolve();
|
|
376
|
-
}
|
|
377
|
-
const cancelled = Promise.resolve(stub._rpcCancelHostProcess(workerKey))
|
|
378
|
-
.then(() => undefined)
|
|
379
|
-
.catch(() => { })
|
|
380
|
-
.finally(() => disposeRpcResource(stub));
|
|
381
|
-
this.ctx.waitUntil(cancelled);
|
|
382
|
-
return cancelled;
|
|
383
|
-
}
|
|
384
|
-
}
|
|
385
|
-
async function routeThroughPeer(stub, workerKey, request) {
|
|
386
|
-
const result = await stub._rpcRouteHostedHttp(workerKey, {
|
|
387
|
-
method: request.method,
|
|
388
|
-
url: request.url,
|
|
389
|
-
headers: headerPairs(request.headers),
|
|
390
|
-
body: request.method === 'GET' || request.method === 'HEAD' ? null : request.body,
|
|
391
|
-
});
|
|
392
|
-
const responseHeaders = new Headers();
|
|
393
|
-
for (const [key, value] of result.headers)
|
|
394
|
-
responseHeaders.append(key, value);
|
|
395
|
-
return new Response(result.body, {
|
|
396
|
-
status: result.status,
|
|
397
|
-
statusText: result.statusText,
|
|
398
|
-
headers: responseHeaders,
|
|
399
|
-
});
|
|
400
|
-
}
|
|
401
|
-
/**
|
|
402
|
-
* Headers as pairs, with every `Set-Cookie` kept separate.
|
|
403
|
-
*
|
|
404
|
-
* Iterating a `Headers` combines same-named fields into one comma-joined
|
|
405
|
-
* value, and for `Set-Cookie` that is not reversible — `append` cannot split
|
|
406
|
-
* `a=1; Path=/, b=2; Path=/` back into two cookies, and a browser reading the
|
|
407
|
-
* merged form sets one malformed cookie instead of two. Every other field
|
|
408
|
-
* combines by comma legally, so only this one needs the separate accessor.
|
|
409
|
-
* A user's server setting two cookies must not depend on which substrate its
|
|
410
|
-
* process happened to run on.
|
|
411
|
-
*/
|
|
412
|
-
export function headerPairs(headers) {
|
|
413
|
-
const pairs = [];
|
|
414
|
-
headers.forEach((value, key) => {
|
|
415
|
-
if (key.toLowerCase() !== 'set-cookie')
|
|
416
|
-
pairs.push([key, value]);
|
|
417
|
-
});
|
|
418
|
-
const getSetCookie = headers.getSetCookie;
|
|
419
|
-
const cookies = typeof getSetCookie === 'function'
|
|
420
|
-
? getSetCookie.call(headers)
|
|
421
|
-
: (headers.get('set-cookie') ? [headers.get('set-cookie')] : []);
|
|
422
|
-
for (const cookie of cookies)
|
|
423
|
-
pairs.push(['set-cookie', cookie]);
|
|
424
|
-
return pairs;
|
|
25
|
+
return createProcessHost(processHostMode(env), ctx, env, disk);
|
|
425
26
|
}
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
*/
|
|
39
39
|
// ── Facet function ──────────────────────────────────────────────────────
|
|
40
40
|
//
|
|
41
|
-
// Runs inside a
|
|
41
|
+
// Runs inside a LoaderPool isolate. Serialised via fn.toString();
|
|
42
42
|
// the helpers it references at top-level scope (streamTarEntries,
|
|
43
43
|
// readableStreamToAsyncIterable, MAX_FILE_BYTES) are NOT in the facet's
|
|
44
44
|
// lexical scope — the pool injects them via the preamble. No static
|
package/dist/npm/installer.d.ts
CHANGED
|
@@ -91,7 +91,7 @@ export declare class NpmInstaller {
|
|
|
91
91
|
/**
|
|
92
92
|
* F-2 frontier-coordinator path. Replaces the single-resolve-facet
|
|
93
93
|
* dispatch with a per-package fanout: each BFS layer becomes ONE
|
|
94
|
-
* `
|
|
94
|
+
* `FanoutPool.submitMany` call, layer N+1 builds from the
|
|
95
95
|
* resolved metadata of layer N.
|
|
96
96
|
*
|
|
97
97
|
* Topology auto-routes per layer:
|
|
@@ -115,12 +115,12 @@ export declare class NpmInstaller {
|
|
|
115
115
|
*/
|
|
116
116
|
private resolveTreeViaFanout;
|
|
117
117
|
/**
|
|
118
|
-
* Batch install via two-tier fan-out (
|
|
118
|
+
* Batch install via two-tier fan-out (FanoutPool).
|
|
119
119
|
*
|
|
120
120
|
* Shard count is `min(specs.length, INSTALL_PEER_CAP)`, and the topology
|
|
121
121
|
* follows from it:
|
|
122
122
|
* shardCount < IN_DO_THRESHOLD (5) → in-DO fanout in-DO
|
|
123
|
-
* 1
|
|
123
|
+
* 1 LoaderPool with concurrency = shardCount, capped at
|
|
124
124
|
* 4 by V8 invariant. Each shard is one facet running its own
|
|
125
125
|
* installPackagesInFacet.
|
|
126
126
|
* shardCount >= IN_DO_THRESHOLD → peer-DO fanout peer-DO
|
|
@@ -132,7 +132,7 @@ export declare class NpmInstaller {
|
|
|
132
132
|
* `shard-${i}` task key deterministically (tests can predict
|
|
133
133
|
* placement).
|
|
134
134
|
*
|
|
135
|
-
* Pre-fix lineage: this site previously ran ONE
|
|
135
|
+
* Pre-fix lineage: this site previously ran ONE LoaderPool
|
|
136
136
|
* with concurrency=1, internal pLimit(3) — the explicit "collapses
|
|
137
137
|
* what was 4 concurrent dynamic workers (pool.map slots) into 1"
|
|
138
138
|
* Two-tier topology re-expands the fan-out without re-introducing
|
package/dist/npm/installer.js
CHANGED
|
@@ -28,8 +28,8 @@ import { npmAddedLine, npmHttpCacheLine, npmHttpFetchLine, npmTitleLine, } from
|
|
|
28
28
|
import { applySwaps, findRejects, lookupSwap, lookupReject, shouldSkipPackage, shouldWarnSkipTransitive, isOptionalNativeBinding, formatSwapNotice, RegistryRejectError, emitRegistryEvent, } from '../facets/wasm-swap-registry.js';
|
|
29
29
|
import { resolvePackageEntry } from '@nimbus-sh/core/_shared/exports-resolver.js';
|
|
30
30
|
import { encodeWriteBatchStream } from '@nimbus-sh/core/_shared/w7-frame.js';
|
|
31
|
-
import {
|
|
32
|
-
import {
|
|
31
|
+
import { LoaderPool } from '@nimbus-sh/fabric/loader-pool.js';
|
|
32
|
+
import { FanoutPool, IN_DO_THRESHOLD } from '@nimbus-sh/fabric/fanout-pool.js';
|
|
33
33
|
import { TAR_STREAM_PREAMBLE, W7_FRAME_PREAMBLE } from '../loaders/generated-workers.js';
|
|
34
34
|
import { installPackagesInFacet, } from './install-batch-facet.js';
|
|
35
35
|
import { setInstallPhase, recordInstallFacetCounters, recordPreBundleSummary, recordR2RaceCounters, recordCacheStatEvents, readDiagCounters, } from '@nimbus-sh/core/observability/diag-counters.js';
|
|
@@ -146,7 +146,7 @@ export class NpmInstaller {
|
|
|
146
146
|
phases['lock-check'] = Date.now() - phaseStart;
|
|
147
147
|
// ── Phase 1: Resolve ──────────────────────────────────────────
|
|
148
148
|
// Frontier-coordinator path. Each BFS layer dispatches to
|
|
149
|
-
//
|
|
149
|
+
// FanoutPool.submitMany — width <5 in-DO (in-DO fanout),
|
|
150
150
|
// width ≥5 peer-DO (peer-DO fanout). Per-package task body is
|
|
151
151
|
// self-contained (resolveOnePackumentInFacet), supervisor builds
|
|
152
152
|
// layer N+1 from layer N's edges. Missing env.LOADER throws at
|
|
@@ -222,7 +222,7 @@ export class NpmInstaller {
|
|
|
222
222
|
setInstallPhase('fetch');
|
|
223
223
|
// Fetch + extract + write new packages.
|
|
224
224
|
//
|
|
225
|
-
// Single fetch path: one
|
|
225
|
+
// Single fetch path: one LoaderPool isolate (the batch facet)
|
|
226
226
|
// runs the entire install. The facet streams tarballs through gunzip+tar
|
|
227
227
|
// and coalesces package-owned paths into shared writeBatchStream waves;
|
|
228
228
|
// every owner awaits each wave it contributed to.
|
|
@@ -419,7 +419,7 @@ export class NpmInstaller {
|
|
|
419
419
|
/**
|
|
420
420
|
* F-2 frontier-coordinator path. Replaces the single-resolve-facet
|
|
421
421
|
* dispatch with a per-package fanout: each BFS layer becomes ONE
|
|
422
|
-
* `
|
|
422
|
+
* `FanoutPool.submitMany` call, layer N+1 builds from the
|
|
423
423
|
* resolved metadata of layer N.
|
|
424
424
|
*
|
|
425
425
|
* Topology auto-routes per layer:
|
|
@@ -486,7 +486,7 @@ export class NpmInstaller {
|
|
|
486
486
|
const RESOLVE_PEER_CAP = 8;
|
|
487
487
|
// F-2 fanout pool. One construction reused across every layer; the
|
|
488
488
|
// pool is stateless across submitMany calls.
|
|
489
|
-
const fanoutPool = new
|
|
489
|
+
const fanoutPool = new FanoutPool(this.env, this.ctx, {
|
|
490
490
|
tag: 'npm-resolve-fanout',
|
|
491
491
|
// 5 minutes per layer is generous; typical layers complete in
|
|
492
492
|
// 1-3 s. Per-task this gates each packument fetch + R2 race.
|
|
@@ -561,8 +561,8 @@ export class NpmInstaller {
|
|
|
561
561
|
};
|
|
562
562
|
return { key: name, args: taskSpec };
|
|
563
563
|
});
|
|
564
|
-
// Dispatch the layer.
|
|
565
|
-
// <5 → in-DO fanout in-DO (
|
|
564
|
+
// Dispatch the layer. FanoutPool routes:
|
|
565
|
+
// <5 → in-DO fanout in-DO (LoaderPool), concurrency = layer.length (capped at 4)
|
|
566
566
|
// ≥5 → peer-DO fanout peer-DO, N peers = min(layer.length, 32)
|
|
567
567
|
let results;
|
|
568
568
|
const layerT0 = Date.now();
|
|
@@ -769,12 +769,12 @@ export class NpmInstaller {
|
|
|
769
769
|
return { resolved, unresolved };
|
|
770
770
|
}
|
|
771
771
|
/**
|
|
772
|
-
* Batch install via two-tier fan-out (
|
|
772
|
+
* Batch install via two-tier fan-out (FanoutPool).
|
|
773
773
|
*
|
|
774
774
|
* Shard count is `min(specs.length, INSTALL_PEER_CAP)`, and the topology
|
|
775
775
|
* follows from it:
|
|
776
776
|
* shardCount < IN_DO_THRESHOLD (5) → in-DO fanout in-DO
|
|
777
|
-
* 1
|
|
777
|
+
* 1 LoaderPool with concurrency = shardCount, capped at
|
|
778
778
|
* 4 by V8 invariant. Each shard is one facet running its own
|
|
779
779
|
* installPackagesInFacet.
|
|
780
780
|
* shardCount >= IN_DO_THRESHOLD → peer-DO fanout peer-DO
|
|
@@ -786,7 +786,7 @@ export class NpmInstaller {
|
|
|
786
786
|
* `shard-${i}` task key deterministically (tests can predict
|
|
787
787
|
* placement).
|
|
788
788
|
*
|
|
789
|
-
* Pre-fix lineage: this site previously ran ONE
|
|
789
|
+
* Pre-fix lineage: this site previously ran ONE LoaderPool
|
|
790
790
|
* with concurrency=1, internal pLimit(3) — the explicit "collapses
|
|
791
791
|
* what was 4 concurrent dynamic workers (pool.map slots) into 1"
|
|
792
792
|
* Two-tier topology re-expands the fan-out without re-introducing
|
|
@@ -851,7 +851,7 @@ export class NpmInstaller {
|
|
|
851
851
|
// Peer shards dispatch in bounded phases and each phase is a barrier, so
|
|
852
852
|
// the phase profile is what distinguishes shard work from barrier count.
|
|
853
853
|
const phaseProfile = [];
|
|
854
|
-
const fanoutPool = new
|
|
854
|
+
const fanoutPool = new FanoutPool(this.env, this.ctx, {
|
|
855
855
|
tag: 'npm-install-batch',
|
|
856
856
|
// Whole-batch timeout. With per-shard parallelism of N=8 peer
|
|
857
857
|
// DOs each running pLimit(3), Mossaic-class 456 packages
|
|
@@ -869,7 +869,7 @@ export class NpmInstaller {
|
|
|
869
869
|
});
|
|
870
870
|
const tasks = nonEmptyShards.map((shardSpecs, shardIdx) => ({
|
|
871
871
|
// Stable-id router key. Same shardIdx → same peer DO across
|
|
872
|
-
// runs. Tests can predict placement via
|
|
872
|
+
// runs. Tests can predict placement via FanoutPool's
|
|
873
873
|
// `peerSiblingId(key, peerCount)` helper.
|
|
874
874
|
key: `shard-${shardIdx}`,
|
|
875
875
|
args: { packages: shardSpecs, concurrency: 3 },
|
|
@@ -974,7 +974,7 @@ export class NpmInstaller {
|
|
|
974
974
|
}
|
|
975
975
|
catch (e) {
|
|
976
976
|
// Final catch — preserved from the pre-fix shape so the error
|
|
977
|
-
// log line shape stays consistent.
|
|
977
|
+
// log line shape stays consistent. FanoutPool's internal
|
|
978
978
|
// pools dispose themselves at the end of each submitMany call,
|
|
979
979
|
// so no explicit dispose() is needed here.
|
|
980
980
|
// The earlier inner-catch already logged + threw; if we reach
|
|
@@ -1304,7 +1304,7 @@ export class NpmInstaller {
|
|
|
1304
1304
|
* JSX elements get rejected as "alien" — silent render failure.
|
|
1305
1305
|
*/
|
|
1306
1306
|
async prebundleUsedModules(projDir, installed) {
|
|
1307
|
-
// Pre-bundle now runs in
|
|
1307
|
+
// Pre-bundle now runs in LoaderPool isolates (src/pre-bundle-facet.ts);
|
|
1308
1308
|
// each facet ships its own bundled esbuild-wasm via the preamble. The
|
|
1309
1309
|
// supervisor's EsbuildService is no longer on the bundle path — it
|
|
1310
1310
|
// still serves the transform path (TS/JSX → JS) which is small and
|
|
@@ -1515,7 +1515,7 @@ export class NpmInstaller {
|
|
|
1515
1515
|
// (where it should live). No supervisor-side caching — see
|
|
1516
1516
|
// src/esbuild-wasm-bytes.ts for the full architectural rationale.
|
|
1517
1517
|
//
|
|
1518
|
-
// Bytes are shipped into each facet via
|
|
1518
|
+
// Bytes are shipped into each facet via LoaderPool's
|
|
1519
1519
|
// `wasmModules` option which workerd registers in the LOADER
|
|
1520
1520
|
// `modules` map as `{ wasm: ArrayBuffer }`. Workerd compiles at
|
|
1521
1521
|
// module-load (startup phase, where wasm code generation is
|
|
@@ -1562,12 +1562,12 @@ export class NpmInstaller {
|
|
|
1562
1562
|
if (wasmBytes.byteLength > maxRetainedWasmBytes) {
|
|
1563
1563
|
throw new RangeError(`pre-bundle wasm payload ${wasmBytes.byteLength} exceeds the ${maxRetainedWasmBytes}-byte retained budget`);
|
|
1564
1564
|
}
|
|
1565
|
-
//
|
|
1565
|
+
// LoaderPool keeps the constructor-time module bytes until
|
|
1566
1566
|
// dispose(), so retain their exact credit rather than treating
|
|
1567
1567
|
// construction as a handoff that immediately frees the ArrayBuffer.
|
|
1568
1568
|
setupAllocation.shrinkTo(wasmBytes.byteLength);
|
|
1569
1569
|
try {
|
|
1570
|
-
pool = new
|
|
1570
|
+
pool = new LoaderPool(this.env, this.ctx, {
|
|
1571
1571
|
concurrency: PRE_BUNDLE_CONCURRENCY,
|
|
1572
1572
|
timeoutMs: 60_000,
|
|
1573
1573
|
retries: 0,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* pre-bundle-facet.ts —
|
|
2
|
+
* pre-bundle-facet.ts — LoaderPool entry for esbuild pre-bundling.
|
|
3
3
|
*
|
|
4
4
|
* Why this exists
|
|
5
5
|
* ───────────────
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* the previous isolate.
|
|
15
15
|
*
|
|
16
16
|
* The fix is to dispatch each per-specifier `esbuild.build` to a
|
|
17
|
-
*
|
|
17
|
+
* LoaderPool isolate. Each facet has its own 128 MB budget and
|
|
18
18
|
* stable-slot reuse keeps the warm-up cost amortized across the 8
|
|
19
19
|
* concurrent specs of a typical install. The install-batch facet uses
|
|
20
20
|
* the same isolate boundary for tarball extraction.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* pre-bundle-facet.ts —
|
|
2
|
+
* pre-bundle-facet.ts — LoaderPool entry for esbuild pre-bundling.
|
|
3
3
|
*
|
|
4
4
|
* Why this exists
|
|
5
5
|
* ───────────────
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* the previous isolate.
|
|
15
15
|
*
|
|
16
16
|
* The fix is to dispatch each per-specifier `esbuild.build` to a
|
|
17
|
-
*
|
|
17
|
+
* LoaderPool isolate. Each facet has its own 128 MB budget and
|
|
18
18
|
* stable-slot reuse keeps the warm-up cost amortized across the 8
|
|
19
19
|
* concurrent specs of a typical install. The install-batch facet uses
|
|
20
20
|
* the same isolate boundary for tarball extraction.
|
|
@@ -181,7 +181,7 @@ export function externalsForSpecifier(specifier) {
|
|
|
181
181
|
}
|
|
182
182
|
// ── Facet function ──────────────────────────────────────────────────────
|
|
183
183
|
//
|
|
184
|
-
// `prebundleOne` runs inside a
|
|
184
|
+
// `prebundleOne` runs inside a LoaderPool isolate. cloudflare-parallel
|
|
185
185
|
// serialises it via fn.toString(); the helpers it references at module
|
|
186
186
|
// scope (ESBUILD_WASM_JS_FN_BODY, resolvePackageEntry) are NOT in the
|
|
187
187
|
// facet's lexical scope at runtime. Instead they're injected by the
|
|
@@ -250,7 +250,7 @@ export const prebundleOne = async function prebundleOne(spec, _env) {
|
|
|
250
250
|
// runs that at module-load time (where eval is permitted) and
|
|
251
251
|
// stashes the result on the module-scope const __NIMBUS_ESBUILD_NS.
|
|
252
252
|
//
|
|
253
|
-
// The WASM module is shipped into the facet via
|
|
253
|
+
// The WASM module is shipped into the facet via LoaderPool's
|
|
254
254
|
// `wasmModules` option, registered in the LOADER's modules map as
|
|
255
255
|
// { wasm: ArrayBuffer }. Workerd compiles it during the worker's
|
|
256
256
|
// module-load phase (eval permitted there), and the pool's
|
|
@@ -285,7 +285,7 @@ export const prebundleOne = async function prebundleOne(spec, _env) {
|
|
|
285
285
|
if (!initPromise) {
|
|
286
286
|
initPromise = (async () => {
|
|
287
287
|
// Read the WebAssembly.Module the pool registered. The key matches
|
|
288
|
-
// the name passed to
|
|
288
|
+
// the name passed to LoaderPool's `wasmModules` option (see
|
|
289
289
|
// src/npm-installer.ts:prebundleUsedModules dispatch site).
|
|
290
290
|
const wasmRegistry = globalThis.__NIMBUS_WASM;
|
|
291
291
|
const wasmModule = wasmRegistry && wasmRegistry['esbuild.wasm'];
|