@nimbus-sh/fabric 0.1.0 → 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 +208 -293
- 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 +3 -0
- package/dist/composition.d.ts.map +1 -0
- package/dist/composition.js +2 -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} +58 -17
- package/dist/fenced-work.d.ts.map +1 -0
- package/dist/fenced-work.js +241 -0
- 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 +33 -15
- package/dist/process-fabric.d.ts.map +1 -1
- package/dist/process-fabric.js +25 -15
- package/dist/process-host.d.ts +1 -1
- package/dist/process-host.d.ts.map +1 -1
- package/dist/process-host.js +19 -11
- 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} +24 -21
- package/dist/turn-budget.d.ts.map +1 -0
- package/dist/{launch-pacer.js → turn-budget.js} +24 -12
- package/dist/workerd-facet-host.d.ts +67 -70
- package/dist/workerd-facet-host.d.ts.map +1 -1
- package/dist/workerd-facet-host.js +129 -181
- 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 +16 -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} +129 -42
- 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 +43 -34
- package/src/process-host.ts +22 -20
- package/src/sealed.ts +150 -0
- package/src/timers.ts +294 -0
- package/src/{launch-pacer.ts → turn-budget.ts} +34 -27
- package/src/workerd-facet-host.ts +159 -208
- 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-journal.js +0 -154
- 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
package/dist/outbox.js
ADDED
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* outbox.ts — a durable retry outbox over a Durable Object's own SQLite.
|
|
3
|
+
*
|
|
4
|
+
* Proteus built this discipline twice by hand and says so: its email outbox
|
|
5
|
+
* (`cf-backend/src/email/outbox.ts:7` — "The discipline mirrors the peer
|
|
6
|
+
* outbox", 8 attempts from a 30s base) and its peer transport
|
|
7
|
+
* (`core/src/events/ingress/peer.ts` — 8 attempts from a 5s base, per-receiver
|
|
8
|
+
* ordering, an ask/reply waiter over the same rows). Both carry the same
|
|
9
|
+
* mechanism: a write-ahead intent row in `state='pending'`, an
|
|
10
|
+
* `attempt_count`, a `next_attempt_at` with a partial index over pending rows,
|
|
11
|
+
* backoff `next = now + base * 2**(attempts-1)`, and a `nextRetryAt()` folded
|
|
12
|
+
* into the object's single alarm. This module is that mechanism once.
|
|
13
|
+
*
|
|
14
|
+
* What the consumers proved and this keeps:
|
|
15
|
+
* - WRITE-AHEAD. The row commits before send runs. On a Durable Object the
|
|
16
|
+
* output gate orders it: an outbound message cannot leave before the
|
|
17
|
+
* writes of its turn are durable, so no explicit sync is needed here.
|
|
18
|
+
* - DISPOSITION, not a boolean. peer.ts:476-526 separates three failures: a
|
|
19
|
+
* RESOLVED refusal is permanent (dlq now), a THROWN send is transport
|
|
20
|
+
* trouble (backoff), a malformed row is poison (dlq now, no send). A
|
|
21
|
+
* boolean cannot carry that, and retrying a refusal re-offends.
|
|
22
|
+
* - ORDERING per key. Rows drain in id order. A transient failure blocks
|
|
23
|
+
* later rows with the same order key until the head clears (peer.ts:465
|
|
24
|
+
* head-of-line set); a dead-lettered head does not block, and keyless rows
|
|
25
|
+
* never block each other (the email outbox has no ordering at all).
|
|
26
|
+
* - IDEMPOTENCY. A dedupe key already queued or sent is refused with the
|
|
27
|
+
* existing id, and a sent key never reaches send again.
|
|
28
|
+
*
|
|
29
|
+
* What the consumers lacked and this adds:
|
|
30
|
+
* - The drain registers with `timers` (one reason in the shared map) instead
|
|
31
|
+
* of owning an alarm, and the dispatch-side handler re-arms through its
|
|
32
|
+
* RETURN value — calling schedule() from inside the dispatcher's chain
|
|
33
|
+
* would deadlock on the chain that serializes the reason map. A host
|
|
34
|
+
* whose alarm belongs to another framework (the Agents SDK, agent-core's
|
|
35
|
+
* reconciler) uses the scheduler-seam form instead: fabric hands every
|
|
36
|
+
* next due time to the policy's `schedule` and touches no timer storage.
|
|
37
|
+
* - The drain is turn-bounded through a {@link TurnBudget}: both consumer
|
|
38
|
+
* drains iterate every due row in one turn, which is the same
|
|
39
|
+
* thread-holding shape the pacer exists to end.
|
|
40
|
+
*
|
|
41
|
+
* ADOPTION IS A DATA MIGRATION for a live consumer. The rows live in
|
|
42
|
+
* fabric's own table (`outbox_<name>`), and nothing moves itself: a consumer
|
|
43
|
+
* with pending intents or a sent-key dedupe history in its old table keeps
|
|
44
|
+
* them there. The ported EmailOutbox went from `email_outbox` to
|
|
45
|
+
* `outbox_email` — deployed live without a migration, its pending mail would
|
|
46
|
+
* never send and every sent key would deliver a second time.
|
|
47
|
+
*/
|
|
48
|
+
import { z } from 'zod/v4';
|
|
49
|
+
import { timers } from './timers.js';
|
|
50
|
+
const PendingRowSchema = z.object({
|
|
51
|
+
id: z.string(),
|
|
52
|
+
message: z.string(),
|
|
53
|
+
order_key: z.string().nullable(),
|
|
54
|
+
attempt_count: z.number(),
|
|
55
|
+
next_attempt_at: z.number(),
|
|
56
|
+
});
|
|
57
|
+
const DlqRowSchema = z.object({
|
|
58
|
+
id: z.string(),
|
|
59
|
+
message: z.string(),
|
|
60
|
+
dedupe_key: z.string().nullable(),
|
|
61
|
+
attempt_count: z.number(),
|
|
62
|
+
last_error: z.string().nullable(),
|
|
63
|
+
});
|
|
64
|
+
const RecordRowSchema = z.object({
|
|
65
|
+
id: z.string(),
|
|
66
|
+
state: z.enum(['pending', 'sent', 'dlq']),
|
|
67
|
+
message: z.string(),
|
|
68
|
+
dedupe_key: z.string().nullable(),
|
|
69
|
+
attempt_count: z.number(),
|
|
70
|
+
last_error: z.string().nullable(),
|
|
71
|
+
});
|
|
72
|
+
const NAME_PATTERN = /^[a-z][a-z0-9_]{0,40}$/;
|
|
73
|
+
export function outbox(...args) {
|
|
74
|
+
if (args.length === 4) {
|
|
75
|
+
const [host, ctx, name, policy] = args;
|
|
76
|
+
return new Outbox(ctx, name, policy, { kind: 'timers', host, ctx });
|
|
77
|
+
}
|
|
78
|
+
const [ctx, name, policy] = args;
|
|
79
|
+
return new Outbox(ctx, name, policy, { kind: 'schedule', schedule: (at) => policy.schedule(at) });
|
|
80
|
+
}
|
|
81
|
+
export class Outbox {
|
|
82
|
+
ctx;
|
|
83
|
+
policy;
|
|
84
|
+
scheduling;
|
|
85
|
+
/** The timer reason this outbox arms in the shared map. */
|
|
86
|
+
reason;
|
|
87
|
+
table;
|
|
88
|
+
schemaReady = false;
|
|
89
|
+
draining = false;
|
|
90
|
+
/** Largest id ever seen, so a replacement instance mints above it. */
|
|
91
|
+
lastId = '';
|
|
92
|
+
seq = 0;
|
|
93
|
+
constructor(ctx, name, policy, scheduling) {
|
|
94
|
+
this.ctx = ctx;
|
|
95
|
+
this.policy = policy;
|
|
96
|
+
this.scheduling = scheduling;
|
|
97
|
+
if (!NAME_PATTERN.test(name)) {
|
|
98
|
+
throw new Error(`fabric: outbox name '${name}' must match ${NAME_PATTERN}`);
|
|
99
|
+
}
|
|
100
|
+
this.table = `outbox_${name}`;
|
|
101
|
+
this.reason = `outbox:${name}`;
|
|
102
|
+
}
|
|
103
|
+
ensureSchema() {
|
|
104
|
+
if (this.schemaReady)
|
|
105
|
+
return;
|
|
106
|
+
const sql = this.ctx.storage.sql;
|
|
107
|
+
sql.exec(`CREATE TABLE IF NOT EXISTS ${this.table} (
|
|
108
|
+
id TEXT PRIMARY KEY,
|
|
109
|
+
dedupe_key TEXT,
|
|
110
|
+
order_key TEXT,
|
|
111
|
+
message TEXT NOT NULL,
|
|
112
|
+
state TEXT NOT NULL DEFAULT 'pending'
|
|
113
|
+
CHECK (state IN ('pending', 'sent', 'dlq')),
|
|
114
|
+
attempt_count INTEGER NOT NULL DEFAULT 0,
|
|
115
|
+
next_attempt_at INTEGER NOT NULL,
|
|
116
|
+
created_at INTEGER NOT NULL,
|
|
117
|
+
sent_at INTEGER,
|
|
118
|
+
last_error TEXT
|
|
119
|
+
)`);
|
|
120
|
+
sql.exec(`CREATE UNIQUE INDEX IF NOT EXISTS idx_${this.table}_dedupe
|
|
121
|
+
ON ${this.table} (dedupe_key) WHERE dedupe_key IS NOT NULL`);
|
|
122
|
+
// The recovery read (`nextRetryAt`, the drain) must not table-scan sent
|
|
123
|
+
// history, which only grows — the same reason both consumer schemas carry
|
|
124
|
+
// exactly this partial index.
|
|
125
|
+
sql.exec(`CREATE INDEX IF NOT EXISTS idx_${this.table}_pending
|
|
126
|
+
ON ${this.table} (next_attempt_at) WHERE state = 'pending'`);
|
|
127
|
+
const rows = [...sql.exec(`SELECT MAX(id) AS id FROM ${this.table}`)];
|
|
128
|
+
this.lastId = rows[0]?.id ?? '';
|
|
129
|
+
this.schemaReady = true;
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Ids order the drain, so they must grow: time-prefixed, tie-broken by a
|
|
133
|
+
* per-instance counter, and forced above the largest stored id so a
|
|
134
|
+
* replacement instance with a lagging clock cannot mint into the past.
|
|
135
|
+
*/
|
|
136
|
+
mintId(now) {
|
|
137
|
+
let id = `${now.toString(36).padStart(9, '0')}-${(this.seq++).toString(36).padStart(6, '0')}`;
|
|
138
|
+
if (this.lastId !== '' && id <= this.lastId)
|
|
139
|
+
id = `${this.lastId}0`;
|
|
140
|
+
this.lastId = id;
|
|
141
|
+
return id;
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Write the intent ahead of any send. Returns `admitted: false` with the
|
|
145
|
+
* existing id when the dedupe key is already queued, sent, or dead-lettered
|
|
146
|
+
* — a sent key never reaches send again (the email outbox's short-circuit).
|
|
147
|
+
*
|
|
148
|
+
* `onDuplicate: 'retry-now'` treats a re-ask for an UNSENT key as new
|
|
149
|
+
* intent: the row's retry instant is clamped to `now` and a dead letter
|
|
150
|
+
* returns to 'pending', its attempt count kept — so every re-ask buys one
|
|
151
|
+
* more delivery attempt, past the attempt budget. The monitor's alert-once
|
|
152
|
+
* contract needs exactly this: a failed alert is retried by each sweep
|
|
153
|
+
* until it lands. A sent key stays final either way.
|
|
154
|
+
*
|
|
155
|
+
* Arms the scheduler at `now`, so delivery is owed by the alarm even
|
|
156
|
+
* when the caller never drains inline.
|
|
157
|
+
*/
|
|
158
|
+
async queue(message, opts = {}) {
|
|
159
|
+
this.ensureSchema();
|
|
160
|
+
const now = opts.now ?? Date.now();
|
|
161
|
+
const sql = this.ctx.storage.sql;
|
|
162
|
+
if (opts.dedupeKey !== undefined) {
|
|
163
|
+
const existing = [...sql.exec(`SELECT id, state FROM ${this.table} WHERE dedupe_key = ?`, opts.dedupeKey)];
|
|
164
|
+
if (existing.length > 0) {
|
|
165
|
+
if (opts.onDuplicate === 'retry-now' && existing[0].state !== 'sent') {
|
|
166
|
+
sql.exec(`UPDATE ${this.table} SET state = 'pending', next_attempt_at = ? WHERE id = ?`, now, existing[0].id);
|
|
167
|
+
await this.arm(now);
|
|
168
|
+
}
|
|
169
|
+
return { id: existing[0].id, admitted: false };
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
const id = this.mintId(now);
|
|
173
|
+
sql.exec(`INSERT INTO ${this.table} (id, dedupe_key, order_key, message, next_attempt_at, created_at)
|
|
174
|
+
VALUES (?, ?, ?, ?, ?, ?)`, id, opts.dedupeKey ?? null, this.policy.orderBy?.(message) ?? null, JSON.stringify(message), now, now);
|
|
175
|
+
await this.arm(now);
|
|
176
|
+
return { id, admitted: true };
|
|
177
|
+
}
|
|
178
|
+
/** Hand a due time to whichever scheduler this outbox was built on. */
|
|
179
|
+
async arm(at) {
|
|
180
|
+
if (this.scheduling.kind === 'timers') {
|
|
181
|
+
await timers(this.scheduling.host, this.scheduling.ctx).schedule(this.reason, at);
|
|
182
|
+
}
|
|
183
|
+
else {
|
|
184
|
+
await this.scheduling.schedule(at);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
/** The single-alarm fold: the earliest pending deadline, or null. */
|
|
188
|
+
nextRetryAt() {
|
|
189
|
+
this.ensureSchema();
|
|
190
|
+
const rows = [...this.ctx.storage.sql.exec(`SELECT MIN(next_attempt_at) AS next FROM ${this.table} WHERE state = 'pending'`)];
|
|
191
|
+
return rows[0]?.next ?? null;
|
|
192
|
+
}
|
|
193
|
+
/** The row queue() named, by its id. Null when no such row exists. */
|
|
194
|
+
status(id) {
|
|
195
|
+
return this.record('id', id);
|
|
196
|
+
}
|
|
197
|
+
/** The row a dedupe key admitted, whatever its state. Null when none. */
|
|
198
|
+
find(dedupeKey) {
|
|
199
|
+
return this.record('dedupe_key', dedupeKey);
|
|
200
|
+
}
|
|
201
|
+
record(column, value) {
|
|
202
|
+
this.ensureSchema();
|
|
203
|
+
const rows = [...this.ctx.storage.sql.exec(`SELECT id, state, message, dedupe_key, attempt_count, last_error
|
|
204
|
+
FROM ${this.table} WHERE ${column} = ?`, value)];
|
|
205
|
+
if (rows.length === 0)
|
|
206
|
+
return null;
|
|
207
|
+
const row = RecordRowSchema.parse(rows[0]);
|
|
208
|
+
let message = null;
|
|
209
|
+
try {
|
|
210
|
+
message = JSON.parse(row.message);
|
|
211
|
+
}
|
|
212
|
+
catch { /* poison-parse row */ }
|
|
213
|
+
return {
|
|
214
|
+
id: row.id,
|
|
215
|
+
state: row.state,
|
|
216
|
+
message,
|
|
217
|
+
dedupeKey: row.dedupe_key,
|
|
218
|
+
attemptCount: row.attempt_count,
|
|
219
|
+
lastError: row.last_error,
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
/** Dead-lettered rows, for inspection. Terminal: nothing retries out. */
|
|
223
|
+
dlq() {
|
|
224
|
+
this.ensureSchema();
|
|
225
|
+
const rows = [...this.ctx.storage.sql.exec(`SELECT id, message, dedupe_key, attempt_count, last_error FROM ${this.table} WHERE state = 'dlq' ORDER BY id`)].map((row) => DlqRowSchema.parse(row));
|
|
226
|
+
return rows.map((row) => {
|
|
227
|
+
let message = null;
|
|
228
|
+
try {
|
|
229
|
+
message = JSON.parse(row.message);
|
|
230
|
+
}
|
|
231
|
+
catch { /* poison-parse row */ }
|
|
232
|
+
return {
|
|
233
|
+
id: row.id,
|
|
234
|
+
message,
|
|
235
|
+
dedupeKey: row.dedupe_key,
|
|
236
|
+
attemptCount: row.attempt_count,
|
|
237
|
+
lastError: row.last_error ?? '',
|
|
238
|
+
};
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* Deliver every due pending row, in id order, honouring per-key blocking.
|
|
243
|
+
* Reentrancy-guarded: the alarm and an inline post-queue drain overlap on
|
|
244
|
+
* the same activation (peer.ts:452 carries the same guard).
|
|
245
|
+
*
|
|
246
|
+
* `budget` bounds the turn: the drain spends each processed row's payload
|
|
247
|
+
* size and suspends when a chunk is full, so a large backlog crosses turns
|
|
248
|
+
* instead of holding the actor's only thread.
|
|
249
|
+
*
|
|
250
|
+
* `context` is handed to every `send` this drain makes — the seam for
|
|
251
|
+
* per-call state such as a transport binding resolved by the caller.
|
|
252
|
+
*/
|
|
253
|
+
async drain(now = Date.now(), ...rest) {
|
|
254
|
+
const opts = rest[0] ?? {};
|
|
255
|
+
const result = { sent: 0, retried: 0, deadLettered: 0 };
|
|
256
|
+
if (this.draining)
|
|
257
|
+
return result;
|
|
258
|
+
this.draining = true;
|
|
259
|
+
try {
|
|
260
|
+
this.ensureSchema();
|
|
261
|
+
const sql = this.ctx.storage.sql;
|
|
262
|
+
const rows = [...sql.exec(`SELECT id, message, order_key, attempt_count, next_attempt_at
|
|
263
|
+
FROM ${this.table} WHERE state = 'pending' ORDER BY id`)].map((row) => PendingRowSchema.parse(row));
|
|
264
|
+
const blocked = new Set();
|
|
265
|
+
for (const row of rows) {
|
|
266
|
+
if (row.order_key !== null && blocked.has(row.order_key))
|
|
267
|
+
continue;
|
|
268
|
+
if (row.next_attempt_at > now) {
|
|
269
|
+
// A backed-off head still blocks the rows queued behind it.
|
|
270
|
+
if (row.order_key !== null)
|
|
271
|
+
blocked.add(row.order_key);
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
let message;
|
|
275
|
+
try {
|
|
276
|
+
message = JSON.parse(row.message);
|
|
277
|
+
}
|
|
278
|
+
catch (e) {
|
|
279
|
+
// Unparseable is poison: it can never succeed, so it never blocks.
|
|
280
|
+
this.deadLetter(row.id, row.attempt_count, `outbox row does not parse: ${errorText(e)}`);
|
|
281
|
+
result.deadLettered++;
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
284
|
+
const attempt = row.attempt_count + 1;
|
|
285
|
+
let disposition;
|
|
286
|
+
try {
|
|
287
|
+
disposition = await this.policy.send(message, { id: row.id, attempt }, opts.context);
|
|
288
|
+
}
|
|
289
|
+
catch (e) {
|
|
290
|
+
disposition = { status: 'retry', reason: errorText(e) };
|
|
291
|
+
}
|
|
292
|
+
if (disposition.status === 'sent') {
|
|
293
|
+
sql.exec(`UPDATE ${this.table} SET state = 'sent', attempt_count = ?, sent_at = ?, last_error = NULL WHERE id = ?`, attempt, now, row.id);
|
|
294
|
+
result.sent++;
|
|
295
|
+
}
|
|
296
|
+
else if (disposition.status === 'poison') {
|
|
297
|
+
this.deadLetter(row.id, attempt, disposition.reason);
|
|
298
|
+
result.deadLettered++;
|
|
299
|
+
}
|
|
300
|
+
else if (attempt >= this.policy.maxAttempts) {
|
|
301
|
+
this.deadLetter(row.id, attempt, `undeliverable after ${attempt} attempts: ${disposition.reason}`);
|
|
302
|
+
result.deadLettered++;
|
|
303
|
+
}
|
|
304
|
+
else {
|
|
305
|
+
const next = now + this.policy.baseMs * 2 ** (attempt - 1);
|
|
306
|
+
sql.exec(`UPDATE ${this.table} SET attempt_count = ?, next_attempt_at = ?, last_error = ? WHERE id = ?`, attempt, next, disposition.reason, row.id);
|
|
307
|
+
result.retried++;
|
|
308
|
+
if (row.order_key !== null)
|
|
309
|
+
blocked.add(row.order_key);
|
|
310
|
+
}
|
|
311
|
+
// Code-unit length: equal to UTF-8 bytes for the ASCII JSON the rows
|
|
312
|
+
// hold, an undercount otherwise — the bound names where a turn ends,
|
|
313
|
+
// it is not an admission ceiling (same reading as budgets.ts).
|
|
314
|
+
await opts.budget?.spend(row.message.length);
|
|
315
|
+
}
|
|
316
|
+
// A seam-scheduled outbox re-arms itself: there is no dispatcher whose
|
|
317
|
+
// return value could, and the ported consumer re-armed by hand after
|
|
318
|
+
// every drain — delivery stays owed either way.
|
|
319
|
+
if (this.scheduling.kind === 'schedule') {
|
|
320
|
+
const next = this.nextRetryAt();
|
|
321
|
+
if (next !== null)
|
|
322
|
+
await this.scheduling.schedule(next);
|
|
323
|
+
}
|
|
324
|
+
return result;
|
|
325
|
+
}
|
|
326
|
+
finally {
|
|
327
|
+
this.draining = false;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
deadLetter(id, attempts, error) {
|
|
331
|
+
this.ctx.storage.sql.exec(`UPDATE ${this.table} SET state = 'dlq', attempt_count = ?, last_error = ? WHERE id = ?`, attempts, error, id);
|
|
332
|
+
}
|
|
333
|
+
/**
|
|
334
|
+
* The dispatch-side entry for the embedder's timer handler map. Re-arms
|
|
335
|
+
* through the RETURN value: the dispatcher runs handlers inside the chain
|
|
336
|
+
* that serializes the reason map, so a schedule() call from here would
|
|
337
|
+
* deadlock on its own chain.
|
|
338
|
+
*
|
|
339
|
+
* An alarm fires with no caller, so a policy that declares a drain context
|
|
340
|
+
* receives it here once, closed over every alarm-driven drain.
|
|
341
|
+
*/
|
|
342
|
+
handler(...context) {
|
|
343
|
+
if (this.scheduling.kind === 'schedule') {
|
|
344
|
+
throw new Error(`fabric: outbox '${this.reason}' uses a scheduler seam — there is no timer dispatcher to hand this handler to`);
|
|
345
|
+
}
|
|
346
|
+
return async (now) => {
|
|
347
|
+
await this.drain(now, ...[{ context: context[0] }]);
|
|
348
|
+
const next = this.nextRetryAt();
|
|
349
|
+
return next === null ? undefined : { rearmAt: next };
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
function errorText(error) {
|
|
354
|
+
return error instanceof Error ? error.message : String(error);
|
|
355
|
+
}
|
package/dist/process-fabric.d.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* Every long-lived process Nimbus runs — node servers, python/ruby socket
|
|
6
6
|
* servers, an agent TUI and its headless server — runs as a **DO Facet**:
|
|
7
7
|
* a named child actor whose class comes from a dynamic worker, opened by
|
|
8
|
-
* `
|
|
8
|
+
* `processes(ctx, env).spawn` in `workerd-facet-host.ts`.
|
|
9
9
|
*
|
|
10
10
|
* ctx.facets.get(`proc-${pid}`, () => ({
|
|
11
11
|
* class: env.LOADER.get(workerKey, buildConfig)
|
|
@@ -75,6 +75,7 @@
|
|
|
75
75
|
*/
|
|
76
76
|
import { z } from 'zod/v4';
|
|
77
77
|
import type { RouteableFacetTarget } from '@nimbus-sh/core/runtime/os-contracts.js';
|
|
78
|
+
import type { ServiceStub } from './vendor/types.js';
|
|
78
79
|
/**
|
|
79
80
|
* The class every generated resident runner exports. One name for every
|
|
80
81
|
* runtime: the fabric names it unconditionally, so nothing about which program
|
|
@@ -107,6 +108,8 @@ export declare const ResidentCodeSpecSchema: z.ZodObject<{
|
|
|
107
108
|
}, z.core.$strip>]>>;
|
|
108
109
|
vfsWasmModules: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
109
110
|
vfsTextModules: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
111
|
+
env: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
112
|
+
globalOutbound: z.ZodOptional<z.ZodNullable<z.ZodCustom<ServiceStub, ServiceStub>>>;
|
|
110
113
|
}, z.core.$strip>;
|
|
111
114
|
export type ResidentCodeSpec = z.infer<typeof ResidentCodeSpecSchema>;
|
|
112
115
|
/**
|
|
@@ -132,6 +135,8 @@ export declare function residentBootSpecSchema<Stage extends z.ZodType>(stageSch
|
|
|
132
135
|
}, z.core.$strip>]>>;
|
|
133
136
|
vfsWasmModules: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
134
137
|
vfsTextModules: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
138
|
+
env: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
139
|
+
globalOutbound: z.ZodOptional<z.ZodNullable<z.ZodCustom<ServiceStub, ServiceStub>>>;
|
|
135
140
|
}, z.core.$strip>;
|
|
136
141
|
}, z.core.$strip>], "kind">;
|
|
137
142
|
export type ResidentBootSpec = {
|
|
@@ -141,18 +146,6 @@ export type ResidentBootSpec = {
|
|
|
141
146
|
kind: 'code';
|
|
142
147
|
code: ResidentCodeSpec;
|
|
143
148
|
};
|
|
144
|
-
/**
|
|
145
|
-
* Assemble a complete Worker Loader config from a staged-artifact spec. The
|
|
146
|
-
* embedder supplies this: a stage names artifact sources only the embedder
|
|
147
|
-
* knows how to fetch (Nimbus's largest staged artifact is a ~23 MB module map from
|
|
148
|
-
* ASSETS), and the assembler runs inside the loader's cache-miss callback so
|
|
149
|
-
* those sources are materialized only while the facet actually loads.
|
|
150
|
-
* `env` is whichever hosting actor's env the facet is opened with.
|
|
151
|
-
*/
|
|
152
|
-
export type StagedBootAssembler = (env: unknown, stage: unknown) => Promise<object>;
|
|
153
|
-
/** Registered once at composition time, first-write-wins. */
|
|
154
|
-
export declare function setStagedBootAssembler(assembler: StagedBootAssembler): void;
|
|
155
|
-
export declare function requireStagedBootAssembler(): StagedBootAssembler;
|
|
156
149
|
/**
|
|
157
150
|
* Where a generated module source is materialized so a boot spec can name it.
|
|
158
151
|
*
|
|
@@ -213,6 +206,11 @@ export interface ResidentDiskReader {
|
|
|
213
206
|
* path, verifying each generated image against the digest its own path claims.
|
|
214
207
|
* Runs inside the loader's cache-miss callback, so the bytes exist only for
|
|
215
208
|
* the duration of the load.
|
|
209
|
+
*
|
|
210
|
+
* The spec's isolation posture rides along verbatim: an explicit `env` is
|
|
211
|
+
* the isolate's whole env (loopback stubs by reference, never cloned or
|
|
212
|
+
* re-minted). An absent env stays absent
|
|
213
|
+
* so the worker config can tell "embedder takes the env" from the default.
|
|
216
214
|
*/
|
|
217
215
|
export declare function residentLoaderConfig(spec: ResidentCodeSpec, disk: ResidentDiskReader): Promise<Record<string, unknown>>;
|
|
218
216
|
/**
|
|
@@ -237,6 +235,17 @@ export interface ProcessHostParams {
|
|
|
237
235
|
writerId: string;
|
|
238
236
|
/** Forwarded verbatim to the runner's startProcess. */
|
|
239
237
|
startArgs: unknown;
|
|
238
|
+
/**
|
|
239
|
+
* Set only by the coordinator's durable-application path: an explicit facet
|
|
240
|
+
* name (`app-slot-<n>`) allocated from DO storage, plus the release split
|
|
241
|
+
* that keeps its SQLite across aborts. Absent, the host allocates an
|
|
242
|
+
* ephemeral `proc-slot-<n>` name from its in-memory free list and deletes
|
|
243
|
+
* the store on release.
|
|
244
|
+
*/
|
|
245
|
+
facet?: {
|
|
246
|
+
name: string;
|
|
247
|
+
durable: boolean;
|
|
248
|
+
};
|
|
240
249
|
}
|
|
241
250
|
/**
|
|
242
251
|
* One resident process, as its coordinator sees it. Identical in meaning on
|
|
@@ -324,7 +333,7 @@ export interface ProcessImageDelivery {
|
|
|
324
333
|
* ends: the source exists and is populated before, the destination is
|
|
325
334
|
* non-empty after. A blocklist of bad names would pass a typo straight
|
|
326
335
|
* through and wipe a process's filesystem while returning ok. Enforced by
|
|
327
|
-
* `
|
|
336
|
+
* `cloneStorage` in the workerd host, which is the one way the fabric
|
|
328
337
|
* calls clone.
|
|
329
338
|
*/
|
|
330
339
|
readonly reflink: 'same-object' | 'impossible';
|
|
@@ -495,10 +504,19 @@ export interface ResidentProcessSpawn {
|
|
|
495
504
|
pid: number;
|
|
496
505
|
/** Keyed dynamic-worker identity (`nimbus-process:${doId}:${pid}`). */
|
|
497
506
|
workerKey: string;
|
|
498
|
-
/** What the
|
|
507
|
+
/** What the process boots from. */
|
|
499
508
|
boot: ResidentBootSpec;
|
|
500
509
|
/** Forwarded verbatim to the runner's startProcess. */
|
|
501
510
|
startArgs?: unknown;
|
|
511
|
+
/**
|
|
512
|
+
* A durable application's explicit facet name (`app-slot-<n>`) and the
|
|
513
|
+
* release split that keeps its SQLite. Coordinator-allocated; absent for an
|
|
514
|
+
* ephemeral process, which takes a `proc-slot-<n>` name from the book.
|
|
515
|
+
*/
|
|
516
|
+
facet?: {
|
|
517
|
+
name: string;
|
|
518
|
+
durable: boolean;
|
|
519
|
+
};
|
|
502
520
|
/**
|
|
503
521
|
* Called before any concrete host capability can expose this writer.
|
|
504
522
|
* A spawn must not proceed unless the supervisor accepts the authority.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"process-fabric.d.ts","sourceRoot":"","sources":["../src/process-fabric.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0EG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,QAAQ,CAAC;AAC3B,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,yCAAyC,CAAC;
|
|
1
|
+
{"version":3,"file":"process-fabric.d.ts","sourceRoot":"","sources":["../src/process-fabric.ts"],"names":[],"mappings":"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"}
|
package/dist/process-fabric.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* Every long-lived process Nimbus runs — node servers, python/ruby socket
|
|
6
6
|
* servers, an agent TUI and its headless server — runs as a **DO Facet**:
|
|
7
7
|
* a named child actor whose class comes from a dynamic worker, opened by
|
|
8
|
-
* `
|
|
8
|
+
* `processes(ctx, env).spawn` in `workerd-facet-host.ts`.
|
|
9
9
|
*
|
|
10
10
|
* ctx.facets.get(`proc-${pid}`, () => ({
|
|
11
11
|
* class: env.LOADER.get(workerKey, buildConfig)
|
|
@@ -115,6 +115,17 @@ export const ResidentCodeSpecSchema = z.object({
|
|
|
115
115
|
* image store below and the spec names it.
|
|
116
116
|
*/
|
|
117
117
|
vfsTextModules: z.record(z.string(), z.string()).optional(),
|
|
118
|
+
/**
|
|
119
|
+
* The isolate's exact `env`: one entry per binding the embedder minted,
|
|
120
|
+
* carried by reference so loopback stubs survive untouched. Defined —
|
|
121
|
+
* even as `{}` — means the embedder takes the whole env and no
|
|
122
|
+
* SUPERVISOR binding is injected; absent keeps the default (a SUPERVISOR
|
|
123
|
+
* minted from the composed supervisor entrypoint).
|
|
124
|
+
*/
|
|
125
|
+
env: z.record(z.string(), z.unknown()).optional(),
|
|
126
|
+
/** Absent inherits outbound, null denies it, a binding mediates it by reference. */
|
|
127
|
+
globalOutbound: z.custom((value) => value !== null && (typeof value === 'object' || typeof value === 'function')
|
|
128
|
+
&& 'fetch' in value && typeof value.fetch === 'function').nullable().optional(),
|
|
118
129
|
});
|
|
119
130
|
/**
|
|
120
131
|
* The boot-spec union, with the staged arm's payload validated by the
|
|
@@ -131,20 +142,11 @@ export function residentBootSpecSchema(stageSchema) {
|
|
|
131
142
|
z.object({ kind: z.literal('code'), code: ResidentCodeSpecSchema }),
|
|
132
143
|
]);
|
|
133
144
|
}
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
_stagedBootAssembler = assembler;
|
|
140
|
-
}
|
|
141
|
-
export function requireStagedBootAssembler() {
|
|
142
|
-
if (!_stagedBootAssembler) {
|
|
143
|
-
throw new Error('fabric: no staged-boot assembler registered; a \'staged\' boot spec '
|
|
144
|
-
+ 'cannot be assembled without one (setStagedBootAssembler)');
|
|
145
|
-
}
|
|
146
|
-
return _stagedBootAssembler;
|
|
147
|
-
}
|
|
145
|
+
// ── Staged boots ────────────────────────────────────────────────────────────
|
|
146
|
+
//
|
|
147
|
+
// A 'staged' boot spec assembles through the embedder's composed
|
|
148
|
+
// StagedBootAssembler — see composition.ts. What a stage IS belongs to
|
|
149
|
+
// whoever composed the assembler.
|
|
148
150
|
// ── Boot-image store ────────────────────────────────────────────────────────
|
|
149
151
|
/**
|
|
150
152
|
* Where a generated module source is materialized so a boot spec can name it.
|
|
@@ -200,6 +202,11 @@ export function facetImagePathDigest(path) {
|
|
|
200
202
|
* path, verifying each generated image against the digest its own path claims.
|
|
201
203
|
* Runs inside the loader's cache-miss callback, so the bytes exist only for
|
|
202
204
|
* the duration of the load.
|
|
205
|
+
*
|
|
206
|
+
* The spec's isolation posture rides along verbatim: an explicit `env` is
|
|
207
|
+
* the isolate's whole env (loopback stubs by reference, never cloned or
|
|
208
|
+
* re-minted). An absent env stays absent
|
|
209
|
+
* so the worker config can tell "embedder takes the env" from the default.
|
|
203
210
|
*/
|
|
204
211
|
export async function residentLoaderConfig(spec, disk) {
|
|
205
212
|
const resolved = {};
|
|
@@ -217,6 +224,8 @@ export async function residentLoaderConfig(spec, disk) {
|
|
|
217
224
|
compatibilityFlags: spec.compatibilityFlags,
|
|
218
225
|
mainModule: spec.mainModule,
|
|
219
226
|
modules: { ...spec.modules, ...resolved },
|
|
227
|
+
...(spec.env !== undefined ? { env: spec.env } : {}),
|
|
228
|
+
...(spec.globalOutbound !== undefined ? { globalOutbound: spec.globalOutbound } : {}),
|
|
220
229
|
};
|
|
221
230
|
}
|
|
222
231
|
/**
|
|
@@ -340,6 +349,7 @@ export class ProcessFabric {
|
|
|
340
349
|
boot: spawn.boot,
|
|
341
350
|
writerId,
|
|
342
351
|
startArgs: spawn.startArgs,
|
|
352
|
+
...(spawn.facet !== undefined ? { facet: spawn.facet } : {}),
|
|
343
353
|
});
|
|
344
354
|
}
|
|
345
355
|
catch (error) {
|
package/dist/process-host.d.ts
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* peer — the process is a named child actor of a SIBLING session DO, and
|
|
10
10
|
* the coordinator reaches it over one held-open RPC.
|
|
11
11
|
*
|
|
12
|
-
* Both call the same `
|
|
12
|
+
* Both call the same `processes(ctx, env).spawn`. The peer leg is not a second process
|
|
13
13
|
* implementation; it is the same call made on a different actor, which is why
|
|
14
14
|
* the runner, the boot spec, the class name, the writer handshake, the start
|
|
15
15
|
* contract and the lifecycle are shared code rather than parallel paths.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"process-host.d.ts","sourceRoot":"","sources":["../src/process-host.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgEG;AAKH,OAAO,EAGL,KAAK,WAAW,EAIhB,KAAK,kBAAkB,EAExB,MAAM,qBAAqB,CAAC;
|
|
1
|
+
{"version":3,"file":"process-host.d.ts","sourceRoot":"","sources":["../src/process-host.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgEG;AAKH,OAAO,EAGL,KAAK,WAAW,EAIhB,KAAK,kBAAkB,EAExB,MAAM,qBAAqB,CAAC;AAQ7B,4DAA4D;AAC5D,MAAM,MAAM,eAAe,GAAG,OAAO,GAAG,MAAM,CAAC;AAE/C;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAC/B,IAAI,EAAE,eAAe,EACrB,GAAG,EAAE,kBAAkB,EACvB,GAAG,EAAE,OAAO,EACZ,IAAI,EAAE,MAAM,kBAAkB,GAC7B,WAAW,CAIb;AA8ED,wBAAgB,YAAY,IAAI,MAAM,CAGrC;AAED,oDAAoD;AACpD,MAAM,WAAW,eAAe;IAC9B,eAAe,EAAE,MAAM,CAAC;IACxB,GAAG,EAAE,MAAM,CAAC;IACZ,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,mEAAmE;IACnE,mBAAmB,EAAE,MAAM,CAAC;IAC5B,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB;AAED;;;;;;GAMG;AACH,MAAM,WAAW,iBAAiB;IAChC,MAAM,EAAE,MAAM,CAAC;IACf,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE,CAAC;IAC5B,IAAI,EAAE,cAAc,GAAG,IAAI,CAAC;CAC7B;AAED,MAAM,WAAW,kBAAkB;IACjC,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE,CAAC;IAC5B,IAAI,EAAE,cAAc,GAAG,IAAI,CAAC;CAC7B;AA2BD;;;;;;;;;GASG;AACH,eAAO,MAAM,2BAA2B,8BAA8B,CAAC;AACvE,eAAO,MAAM,kCAAkC,yCAAyC,CAAC;AAgSzF;;;;;;;;;;GAUG;AACH,wBAAgB,WAAW,CAAC,OAAO,EAAE,OAAO,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE,CAWhE"}
|
package/dist/process-host.js
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* peer — the process is a named child actor of a SIBLING session DO, and
|
|
10
10
|
* the coordinator reaches it over one held-open RPC.
|
|
11
11
|
*
|
|
12
|
-
* Both call the same `
|
|
12
|
+
* Both call the same `processes(ctx, env).spawn`. The peer leg is not a second process
|
|
13
13
|
* implementation; it is the same call made on a different actor, which is why
|
|
14
14
|
* the runner, the boot spec, the class name, the writer handshake, the start
|
|
15
15
|
* contract and the lifecycle are shared code rather than parallel paths.
|
|
@@ -63,11 +63,12 @@
|
|
|
63
63
|
* {@link HOSTED_WEBSOCKET_KEY_HEADER} — and a per-process capability makes
|
|
64
64
|
* that pair unforgeable by anything that did not open the process.
|
|
65
65
|
*/
|
|
66
|
-
import { disposeRpcResource } from '@nimbus-sh/
|
|
67
|
-
import { isTransientDoReset } from '@nimbus-sh/
|
|
68
|
-
import { PEER_RETRY_BACKOFF_MS, PEER_TRANSIENT_RESET_RETRIES } from './fanout
|
|
69
|
-
import { DYNAMIC_WORKER_CODE_LIMIT_BYTES
|
|
66
|
+
import { disposeRpcResource } from '@nimbus-sh/platform/rpc-dispose.js';
|
|
67
|
+
import { isTransientDoReset } from '@nimbus-sh/platform/oom-classify.js';
|
|
68
|
+
import { PEER_RETRY_BACKOFF_MS, PEER_TRANSIENT_RESET_RETRIES } from './fanout.js';
|
|
69
|
+
import { DYNAMIC_WORKER_CODE_LIMIT_BYTES } from './budgets.js';
|
|
70
70
|
import { BindingError } from './vendor/errors.js';
|
|
71
|
+
import { processes, } from './workerd-facet-host.js';
|
|
71
72
|
/**
|
|
72
73
|
* The substrate for this deployment, resolved once. The mode arrives already
|
|
73
74
|
* decided — the embedder owns the config var that picks it, and refuses an
|
|
@@ -105,7 +106,7 @@ class FacetProcessHost {
|
|
|
105
106
|
this.coordDoId = ctx.id.toString();
|
|
106
107
|
}
|
|
107
108
|
runOnce(params, consume) {
|
|
108
|
-
return
|
|
109
|
+
return processes(this.ctx, this.env).run({ doId: this.coordDoId, pid: params.pid, writerId: params.writerId }, params, consume);
|
|
109
110
|
}
|
|
110
111
|
async open(params) {
|
|
111
112
|
const supervisor = {
|
|
@@ -113,10 +114,10 @@ class FacetProcessHost {
|
|
|
113
114
|
pid: params.pid,
|
|
114
115
|
writerId: params.writerId,
|
|
115
116
|
};
|
|
116
|
-
const {
|
|
117
|
+
const { name, ...facet } = processes(this.ctx, this.env).spawn(this.disk, supervisor, params);
|
|
117
118
|
return {
|
|
118
119
|
...facet,
|
|
119
|
-
describe: () => `facet '${
|
|
120
|
+
describe: () => `facet '${name}' (pid ${params.pid})`
|
|
120
121
|
+ ` of session ${this.coordDoId.slice(-12)}`
|
|
121
122
|
+ `; ${describeImageDelivery(this.imageDelivery)}`,
|
|
122
123
|
};
|
|
@@ -231,11 +232,18 @@ class PeerProcessHost {
|
|
|
231
232
|
* worker of the coordinator here exactly as it does on `facet`.
|
|
232
233
|
*/
|
|
233
234
|
runOnce(params, consume) {
|
|
234
|
-
return
|
|
235
|
+
return processes(this.ctx, this.env).run({ doId: this.coordDoId, pid: params.pid, writerId: params.writerId }, params, consume);
|
|
235
236
|
}
|
|
236
237
|
async open(params) {
|
|
238
|
+
if (params.facet) {
|
|
239
|
+
// A durable application's facet must be a child of the COORDINATOR's
|
|
240
|
+
// Durable Object — its `app-slot-<n>` row and retained SQLite live in
|
|
241
|
+
// that DO's storage. A sibling host would own storage the coordinator's
|
|
242
|
+
// durable-slot book and removeDurableApp cannot reach.
|
|
243
|
+
throw new Error('Nimbus: a durable spawn must be facet-hosted on its own coordinator; '
|
|
244
|
+
+ 'the peer substrate cannot serve one');
|
|
245
|
+
}
|
|
237
246
|
const placement = await this._place(params.pid);
|
|
238
|
-
this.tokensInUse.set(params.pid, placement.isolateToken);
|
|
239
247
|
// Minted per open, held only by this coordinator and the peer that hosts
|
|
240
248
|
// the process. The workerKey is derivable from a pid; this is not.
|
|
241
249
|
const webSocketCapability = crypto.randomUUID();
|
|
@@ -273,7 +281,7 @@ class PeerProcessHost {
|
|
|
273
281
|
catch (error) {
|
|
274
282
|
this.tokensInUse.delete(params.pid);
|
|
275
283
|
// Awaited, not fired off: a spawn that rejects must mean nothing was
|
|
276
|
-
// left running, which is what a throw from `
|
|
284
|
+
// left running, which is what a throw from `processes().spawn` means on
|
|
277
285
|
// the other substrate.
|
|
278
286
|
try {
|
|
279
287
|
await this._cancel(params.workerKey, placement.peerName);
|