@nimbus-sh/fabric 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (103) hide show
  1. package/README.md +84 -55
  2. package/dist/bindings.js +5 -5
  3. package/dist/budgets.d.ts +132 -0
  4. package/dist/budgets.d.ts.map +1 -0
  5. package/dist/budgets.js +248 -0
  6. package/dist/composition.d.ts +87 -0
  7. package/dist/composition.d.ts.map +1 -0
  8. package/dist/composition.js +76 -0
  9. package/dist/connections.d.ts +81 -0
  10. package/dist/connections.d.ts.map +1 -0
  11. package/dist/connections.js +114 -0
  12. package/dist/derived.d.ts +65 -0
  13. package/dist/derived.d.ts.map +1 -0
  14. package/dist/derived.js +95 -0
  15. package/dist/do-calls.d.ts +94 -0
  16. package/dist/do-calls.d.ts.map +1 -0
  17. package/dist/do-calls.js +111 -0
  18. package/dist/facet-pool.d.ts +90 -0
  19. package/dist/facet-pool.d.ts.map +1 -0
  20. package/dist/facet-pool.js +113 -0
  21. package/dist/{fanout-pool.d.ts → fanout.d.ts} +20 -20
  22. package/dist/fanout.d.ts.map +1 -0
  23. package/dist/{fanout-pool.js → fanout.js} +20 -20
  24. package/dist/{launch-journal.d.ts → fenced-work.d.ts} +25 -13
  25. package/dist/fenced-work.d.ts.map +1 -0
  26. package/dist/{launch-journal.js → fenced-work.js} +47 -13
  27. package/dist/generation.d.ts +69 -0
  28. package/dist/generation.d.ts.map +1 -0
  29. package/dist/generation.js +118 -0
  30. package/dist/{facet-image-store.d.ts → image-store.d.ts} +8 -8
  31. package/dist/image-store.d.ts.map +1 -0
  32. package/dist/{facet-image-store.js → image-store.js} +4 -4
  33. package/dist/index.d.ts +16 -8
  34. package/dist/index.d.ts.map +1 -1
  35. package/dist/index.js +16 -8
  36. package/dist/{loader-pool.d.ts → isolate-pool.d.ts} +19 -19
  37. package/dist/isolate-pool.d.ts.map +1 -0
  38. package/dist/{loader-pool.js → isolate-pool.js} +20 -20
  39. package/dist/journal.d.ts +111 -0
  40. package/dist/journal.d.ts.map +1 -0
  41. package/dist/journal.js +177 -0
  42. package/dist/outbox.d.ts +249 -0
  43. package/dist/outbox.d.ts.map +1 -0
  44. package/dist/outbox.js +355 -0
  45. package/dist/process-fabric.d.ts +2 -14
  46. package/dist/process-fabric.d.ts.map +1 -1
  47. package/dist/process-fabric.js +6 -15
  48. package/dist/process-host.d.ts +1 -1
  49. package/dist/process-host.d.ts.map +1 -1
  50. package/dist/process-host.js +10 -9
  51. package/dist/sealed.d.ts +78 -0
  52. package/dist/sealed.d.ts.map +1 -0
  53. package/dist/sealed.js +145 -0
  54. package/dist/timers.d.ts +138 -0
  55. package/dist/timers.d.ts.map +1 -0
  56. package/dist/timers.js +231 -0
  57. package/dist/{launch-pacer.d.ts → turn-budget.d.ts} +19 -21
  58. package/dist/turn-budget.d.ts.map +1 -0
  59. package/dist/{launch-pacer.js → turn-budget.js} +22 -11
  60. package/dist/workerd-facet-host.d.ts +28 -67
  61. package/dist/workerd-facet-host.d.ts.map +1 -1
  62. package/dist/workerd-facet-host.js +49 -171
  63. package/examples/agent-core-adapter.ts +191 -0
  64. package/package.json +4 -2
  65. package/src/bindings.ts +6 -6
  66. package/src/budgets.ts +308 -0
  67. package/src/composition.ts +127 -0
  68. package/src/connections.ts +140 -0
  69. package/src/derived.ts +135 -0
  70. package/src/do-calls.ts +156 -0
  71. package/src/facet-pool.ts +157 -0
  72. package/src/{fanout-pool.ts → fanout.ts} +35 -35
  73. package/src/{launch-journal.ts → fenced-work.ts} +58 -22
  74. package/src/generation.ts +144 -0
  75. package/src/{facet-image-store.ts → image-store.ts} +9 -9
  76. package/src/index.ts +16 -8
  77. package/src/{loader-pool.ts → isolate-pool.ts} +34 -34
  78. package/src/journal.ts +242 -0
  79. package/src/node-async-hooks.d.ts +14 -0
  80. package/src/outbox.ts +520 -0
  81. package/src/process-fabric.ts +6 -33
  82. package/src/process-host.ts +10 -15
  83. package/src/sealed.ts +150 -0
  84. package/src/timers.ts +294 -0
  85. package/src/{launch-pacer.ts → turn-budget.ts} +30 -24
  86. package/src/workerd-facet-host.ts +67 -193
  87. package/dist/alarms.d.ts +0 -134
  88. package/dist/alarms.d.ts.map +0 -1
  89. package/dist/alarms.js +0 -214
  90. package/dist/ctx-exports.d.ts +0 -47
  91. package/dist/ctx-exports.d.ts.map +0 -1
  92. package/dist/ctx-exports.js +0 -54
  93. package/dist/facet-image-store.d.ts.map +0 -1
  94. package/dist/fanout-pool.d.ts.map +0 -1
  95. package/dist/launch-journal.d.ts.map +0 -1
  96. package/dist/launch-pacer.d.ts.map +0 -1
  97. package/dist/loader-ledger.d.ts +0 -57
  98. package/dist/loader-ledger.d.ts.map +0 -1
  99. package/dist/loader-ledger.js +0 -91
  100. package/dist/loader-pool.d.ts.map +0 -1
  101. package/src/alarms.ts +0 -275
  102. package/src/ctx-exports.ts +0 -77
  103. package/src/loader-ledger.ts +0 -112
@@ -4,7 +4,7 @@
4
4
  *
5
5
  * A single Durable Object method can drive at most four concurrent
6
6
  * Worker Loader fetches before extra dispatches serialize or fail. Small
7
- * batches therefore run in the coordinator DO through LoaderPool.
7
+ * batches therefore run in the coordinator DO through IsolatePool.
8
8
  * Wider batches are sharded across sibling NimbusSession DOs, each of
9
9
  * which owns its own four-loader budget.
10
10
  *
@@ -13,7 +13,7 @@
13
13
  * missing LOADER or NIMBUS_SESSION bindings fail loudly so install and
14
14
  * runtime operations do not appear successful after partial dispatch.
15
15
  */
16
- import { type FacetTaskFn } from './loader-pool.js';
16
+ import { type FacetTaskFn } from './isolate-pool.js';
17
17
  import type { WorkerLoader } from './vendor/types.js';
18
18
  /**
19
19
  * The sibling-session namespace the peer-DO topology routes through. Ids are
@@ -24,7 +24,7 @@ interface PeerSessionNamespace {
24
24
  get(id: DurableObjectId): unknown;
25
25
  }
26
26
  /** The bindings a fan-out needs off the coordinator DO's env. */
27
- export interface FanoutPoolEnv {
27
+ export interface FanoutEnv {
28
28
  LOADER?: WorkerLoader;
29
29
  NIMBUS_SESSION?: PeerSessionNamespace;
30
30
  }
@@ -90,45 +90,45 @@ export interface FanoutTask<A> {
90
90
  /** Argument passed to the user fn. */
91
91
  args: A;
92
92
  }
93
- /** Options handed to FanoutPool's constructor. */
94
- export interface FanoutPoolOptions {
93
+ /** Options handed to Fanout's constructor. */
94
+ export interface FanoutOptions {
95
95
  /**
96
96
  * Tag prepended to peer-DO ids and in-DO loader ids for debugging
97
97
  * (e.g. "npm-install-batch"). Affects neither isolate identity (in-DO
98
- * path uses the existing LoaderPool's tag-fold) nor peer-DO
98
+ * path uses the existing IsolatePool's tag-fold) nor peer-DO
99
99
  * deterministic placement (peer ids fold tag + key).
100
100
  */
101
101
  tag: string;
102
102
  /**
103
103
  * Per-task timeout in ms. Default 60_000. Forwarded to the in-DO
104
- * LoaderPool's submit calls and to the peer-DO RPC's own
105
- * LoaderPool.
104
+ * IsolatePool's submit calls and to the peer-DO RPC's own
105
+ * IsolatePool.
106
106
  */
107
107
  timeoutMs?: number;
108
108
  /**
109
109
  * Preamble bundled into every facet (in-DO and inside each peer
110
- * DO). Same semantics as LoaderPool's preamble option.
110
+ * DO). Same semantics as IsolatePool's preamble option.
111
111
  */
112
112
  preamble?: string;
113
113
  /**
114
114
  * Wasm modules forwarded to every facet. Same semantics as
115
- * LoaderPool's wasmModules option.
115
+ * IsolatePool's wasmModules option.
116
116
  */
117
117
  wasmModules?: Record<string, ArrayBuffer>;
118
118
  /**
119
119
  * Extra bindings forwarded to every facet. Same semantics as
120
- * LoaderPool's extraBindings option.
120
+ * IsolatePool's extraBindings option.
121
121
  */
122
122
  extraBindings?: Record<string, unknown>;
123
123
  /**
124
124
  * If set, skip the supervisor-RPC binding injection (mirrors
125
- * LoaderPool's omitSupervisor flag).
125
+ * IsolatePool's omitSupervisor flag).
126
126
  */
127
127
  omitSupervisor?: boolean;
128
128
  /**
129
129
  * Invoking process pid, baked into each facet's SUPERVISOR binding so
130
130
  * filesystem RPCs (writeBatchStream) are authorized under the caller's
131
- * credential (mirrors LoaderPool's supervisorPid). Threaded to both
131
+ * credential (mirrors IsolatePool's supervisorPid). Threaded to both
132
132
  * the in-DO loader pool and, via `_rpcFanoutExecute`, the peer-DO pools.
133
133
  * npm install passes the shell command's `ctx.pid`; resolve leaves it 0.
134
134
  */
@@ -162,33 +162,33 @@ export interface FanoutPoolOptions {
162
162
  * exists primarily as a clean API surface; per-call dispatch state
163
163
  * lives only inside submitMany's promise.
164
164
  */
165
- export declare class FanoutPool {
165
+ export declare class Fanout {
166
166
  private readonly env;
167
167
  private readonly ctx;
168
168
  private readonly opts;
169
169
  private readonly coordDoId;
170
170
  private readonly coordDoIdShort;
171
- constructor(rawEnv: unknown, ctx: DurableObjectState, opts: FanoutPoolOptions);
171
+ constructor(rawEnv: unknown, ctx: DurableObjectState, opts: FanoutOptions);
172
172
  /**
173
173
  * Dispatch `tasks` across the appropriate topology and return
174
174
  * results in input order.
175
175
  *
176
176
  * Routing:
177
- * tasks.length < 5 -> coordinator-local LoaderPool
177
+ * tasks.length < 5 -> coordinator-local IsolatePool
178
178
  * tasks.length >= 5 -> sibling NimbusSession DOs
179
179
  *
180
180
  * Backpressure: if `tasks.length > MAX_PEER_FANOUT (32)`, tasks
181
181
  * are sharded modulo `MAX_PEER_FANOUT` and each shard's bucket
182
182
  * runs serially inside its assigned peer DO via the in-peer
183
- * LoaderPool's concurrency (capped at 4 there too). A
183
+ * IsolatePool's concurrency (capped at 4 there too). A
184
184
  * single submitMany call returns when ALL tasks complete (or any
185
185
  * throws).
186
186
  *
187
187
  * `fn` is the user function executed per task. It runs INSIDE a
188
188
  * Worker Loader isolate (in the in-DO path) or inside a peer DO's
189
189
  * Worker Loader isolate (in the peer-DO path); same trust posture
190
- * as LoaderPool.submit. The function is serialized via
191
- * the vendored serializeFunction (same as LoaderPool#prepare).
190
+ * as IsolatePool.submit. The function is serialized via
191
+ * the vendored serializeFunction (same as IsolatePool#prepare).
192
192
  */
193
193
  submitMany<A, R>(tasks: FanoutTask<A>[], fn: FacetTaskFn<A, R>): Promise<R[]>;
194
194
  /** Report which topology a task count uses without dispatching. */
@@ -220,4 +220,4 @@ export declare class FanoutPool {
220
220
  */
221
221
  export declare function hashKeyToShard(key: string, peerCount: number): number;
222
222
  export {};
223
- //# sourceMappingURL=fanout-pool.d.ts.map
223
+ //# sourceMappingURL=fanout.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fanout.d.ts","sourceRoot":"","sources":["../src/fanout.ts"],"names":[],"mappings":"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"}
@@ -4,7 +4,7 @@
4
4
  *
5
5
  * A single Durable Object method can drive at most four concurrent
6
6
  * Worker Loader fetches before extra dispatches serialize or fail. Small
7
- * batches therefore run in the coordinator DO through LoaderPool.
7
+ * batches therefore run in the coordinator DO through IsolatePool.
8
8
  * Wider batches are sharded across sibling NimbusSession DOs, each of
9
9
  * which owns its own four-loader budget.
10
10
  *
@@ -15,9 +15,9 @@
15
15
  */
16
16
  import { serializeFunction } from './vendor/serialize.js';
17
17
  import { BindingError } from './vendor/errors.js';
18
- import { LoaderPool } from './loader-pool.js';
19
- import { disposeRpcResource } from '@nimbus-sh/core/_shared/rpc-dispose.js';
20
- import { describeError, isDoOverloaded, isTransientDoReset } from '@nimbus-sh/core/observability/oom-classify.js';
18
+ import { IsolatePool } from './isolate-pool.js';
19
+ import { disposeRpcResource } from '@nimbus-sh/platform/rpc-dispose.js';
20
+ import { describeError, isDoOverloaded, isTransientDoReset } from '@nimbus-sh/platform/oom-classify.js';
21
21
  /**
22
22
  * Threshold at which routing switches from coordinator-local loaders to
23
23
  * sibling Durable Objects.
@@ -79,10 +79,10 @@ function isFanoutPeerStub(value) {
79
79
  }
80
80
  function fanoutPeerStub(value) {
81
81
  if ((typeof value !== 'object' && typeof value !== 'function') || value === null) {
82
- throw new BindingError('FanoutPool: NIMBUS_SESSION.get() did not return a peer stub.');
82
+ throw new BindingError('Fanout: NIMBUS_SESSION.get() did not return a peer stub.');
83
83
  }
84
84
  if (!isFanoutPeerStub(value)) {
85
- throw new BindingError('FanoutPool: peer stub does not expose _rpcFanoutExecute().');
85
+ throw new BindingError('Fanout: peer stub does not expose _rpcFanoutExecute().');
86
86
  }
87
87
  return value;
88
88
  }
@@ -95,7 +95,7 @@ function fanoutPeerStub(value) {
95
95
  * exists primarily as a clean API surface; per-call dispatch state
96
96
  * lives only inside submitMany's promise.
97
97
  */
98
- export class FanoutPool {
98
+ export class Fanout {
99
99
  env;
100
100
  ctx;
101
101
  opts;
@@ -104,12 +104,12 @@ export class FanoutPool {
104
104
  constructor(rawEnv, ctx, opts) {
105
105
  // A host hands its whole env over; the bindings are claimed here and the
106
106
  // LOADER claim is checked immediately. Hard-fail on a missing LOADER —
107
- // LoaderPool also enforces this, but checking up front points the
108
- // diagnostic at the fanout-pool construction site rather than the
109
- // deferred loader-pool one.
107
+ // IsolatePool also enforces this, but checking up front points the
108
+ // diagnostic at the fanout construction site rather than the
109
+ // deferred isolate-pool one.
110
110
  const env = rawEnv ?? {};
111
111
  if (!env.LOADER || typeof env.LOADER.get !== 'function') {
112
- throw new BindingError('FanoutPool: env.LOADER binding missing or invalid. ' +
112
+ throw new BindingError('Fanout: env.LOADER binding missing or invalid. ' +
113
113
  'Add a [[worker_loaders]] entry to wrangler.jsonc.');
114
114
  }
115
115
  this.env = env;
@@ -123,21 +123,21 @@ export class FanoutPool {
123
123
  * results in input order.
124
124
  *
125
125
  * Routing:
126
- * tasks.length < 5 -> coordinator-local LoaderPool
126
+ * tasks.length < 5 -> coordinator-local IsolatePool
127
127
  * tasks.length >= 5 -> sibling NimbusSession DOs
128
128
  *
129
129
  * Backpressure: if `tasks.length > MAX_PEER_FANOUT (32)`, tasks
130
130
  * are sharded modulo `MAX_PEER_FANOUT` and each shard's bucket
131
131
  * runs serially inside its assigned peer DO via the in-peer
132
- * LoaderPool's concurrency (capped at 4 there too). A
132
+ * IsolatePool's concurrency (capped at 4 there too). A
133
133
  * single submitMany call returns when ALL tasks complete (or any
134
134
  * throws).
135
135
  *
136
136
  * `fn` is the user function executed per task. It runs INSIDE a
137
137
  * Worker Loader isolate (in the in-DO path) or inside a peer DO's
138
138
  * Worker Loader isolate (in the peer-DO path); same trust posture
139
- * as LoaderPool.submit. The function is serialized via
140
- * the vendored serializeFunction (same as LoaderPool#prepare).
139
+ * as IsolatePool.submit. The function is serialized via
140
+ * the vendored serializeFunction (same as IsolatePool#prepare).
141
141
  */
142
142
  async submitMany(tasks, fn) {
143
143
  if (tasks.length === 0)
@@ -166,12 +166,12 @@ export class FanoutPool {
166
166
  }
167
167
  // ── Private: in-DO dispatch (in-DO fanout) ──────────────────────────────
168
168
  async _dispatchInDo(tasks, fn) {
169
- // Use the existing LoaderPool. Concurrency = task count
169
+ // Use the existing IsolatePool. Concurrency = task count
170
170
  // (capped at 4 by constructor — tasks.length is already < 5
171
171
  // here, so the cap won't bite). Each task = one pool.submit;
172
172
  // pool.map runs them with stable-slot reuse.
173
173
  const concurrency = Math.min(tasks.length, IN_DO_THRESHOLD - 1);
174
- const pool = new LoaderPool(this.env, this.ctx, {
174
+ const pool = new IsolatePool(this.env, this.ctx, {
175
175
  concurrency,
176
176
  timeoutMs: this.opts.timeoutMs,
177
177
  tag: this.opts.tag,
@@ -203,7 +203,7 @@ export class FanoutPool {
203
203
  async _dispatchPeerDo(tasks, fn) {
204
204
  const ns = this.env?.NIMBUS_SESSION;
205
205
  if (!ns || typeof ns.idFromName !== 'function' || typeof ns.get !== 'function') {
206
- throw new BindingError('FanoutPool: env.NIMBUS_SESSION binding missing or invalid. ' +
206
+ throw new BindingError('Fanout: env.NIMBUS_SESSION binding missing or invalid. ' +
207
207
  'The peer-DO topology requires it. ' +
208
208
  'Add the binding via durable_objects.bindings in wrangler.jsonc.');
209
209
  }
@@ -214,7 +214,7 @@ export class FanoutPool {
214
214
  const fnSource = serializeFunction(fn);
215
215
  // Cap peer count at MAX_PEER_FANOUT. Tasks beyond N=32 are
216
216
  // bucketed into existing shards — each shard's peer DO then
217
- // runs its bucket through its in-DO LoaderPool.map
217
+ // runs its bucket through its in-DO IsolatePool.map
218
218
  // (concurrency capped at 4 there).
219
219
  const peerCount = Math.min(tasks.length, this.opts.maxPeers ?? MAX_PEER_FANOUT);
220
220
  // Group tasks by deterministic shard. Same key → same shard, so
@@ -266,7 +266,7 @@ export class FanoutPool {
266
266
  extraBindings: this.opts.extraBindings,
267
267
  omitSupervisor: this.opts.omitSupervisor,
268
268
  // INSTALL-HONESTY: forward the COORDINATOR's full doId so
269
- // the peer's LoaderPool can mint a SUPERVISOR
269
+ // the peer's IsolatePool can mint a SUPERVISOR
270
270
  // binding that routes back HERE (the user's session DO),
271
271
  // not to the peer DO itself. Without this, peer DOs'
272
272
  // env.SUPERVISOR.writeBatch / writeBatchStream / stdout /
@@ -1,5 +1,5 @@
1
1
  /**
2
- * launch-journal.ts — durable record of the resident launches a Durable Object
2
+ * fenced-work.ts — durable record of the resident launches a Durable Object
3
3
  * owes, and their recovery after an instance reset.
4
4
  *
5
5
  * The platform resets a session Durable Object over what one turn has
@@ -14,7 +14,7 @@
14
14
  *
15
15
  * What a launch IS stays the embedder's: the journal stores the record it is
16
16
  * given and hands it back on recovery. The mechanism reads only the fields in
17
- * {@link ResidentLaunchRecord}; everything else in the record rides through
17
+ * {@link FencedWorkRecord}; everything else in the record rides through
18
18
  * opaquely.
19
19
  */
20
20
  /**
@@ -36,9 +36,9 @@
36
36
  * storage key is a migration, and orphaned rows are the least of what it
37
37
  * breaks.
38
38
  */
39
- export declare const RESIDENT_LAUNCH_KEY_PREFIX = "resident-launch:";
39
+ export declare const FENCED_WORK_KEY_PREFIX = "resident-launch:";
40
40
  /** A launch is re-driven once. A reset that recurs is not the transient one. */
41
- export declare const RESIDENT_LAUNCH_MAX_ATTEMPT = 1;
41
+ export declare const FENCED_WORK_MAX_ATTEMPT = 1;
42
42
  /**
43
43
  * A resident process this session owes the user, as a later instance would
44
44
  * have to re-drive it.
@@ -57,7 +57,7 @@ export declare const RESIDENT_LAUNCH_MAX_ATTEMPT = 1;
57
57
  * process host's held-open leg dies with it), so a row from a previous
58
58
  * generation always names a process that is genuinely gone.
59
59
  */
60
- export interface ResidentLaunchRecord {
60
+ export interface FencedWorkRecord {
61
61
  pid: number;
62
62
  command: string;
63
63
  /** 0 for a launch the user asked for; 1 for the one re-drive it may get. */
@@ -70,9 +70,9 @@ export interface ResidentLaunchRecord {
70
70
  /**
71
71
  * The slice of Durable Object storage the journal writes through. Exactly a
72
72
  * `DurableObjectStorage`, narrowed to what the mechanism performs — `sync()`
73
- * is load-bearing, see {@link ResidentLaunchJournal.journal}.
73
+ * is load-bearing, see {@link FencedWork.journal}.
74
74
  */
75
- export interface LaunchJournalStorage {
75
+ export interface FencedWorkStorage {
76
76
  put(key: string, value: unknown): Promise<void>;
77
77
  delete(key: string): Promise<boolean>;
78
78
  list<T = unknown>(options: {
@@ -81,7 +81,7 @@ export interface LaunchJournalStorage {
81
81
  sync(): Promise<void>;
82
82
  }
83
83
  /** What the journal's recovery needs from its embedder. */
84
- export interface LaunchJournalHost<R extends ResidentLaunchRecord> {
84
+ export interface FencedWorkHost<R extends FencedWorkRecord> {
85
85
  /**
86
86
  * The current instance generation's pid floor. A pid at or below it was
87
87
  * allocated by a PREVIOUS instance (core's process-table, PID_GEN_STRIDE),
@@ -91,8 +91,13 @@ export interface LaunchJournalHost<R extends ResidentLaunchRecord> {
91
91
  */
92
92
  generationBase(): number;
93
93
  /**
94
- * Root a recovery re-drive on the instance (`ctx.waitUntil`) so it is not
95
- * an abandoned promise between turns.
94
+ * Where an un-awaited re-drive goes so it is not a floating rejection —
95
+ * `ctx.waitUntil` in practice. Hygiene, not retention: a Durable Object
96
+ * cancels an in-flight promise on reset with no signal, and workerd's
97
+ * `waitUntil` is a no-op there (PLATFORM.md, probe 2026-08-17 — awaiting
98
+ * inside the invocation is the only retention an actor has). The recovery
99
+ * guarantee comes from the journal row and the platform's alarm
100
+ * re-delivery, never from this hook.
96
101
  */
97
102
  waitUntil(promise: Promise<unknown>): void;
98
103
  /**
@@ -117,7 +122,7 @@ export interface LaunchJournalHost<R extends ResidentLaunchRecord> {
117
122
  * read the journal a reset leaves behind. Rows from a previous instance are
118
123
  * recovery's to consume, never the release path's.
119
124
  */
120
- export declare class ResidentLaunchJournal<R extends ResidentLaunchRecord> {
125
+ export declare class FencedWork<R extends FencedWorkRecord> {
121
126
  private readonly storage;
122
127
  private readonly host;
123
128
  /**
@@ -128,7 +133,7 @@ export declare class ResidentLaunchJournal<R extends ResidentLaunchRecord> {
128
133
  private journalledPids;
129
134
  /** Whether this instance has already read the journal a reset leaves behind. */
130
135
  private recovered;
131
- constructor(storage: LaunchJournalStorage, host: LaunchJournalHost<R>);
136
+ constructor(storage: FencedWorkStorage, host: FencedWorkHost<R>);
132
137
  /**
133
138
  * Record a launch as in flight, so an instance that replaces this one knows
134
139
  * it never finished. Best-effort: a launch that cannot be journalled still
@@ -166,5 +171,12 @@ export declare class ResidentLaunchJournal<R extends ResidentLaunchRecord> {
166
171
  * instance starts or settles, and those are rows this instance wrote.
167
172
  */
168
173
  recoverInterrupted(): Promise<void>;
174
+ /**
175
+ * Delete a row whose re-drive has settled — succeeded, failed and been
176
+ * reported, or handed off to its own journal row. Not `release()`: that
177
+ * path is for pids THIS instance journalled, and this row's pid belongs to
178
+ * a previous generation the terminal hook will never fire for.
179
+ */
180
+ private supersede;
169
181
  }
170
- //# sourceMappingURL=launch-journal.d.ts.map
182
+ //# sourceMappingURL=fenced-work.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fenced-work.d.ts","sourceRoot":"","sources":["../src/fenced-work.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH;;;;;;;;;;;;;;;;;;GAkBG;AACH,eAAO,MAAM,sBAAsB,qBAAqB,CAAC;AAEzD,gFAAgF;AAChF,eAAO,MAAM,uBAAuB,IAAI,CAAC;AAEzC;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,WAAW,gBAAgB;IAC/B,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,MAAM,CAAC;IAChB,4EAA4E;IAC5E,OAAO,EAAE,MAAM,CAAC;IAChB;;4DAEwD;IACxD,KAAK,EAAE,UAAU,GAAG,SAAS,CAAC;CAC/B;AAED;;;;GAIG;AACH,MAAM,WAAW,iBAAiB;IAChC,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAChD,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IACtC,IAAI,CAAC,CAAC,GAAG,OAAO,EAAE,OAAO,EAAE;QAAE,MAAM,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,CAAC;IACxE,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACvB;AAED,2DAA2D;AAC3D,MAAM,WAAW,cAAc,CAAC,CAAC,SAAS,gBAAgB;IACxD;;;;;;OAMG;IACH,cAAc,IAAI,MAAM,CAAC;IACzB;;;;;;;;OAQG;IACH,SAAS,CAAC,OAAO,EAAE,OAAO,CAAC,OAAO,CAAC,GAAG,IAAI,CAAC;IAC3C;;;;;OAKG;IACH,OAAO,CAAC,MAAM,EAAE,CAAC,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IACtD,mDAAmD;IACnD,SAAS,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,IAAI,CAAC;IAC5B,yEAAyE;IACzE,WAAW,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,IAAI,CAAC;IAC9B,kCAAkC;IAClC,eAAe,CAAC,CAAC,MAAM,EAAE,CAAC,EAAE,KAAK,EAAE,OAAO,GAAG,IAAI,CAAC;CACnD;AAED;;;;;;;GAOG;AACH,qBAAa,UAAU,CAAC,CAAC,SAAS,gBAAgB;IAW9C,OAAO,CAAC,QAAQ,CAAC,OAAO;IACxB,OAAO,CAAC,QAAQ,CAAC,IAAI;IAXvB;;;;OAIG;IACH,OAAO,CAAC,cAAc,CAAqB;IAC3C,gFAAgF;IAChF,OAAO,CAAC,SAAS,CAAS;gBAGP,OAAO,EAAE,iBAAiB,EAC1B,IAAI,EAAE,cAAc,CAAC,CAAC,CAAC;IAG1C;;;;;;;;;;;;;;OAcG;IACG,OAAO,CAAC,MAAM,EAAE,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC;IAUvC,8DAA8D;IAC9D,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO;IAIzB;;;;OAIG;IACG,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAUzC;;;;;;;;;;;OAWG;IACG,kBAAkB,IAAI,OAAO,CAAC,IAAI,CAAC;IA8CzC;;;;;OAKG;YACW,SAAS;CAQxB"}
@@ -1,5 +1,5 @@
1
1
  /**
2
- * launch-journal.ts — durable record of the resident launches a Durable Object
2
+ * fenced-work.ts — durable record of the resident launches a Durable Object
3
3
  * owes, and their recovery after an instance reset.
4
4
  *
5
5
  * The platform resets a session Durable Object over what one turn has
@@ -14,7 +14,7 @@
14
14
  *
15
15
  * What a launch IS stays the embedder's: the journal stores the record it is
16
16
  * given and hands it back on recovery. The mechanism reads only the fields in
17
- * {@link ResidentLaunchRecord}; everything else in the record rides through
17
+ * {@link FencedWorkRecord}; everything else in the record rides through
18
18
  * opaquely.
19
19
  */
20
20
  /**
@@ -36,9 +36,9 @@
36
36
  * storage key is a migration, and orphaned rows are the least of what it
37
37
  * breaks.
38
38
  */
39
- export const RESIDENT_LAUNCH_KEY_PREFIX = 'resident-launch:';
39
+ export const FENCED_WORK_KEY_PREFIX = 'resident-launch:';
40
40
  /** A launch is re-driven once. A reset that recurs is not the transient one. */
41
- export const RESIDENT_LAUNCH_MAX_ATTEMPT = 1;
41
+ export const FENCED_WORK_MAX_ATTEMPT = 1;
42
42
  /**
43
43
  * The resident-launch journal of one Durable Object instance.
44
44
  *
@@ -47,7 +47,7 @@ export const RESIDENT_LAUNCH_MAX_ATTEMPT = 1;
47
47
  * read the journal a reset leaves behind. Rows from a previous instance are
48
48
  * recovery's to consume, never the release path's.
49
49
  */
50
- export class ResidentLaunchJournal {
50
+ export class FencedWork {
51
51
  storage;
52
52
  host;
53
53
  /**
@@ -80,7 +80,7 @@ export class ResidentLaunchJournal {
80
80
  async journal(record) {
81
81
  try {
82
82
  this.journalledPids.add(record.pid);
83
- await this.storage.put(`${RESIDENT_LAUNCH_KEY_PREFIX}${record.pid}`, record);
83
+ await this.storage.put(`${FENCED_WORK_KEY_PREFIX}${record.pid}`, record);
84
84
  await this.storage.sync();
85
85
  }
86
86
  catch (e) {
@@ -100,7 +100,7 @@ export class ResidentLaunchJournal {
100
100
  if (!this.journalledPids.delete(pid))
101
101
  return;
102
102
  try {
103
- await this.storage.delete(`${RESIDENT_LAUNCH_KEY_PREFIX}${pid}`);
103
+ await this.storage.delete(`${FENCED_WORK_KEY_PREFIX}${pid}`);
104
104
  await this.storage.sync();
105
105
  }
106
106
  catch (e) {
@@ -123,8 +123,10 @@ export class ResidentLaunchJournal {
123
123
  if (this.recovered)
124
124
  return;
125
125
  this.recovered = true;
126
- const journal = await this.storage.list({ prefix: RESIDENT_LAUNCH_KEY_PREFIX });
126
+ const journal = await this.storage.list({ prefix: FENCED_WORK_KEY_PREFIX });
127
127
  const base = this.host.generationBase();
128
+ const abandoned = [];
129
+ const redriven = [];
128
130
  for (const [key, record] of journal) {
129
131
  // A pid at or below this instance's base was allocated by a PREVIOUS one
130
132
  // (process-table.ts, PID_GEN_STRIDE), so its launch never finished; above
@@ -132,11 +134,27 @@ export class ResidentLaunchJournal {
132
134
  // `session/rpc.ts` uses to attribute a prior generation's pid.
133
135
  if (!(record.pid > 0 && record.pid <= base))
134
136
  continue;
137
+ (record.attempt >= FENCED_WORK_MAX_ATTEMPT ? abandoned : redriven).push([key, record]);
138
+ }
139
+ // The attempt is SPENT in storage, synced, before any re-drive starts.
140
+ // Deleting the row here instead would open a loss window: writes flush in
141
+ // order, so a reset between the delete's flush and the re-driven launch's
142
+ // own journal write leaves a durable state with no row for an owed
143
+ // launch — and the un-awaited re-drive dies with the instance (waitUntil
144
+ // retains nothing). With the rewrite, every durable cut is either the
145
+ // untouched row (recovery re-runs) or a spent attempt (the recurrence
146
+ // abandons loudly). The superseded row is deleted only when its re-drive
147
+ // settles, after the launch's own row exists.
148
+ for (const [key] of abandoned)
135
149
  await this.storage.delete(key);
136
- if (record.attempt >= RESIDENT_LAUNCH_MAX_ATTEMPT) {
137
- this.host.onAbandoned?.(record);
138
- continue;
139
- }
150
+ for (const [key, record] of redriven) {
151
+ await this.storage.put(key, { ...record, attempt: record.attempt + 1 });
152
+ }
153
+ if (abandoned.length > 0 || redriven.length > 0)
154
+ await this.storage.sync();
155
+ for (const [, record] of abandoned)
156
+ this.host.onAbandoned?.(record);
157
+ for (const [key, record] of redriven) {
140
158
  this.host.onRedrive?.(record);
141
159
  // Not awaited: this call is running inside the alarm that granted the
142
160
  // turn, and the launch it starts asks for turns of its own through that
@@ -145,7 +163,23 @@ export class ResidentLaunchJournal {
145
163
  this.host.waitUntil(this.host.redrive(record, record.attempt + 1)
146
164
  .catch((e) => {
147
165
  this.host.onRedriveFailed?.(record, e);
148
- }));
166
+ })
167
+ .then(() => this.supersede(key)));
168
+ }
169
+ }
170
+ /**
171
+ * Delete a row whose re-drive has settled — succeeded, failed and been
172
+ * reported, or handed off to its own journal row. Not `release()`: that
173
+ * path is for pids THIS instance journalled, and this row's pid belongs to
174
+ * a previous generation the terminal hook will never fire for.
175
+ */
176
+ async supersede(key) {
177
+ try {
178
+ await this.storage.delete(key);
179
+ await this.storage.sync();
180
+ }
181
+ catch (e) {
182
+ console.warn('[nimbus] resident launch journal supersede failed:', errorMessage(e));
149
183
  }
150
184
  }
151
185
  }
@@ -0,0 +1,69 @@
1
+ /**
2
+ * generation.ts — the isolate-generation clock, and the deferred
3
+ * reconciliation that runs once per fresh incarnation.
4
+ *
5
+ * Workerd hibernates Durable Objects between requests to free memory. On
6
+ * wake, the new isolate must rebuild its in-memory state from SQL — but it
7
+ * also needs to know "is this the same lifecycle as before, or did workerd
8
+ * recycle me?" That distinction matters for recovery (warmJoin vs cold init)
9
+ * and is captured by the isolate generation, a counter persisted across
10
+ * hibernations.
11
+ *
12
+ * State is keyed weakly off the actor's `ctx`, which lives exactly as long as
13
+ * the incarnation: a fresh isolate gets a fresh `ctx`, adopts the persisted
14
+ * counter, and bumps it once. The embedder reads `generation(ctx)` wherever it
15
+ * needs the incarnation number — one source of truth instead of mirrored host
16
+ * fields.
17
+ */
18
+ /**
19
+ * Storage key for the isolate-generation counter (cold-start +
20
+ * post-hibernation wake; one increment per fresh isolate).
21
+ *
22
+ * The VALUE is live production DO storage ('w9_isolate_gen') and must never
23
+ * change — renaming a storage key is a migration, and orphaned rows are the
24
+ * least of what it breaks.
25
+ */
26
+ export declare const GENERATION_KEY = "w9_isolate_gen";
27
+ /** The storage the generation counter persists through. */
28
+ export interface GenerationStorage {
29
+ get(key: string): Promise<unknown>;
30
+ put(key: string, value: unknown): Promise<void>;
31
+ }
32
+ /** The hosting actor's context, as the generation clock reads it. */
33
+ export interface GenerationContext {
34
+ storage: GenerationStorage;
35
+ }
36
+ /** This incarnation's generation. Zero until {@link adoptGeneration} ran. */
37
+ export declare function generation(ctx: object): number;
38
+ /** Increment + persist the generation counter once per fresh isolate. */
39
+ export declare function adoptGeneration(ctx: GenerationContext): Promise<void>;
40
+ /**
41
+ * Take on a generation without persisting it, and clear the adopted guard so
42
+ * a later {@link adoptGeneration} re-derives from storage. The
43
+ * destroy-and-recreate path uses this: it wipes storage, re-persists the
44
+ * pre-destroy counter, and runs the rest of this incarnation on the successor
45
+ * generation the next boot will derive.
46
+ */
47
+ export declare function assumeGeneration(ctx: object, value: number): void;
48
+ /**
49
+ * Queue deferred async reconciliation for this incarnation.
50
+ *
51
+ * The task runs on the first {@link runColdStart} after registration — a turn
52
+ * the embedder already owns, NEVER the constructor's init gate. Awaiting
53
+ * recovery on the gate path is the trap this helper exists to avoid: the gate
54
+ * blocks every request to the object, and reconciliation wants a filesystem
55
+ * and a terminal that only a later turn has. The gate is also a wall, not
56
+ * just a stall: a `blockConcurrencyWhile` callback still pending at ~30 s
57
+ * (BLOCK_CONCURRENCY_CANCEL_MS, proven by probe) is cancelled and RESETS the
58
+ * object with every queued event — so never call {@link runColdStart} from
59
+ * inside one; the pump and the embedder's own turns are the places it runs.
60
+ */
61
+ export declare function onColdStart(ctx: object, task: () => Promise<unknown>): void;
62
+ /**
63
+ * Drain the queued cold-start tasks, serialized, each awaited so the turn
64
+ * that runs them pays for them. Idempotent between registrations: a drained
65
+ * queue is a cheap no-op, and a task registered after a drain runs on the
66
+ * next call.
67
+ */
68
+ export declare function runColdStart(ctx: object): Promise<void>;
69
+ //# sourceMappingURL=generation.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"generation.d.ts","sourceRoot":"","sources":["../src/generation.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAIH;;;;;;;GAOG;AACH,eAAO,MAAM,cAAc,mBAAmB,CAAC;AAE/C,2DAA2D;AAC3D,MAAM,WAAW,iBAAiB;IAChC,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IACnC,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACjD;AAED,qEAAqE;AACrE,MAAM,WAAW,iBAAiB;IAChC,OAAO,EAAE,iBAAiB,CAAC;CAC5B;AAsBD,6EAA6E;AAC7E,wBAAgB,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAE9C;AAED,yEAAyE;AACzE,wBAAsB,eAAe,CAAC,GAAG,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC,CA0B3E;AAED;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAIjE;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,GAAG,IAAI,CAE3E;AAED;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAYvD"}