@nimbus-sh/fabric 0.7.0 → 0.8.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 +19 -1
- package/dist/bindings.d.ts +11 -0
- package/dist/bindings.d.ts.map +1 -1
- package/dist/bindings.js +19 -8
- package/dist/connections.d.ts +1 -1
- package/dist/connections.js +1 -1
- package/dist/do-calls.d.ts +71 -9
- package/dist/do-calls.d.ts.map +1 -1
- package/dist/do-calls.js +148 -22
- package/dist/facet-pool.d.ts +3 -0
- package/dist/facet-pool.d.ts.map +1 -1
- package/dist/facet-pool.js +3 -0
- package/dist/fanout.d.ts.map +1 -1
- package/dist/fanout.js +6 -3
- package/dist/fenced-work.d.ts +3 -3
- package/dist/host-dispatch.d.ts +12 -4
- package/dist/host-dispatch.d.ts.map +1 -1
- package/dist/host-dispatch.js +11 -4
- package/dist/image-store.js +2 -2
- package/dist/inner-do-registry.d.ts +9 -0
- package/dist/inner-do-registry.d.ts.map +1 -1
- package/dist/inner-do-registry.js +35 -0
- package/dist/isolate-pool.d.ts +8 -1
- package/dist/isolate-pool.d.ts.map +1 -1
- package/dist/isolate-pool.js +9 -7
- package/dist/process-fabric.d.ts +16 -5
- package/dist/process-fabric.d.ts.map +1 -1
- package/dist/process-fabric.js +18 -6
- package/dist/process-host.d.ts +5 -0
- package/dist/process-host.d.ts.map +1 -1
- package/dist/process-host.js +13 -11
- package/dist/supervisor-props.d.ts +47 -0
- package/dist/supervisor-props.d.ts.map +1 -0
- package/dist/supervisor-props.js +37 -0
- package/dist/workerd-facet-host.d.ts +4 -17
- package/dist/workerd-facet-host.d.ts.map +1 -1
- package/dist/workerd-facet-host.js +98 -32
- package/package.json +6 -6
- package/src/bindings.ts +25 -10
- package/src/connections.ts +1 -1
- package/src/do-calls.ts +182 -25
- package/src/facet-pool.ts +5 -0
- package/src/fanout.ts +6 -3
- package/src/fenced-work.ts +3 -3
- package/src/host-dispatch.ts +22 -7
- package/src/image-store.ts +2 -2
- package/src/inner-do-registry.ts +31 -0
- package/src/isolate-pool.ts +16 -8
- package/src/process-fabric.ts +33 -10
- package/src/process-host.ts +18 -11
- package/src/supervisor-props.ts +56 -0
- package/src/workerd-facet-host.ts +99 -37
package/src/facet-pool.ts
CHANGED
|
@@ -42,6 +42,8 @@
|
|
|
42
42
|
* and the facet-id ledger here counts only the names this pool minted.
|
|
43
43
|
*/
|
|
44
44
|
|
|
45
|
+
import { forgetFacetStorage } from '@nimbus-sh/core/runtime/storage-ledger.js';
|
|
46
|
+
import type { SqlDatabase } from '@nimbus-sh/core/runtime/os-contracts.js';
|
|
45
47
|
import {
|
|
46
48
|
FACET_ID_LIFETIME_BUDGET,
|
|
47
49
|
facetNameCount,
|
|
@@ -63,6 +65,8 @@ export interface FacetPoolContext {
|
|
|
63
65
|
storage: {
|
|
64
66
|
get(key: string): Promise<unknown> | unknown;
|
|
65
67
|
put(key: string, value: unknown): Promise<void>;
|
|
68
|
+
/** The session's SQL, where the storage ledger (N18) records facet databases. */
|
|
69
|
+
sql?: SqlDatabase;
|
|
66
70
|
};
|
|
67
71
|
}
|
|
68
72
|
|
|
@@ -134,6 +138,7 @@ export class FacetPool {
|
|
|
134
138
|
if (keepStorage) return;
|
|
135
139
|
try {
|
|
136
140
|
facets.delete(name);
|
|
141
|
+
if (this.ctx.storage.sql) forgetFacetStorage(this.ctx.storage.sql, name);
|
|
137
142
|
} catch (e) {
|
|
138
143
|
throw new Error(
|
|
139
144
|
`fabric: facet '${name}' was evicted but its storage was not reclaimed — `
|
package/src/fanout.ts
CHANGED
|
@@ -17,8 +17,9 @@
|
|
|
17
17
|
import { serializeFunction } from './vendor/serialize.js';
|
|
18
18
|
import { BindingError } from './vendor/errors.js';
|
|
19
19
|
import { IsolatePool, type FacetTaskFn } from './isolate-pool.js';
|
|
20
|
+
import { hostRoute } from './composition.js';
|
|
20
21
|
import { disposeRpcResource } from '@nimbus-sh/platform/rpc-dispose.js';
|
|
21
|
-
import {
|
|
22
|
+
import { classifyDoCall, describeError, isRetryableDoCall } from '@nimbus-sh/platform/oom-classify.js';
|
|
22
23
|
import type { WorkerLoader } from './vendor/types.js';
|
|
23
24
|
import {
|
|
24
25
|
hostNamespaceBinding,
|
|
@@ -383,6 +384,7 @@ export class Fanout {
|
|
|
383
384
|
// ... write into the peer's own VFS — invisible to the
|
|
384
385
|
// user. See INSTALL-HONESTY-retro.md.
|
|
385
386
|
coordinatorDoId: this.coordDoId,
|
|
387
|
+
coordinatorRoute: hostRoute() ?? undefined,
|
|
386
388
|
// Credential source for peer-side writeBatchStream — the
|
|
387
389
|
// invoking process pid, so package writes are authorized
|
|
388
390
|
// as the user (not rejected as pid:0).
|
|
@@ -412,8 +414,9 @@ export class Fanout {
|
|
|
412
414
|
}
|
|
413
415
|
return;
|
|
414
416
|
} catch (err) {
|
|
415
|
-
const
|
|
416
|
-
|
|
417
|
+
const cls = classifyDoCall(err);
|
|
418
|
+
const schedule = cls === 'overloaded' ? PEER_OVERLOAD_BACKOFF_MS
|
|
419
|
+
: isRetryableDoCall(cls) ? PEER_RETRY_BACKOFF_MS
|
|
417
420
|
: null;
|
|
418
421
|
if (schedule && attempt < PEER_TRANSIENT_RESET_RETRIES) {
|
|
419
422
|
const backoff = schedule[Math.min(attempt, schedule.length - 1)];
|
package/src/fenced-work.ts
CHANGED
|
@@ -56,9 +56,9 @@ export const FENCED_WORK_MAX_ATTEMPT = 1;
|
|
|
56
56
|
* (staging, 2026-08-13): every observed reset struck seconds AFTER the launch
|
|
57
57
|
* settled — the platform kills the object while the resident runs, which is
|
|
58
58
|
* when a launch-scoped row had already been deleted and recovery had nothing
|
|
59
|
-
* to find. A resident's facet
|
|
60
|
-
*
|
|
61
|
-
*
|
|
59
|
+
* to find. A resident's facet can outlive its session instance (a pending
|
|
60
|
+
* timer keeps it running), but nothing routes to it from the next one, and a
|
|
61
|
+
* spawn that takes its facet name ends it first.
|
|
62
62
|
*/
|
|
63
63
|
export interface FencedWorkRecord {
|
|
64
64
|
pid: number;
|
package/src/host-dispatch.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { BindingError } from './vendor/errors.js';
|
|
2
|
-
import { hostDispatchMethod, hostNamespace } from './composition.js';
|
|
3
|
-
import type {
|
|
2
|
+
import { hostDispatchMethod, hostNamespace, type HostRoute } from './composition.js';
|
|
3
|
+
import type { SupervisorOpDispatch } from '@nimbus-sh/core/workspace/supervisor-op.js';
|
|
4
4
|
|
|
5
5
|
export type HostNamespaceBinding = Pick<DurableObjectNamespace, 'get' | 'idFromName' | 'idFromString'>;
|
|
6
|
-
export type HostOpDispatch =
|
|
6
|
+
export type HostOpDispatch = SupervisorOpDispatch;
|
|
7
7
|
|
|
8
8
|
function isNamespace(value: object): value is HostNamespaceBinding {
|
|
9
9
|
return typeof Reflect.get(value, 'idFromName') === 'function'
|
|
@@ -11,8 +11,19 @@ function isNamespace(value: object): value is HostNamespaceBinding {
|
|
|
11
11
|
&& typeof Reflect.get(value, 'get') === 'function';
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
/**
|
|
15
|
+
* The host's namespace binding. An entrypoint answering a facet passes the
|
|
16
|
+
* route the binding's props carry, minted in the host's isolate; the host
|
|
17
|
+
* itself, and a binding minted before routes travelled (a facet outlives
|
|
18
|
+
* the deploy that minted its binding), resolve from this isolate's
|
|
19
|
+
* composition.
|
|
20
|
+
*/
|
|
21
|
+
export function hostNamespaceBinding(
|
|
22
|
+
env: object | null | undefined,
|
|
23
|
+
usage: string,
|
|
24
|
+
route?: Pick<HostRoute, 'hostNamespace'>,
|
|
25
|
+
): HostNamespaceBinding {
|
|
26
|
+
const name = route?.hostNamespace ?? hostNamespace();
|
|
16
27
|
const binding = env ? Reflect.get(env, name) : undefined;
|
|
17
28
|
if (binding === null || (typeof binding !== 'object' && typeof binding !== 'function') || !isNamespace(binding)) {
|
|
18
29
|
throw new BindingError(`${usage}: env.${name} must be the Durable Object namespace configured by composeFabric`);
|
|
@@ -20,8 +31,12 @@ export function hostNamespaceBinding(env: object | null | undefined, usage: stri
|
|
|
20
31
|
return binding;
|
|
21
32
|
}
|
|
22
33
|
|
|
23
|
-
export function hostOpDispatch(
|
|
24
|
-
|
|
34
|
+
export function hostOpDispatch(
|
|
35
|
+
stub: object,
|
|
36
|
+
usage: string,
|
|
37
|
+
route?: Pick<HostRoute, 'hostDispatchMethod'>,
|
|
38
|
+
): HostOpDispatch {
|
|
39
|
+
const name = route?.hostDispatchMethod ?? hostDispatchMethod();
|
|
25
40
|
if (stub === null || (typeof stub !== 'object' && typeof stub !== 'function')) {
|
|
26
41
|
throw new BindingError(`${usage}: the workspace namespace returned no stub`);
|
|
27
42
|
}
|
package/src/image-store.ts
CHANGED
|
@@ -138,12 +138,12 @@ export class ImageStore {
|
|
|
138
138
|
fs.mkdirp(FACET_IMAGE_DIR);
|
|
139
139
|
let count = 0;
|
|
140
140
|
for await (const [moduleName, source] of images) {
|
|
141
|
-
const
|
|
141
|
+
const bytes = new TextEncoder().encode(source);
|
|
142
|
+
const path = facetImagePath(await facetImageDigest(bytes));
|
|
142
143
|
paths[moduleName] = path;
|
|
143
144
|
rooted.push(path);
|
|
144
145
|
count++;
|
|
145
146
|
const stored = path.replace(/^\/+/, '');
|
|
146
|
-
const bytes = new TextEncoder().encode(source);
|
|
147
147
|
console.log(
|
|
148
148
|
'[image-store] pid=' + pid + ' image ' + count + ' ' + moduleName + ' → '
|
|
149
149
|
+ path.slice(-12) + ' ' + bytes.byteLength + ' bytes, slice=' + FACET_IMAGE_WRITE_SLICE_BYTES
|
package/src/inner-do-registry.ts
CHANGED
|
@@ -56,3 +56,34 @@ export function clearInnerDoClasses(supervisorDoId: string): void {
|
|
|
56
56
|
if (k.startsWith(prefix)) _NIMBUS_INNER_DO_CLASSES.delete(k);
|
|
57
57
|
}
|
|
58
58
|
}
|
|
59
|
+
|
|
60
|
+
/** Inner-DO facet names each incarnation has opened, by binding; keyed off ctx, so a new incarnation starts empty. */
|
|
61
|
+
const openedInnerDoFacets = new WeakMap<DurableObjectState, Map<string, Set<string>>>();
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Record that this incarnation opens `facetName` for `bindingName`. True on the
|
|
65
|
+
* name's first open since the incarnation began or the binding's facets were
|
|
66
|
+
* last aborted: whatever still runs under it has a class this build no longer
|
|
67
|
+
* uses, and a get() with the new class on it resets the whole object.
|
|
68
|
+
*/
|
|
69
|
+
export function noteInnerDoFacetOpened(ctx: DurableObjectState, bindingName: string, facetName: string): boolean {
|
|
70
|
+
let byBinding = openedInnerDoFacets.get(ctx);
|
|
71
|
+
if (!byBinding) openedInnerDoFacets.set(ctx, byBinding = new Map());
|
|
72
|
+
let names = byBinding.get(bindingName);
|
|
73
|
+
if (!names) byBinding.set(bindingName, names = new Set());
|
|
74
|
+
if (names.has(facetName)) return false;
|
|
75
|
+
names.add(facetName);
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Abort the facets this incarnation opened for `bindingNames`, keeping their storage, so each next request starts the class registered then. */
|
|
80
|
+
export function abortInnerDoFacets(ctx: DurableObjectState, bindingNames: Iterable<string>, reason: Error): void {
|
|
81
|
+
const byBinding = openedInnerDoFacets.get(ctx);
|
|
82
|
+
if (!byBinding) return;
|
|
83
|
+
for (const bindingName of bindingNames) {
|
|
84
|
+
for (const facetName of byBinding.get(bindingName) ?? []) {
|
|
85
|
+
try { ctx.facets.abort(facetName, reason); } catch { /* already gone */ }
|
|
86
|
+
}
|
|
87
|
+
byBinding.delete(bindingName);
|
|
88
|
+
}
|
|
89
|
+
}
|
package/src/isolate-pool.ts
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* 3. **Supervisor autoinjection**. The pool grabs the embedder's
|
|
16
16
|
* registered supervisor entrypoint stub (see `supervisorEntrypoint` in
|
|
17
17
|
* composition.ts) and forwards it as `env.SUPERVISOR` to every facet,
|
|
18
|
-
* same pattern as git
|
|
18
|
+
* same pattern as git/network-facet.ts. Callers can add more bindings
|
|
19
19
|
* via `extraBindings`.
|
|
20
20
|
* 4. **Fail-loud defaults**: timeout 60s, retries 0, onError 'throw'.
|
|
21
21
|
* Caller opts in to leniency.
|
|
@@ -25,7 +25,8 @@
|
|
|
25
25
|
*/
|
|
26
26
|
|
|
27
27
|
import { CF_COMPAT_DATE } from '@nimbus-sh/core/constants.js';
|
|
28
|
-
import { supervisorEntrypoint } from './composition.js';
|
|
28
|
+
import { supervisorEntrypoint, type HostRoute } from './composition.js';
|
|
29
|
+
import { supervisorBindingProps, supervisorLoaderKey } from './supervisor-props.js';
|
|
29
30
|
import { disposeRpcResource } from '@nimbus-sh/platform/rpc-dispose.js';
|
|
30
31
|
import { serializeFunction, hashSource } from './vendor/serialize.js';
|
|
31
32
|
import { beginLoaderFetch, recordLoaderId, withDynamicWorkerCapNamed } from './budgets.js';
|
|
@@ -120,6 +121,12 @@ export interface IsolatePoolOptions {
|
|
|
120
121
|
* coordinator. Effective only when `omitSupervisor !== true`.
|
|
121
122
|
*/
|
|
122
123
|
supervisorDoIdOverride?: string;
|
|
124
|
+
/**
|
|
125
|
+
* The coordinator's route, forwarded with `supervisorDoIdOverride`: the
|
|
126
|
+
* binding is the coordinator's, so its way back is the coordinator's
|
|
127
|
+
* too, whatever this peer's isolate composed.
|
|
128
|
+
*/
|
|
129
|
+
supervisorRoute?: HostRoute;
|
|
123
130
|
/**
|
|
124
131
|
* Process pid baked into the auto-injected SUPERVISOR binding's props.
|
|
125
132
|
* The supervisor derives the write credential from this pid
|
|
@@ -521,11 +528,11 @@ export class IsolatePool {
|
|
|
521
528
|
// via supervisorDoIdOverride so SUPERVISOR.* RPCs route back
|
|
522
529
|
// to the user's session DO, not the peer DO. Default to the
|
|
523
530
|
// local ctx.id (single-DO callers and the in-DO in-DO fanout path).
|
|
524
|
-
const
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
props: { doId: supDoId, pid: supPid },
|
|
531
|
+
const supervisor = supervisorBindingProps(ctx, opts?.supervisorPid ?? 0, {
|
|
532
|
+
doId: opts?.supervisorDoIdOverride,
|
|
533
|
+
route: opts?.supervisorRoute,
|
|
528
534
|
});
|
|
535
|
+
bindings.SUPERVISOR = supervisorRpc({ props: supervisor });
|
|
529
536
|
// Whatever the minted worker's env carries must be in its loader
|
|
530
537
|
// cache key — workerd's loader cache survives a DO hibernation
|
|
531
538
|
// wake while generation-strided pids (1000001 → 2000001) do not:
|
|
@@ -533,8 +540,9 @@ export class IsolatePool {
|
|
|
533
540
|
// the new generation still credentialed to the dead pid, and
|
|
534
541
|
// every pid-authorized RPC from it fails "process pid … does
|
|
535
542
|
// not exist". doIdShort alone cannot cover this — it changes
|
|
536
|
-
// across sessions, not across wakes of the same session.
|
|
537
|
-
|
|
543
|
+
// across sessions, not across wakes of the same session. So does
|
|
544
|
+
// the instance a binding delivers mutations to, when it names one.
|
|
545
|
+
this.supervisorKey = supervisorLoaderKey(`s${supervisor.doId.slice(0, 12)}-${supervisor.pid}`, supervisor);
|
|
538
546
|
} else {
|
|
539
547
|
// Supervisor entrypoint unavailable — running without ctx.exports
|
|
540
548
|
// (e.g. unit-test harness, or LOADER.load contexts where the
|
package/src/process-fabric.ts
CHANGED
|
@@ -74,6 +74,7 @@
|
|
|
74
74
|
* `ResidentDiskReader` it was given.
|
|
75
75
|
*/
|
|
76
76
|
|
|
77
|
+
import type { SupervisorBindingProps } from './supervisor-props.js';
|
|
77
78
|
import { z } from 'zod/v4';
|
|
78
79
|
import type { RouteableFacetTarget } from '@nimbus-sh/core/runtime/os-contracts.js';
|
|
79
80
|
import type { ServiceStub } from './vendor/types.js';
|
|
@@ -209,8 +210,10 @@ export const FACET_IMAGE_DIR = 'var/lib/nimbus/facet-images';
|
|
|
209
210
|
* generated text into `startArgs` would make every image per-PROGRAM and
|
|
210
211
|
* shareable across spawns and sessions; the sweep bounds the store either way.
|
|
211
212
|
*/
|
|
212
|
-
export async function facetImageDigest(
|
|
213
|
-
|
|
213
|
+
export async function facetImageDigest(image: string | Uint8Array): Promise<string> {
|
|
214
|
+
// An image is its UTF-8 bytes; a caller holding them is not made to encode a second copy.
|
|
215
|
+
const bytes = typeof image === 'string' ? new TextEncoder().encode(image) : image;
|
|
216
|
+
const digest = await crypto.subtle.digest('SHA-256', bytes);
|
|
214
217
|
return Array.from(new Uint8Array(digest), (b) => b.toString(16).padStart(2, '0')).join('');
|
|
215
218
|
}
|
|
216
219
|
|
|
@@ -265,8 +268,16 @@ export async function residentLoaderConfig(
|
|
|
265
268
|
const resolved: Record<string, string | { wasm: ArrayBuffer }> = {};
|
|
266
269
|
for (const [moduleName, path] of Object.entries(spec.vfsWasmModules ?? {})) {
|
|
267
270
|
const bytes = await disk.readFile(path);
|
|
271
|
+
// The read's own buffer when it fits exactly, and only otherwise a copy.
|
|
272
|
+
// These are the largest members a spec carries — ruby's interpreter image
|
|
273
|
+
// is 34.3 MiB, esbuild's 13.3 — and an unconditional slice held both
|
|
274
|
+
// copies at once in the coordinator's 128 MiB isolate, at the one moment
|
|
275
|
+
// the module map is also resident.
|
|
276
|
+
const exact = bytes.byteOffset === 0 && bytes.byteLength === bytes.buffer.byteLength;
|
|
268
277
|
resolved[moduleName] = {
|
|
269
|
-
wasm:
|
|
278
|
+
wasm: exact
|
|
279
|
+
? bytes.buffer as ArrayBuffer
|
|
280
|
+
: bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength) as ArrayBuffer,
|
|
270
281
|
};
|
|
271
282
|
}
|
|
272
283
|
for (const [moduleName, path] of Object.entries(spec.vfsTextModules ?? {})) {
|
|
@@ -294,15 +305,16 @@ async function readFacetImage(disk: ResidentDiskReader, path: string): Promise<s
|
|
|
294
305
|
if (!expected) {
|
|
295
306
|
throw new Error(`Nimbus: '${path}' is not a content-addressed facet image path`);
|
|
296
307
|
}
|
|
297
|
-
|
|
298
|
-
const
|
|
308
|
+
// Verified from the bytes read, and decoded only after: re-encoding the decoded string held a third copy of the largest member.
|
|
309
|
+
const bytes = await disk.readFile(path);
|
|
310
|
+
const actual = await facetImageDigest(bytes);
|
|
299
311
|
if (actual !== expected) {
|
|
300
312
|
throw new Error(
|
|
301
313
|
`Nimbus: facet image '${path}' does not match its digest (read ${actual}); `
|
|
302
314
|
+ 'the image store is corrupt and the process cannot boot from it',
|
|
303
315
|
);
|
|
304
316
|
}
|
|
305
|
-
return
|
|
317
|
+
return new TextDecoder().decode(bytes);
|
|
306
318
|
}
|
|
307
319
|
|
|
308
320
|
// ── The hosting substrate ───────────────────────────────────────────────────
|
|
@@ -310,11 +322,12 @@ async function readFacetImage(disk: ResidentDiskReader, path: string): Promise<s
|
|
|
310
322
|
/**
|
|
311
323
|
* The identity a resident process's SUPERVISOR binding is minted for. Always
|
|
312
324
|
* the COORDINATOR's — a process hosted somewhere else still reads and writes
|
|
313
|
-
* the user's disk, and still reports to the user's process table.
|
|
325
|
+
* the user's disk, and still reports to the user's process table. Minted by
|
|
326
|
+
* `supervisorBindingProps` on the coordinator; `route` is absent only when
|
|
327
|
+
* the coordinator's isolate composed nothing, in which case no supervisor
|
|
328
|
+
* binding is minted either.
|
|
314
329
|
*/
|
|
315
|
-
export interface ResidentSupervisorProps {
|
|
316
|
-
doId: string;
|
|
317
|
-
pid: number;
|
|
330
|
+
export interface ResidentSupervisorProps extends SupervisorBindingProps {
|
|
318
331
|
writerId: string;
|
|
319
332
|
}
|
|
320
333
|
|
|
@@ -338,6 +351,13 @@ export interface ProcessHostParams {
|
|
|
338
351
|
* the store on release.
|
|
339
352
|
*/
|
|
340
353
|
facet?: { name: string; durable: boolean };
|
|
354
|
+
/**
|
|
355
|
+
* Bytes the process's store is filled with before it runs (its data plan).
|
|
356
|
+
* The hosting actor's storage ledger (N18) admits them under the facet's
|
|
357
|
+
* name before the facet starts: ENOSPC, and no facet, when they would cross
|
|
358
|
+
* the storage limit.
|
|
359
|
+
*/
|
|
360
|
+
storageBytes?: number;
|
|
341
361
|
}
|
|
342
362
|
|
|
343
363
|
/**
|
|
@@ -650,6 +670,8 @@ export interface ResidentProcessSpawn {
|
|
|
650
670
|
* ephemeral process, which takes a `proc-slot-<n>` name from the book.
|
|
651
671
|
*/
|
|
652
672
|
facet?: { name: string; durable: boolean };
|
|
673
|
+
/** See {@link ProcessHostParams.storageBytes}. */
|
|
674
|
+
storageBytes?: number;
|
|
653
675
|
/**
|
|
654
676
|
* Called before any concrete host capability can expose this writer.
|
|
655
677
|
* A spawn must not proceed unless the supervisor accepts the authority.
|
|
@@ -694,6 +716,7 @@ export class ProcessFabric {
|
|
|
694
716
|
writerId,
|
|
695
717
|
startArgs: spawn.startArgs,
|
|
696
718
|
...(spawn.facet !== undefined ? { facet: spawn.facet } : {}),
|
|
719
|
+
...(spawn.storageBytes !== undefined ? { storageBytes: spawn.storageBytes } : {}),
|
|
697
720
|
});
|
|
698
721
|
} catch (error) {
|
|
699
722
|
spawn.onWriterRetired(writerId);
|
package/src/process-host.ts
CHANGED
|
@@ -65,7 +65,7 @@
|
|
|
65
65
|
*/
|
|
66
66
|
|
|
67
67
|
import { disposeRpcResource } from '@nimbus-sh/platform/rpc-dispose.js';
|
|
68
|
-
import {
|
|
68
|
+
import { classifyDoCall, isRetryableDoCall } from '@nimbus-sh/platform/oom-classify.js';
|
|
69
69
|
import { PEER_RETRY_BACKOFF_MS, PEER_TRANSIENT_RESET_RETRIES } from './fanout.js';
|
|
70
70
|
import { hostNamespaceBinding, hostOpDispatch, type HostNamespaceBinding } from './host-dispatch.js';
|
|
71
71
|
import { z } from 'zod/v4';
|
|
@@ -85,6 +85,8 @@ import {
|
|
|
85
85
|
processes,
|
|
86
86
|
type ResidentFacetEnv,
|
|
87
87
|
} from './workerd-facet-host.js';
|
|
88
|
+
import type { HostRoute } from './composition.js';
|
|
89
|
+
import { supervisorBindingProps } from './supervisor-props.js';
|
|
88
90
|
|
|
89
91
|
/** The substrates this deployment can be configured for. */
|
|
90
92
|
export type ProcessHostMode = 'facet' | 'peer';
|
|
@@ -137,18 +139,14 @@ class FacetProcessHost implements ProcessHost {
|
|
|
137
139
|
|
|
138
140
|
runOnce<T>(params: OneShotParams, consume: (response: Response) => Promise<T>): Promise<T> {
|
|
139
141
|
return processes(this.ctx, this.env).run(
|
|
140
|
-
{
|
|
142
|
+
{ ...supervisorBindingProps(this.ctx, params.pid), writerId: params.writerId },
|
|
141
143
|
params,
|
|
142
144
|
consume,
|
|
143
145
|
);
|
|
144
146
|
}
|
|
145
147
|
|
|
146
148
|
async open(params: ProcessHostParams): Promise<HostedProcess> {
|
|
147
|
-
const supervisor: ResidentSupervisorProps = {
|
|
148
|
-
doId: this.coordDoId,
|
|
149
|
-
pid: params.pid,
|
|
150
|
-
writerId: params.writerId,
|
|
151
|
-
};
|
|
149
|
+
const supervisor: ResidentSupervisorProps = { ...supervisorBindingProps(this.ctx, params.pid), writerId: params.writerId };
|
|
152
150
|
const { name, ...facet } = processes(this.ctx, this.env).spawn(this.disk, supervisor, params);
|
|
153
151
|
return {
|
|
154
152
|
...facet,
|
|
@@ -192,8 +190,12 @@ export function isolateToken(): string {
|
|
|
192
190
|
/** Options the coordinator hands a hosting peer. */
|
|
193
191
|
export interface HostProcessOpts {
|
|
194
192
|
coordinatorDoId: string;
|
|
193
|
+
/** The coordinator's route, minted into the process's SUPERVISOR binding. */
|
|
194
|
+
route?: HostRoute;
|
|
195
195
|
pid: number;
|
|
196
196
|
writerId: string;
|
|
197
|
+
/** The coordinator instance's delivery incarnation, minted into the SUPERVISOR binding (ResidentSupervisorProps). */
|
|
198
|
+
hostIncarnation?: string;
|
|
197
199
|
workerKey: string;
|
|
198
200
|
/** Unforgeable capability for the fetch-semantic WebSocket hop. */
|
|
199
201
|
webSocketCapability: string;
|
|
@@ -354,7 +356,7 @@ class PeerProcessHost implements ProcessHost {
|
|
|
354
356
|
*/
|
|
355
357
|
runOnce<T>(params: OneShotParams, consume: (response: Response) => Promise<T>): Promise<T> {
|
|
356
358
|
return processes(this.ctx, this.env).run(
|
|
357
|
-
{
|
|
359
|
+
{ ...supervisorBindingProps(this.ctx, params.pid), writerId: params.writerId },
|
|
358
360
|
params,
|
|
359
361
|
consume,
|
|
360
362
|
);
|
|
@@ -379,10 +381,15 @@ class PeerProcessHost implements ProcessHost {
|
|
|
379
381
|
// also what keeps the hosting DO resident. It settles when a `lifetime`
|
|
380
382
|
// runner exits or when a `boot` runner's host is cancelled, and rejects if
|
|
381
383
|
// the peer dies under either.
|
|
384
|
+
// The peer mints the process's binding from these, for THIS object: the
|
|
385
|
+
// coordinator's doId, route and delivery instance.
|
|
386
|
+
const supervisor = supervisorBindingProps(this.ctx, params.pid);
|
|
382
387
|
const hostLeg = placement.stub._rpcHostProcess(params.boot, {
|
|
383
|
-
coordinatorDoId:
|
|
384
|
-
|
|
388
|
+
coordinatorDoId: supervisor.doId,
|
|
389
|
+
route: supervisor.route,
|
|
390
|
+
pid: supervisor.pid,
|
|
385
391
|
writerId: params.writerId,
|
|
392
|
+
hostIncarnation: supervisor.hostIncarnation,
|
|
386
393
|
workerKey: params.workerKey,
|
|
387
394
|
webSocketCapability,
|
|
388
395
|
startArgs: params.startArgs,
|
|
@@ -491,7 +498,7 @@ class PeerProcessHost implements ProcessHost {
|
|
|
491
498
|
}
|
|
492
499
|
return probe;
|
|
493
500
|
} catch (err) {
|
|
494
|
-
if (attempt < PEER_TRANSIENT_RESET_RETRIES &&
|
|
501
|
+
if (attempt < PEER_TRANSIENT_RESET_RETRIES && isRetryableDoCall(classifyDoCall(err))) {
|
|
495
502
|
await new Promise((r) => setTimeout(
|
|
496
503
|
r, PEER_RETRY_BACKOFF_MS[Math.min(attempt, PEER_RETRY_BACKOFF_MS.length - 1)],
|
|
497
504
|
));
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* supervisor-props.ts — what every SUPERVISOR binding a Durable Object mints
|
|
3
|
+
* for a process carries, and the one rule for naming the instance in it.
|
|
4
|
+
*
|
|
5
|
+
* A binding names its host INSTANCE (`hostIncarnation`) exactly when a
|
|
6
|
+
* mutation sent through it can be delivered once
|
|
7
|
+
* (@nimbus-sh/core/workspace/supervisor-delivery.js): the host opened a
|
|
8
|
+
* delivery store, the binding acts as a real process (SupervisorRPC refuses
|
|
9
|
+
* every filesystem mutation of pid 0), and it routes back to this very
|
|
10
|
+
* object rather than another (a fanout pool writes to its coordinator).
|
|
11
|
+
* Minting every binding here is what keeps a new mint site from forgetting
|
|
12
|
+
* that, which would fail silently: its mutations would simply never be
|
|
13
|
+
* re-sent.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { supervisorDeliveryProps } from '@nimbus-sh/core/workspace/supervisor-delivery.js';
|
|
17
|
+
import { hostRoute, type HostRoute } from './composition.js';
|
|
18
|
+
|
|
19
|
+
/** The props every SUPERVISOR binding for a process carries. */
|
|
20
|
+
export interface SupervisorBindingProps {
|
|
21
|
+
/** The Durable Object the binding's calls reach. */
|
|
22
|
+
doId: string;
|
|
23
|
+
/** The process the calls act as; 0 is none, and can mutate nothing. */
|
|
24
|
+
pid: number;
|
|
25
|
+
/** The way back, minted with the binding in the host's isolate. */
|
|
26
|
+
route?: HostRoute;
|
|
27
|
+
/** The host instance that applies this binding's mutations once, when there is one. */
|
|
28
|
+
hostIncarnation?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* The props of a SUPERVISOR binding minted in the Durable Object whose state
|
|
33
|
+
* is `ctx`, for process `pid`, reaching `options.doId` (this object by
|
|
34
|
+
* default) by `options.route` (this isolate's composition by default).
|
|
35
|
+
*/
|
|
36
|
+
export function supervisorBindingProps(
|
|
37
|
+
ctx: { readonly id: { toString(): string } },
|
|
38
|
+
pid: number,
|
|
39
|
+
options: { doId?: string; route?: HostRoute } = {},
|
|
40
|
+
): SupervisorBindingProps {
|
|
41
|
+
const own = ctx.id.toString();
|
|
42
|
+
const doId = options.doId ?? own;
|
|
43
|
+
const route = options.route ?? hostRoute() ?? undefined;
|
|
44
|
+
const delivery = pid > 0 && doId === own ? supervisorDeliveryProps(ctx) : {};
|
|
45
|
+
return { doId, pid, route, ...delivery };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* `key`, for a loader cache entry whose worker holds a binding with `props`:
|
|
50
|
+
* made specific to the host instance the binding names, since the loader
|
|
51
|
+
* outlives that instance and the next one refuses every mutation the binding
|
|
52
|
+
* would deliver. A binding that names none keeps `key`, and its warm worker.
|
|
53
|
+
*/
|
|
54
|
+
export function supervisorLoaderKey(key: string, props: Pick<SupervisorBindingProps, 'hostIncarnation'>): string {
|
|
55
|
+
return props.hostIncarnation === undefined ? key : `${key}:${props.hostIncarnation}`;
|
|
56
|
+
}
|