@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.
Files changed (104) hide show
  1. package/README.md +208 -293
  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 +3 -0
  7. package/dist/composition.d.ts.map +1 -0
  8. package/dist/composition.js +2 -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} +58 -17
  25. package/dist/fenced-work.d.ts.map +1 -0
  26. package/dist/fenced-work.js +241 -0
  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 +33 -15
  46. package/dist/process-fabric.d.ts.map +1 -1
  47. package/dist/process-fabric.js +25 -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 +19 -11
  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} +24 -21
  58. package/dist/turn-budget.d.ts.map +1 -0
  59. package/dist/{launch-pacer.js → turn-budget.js} +24 -12
  60. package/dist/workerd-facet-host.d.ts +67 -70
  61. package/dist/workerd-facet-host.d.ts.map +1 -1
  62. package/dist/workerd-facet-host.js +129 -181
  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 +16 -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} +129 -42
  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 +43 -34
  82. package/src/process-host.ts +22 -20
  83. package/src/sealed.ts +150 -0
  84. package/src/timers.ts +294 -0
  85. package/src/{launch-pacer.ts → turn-budget.ts} +34 -27
  86. package/src/workerd-facet-host.ts +159 -208
  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-journal.js +0 -154
  97. package/dist/launch-pacer.d.ts.map +0 -1
  98. package/dist/loader-ledger.d.ts +0 -57
  99. package/dist/loader-ledger.d.ts.map +0 -1
  100. package/dist/loader-ledger.js +0 -91
  101. package/dist/loader-pool.d.ts.map +0 -1
  102. package/src/alarms.ts +0 -275
  103. package/src/ctx-exports.ts +0 -77
  104. package/src/loader-ledger.ts +0 -112
@@ -1,57 +0,0 @@
1
- /**
2
- * loader-ledger.ts — per-DO accounting for the Worker Loader's two caps.
3
- *
4
- * Measured on production workerd: a Durable Object admits ~5–6 concurrent
5
- * dynamic workers before the platform refuses with "Too many concurrent
6
- * dynamic workers", one DO method can drive at most 4 concurrent Loader
7
- * fetches, and loader-cache entries are never released — every DISTINCT
8
- * `loader.get(id)` permanently consumes one of the dynamic-worker slots for
9
- * the object's lifetime. Nimbus stays under the caps by construction
10
- * (`IN_DO_THRESHOLD` = 5 in the fanout pool), which until now meant the slots
11
- * were counted in prose. This ledger counts them at the fabric's loader call
12
- * sites instead — the loader pool's slots, a resident process's keyed worker,
13
- * a one-shot's load — so proximity is measurable and a cap failure can name
14
- * the ids actually holding slots.
15
- *
16
- * Measurement only: no admission control. The caps are the platform's, they
17
- * are approximate ("~5–6"), and a gate on an approximate number would refuse
18
- * work the platform would have run.
19
- *
20
- * Keyed weakly off the hosting actor's `ctx`, like the facet slot books: the
21
- * caps are per Durable Object, and dynamic workers die with the isolate that
22
- * loaded them, so a ledger that goes away with its host describes nothing
23
- * that still exists.
24
- */
25
- /** Record a keyed `loader.get(id)` — a permanent slot if the id is new. */
26
- export declare function recordLoaderId(ctx: object, id: string): void;
27
- /**
28
- * Count one call into a dynamic worker as a live Loader fetch; the returned
29
- * function ends it (idempotently), from the caller's own `finally`.
30
- *
31
- * A begin/end pair rather than a wrapper on purpose, and the shape is
32
- * load-bearing: wrapping the stub call in a ledger-owned async frame
33
- * (`trackLoaderFetch(ctx, () => entrypoint.execute(...))`) left the hosting
34
- * Durable Object poisoned after every pooled dispatch — the next fabric
35
- * activity hung the object or reset the instance outright (pid base jumped,
36
- * every attached WebSocket dropped with no close frame), measured 7/7 on
37
- * staging and gone 3/3 with the direct call restored. Same seam-quirk class
38
- * as pipelined `fetch.call`, which workerd refuses for dynamically-loaded
39
- * workers: an RPC stub call must stay a direct property call awaited by the
40
- * frame that made it, so the ledger only brackets it.
41
- */
42
- export declare function beginLoaderFetch(ctx: object): () => void;
43
- /** Snapshot for the diag surface. Pure read; no I/O. */
44
- export declare function loaderLedgerStats(ctx: object): {
45
- idsEverGotten: string[];
46
- liveFetches: number;
47
- peakLiveFetches: number;
48
- };
49
- /**
50
- * Name the per-DO accounting on a "Too many concurrent dynamic workers"
51
- * failure; hand every other error back untouched. The platform's message
52
- * says only that the cap was hit — which ids hold the slots, and that a
53
- * keyed id can never give one back, is what the operator needs to know to
54
- * shrink anything.
55
- */
56
- export declare function withDynamicWorkerCapNamed<E>(ctx: object, error: E): E | Error;
57
- //# sourceMappingURL=loader-ledger.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"loader-ledger.d.ts","sourceRoot":"","sources":["../src/loader-ledger.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAwBH,2EAA2E;AAC3E,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,IAAI,CAE5D;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,IAAI,CAUxD;AAED,wDAAwD;AACxD,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG;IAC9C,aAAa,EAAE,MAAM,EAAE,CAAC;IACxB,WAAW,EAAE,MAAM,CAAC;IACpB,eAAe,EAAE,MAAM,CAAC;CACzB,CAOA;AAED;;;;;;GAMG;AACH,wBAAgB,yBAAyB,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,GAAG,CAAC,GAAG,KAAK,CAW7E"}
@@ -1,91 +0,0 @@
1
- /**
2
- * loader-ledger.ts — per-DO accounting for the Worker Loader's two caps.
3
- *
4
- * Measured on production workerd: a Durable Object admits ~5–6 concurrent
5
- * dynamic workers before the platform refuses with "Too many concurrent
6
- * dynamic workers", one DO method can drive at most 4 concurrent Loader
7
- * fetches, and loader-cache entries are never released — every DISTINCT
8
- * `loader.get(id)` permanently consumes one of the dynamic-worker slots for
9
- * the object's lifetime. Nimbus stays under the caps by construction
10
- * (`IN_DO_THRESHOLD` = 5 in the fanout pool), which until now meant the slots
11
- * were counted in prose. This ledger counts them at the fabric's loader call
12
- * sites instead — the loader pool's slots, a resident process's keyed worker,
13
- * a one-shot's load — so proximity is measurable and a cap failure can name
14
- * the ids actually holding slots.
15
- *
16
- * Measurement only: no admission control. The caps are the platform's, they
17
- * are approximate ("~5–6"), and a gate on an approximate number would refuse
18
- * work the platform would have run.
19
- *
20
- * Keyed weakly off the hosting actor's `ctx`, like the facet slot books: the
21
- * caps are per Durable Object, and dynamic workers die with the isolate that
22
- * loaded them, so a ledger that goes away with its host describes nothing
23
- * that still exists.
24
- */
25
- import { classifyError } from '@nimbus-sh/core/observability/oom-classify.js';
26
- const ledgers = new WeakMap();
27
- function ledger(ctx) {
28
- let entry = ledgers.get(ctx);
29
- if (!entry) {
30
- entry = { ids: new Set(), liveFetches: 0, peakLiveFetches: 0 };
31
- ledgers.set(ctx, entry);
32
- }
33
- return entry;
34
- }
35
- /** Record a keyed `loader.get(id)` — a permanent slot if the id is new. */
36
- export function recordLoaderId(ctx, id) {
37
- ledger(ctx).ids.add(id);
38
- }
39
- /**
40
- * Count one call into a dynamic worker as a live Loader fetch; the returned
41
- * function ends it (idempotently), from the caller's own `finally`.
42
- *
43
- * A begin/end pair rather than a wrapper on purpose, and the shape is
44
- * load-bearing: wrapping the stub call in a ledger-owned async frame
45
- * (`trackLoaderFetch(ctx, () => entrypoint.execute(...))`) left the hosting
46
- * Durable Object poisoned after every pooled dispatch — the next fabric
47
- * activity hung the object or reset the instance outright (pid base jumped,
48
- * every attached WebSocket dropped with no close frame), measured 7/7 on
49
- * staging and gone 3/3 with the direct call restored. Same seam-quirk class
50
- * as pipelined `fetch.call`, which workerd refuses for dynamically-loaded
51
- * workers: an RPC stub call must stay a direct property call awaited by the
52
- * frame that made it, so the ledger only brackets it.
53
- */
54
- export function beginLoaderFetch(ctx) {
55
- const entry = ledger(ctx);
56
- entry.liveFetches++;
57
- entry.peakLiveFetches = Math.max(entry.peakLiveFetches, entry.liveFetches);
58
- let ended = false;
59
- return () => {
60
- if (ended)
61
- return;
62
- ended = true;
63
- entry.liveFetches--;
64
- };
65
- }
66
- /** Snapshot for the diag surface. Pure read; no I/O. */
67
- export function loaderLedgerStats(ctx) {
68
- const entry = ledger(ctx);
69
- return {
70
- idsEverGotten: [...entry.ids],
71
- liveFetches: entry.liveFetches,
72
- peakLiveFetches: entry.peakLiveFetches,
73
- };
74
- }
75
- /**
76
- * Name the per-DO accounting on a "Too many concurrent dynamic workers"
77
- * failure; hand every other error back untouched. The platform's message
78
- * says only that the cap was hit — which ids hold the slots, and that a
79
- * keyed id can never give one back, is what the operator needs to know to
80
- * shrink anything.
81
- */
82
- export function withDynamicWorkerCapNamed(ctx, error) {
83
- if (classifyError(error) !== 'dynamic_worker_cap')
84
- return error;
85
- const entry = ledger(ctx);
86
- const platform = error instanceof Error ? error.message : String(error);
87
- return new Error(`${platform} — this Durable Object has ${entry.ids.size} loader id(s) permanently `
88
- + `holding dynamic-worker slots (a loader.get id is never released): `
89
- + `${[...entry.ids].join(', ') || '(none recorded)'}; live Loader fetches ${entry.liveFetches}, `
90
- + `peak ${entry.peakLiveFetches}`, { cause: error });
91
- }
@@ -1 +0,0 @@
1
- {"version":3,"file":"loader-pool.d.ts","sourceRoot":"","sources":["../src/loader-pool.ts"],"names":[],"mappings":"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"}
package/src/alarms.ts DELETED
@@ -1,275 +0,0 @@
1
- /**
2
- * alarms.ts — Durable Object alarm multiplexing + isolate-generation
3
- * machinery, persisted across hibernation.
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
- * A Durable Object has ONE alarm, and a second `setAlarm()` silently
13
- * overwrites the first — so every alarm-driven subsystem coordinates through
14
- * a single reason→deadline map and one dispatcher. Reasons are plain strings
15
- * registered by the embedder: `scheduleAlarm` arms one, and `dispatchAlarm`
16
- * runs the embedder-supplied handler for every reason whose deadline has
17
- * passed.
18
- */
19
-
20
- import { errorText } from '@nimbus-sh/core/_shared/error-text.js';
21
-
22
- /**
23
- * The storage the alarm map lives in. `setAlarm` is optional because
24
- * `wrangler dev` serves a storage without it, which is the whole reason
25
- * scheduling degrades to a no-op instead of throwing.
26
- */
27
- export interface AlarmStorage {
28
- get(key: string): Promise<unknown>;
29
- put(key: string, value: unknown): Promise<void>;
30
- delete(key: string): Promise<boolean>;
31
- setAlarm?(scheduledTime: number): Promise<void>;
32
- }
33
-
34
- /** The hosting actor's context, as the alarm coordination reads it. */
35
- export interface AlarmContext {
36
- storage: AlarmStorage;
37
- }
38
-
39
- /**
40
- * Multi-reason alarm coordination map.
41
- *
42
- * JSON-serialised `Record<reason, deadlineMsEpoch>` where keys are the
43
- * embedder's canonical reason strings (e.g. 'w9-flush', 'log-janitor'). The
44
- * alarm() dispatcher reads this on fire, dispatches every reason whose
45
- * deadline has passed, and re-arms `ctx.storage.setAlarm` at the earliest
46
- * remaining deadline.
47
- *
48
- * Why a map (not a single nextAlarmAt + reason): two subsystems can have
49
- * distinct deadlines. Without the map, the later setAlarm() call would
50
- * overwrite the earlier reason silently, breaking whichever subsystem
51
- * expected its deadline.
52
- *
53
- * Forward-compat: the dispatcher silently drops unknown reasons so a
54
- * rollback from a future deploy that added new reasons doesn't leave the
55
- * alarm stuck.
56
- *
57
- * The VALUE is live production DO storage ('w1_next_alarm_reasons', from the
58
- * workstream that introduced it) and must never change — renaming a storage
59
- * key is a migration, and orphaned rows are the least of what it breaks.
60
- */
61
- export const ALARM_REASONS_KEY = 'w1_next_alarm_reasons';
62
-
63
- /**
64
- * Storage key for the isolate-generation counter (cold-start +
65
- * post-hibernation wake; one increment per fresh isolate).
66
- *
67
- * The VALUE is live production DO storage ('w9_isolate_gen') and must never
68
- * change, same contract as {@link ALARM_REASONS_KEY}.
69
- */
70
- export const ISOLATE_GEN_KEY = 'w9_isolate_gen';
71
-
72
- /**
73
- * The host instance carrying the per-instance alarm chain. The field lives on
74
- * the embedder's DO instance so one chain serializes every alarm-map
75
- * read-modify-write for that instance (see {@link scheduleAlarm}).
76
- */
77
- export interface AlarmHost {
78
- _alarmChain?: Promise<unknown>;
79
- }
80
-
81
- /** The host instance carrying the isolate-generation state. */
82
- export interface IsolateGenHost {
83
- _isolateGen: number;
84
- _isolateGenPersisted: boolean;
85
- }
86
-
87
- /**
88
- * Schedule (or re-schedule) an alarm reason. Coordinated via a single map in
89
- * DO storage so multiple subsystems don't clobber each other's `setAlarm()`
90
- * calls.
91
- *
92
- * Semantics:
93
- * - Reads the existing reasons map.
94
- * - Sets `map[reason] = whenMs` IF `whenMs` is sooner than the
95
- * currently-pending deadline for that reason (or no entry exists).
96
- * Later-than-pending requests are silently ignored — the existing
97
- * alarm will fire and re-arm anyway.
98
- * - Writes the map back and calls `ctx.storage.setAlarm(min(deadlines))`.
99
- *
100
- * Cost: 1 storage read + 1 storage write + 1 setAlarm per call. setAlarm
101
- * itself is billed as 1 row written per DO pricing. At a 60s janitor
102
- * cadence, this is ~$0.05/mo/session at scale — dwarfed by the
103
- * hibernation duration savings.
104
- *
105
- * Fail-soft: any throw is swallowed with a warn. On older runtimes /
106
- * wrangler-dev where setAlarm is unavailable, this is a no-op (the
107
- * subsystem's in-isolate setTimeout fallback continues to work).
108
- */
109
- export function scheduleAlarm(
110
- host: AlarmHost,
111
- ctx: AlarmContext,
112
- reason: string,
113
- whenMs: number,
114
- ): Promise<boolean> {
115
- // Serialize every read-modify-write of the reasons map through one
116
- // per-instance chain: two schedulers firing back-to-back from one activity
117
- // hook would otherwise interleave their get→put cycles and silently drop
118
- // whichever reason wrote first.
119
- const run = async (): Promise<boolean> => {
120
- try {
121
- const setAlarmFn = ctx?.storage?.setAlarm;
122
- if (typeof setAlarmFn !== 'function') return false;
123
- const existing = (await ctx.storage.get(ALARM_REASONS_KEY)) as
124
- | Record<string, number>
125
- | undefined;
126
- const map: Record<string, number> = { ...(existing || {}) };
127
- // Earliest-deadline-first: only update if new request is sooner or
128
- // this reason has no pending entry.
129
- if (!(reason in map) || whenMs < map[reason]) {
130
- map[reason] = whenMs;
131
- await ctx.storage.put(ALARM_REASONS_KEY, map);
132
- }
133
- const earliest = Math.min(...Object.values(map));
134
- setAlarmFn.call(ctx.storage, earliest);
135
- return true;
136
- } catch (e) {
137
- console.warn('[nimbus/W1] scheduleAlarm threw:', errorText(e));
138
- return false;
139
- }
140
- };
141
- const chained = (host._alarmChain ?? Promise.resolve()).then(run, run);
142
- host._alarmChain = chained;
143
- return chained;
144
- }
145
-
146
- /**
147
- * What one alarm handler may return: nothing, or a deadline this reason
148
- * re-arms itself at. Re-arming through the return value keeps the map's
149
- * read-modify-write inside the dispatcher, where it is serialized.
150
- */
151
- export type AlarmHandlerResult = void | { rearmAt: number };
152
-
153
- /** The embedder's reasons, each with the handler that answers it. */
154
- export type AlarmHandlers = Record<
155
- string,
156
- (now: number) => AlarmHandlerResult | Promise<AlarmHandlerResult>
157
- >;
158
-
159
- /**
160
- * Multi-reason alarm dispatcher. Called from the DO's `alarm()` handler with
161
- * the embedder's handler map.
162
- *
163
- * For each pending reason whose deadline has passed, run its handler.
164
- * Handlers are awaited in place: the alarm invocation is the fresh turn a
165
- * re-entering subsystem asked for, and it has to stay the one paying for the
166
- * work it just released.
167
- *
168
- * After running fireable reasons, re-arms `ctx.storage.setAlarm` at the
169
- * earliest remaining deadline. If no reasons remain, deletes the map key and
170
- * does NOT call setAlarm — the DO becomes hibernation-eligible after the 10s
171
- * idle window.
172
- *
173
- * Forward/back-compat: unknown reasons silently dropped. `onLegacyAlarm`
174
- * covers an alarm that fires with no map at all — a deploy from before the
175
- * map existed left a bare `setAlarm` behind, and the embedder decides what
176
- * that one-time fire means (one dispatch later the map is populated by the
177
- * next scheduleAlarm call).
178
- */
179
- export function dispatchAlarm(
180
- host: AlarmHost,
181
- ctx: AlarmContext,
182
- handlers: AlarmHandlers,
183
- onLegacyAlarm?: () => void,
184
- ): Promise<void> {
185
- // Same serialization as scheduleAlarm: the dispatcher's read→handlers→write
186
- // cycle must not interleave with an activity-hook scheduleAlarm.
187
- const chained = (host._alarmChain ?? Promise.resolve()).then(
188
- () => dispatchAlarmBody(ctx, handlers, onLegacyAlarm),
189
- () => dispatchAlarmBody(ctx, handlers, onLegacyAlarm),
190
- );
191
- host._alarmChain = chained;
192
- return chained;
193
- }
194
-
195
- async function dispatchAlarmBody(
196
- ctx: AlarmContext,
197
- handlers: AlarmHandlers,
198
- onLegacyAlarm?: () => void,
199
- ): Promise<void> {
200
- try {
201
- const now = Date.now();
202
- const existing = (await ctx?.storage?.get?.(ALARM_REASONS_KEY)) as
203
- | Record<string, number>
204
- | undefined;
205
- if (!existing || Object.keys(existing).length === 0) {
206
- onLegacyAlarm?.();
207
- return;
208
- }
209
- const map: Record<string, number> = { ...existing };
210
- // Snapshot fireable reasons BEFORE running any of them, so a
211
- // handler that schedules itself for the next cycle doesn't get
212
- // immediately re-fired in the same dispatch.
213
- const fired: string[] = [];
214
- for (const [reason, when] of Object.entries(map)) {
215
- if (when <= now) fired.push(reason);
216
- }
217
- for (const reason of fired) {
218
- delete map[reason];
219
- const handler = handlers[reason];
220
- // Unknown reasons silently dropped (forward-compat).
221
- if (!handler) continue;
222
- try {
223
- const result = await handler(now);
224
- if (result && typeof result.rearmAt === 'number') {
225
- map[reason] = result.rearmAt;
226
- }
227
- } catch (e) {
228
- console.warn(`[nimbus/W1] dispatch ${reason} threw:`, errorText(e));
229
- }
230
- }
231
- // Re-arm or clear.
232
- const setAlarmFn = ctx?.storage?.setAlarm;
233
- if (Object.keys(map).length > 0) {
234
- await ctx.storage.put(ALARM_REASONS_KEY, map);
235
- const earliest = Math.min(...Object.values(map));
236
- if (typeof setAlarmFn === 'function') {
237
- setAlarmFn.call(ctx.storage, earliest);
238
- }
239
- } else {
240
- try { await ctx.storage.delete(ALARM_REASONS_KEY); } catch {}
241
- // No remaining reasons → no setAlarm call → DO becomes
242
- // hibernation-eligible after the 10s idle window.
243
- }
244
- } catch (e) {
245
- console.warn('[nimbus/W1] dispatchAlarm threw:', errorText(e));
246
- }
247
- }
248
-
249
- /** Increment + persist the isolate-gen counter once per fresh isolate. */
250
- export async function maybeBumpIsolateGen(host: IsolateGenHost, ctx: AlarmContext): Promise<void> {
251
- if (host._isolateGenPersisted) return;
252
- host._isolateGenPersisted = true;
253
- try {
254
- const prev = (await ctx.storage.get(ISOLATE_GEN_KEY)) as number | undefined;
255
- // Adopt the persisted truth first, and adopt the bump only after the
256
- // put resolves. An unpersisted `next` would be re-read as `prev` by the
257
- // NEXT boot and re-issued — two instances sharing one generation is
258
- // exactly the pid-aliasing this counter exists to prevent. Running on
259
- // the previous persisted generation is the lesser lapse, and the
260
- // put-failure case is replica-only in practice (replicas never spawn).
261
- //
262
- // What holds the guarantee is the output gate, not this await: measured,
263
- // the block body resolves in 0 ms even with a confirmed put, because
264
- // `await storage.put()` returns before durability. The gate is what
265
- // keeps a pid from generation N from escaping before N is durable, which
266
- // is why marking this put `allowUnconfirmed` is not a free speedup — see
267
- // scratchpad/coldstart-s1.md.
268
- host._isolateGen = typeof prev === 'number' ? prev : 0;
269
- const next = host._isolateGen + 1;
270
- await ctx.storage.put(ISOLATE_GEN_KEY, next);
271
- host._isolateGen = next;
272
- } catch (e) {
273
- console.warn('[nimbus/W9] isolate-gen bump failed:', errorText(e));
274
- }
275
- }
@@ -1,77 +0,0 @@
1
- /**
2
- * ctx-exports.ts — leaf module holding the ctx.exports reference.
3
- *
4
- * Isolated from the embedder's entry module so helpers (notably
5
- * loader-pool.ts) can read `ctx.exports` without transitively importing the
6
- * Durable Object classes. Keeping this a leaf (no imports) lets the pool be
7
- * unit-tested in a plain Node/Bun process.
8
- *
9
- * The embedder's fetch handler calls `setCtxExports(ctx.exports)` on the
10
- * first request; callers like the loader pool read via `getCtxExports()`.
11
- * If the pool is constructed before the first fetch (unlikely) it just gets
12
- * null — the caller decides how to degrade.
13
- */
14
-
15
- /**
16
- * One entry of `ctx.exports`: a top-level entrypoint's loopback factory, which
17
- * mints a Service Binding stub for that entrypoint when called with props.
18
- *
19
- * The stub's RPC surface belongs to the entrypoint CLASS, which this leaf
20
- * cannot see — `Cloudflare.Exports` is derived from the embedder's own main
21
- * module, so for a library it evaluates to `{}`. A caller that knows the class
22
- * names the surface it expects (`factory<MySupervisorRpc>({ props })`); one
23
- * that does not gets `unknown` and has to narrow, same as
24
- * `DurableObjectNamespace<T>` and `RpcStub<T>` in @cloudflare/workers-types.
25
- */
26
- export type EntrypointLoopbackFactory = <Stub = unknown>(options: { props: object }) => Stub;
27
-
28
- /**
29
- * `ctx.exports` itself — one factory per top-level entrypoint export, keyed by
30
- * export name. Absent names read as undefined, which is how a caller finds out
31
- * the embedder's entry module does not re-export the class it needs.
32
- */
33
- export type CtxExports = Record<string, EntrypointLoopbackFactory | undefined>;
34
-
35
- let _ctxExports: CtxExports | null = null;
36
-
37
- export function setCtxExports(value: CtxExports): void {
38
- if (_ctxExports) return; // first-write-wins, same as the prior inline impl
39
- _ctxExports = value;
40
- }
41
-
42
- export function getCtxExports(): CtxExports | null {
43
- return _ctxExports;
44
- }
45
-
46
- /**
47
- * The fabric mints supervisor bindings for the programs it hosts, but the
48
- * entrypoint class that answers them belongs to the embedder, so its
49
- * ctx.exports name is registered once at composition time rather than
50
- * hardcoded here. First-write-wins, same as the ctx.exports holder above.
51
- */
52
- let _supervisorEntrypointName: string | null = null;
53
-
54
- export function setSupervisorEntrypointName(name: string): void {
55
- if (_supervisorEntrypointName) return;
56
- _supervisorEntrypointName = name;
57
- }
58
-
59
- /**
60
- * Resolve the registered supervisor entrypoint on an exports object —
61
- * `exportsObj` when given (a WorkerEntrypoint reads its own ctx.exports),
62
- * the held ctx.exports otherwise. Calling the result with props mints one
63
- * supervisor binding (`env.SUPERVISOR`) for one hosted program. Null when
64
- * either half is missing; the caller decides whether that degrades or throws.
65
- */
66
- export function supervisorEntrypoint(exportsObj?: unknown): EntrypointLoopbackFactory | null {
67
- const exports = exportsObj ?? _ctxExports;
68
- if (!_supervisorEntrypointName) return null;
69
- if ((typeof exports !== 'object' && typeof exports !== 'function') || exports === null) return null;
70
- const factory = (exports as Record<string, unknown>)[_supervisorEntrypointName];
71
- return typeof factory === 'function' ? (factory as EntrypointLoopbackFactory) : null;
72
- }
73
-
74
- /** The registered name, for error messages that point at the missing export. */
75
- export function supervisorEntrypointName(): string | null {
76
- return _supervisorEntrypointName;
77
- }
@@ -1,112 +0,0 @@
1
- /**
2
- * loader-ledger.ts — per-DO accounting for the Worker Loader's two caps.
3
- *
4
- * Measured on production workerd: a Durable Object admits ~5–6 concurrent
5
- * dynamic workers before the platform refuses with "Too many concurrent
6
- * dynamic workers", one DO method can drive at most 4 concurrent Loader
7
- * fetches, and loader-cache entries are never released — every DISTINCT
8
- * `loader.get(id)` permanently consumes one of the dynamic-worker slots for
9
- * the object's lifetime. Nimbus stays under the caps by construction
10
- * (`IN_DO_THRESHOLD` = 5 in the fanout pool), which until now meant the slots
11
- * were counted in prose. This ledger counts them at the fabric's loader call
12
- * sites instead — the loader pool's slots, a resident process's keyed worker,
13
- * a one-shot's load — so proximity is measurable and a cap failure can name
14
- * the ids actually holding slots.
15
- *
16
- * Measurement only: no admission control. The caps are the platform's, they
17
- * are approximate ("~5–6"), and a gate on an approximate number would refuse
18
- * work the platform would have run.
19
- *
20
- * Keyed weakly off the hosting actor's `ctx`, like the facet slot books: the
21
- * caps are per Durable Object, and dynamic workers die with the isolate that
22
- * loaded them, so a ledger that goes away with its host describes nothing
23
- * that still exists.
24
- */
25
-
26
- import { classifyError } from '@nimbus-sh/core/observability/oom-classify.js';
27
-
28
- interface LoaderLedger {
29
- /** Distinct loader ids ever gotten — each one a permanently consumed slot. */
30
- ids: Set<string>;
31
- /** In-flight calls into dynamic workers, right now. */
32
- liveFetches: number;
33
- /** The most that were ever in flight at once. */
34
- peakLiveFetches: number;
35
- }
36
-
37
- const ledgers = new WeakMap<object, LoaderLedger>();
38
-
39
- function ledger(ctx: object): LoaderLedger {
40
- let entry = ledgers.get(ctx);
41
- if (!entry) {
42
- entry = { ids: new Set(), liveFetches: 0, peakLiveFetches: 0 };
43
- ledgers.set(ctx, entry);
44
- }
45
- return entry;
46
- }
47
-
48
- /** Record a keyed `loader.get(id)` — a permanent slot if the id is new. */
49
- export function recordLoaderId(ctx: object, id: string): void {
50
- ledger(ctx).ids.add(id);
51
- }
52
-
53
- /**
54
- * Count one call into a dynamic worker as a live Loader fetch; the returned
55
- * function ends it (idempotently), from the caller's own `finally`.
56
- *
57
- * A begin/end pair rather than a wrapper on purpose, and the shape is
58
- * load-bearing: wrapping the stub call in a ledger-owned async frame
59
- * (`trackLoaderFetch(ctx, () => entrypoint.execute(...))`) left the hosting
60
- * Durable Object poisoned after every pooled dispatch — the next fabric
61
- * activity hung the object or reset the instance outright (pid base jumped,
62
- * every attached WebSocket dropped with no close frame), measured 7/7 on
63
- * staging and gone 3/3 with the direct call restored. Same seam-quirk class
64
- * as pipelined `fetch.call`, which workerd refuses for dynamically-loaded
65
- * workers: an RPC stub call must stay a direct property call awaited by the
66
- * frame that made it, so the ledger only brackets it.
67
- */
68
- export function beginLoaderFetch(ctx: object): () => void {
69
- const entry = ledger(ctx);
70
- entry.liveFetches++;
71
- entry.peakLiveFetches = Math.max(entry.peakLiveFetches, entry.liveFetches);
72
- let ended = false;
73
- return () => {
74
- if (ended) return;
75
- ended = true;
76
- entry.liveFetches--;
77
- };
78
- }
79
-
80
- /** Snapshot for the diag surface. Pure read; no I/O. */
81
- export function loaderLedgerStats(ctx: object): {
82
- idsEverGotten: string[];
83
- liveFetches: number;
84
- peakLiveFetches: number;
85
- } {
86
- const entry = ledger(ctx);
87
- return {
88
- idsEverGotten: [...entry.ids],
89
- liveFetches: entry.liveFetches,
90
- peakLiveFetches: entry.peakLiveFetches,
91
- };
92
- }
93
-
94
- /**
95
- * Name the per-DO accounting on a "Too many concurrent dynamic workers"
96
- * failure; hand every other error back untouched. The platform's message
97
- * says only that the cap was hit — which ids hold the slots, and that a
98
- * keyed id can never give one back, is what the operator needs to know to
99
- * shrink anything.
100
- */
101
- export function withDynamicWorkerCapNamed<E>(ctx: object, error: E): E | Error {
102
- if (classifyError(error) !== 'dynamic_worker_cap') return error;
103
- const entry = ledger(ctx);
104
- const platform = error instanceof Error ? error.message : String(error);
105
- return new Error(
106
- `${platform} — this Durable Object has ${entry.ids.size} loader id(s) permanently `
107
- + `holding dynamic-worker slots (a loader.get id is never released): `
108
- + `${[...entry.ids].join(', ') || '(none recorded)'}; live Loader fetches ${entry.liveFetches}, `
109
- + `peak ${entry.peakLiveFetches}`,
110
- { cause: error },
111
- );
112
- }