@nimbus-sh/fabric 0.1.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 (75) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +487 -0
  3. package/dist/alarms.d.ts +134 -0
  4. package/dist/alarms.d.ts.map +1 -0
  5. package/dist/alarms.js +214 -0
  6. package/dist/bindings.d.ts +316 -0
  7. package/dist/bindings.d.ts.map +1 -0
  8. package/dist/bindings.js +678 -0
  9. package/dist/ctx-exports.d.ts +47 -0
  10. package/dist/ctx-exports.d.ts.map +1 -0
  11. package/dist/ctx-exports.js +54 -0
  12. package/dist/facet-image-store.d.ts +112 -0
  13. package/dist/facet-image-store.d.ts.map +1 -0
  14. package/dist/facet-image-store.js +181 -0
  15. package/dist/fanout-pool.d.ts +223 -0
  16. package/dist/fanout-pool.d.ts.map +1 -0
  17. package/dist/fanout-pool.js +368 -0
  18. package/dist/index.d.ts +26 -0
  19. package/dist/index.d.ts.map +1 -0
  20. package/dist/index.js +25 -0
  21. package/dist/inner-do-registry.d.ts +41 -0
  22. package/dist/inner-do-registry.d.ts.map +1 -0
  23. package/dist/inner-do-registry.js +51 -0
  24. package/dist/launch-journal.d.ts +170 -0
  25. package/dist/launch-journal.d.ts.map +1 -0
  26. package/dist/launch-journal.js +154 -0
  27. package/dist/launch-pacer.d.ts +173 -0
  28. package/dist/launch-pacer.d.ts.map +1 -0
  29. package/dist/launch-pacer.js +193 -0
  30. package/dist/loader-ledger.d.ts +57 -0
  31. package/dist/loader-ledger.d.ts.map +1 -0
  32. package/dist/loader-ledger.js +91 -0
  33. package/dist/loader-pool.d.ts +315 -0
  34. package/dist/loader-pool.d.ts.map +1 -0
  35. package/dist/loader-pool.js +666 -0
  36. package/dist/process-fabric.d.ts +524 -0
  37. package/dist/process-fabric.d.ts.map +1 -0
  38. package/dist/process-fabric.js +388 -0
  39. package/dist/process-host.d.ts +132 -0
  40. package/dist/process-host.d.ts.map +1 -0
  41. package/dist/process-host.js +444 -0
  42. package/dist/vendor/errors.d.ts +24 -0
  43. package/dist/vendor/errors.d.ts.map +1 -0
  44. package/dist/vendor/errors.js +46 -0
  45. package/dist/vendor/serialize.d.ts +3 -0
  46. package/dist/vendor/serialize.d.ts.map +1 -0
  47. package/dist/vendor/serialize.js +25 -0
  48. package/dist/vendor/types.d.ts +69 -0
  49. package/dist/vendor/types.d.ts.map +1 -0
  50. package/dist/vendor/types.js +4 -0
  51. package/dist/workerd-facet-host.d.ts +207 -0
  52. package/dist/workerd-facet-host.d.ts.map +1 -0
  53. package/dist/workerd-facet-host.js +508 -0
  54. package/dist/ws-hibernation-config.d.ts +73 -0
  55. package/dist/ws-hibernation-config.d.ts.map +1 -0
  56. package/dist/ws-hibernation-config.js +93 -0
  57. package/package.json +62 -0
  58. package/src/alarms.ts +275 -0
  59. package/src/bindings.ts +871 -0
  60. package/src/ctx-exports.ts +77 -0
  61. package/src/facet-image-store.ts +196 -0
  62. package/src/fanout-pool.ts +503 -0
  63. package/src/index.ts +26 -0
  64. package/src/inner-do-registry.ts +58 -0
  65. package/src/launch-journal.ts +229 -0
  66. package/src/launch-pacer.ts +231 -0
  67. package/src/loader-ledger.ts +112 -0
  68. package/src/loader-pool.ts +984 -0
  69. package/src/process-fabric.ts +729 -0
  70. package/src/process-host.ts +566 -0
  71. package/src/vendor/errors.ts +56 -0
  72. package/src/vendor/serialize.ts +37 -0
  73. package/src/vendor/types.ts +75 -0
  74. package/src/workerd-facet-host.ts +694 -0
  75. package/src/ws-hibernation-config.ts +123 -0
@@ -0,0 +1,170 @@
1
+ /**
2
+ * launch-journal.ts — durable record of the resident launches a Durable Object
3
+ * owes, and their recovery after an instance reset.
4
+ *
5
+ * The platform resets a session Durable Object over what one turn has
6
+ * outstanding in storage ("Internal error in Durable Object storage caused
7
+ * object to be reset"), and a resident launch is the largest writer a session
8
+ * has. Everything a launch holds is in memory, so the process it is building
9
+ * and the terminal watching it both go with the instance — the journal is what
10
+ * a LATER instance reads to know that happened, and this module is the whole
11
+ * of that mechanism: the put→sync durability barrier on the way in, the
12
+ * delete→sync release on the way out, and the once-per-instance recovery pump
13
+ * that re-drives what a previous generation left behind.
14
+ *
15
+ * What a launch IS stays the embedder's: the journal stores the record it is
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
18
+ * opaquely.
19
+ */
20
+ /**
21
+ * Prefix for the resident-process journal: one row per resident this session
22
+ * owes the user, keyed by the pid it was built for.
23
+ *
24
+ * A resident holds its state in memory — the process table entry, the facet
25
+ * handle, the terminal — so an instance reset destroys it silently. The row
26
+ * is what a LATER instance reads to know a resident ended that way rather
27
+ * than on purpose: a pid at or below the reader's own pid base was allocated
28
+ * by a previous generation (PID_GEN_STRIDE, core's process-table). Written
29
+ * (and synced) before the launch's first byte of work, rewritten as `running`
30
+ * when the launch settles, and released only when the PROCESS ends — because
31
+ * the resets this row survives strike after the launch as often as during it
32
+ * (measured live, staging 2026-08-13: every observed reset landed seconds
33
+ * AFTER settle).
34
+ *
35
+ * The VALUE is live production DO storage and must never change — renaming a
36
+ * storage key is a migration, and orphaned rows are the least of what it
37
+ * breaks.
38
+ */
39
+ export declare const RESIDENT_LAUNCH_KEY_PREFIX = "resident-launch:";
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;
42
+ /**
43
+ * A resident process this session owes the user, as a later instance would
44
+ * have to re-drive it.
45
+ *
46
+ * The launch's own inputs and nothing derived from them: everything a launch
47
+ * builds is a pure function of these, and the images it writes are content-
48
+ * addressed, so re-driving is the same work again rather than a repair. The
49
+ * inputs themselves are the embedder's — a record type extends this base with
50
+ * whatever its `redrive` needs, and the journal never reads those fields.
51
+ *
52
+ * The row lives for the PROCESS's lifetime, not the launch's. Measured live
53
+ * (staging, 2026-08-13): every observed reset struck seconds AFTER the launch
54
+ * settled — the platform kills the object while the resident runs, which is
55
+ * when a launch-scoped row had already been deleted and recovery had nothing
56
+ * to find. A resident's facet cannot outlive its session instance (the
57
+ * process host's held-open leg dies with it), so a row from a previous
58
+ * generation always names a process that is genuinely gone.
59
+ */
60
+ export interface ResidentLaunchRecord {
61
+ pid: number;
62
+ command: string;
63
+ /** 0 for a launch the user asked for; 1 for the one re-drive it may get. */
64
+ attempt: number;
65
+ /** Where the resident was when its instance died: still being built, or
66
+ * booted and running. Running residents re-drive with a fresh attempt
67
+ * budget — their launch already proved itself once. */
68
+ phase: 'starting' | 'running';
69
+ }
70
+ /**
71
+ * The slice of Durable Object storage the journal writes through. Exactly a
72
+ * `DurableObjectStorage`, narrowed to what the mechanism performs — `sync()`
73
+ * is load-bearing, see {@link ResidentLaunchJournal.journal}.
74
+ */
75
+ export interface LaunchJournalStorage {
76
+ put(key: string, value: unknown): Promise<void>;
77
+ delete(key: string): Promise<boolean>;
78
+ list<T = unknown>(options: {
79
+ prefix: string;
80
+ }): Promise<Map<string, T>>;
81
+ sync(): Promise<void>;
82
+ }
83
+ /** What the journal's recovery needs from its embedder. */
84
+ export interface LaunchJournalHost<R extends ResidentLaunchRecord> {
85
+ /**
86
+ * The current instance generation's pid floor. A pid at or below it was
87
+ * allocated by a PREVIOUS instance (core's process-table, PID_GEN_STRIDE),
88
+ * so its launch never finished; above it is this instance's own, still
89
+ * running. The journal takes the base rather than the predicate so the one
90
+ * definition of what a prior-generation pid is stays in the process table.
91
+ */
92
+ generationBase(): number;
93
+ /**
94
+ * Root a recovery re-drive on the instance (`ctx.waitUntil`) so it is not
95
+ * an abandoned promise between turns.
96
+ */
97
+ waitUntil(promise: Promise<unknown>): void;
98
+ /**
99
+ * Re-drive an interrupted launch from its journalled inputs. `attempt` is
100
+ * the budget the re-drive spends — the mechanism computes it, the embedder
101
+ * carries it into the launch it starts. The result is discarded: a re-drive
102
+ * owns its own process, and nobody is waiting on the pid it allocates.
103
+ */
104
+ redrive(record: R, attempt: number): Promise<unknown>;
105
+ /** A re-drive is being started for this record. */
106
+ onRedrive?(record: R): void;
107
+ /** The record's re-drive budget is spent; the resident stays stopped. */
108
+ onAbandoned?(record: R): void;
109
+ /** The re-drive itself failed. */
110
+ onRedriveFailed?(record: R, error: unknown): void;
111
+ }
112
+ /**
113
+ * The resident-launch journal of one Durable Object instance.
114
+ *
115
+ * In-memory state here is per-instance on purpose: `journalledPids` tracks the
116
+ * rows THIS instance wrote, and `recovered` whether this instance has already
117
+ * read the journal a reset leaves behind. Rows from a previous instance are
118
+ * recovery's to consume, never the release path's.
119
+ */
120
+ export declare class ResidentLaunchJournal<R extends ResidentLaunchRecord> {
121
+ private readonly storage;
122
+ private readonly host;
123
+ /**
124
+ * Pids THIS instance holds journal rows for. What keeps the terminal hook —
125
+ * which fires for every process, shells and one-shots included — from
126
+ * paying a storage delete for pids that never had a row.
127
+ */
128
+ private journalledPids;
129
+ /** Whether this instance has already read the journal a reset leaves behind. */
130
+ private recovered;
131
+ constructor(storage: LaunchJournalStorage, host: LaunchJournalHost<R>);
132
+ /**
133
+ * Record a launch as in flight, so an instance that replaces this one knows
134
+ * it never finished. Best-effort: a launch that cannot be journalled still
135
+ * runs, and a reset then costs exactly what it cost before the journal.
136
+ *
137
+ * Synced, not merely put: `await put()` resolves before durability, and the
138
+ * reset this journal exists for destroys every write its turn still had
139
+ * outstanding — measured live, a launch killed in its first chunks left NO
140
+ * row for the replacement instance to find, which is how the recovery this
141
+ * feeds sat inert while its own test stayed green. `sync()` is the storage
142
+ * layer's durability barrier: the row is on disk before the launch performs
143
+ * its first byte of real work. What remains is a reset between the put and
144
+ * the sync's completion — and a launch that dies there has not started, so
145
+ * losing its row costs a retype, not a recovery.
146
+ */
147
+ journal(record: R): Promise<void>;
148
+ /** True while this instance holds a journal row for `pid`. */
149
+ has(pid: number): boolean;
150
+ /**
151
+ * The journal row's one release: the process is over, nothing is owed.
152
+ * Synced so an instance reset moments later cannot roll the delete back and
153
+ * resurrect a process the user watched end.
154
+ */
155
+ release(pid: number): Promise<void>;
156
+ /**
157
+ * Re-drive the launches a previous instance was building when it was reset.
158
+ *
159
+ * Sited on the launch-turn pump because the pump is what an alarm calls, and
160
+ * a launch that was suspended has an alarm armed for it — a reset during a
161
+ * chunk fails that alarm, and the platform re-delivers it to the instance
162
+ * that replaces this one. So the first turn after a reset is already this
163
+ * one.
164
+ *
165
+ * Runs once per instance: the journal only changes when a launch of THIS
166
+ * instance starts or settles, and those are rows this instance wrote.
167
+ */
168
+ recoverInterrupted(): Promise<void>;
169
+ }
170
+ //# sourceMappingURL=launch-journal.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"launch-journal.d.ts","sourceRoot":"","sources":["../src/launch-journal.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH;;;;;;;;;;;;;;;;;;GAkBG;AACH,eAAO,MAAM,0BAA0B,qBAAqB,CAAC;AAE7D,gFAAgF;AAChF,eAAO,MAAM,2BAA2B,IAAI,CAAC;AAE7C;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,WAAW,oBAAoB;IACnC,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,oBAAoB;IACnC,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,iBAAiB,CAAC,CAAC,SAAS,oBAAoB;IAC/D;;;;;;OAMG;IACH,cAAc,IAAI,MAAM,CAAC;IACzB;;;OAGG;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,qBAAqB,CAAC,CAAC,SAAS,oBAAoB;IAW7D,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,oBAAoB,EAC7B,IAAI,EAAE,iBAAiB,CAAC,CAAC,CAAC;IAG7C;;;;;;;;;;;;;;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;CA6B1C"}
@@ -0,0 +1,154 @@
1
+ /**
2
+ * launch-journal.ts — durable record of the resident launches a Durable Object
3
+ * owes, and their recovery after an instance reset.
4
+ *
5
+ * The platform resets a session Durable Object over what one turn has
6
+ * outstanding in storage ("Internal error in Durable Object storage caused
7
+ * object to be reset"), and a resident launch is the largest writer a session
8
+ * has. Everything a launch holds is in memory, so the process it is building
9
+ * and the terminal watching it both go with the instance — the journal is what
10
+ * a LATER instance reads to know that happened, and this module is the whole
11
+ * of that mechanism: the put→sync durability barrier on the way in, the
12
+ * delete→sync release on the way out, and the once-per-instance recovery pump
13
+ * that re-drives what a previous generation left behind.
14
+ *
15
+ * What a launch IS stays the embedder's: the journal stores the record it is
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
18
+ * opaquely.
19
+ */
20
+ /**
21
+ * Prefix for the resident-process journal: one row per resident this session
22
+ * owes the user, keyed by the pid it was built for.
23
+ *
24
+ * A resident holds its state in memory — the process table entry, the facet
25
+ * handle, the terminal — so an instance reset destroys it silently. The row
26
+ * is what a LATER instance reads to know a resident ended that way rather
27
+ * than on purpose: a pid at or below the reader's own pid base was allocated
28
+ * by a previous generation (PID_GEN_STRIDE, core's process-table). Written
29
+ * (and synced) before the launch's first byte of work, rewritten as `running`
30
+ * when the launch settles, and released only when the PROCESS ends — because
31
+ * the resets this row survives strike after the launch as often as during it
32
+ * (measured live, staging 2026-08-13: every observed reset landed seconds
33
+ * AFTER settle).
34
+ *
35
+ * The VALUE is live production DO storage and must never change — renaming a
36
+ * storage key is a migration, and orphaned rows are the least of what it
37
+ * breaks.
38
+ */
39
+ export const RESIDENT_LAUNCH_KEY_PREFIX = 'resident-launch:';
40
+ /** A launch is re-driven once. A reset that recurs is not the transient one. */
41
+ export const RESIDENT_LAUNCH_MAX_ATTEMPT = 1;
42
+ /**
43
+ * The resident-launch journal of one Durable Object instance.
44
+ *
45
+ * In-memory state here is per-instance on purpose: `journalledPids` tracks the
46
+ * rows THIS instance wrote, and `recovered` whether this instance has already
47
+ * read the journal a reset leaves behind. Rows from a previous instance are
48
+ * recovery's to consume, never the release path's.
49
+ */
50
+ export class ResidentLaunchJournal {
51
+ storage;
52
+ host;
53
+ /**
54
+ * Pids THIS instance holds journal rows for. What keeps the terminal hook —
55
+ * which fires for every process, shells and one-shots included — from
56
+ * paying a storage delete for pids that never had a row.
57
+ */
58
+ journalledPids = new Set();
59
+ /** Whether this instance has already read the journal a reset leaves behind. */
60
+ recovered = false;
61
+ constructor(storage, host) {
62
+ this.storage = storage;
63
+ this.host = host;
64
+ }
65
+ /**
66
+ * Record a launch as in flight, so an instance that replaces this one knows
67
+ * it never finished. Best-effort: a launch that cannot be journalled still
68
+ * runs, and a reset then costs exactly what it cost before the journal.
69
+ *
70
+ * Synced, not merely put: `await put()` resolves before durability, and the
71
+ * reset this journal exists for destroys every write its turn still had
72
+ * outstanding — measured live, a launch killed in its first chunks left NO
73
+ * row for the replacement instance to find, which is how the recovery this
74
+ * feeds sat inert while its own test stayed green. `sync()` is the storage
75
+ * layer's durability barrier: the row is on disk before the launch performs
76
+ * its first byte of real work. What remains is a reset between the put and
77
+ * the sync's completion — and a launch that dies there has not started, so
78
+ * losing its row costs a retype, not a recovery.
79
+ */
80
+ async journal(record) {
81
+ try {
82
+ this.journalledPids.add(record.pid);
83
+ await this.storage.put(`${RESIDENT_LAUNCH_KEY_PREFIX}${record.pid}`, record);
84
+ await this.storage.sync();
85
+ }
86
+ catch (e) {
87
+ console.warn('[nimbus] resident launch journal write failed:', errorMessage(e));
88
+ }
89
+ }
90
+ /** True while this instance holds a journal row for `pid`. */
91
+ has(pid) {
92
+ return this.journalledPids.has(pid);
93
+ }
94
+ /**
95
+ * The journal row's one release: the process is over, nothing is owed.
96
+ * Synced so an instance reset moments later cannot roll the delete back and
97
+ * resurrect a process the user watched end.
98
+ */
99
+ async release(pid) {
100
+ if (!this.journalledPids.delete(pid))
101
+ return;
102
+ try {
103
+ await this.storage.delete(`${RESIDENT_LAUNCH_KEY_PREFIX}${pid}`);
104
+ await this.storage.sync();
105
+ }
106
+ catch (e) {
107
+ console.warn('[nimbus] resident launch journal delete failed:', errorMessage(e));
108
+ }
109
+ }
110
+ /**
111
+ * Re-drive the launches a previous instance was building when it was reset.
112
+ *
113
+ * Sited on the launch-turn pump because the pump is what an alarm calls, and
114
+ * a launch that was suspended has an alarm armed for it — a reset during a
115
+ * chunk fails that alarm, and the platform re-delivers it to the instance
116
+ * that replaces this one. So the first turn after a reset is already this
117
+ * one.
118
+ *
119
+ * Runs once per instance: the journal only changes when a launch of THIS
120
+ * instance starts or settles, and those are rows this instance wrote.
121
+ */
122
+ async recoverInterrupted() {
123
+ if (this.recovered)
124
+ return;
125
+ this.recovered = true;
126
+ const journal = await this.storage.list({ prefix: RESIDENT_LAUNCH_KEY_PREFIX });
127
+ const base = this.host.generationBase();
128
+ for (const [key, record] of journal) {
129
+ // A pid at or below this instance's base was allocated by a PREVIOUS one
130
+ // (process-table.ts, PID_GEN_STRIDE), so its launch never finished; above
131
+ // the base is this instance's own, still running. Same predicate as
132
+ // `session/rpc.ts` uses to attribute a prior generation's pid.
133
+ if (!(record.pid > 0 && record.pid <= base))
134
+ continue;
135
+ await this.storage.delete(key);
136
+ if (record.attempt >= RESIDENT_LAUNCH_MAX_ATTEMPT) {
137
+ this.host.onAbandoned?.(record);
138
+ continue;
139
+ }
140
+ this.host.onRedrive?.(record);
141
+ // Not awaited: this call is running inside the alarm that granted the
142
+ // turn, and the launch it starts asks for turns of its own through that
143
+ // same alarm — awaiting it here would be waiting on an alarm that cannot
144
+ // be scheduled until this one returns.
145
+ this.host.waitUntil(this.host.redrive(record, record.attempt + 1)
146
+ .catch((e) => {
147
+ this.host.onRedriveFailed?.(record, e);
148
+ }));
149
+ }
150
+ }
151
+ }
152
+ function errorMessage(error) {
153
+ return error instanceof Error ? error.message : String(error);
154
+ }
@@ -0,0 +1,173 @@
1
+ /**
2
+ * launch-pacer.ts — spreading a resident launch across Durable Object turns.
3
+ *
4
+ * Building a resident process is the largest single span of computation this
5
+ * session performs: for pi it walks a 17 MB source tree through eight
6
+ * enrichment passes, serializes a 22.9 MB module map, and writes that map into
7
+ * the image store. Done in one turn it occupied the session DO's only thread
8
+ * for 15-35 s, and a session that cannot reach its thread cannot service the
9
+ * terminal WebSocket — the launch turn finished `outcome=ok` and the terminal
10
+ * died anyway, painting "[process terminal closed]" over a process that was
11
+ * still running.
12
+ *
13
+ * A faster launch does not fix that. A launch half the length still blocks the
14
+ * thread for as long as it runs, and the socket is dropped inside that window
15
+ * whether or not the work succeeds. What fixes it is never holding the thread
16
+ * for long in the first place, which means suspending the launch at bounded
17
+ * intervals and resuming it on a fresh turn. Responsiveness stops depending on
18
+ * how long the total work takes.
19
+ *
20
+ * A fresh turn is also a fresh CPU budget. The same launches that dropped the
21
+ * socket were also being killed with `exceededCpu` at 31.8 s and 32.5 s
22
+ * against a 30 s ceiling, and no amount of yielding *within* one invocation
23
+ * moves that: CPU accrues to the invocation, not to the pause. Only genuinely
24
+ * re-entering the object resets it.
25
+ *
26
+ * Progress is measured in bytes rather than milliseconds because workerd's
27
+ * clock does not advance without I/O — a wall-clock guard inside a span of
28
+ * pure computation reads zero however many seconds it burns, which is why the
29
+ * phase costs behind this module had to be recovered from per-turn `cpuTime`
30
+ * rather than measured in place. Bytes are what the work is actually
31
+ * proportional to, and they are exact. The same reasoning is why
32
+ * `git/network-facet.ts` bounds its checkout chunks by entries and decoded
33
+ * bytes and treats its wall guard as coarse.
34
+ */
35
+ /** How a paced launch gets back onto a fresh Durable Object turn. */
36
+ export interface LaunchTurnScheduler {
37
+ /**
38
+ * Suspend until a fresh turn is running this launch again.
39
+ *
40
+ * `chunkEnded` settles when the resumed launch reaches its next suspension
41
+ * point or finishes, so whoever grants the turn can await the work it just
42
+ * released rather than letting it run detached in a handler's microtask
43
+ * drain.
44
+ */
45
+ nextTurn(chunkEnded: Promise<void>): Promise<void>;
46
+ }
47
+ /**
48
+ * Bytes of launch work one turn may perform before it must yield.
49
+ *
50
+ * Sized so a chunk stays far below both the CPU ceiling and the span in which
51
+ * a terminal socket is at risk, while keeping the number of turn handoffs —
52
+ * each an alarm round trip — small enough not to dominate a launch. pi's
53
+ * 22.9 MB map crosses this about a dozen times per phase that handles it.
54
+ */
55
+ export declare const LAUNCH_CHUNK_MAX_BYTES = 2000000;
56
+ /**
57
+ * Accounts launch progress and ends the turn when a chunk's worth has been
58
+ * spent.
59
+ *
60
+ * Callers report the work they are about to do or have just done and await
61
+ * the result; a pacer that is not yielding returns without suspending, so the
62
+ * one-shot exec path — which passes no pacer at all — keeps its exact
63
+ * behaviour and cost. Nothing here decides WHAT the launch does, only where it
64
+ * is allowed to stop.
65
+ */
66
+ export declare class LaunchPacer {
67
+ private readonly scheduler;
68
+ private readonly maxChunkBytes;
69
+ private readonly stillWanted?;
70
+ /** Turn handoffs this launch has taken. Reported with the launch. */
71
+ chunks: number;
72
+ /** Total work accounted, for the same report. */
73
+ bytes: number;
74
+ private spent;
75
+ private chunkEnded;
76
+ /**
77
+ * @param stillWanted Checked every time the launch resumes. A launch spans
78
+ * many turns, so anything may have happened to what it is building for
79
+ * while it was suspended; throwing from here is how a launch stops instead
80
+ * of spending turn after turn on work nothing will use. Checked at the one
81
+ * place a launch can be interrupted, rather than at whichever phases
82
+ * remembered to ask.
83
+ */
84
+ constructor(scheduler: LaunchTurnScheduler, maxChunkBytes?: number, stillWanted?: (() => void) | undefined);
85
+ /**
86
+ * Account `bytes` of completed work, ending the turn if a chunk is full.
87
+ *
88
+ * Safe to call anywhere the launch holds no state that a concurrent turn
89
+ * could invalidate — which is why the image store registers its whole root
90
+ * set before the first call rather than one entry at a time.
91
+ */
92
+ spend(bytes: number): Promise<void>;
93
+ /**
94
+ * The launch has finished (or failed). Releases the turn still waiting on
95
+ * the chunk it resumed, so a launch that ends mid-chunk does not strand the
96
+ * handler that granted it.
97
+ */
98
+ settle(): void;
99
+ }
100
+ /** What {@link LaunchTurnPump} needs from the Durable Object hosting it. */
101
+ export interface LaunchTurnPumpHost {
102
+ /**
103
+ * Arrange for {@link LaunchTurnPump.pump} to run on a fresh Durable Object
104
+ * turn.
105
+ *
106
+ * The embedder satisfies this with an alarm, which is the only primitive
107
+ * that genuinely re-enters the object: a fresh turn is both a released
108
+ * thread and a fresh CPU budget, and a launch needs each for a different
109
+ * reason. Without it the pump degrades to a same-context timer — see
110
+ * {@link LaunchTurnPump.nextTurn}.
111
+ */
112
+ requestTurn?: () => void;
113
+ /**
114
+ * Awaited first on every pump, before any waiter resumes. Where the
115
+ * resident-launch journal's recovery sits: the pump is what an alarm calls,
116
+ * and the first turn after a reset is the re-delivered alarm of a launch
117
+ * the reset interrupted.
118
+ */
119
+ recover?: () => Promise<void>;
120
+ }
121
+ /**
122
+ * The granting side of {@link LaunchTurnScheduler}: parks suspended launches
123
+ * and resumes every one of them when the host grants a fresh turn.
124
+ */
125
+ export declare class LaunchTurnPump implements LaunchTurnScheduler {
126
+ private readonly host;
127
+ /**
128
+ * Launches suspended between chunks, waiting for a turn of their own.
129
+ *
130
+ * In-memory on purpose: a launch is only meaningful while the process table
131
+ * entry it is building for exists, and both are lost together if the isolate
132
+ * resets. What survives a reset is the journal, which names the launch's
133
+ * INPUTS rather than its position — a resumed queue would be resurrecting
134
+ * half-built work for pids that no longer exist, where re-driving a launch
135
+ * from its inputs is the same idempotent work again.
136
+ */
137
+ private waiters;
138
+ constructor(host: LaunchTurnPumpHost);
139
+ /**
140
+ * How a paced launch asks for a fresh turn.
141
+ *
142
+ * The host grants one by calling {@link pump} from a context that is
143
+ * genuinely a new invocation — the session's alarm. Without such a host
144
+ * there is no fresh turn to be had, and the launch continues on this one
145
+ * rather than hanging: that is exactly the single-turn launch this path has
146
+ * always performed, so a harness or a runtime without alarms loses the
147
+ * responsiveness but keeps the behaviour.
148
+ */
149
+ nextTurn(chunkEnded: Promise<void>): Promise<void>;
150
+ /**
151
+ * Run one chunk of every launch waiting for a turn.
152
+ *
153
+ * Awaits the chunk each resumed launch then performs, so the invocation that
154
+ * granted the turn is the invocation that pays for the work — rather than
155
+ * releasing it into a handler's microtask drain, where nothing owns it and
156
+ * the runtime may tear the context down mid-chunk.
157
+ */
158
+ pump(): Promise<void>;
159
+ /** Whether any launch is suspended waiting for a turn. */
160
+ get hasPending(): boolean;
161
+ }
162
+ /**
163
+ * Chunk bound for this session, honouring the verification knob.
164
+ *
165
+ * `NIMBUS_LAUNCH_CHUNK_BYTES` forces a small bound so an ordinary launch —
166
+ * not just a pathological one — crosses several turns and exercises every
167
+ * suspension point. Without it the multi-turn path would only ever be
168
+ * reached by the largest programs, which is the same reason
169
+ * `git/commands.ts` carries `NIMBUS_GIT_CHECKOUT_CHUNK_ENTRIES`. Unset in
170
+ * production, where the default applies.
171
+ */
172
+ export declare function launchChunkMaxBytes(env: unknown): number;
173
+ //# sourceMappingURL=launch-pacer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"launch-pacer.d.ts","sourceRoot":"","sources":["../src/launch-pacer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AAEH,qEAAqE;AACrE,MAAM,WAAW,mBAAmB;IAClC;;;;;;;OAOG;IACH,QAAQ,CAAC,UAAU,EAAE,OAAO,CAAC,IAAI,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACpD;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,sBAAsB,UAAY,CAAC;AAEhD;;;;;;;;;GASG;AACH,qBAAa,WAAW;IAkBpB,OAAO,CAAC,QAAQ,CAAC,SAAS;IAC1B,OAAO,CAAC,QAAQ,CAAC,aAAa;IAC9B,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAC;IAnB/B,qEAAqE;IACrE,MAAM,SAAK;IACX,iDAAiD;IACjD,KAAK,SAAK;IAEV,OAAO,CAAC,KAAK,CAAK;IAClB,OAAO,CAAC,UAAU,CAA8D;IAEhF;;;;;;;OAOG;gBAEgB,SAAS,EAAE,mBAAmB,EAC9B,aAAa,GAAE,MAA+B,EAC9C,WAAW,CAAC,GAAE,MAAM,IAAI,aAAA;IAG3C;;;;;;OAMG;IACG,KAAK,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAczC;;;;OAIG;IACH,MAAM,IAAI,IAAI;CAIf;AAQD,4EAA4E;AAC5E,MAAM,WAAW,kBAAkB;IACjC;;;;;;;;;OASG;IACH,WAAW,CAAC,EAAE,MAAM,IAAI,CAAC;IACzB;;;;;OAKG;IACH,OAAO,CAAC,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC/B;AAED;;;GAGG;AACH,qBAAa,cAAe,YAAW,mBAAmB;IAa5C,OAAO,CAAC,QAAQ,CAAC,IAAI;IAZjC;;;;;;;;;OASG;IACH,OAAO,CAAC,OAAO,CAAgE;gBAElD,IAAI,EAAE,kBAAkB;IAErD;;;;;;;;;OASG;IACH,QAAQ,CAAC,UAAU,EAAE,OAAO,CAAC,IAAI,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC;IAWlD;;;;;;;OAOG;IACG,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAS3B,0DAA0D;IAC1D,IAAI,UAAU,IAAI,OAAO,CAExB;CACF;AAED;;;;;;;;;GASG;AACH,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,OAAO,GAAG,MAAM,CAMxD"}