@nimbus-sh/platform 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 (47) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +36 -0
  3. package/dist/diag-counters.d.ts +210 -0
  4. package/dist/diag-counters.d.ts.map +1 -0
  5. package/dist/diag-counters.js +170 -0
  6. package/dist/heap-estimate.d.ts +225 -0
  7. package/dist/heap-estimate.d.ts.map +1 -0
  8. package/dist/heap-estimate.js +185 -0
  9. package/dist/heavy-alloc-coord.d.ts +93 -0
  10. package/dist/heavy-alloc-coord.d.ts.map +1 -0
  11. package/dist/heavy-alloc-coord.js +181 -0
  12. package/dist/index.d.ts +19 -0
  13. package/dist/index.d.ts.map +1 -0
  14. package/dist/index.js +18 -0
  15. package/dist/install-phase.d.ts +47 -0
  16. package/dist/install-phase.d.ts.map +1 -0
  17. package/dist/install-phase.js +1 -0
  18. package/dist/limits.d.ts +83 -0
  19. package/dist/limits.d.ts.map +1 -0
  20. package/dist/limits.js +146 -0
  21. package/dist/oom-classify.d.ts +172 -0
  22. package/dist/oom-classify.d.ts.map +1 -0
  23. package/dist/oom-classify.js +336 -0
  24. package/dist/oom-discriminator.d.ts +176 -0
  25. package/dist/oom-discriminator.d.ts.map +1 -0
  26. package/dist/oom-discriminator.js +272 -0
  27. package/dist/rpc-dispose.d.ts +4 -0
  28. package/dist/rpc-dispose.d.ts.map +1 -0
  29. package/dist/rpc-dispose.js +30 -0
  30. package/dist/w7-frame.d.ts +107 -0
  31. package/dist/w7-frame.d.ts.map +1 -0
  32. package/dist/w7-frame.js +842 -0
  33. package/dist/weighted-credit-pool.d.ts +61 -0
  34. package/dist/weighted-credit-pool.d.ts.map +1 -0
  35. package/dist/weighted-credit-pool.js +196 -0
  36. package/package.json +57 -0
  37. package/src/diag-counters.ts +310 -0
  38. package/src/heap-estimate.ts +331 -0
  39. package/src/heavy-alloc-coord.ts +241 -0
  40. package/src/index.ts +19 -0
  41. package/src/install-phase.ts +56 -0
  42. package/src/limits.ts +165 -0
  43. package/src/oom-classify.ts +350 -0
  44. package/src/oom-discriminator.ts +405 -0
  45. package/src/rpc-dispose.ts +31 -0
  46. package/src/w7-frame.ts +1046 -0
  47. package/src/weighted-credit-pool.ts +244 -0
@@ -0,0 +1,185 @@
1
+ /**
2
+ * heap-estimate.ts — deterministic supervisor-heap estimator [C'.1]
3
+ *
4
+ * Why this module exists
5
+ * ──────────────────────
6
+ * `process.memoryUsage()` returns 0 for every field inside a Durable
7
+ * Object class context (only dynamic-worker isolates under nodejs_compat
8
+ * get the real implementation). The previous `readNodeMem` /
9
+ * `sampleMemory` helpers in nimbus-session-diag.ts called it anyway and
10
+ * therefore reported zero forever — useless for verifying memory-
11
+ * containment work in plan §3 Track A'.
12
+ *
13
+ * Replacement: a deterministic estimator that sums the INSTRUMENTED
14
+ * supervisor heap allocation sources — runtime counters that ARE accurate
15
+ * (diag-counters.ts singleton + SqliteVFS.getStats()). Every byte it
16
+ * reports has a named contributor, so a regression in any one component
17
+ * is locatable.
18
+ *
19
+ * `estimatedBytes` is a LOWER BOUND, not a total
20
+ * ─────────────────────────────────────────────
21
+ * Within each instrumented component the estimator over-reports on
22
+ * purpose: components carry a peak-or-current value, in-flight bytes are
23
+ * counted before they are freed, and LRU bytes are the cap rather than the
24
+ * live footprint.
25
+ *
26
+ * Across components it can only sum what is instrumented. Allocation sites
27
+ * that neither bump a diag counter nor take a lease via
28
+ * `acquireSupervisorAllocation` are invisible to it, and it has no way to
29
+ * infer their size — so any site known to be uninstrumented is named in
30
+ * HEAP_BLIND_SPOTS rather than left silently absent.
31
+ *
32
+ * The prefetch-bundle path used to be exactly that gap, and a large one: the
33
+ * supervisor DO was reset three times under bundle construction while this
34
+ * estimator reported a 9.4 MiB baseline and 35 KB of LRU. Both of its sites
35
+ * are now instrumented where they allocate — the per-exec build leases the
36
+ * budget it spends (`prefetchBundleBytes`) and the cross-exec cache is bounded
37
+ * by bytes and reports its retained total (`prefetchCacheBytes`) — so they are
38
+ * components of the breakdown rather than caveats beside it.
39
+ *
40
+ * `blindSpots` is still returned alongside `estimatedBytes` because a list
41
+ * being empty is a claim that has to be re-earned as the system grows, not a
42
+ * property of the estimator. Closing a blind spot always means instrumenting
43
+ * it at its own allocation site and adding it to the breakdown — never
44
+ * estimating it from here.
45
+ *
46
+ * Eviction-label taxonomy
47
+ * ───────────────────────
48
+ * Workerd distinguishes five labelled eviction reasons:
49
+ * - lru → memory pressure on the runtime process
50
+ * - condemned → kill (operator / abuse pipeline)
51
+ * - inactive → idle eviction (70-140 s of no traffic)
52
+ * - dynamic_worker → per-owner LRU cap (default 50)
53
+ * - dynamic_worker_banned → Dice abuse-detection ban
54
+ *
55
+ * Nimbus surfaces the labels here so any tool that reads
56
+ * /api/_diag/memory has a fixed, well-known taxonomy to count against.
57
+ * The actual count of evictions Nimbus has observed lives in the C'.2
58
+ * recovery_event ring, separate from this module.
59
+ */
60
+ import { PRE_BUNDLE_CONCURRENCY, PRE_BUNDLE_SLICE_CAP_BYTES, SUPERVISOR_HEAP_CEILING_BYTES, } from './limits.js';
61
+ import { readSupervisorAllocationBudget, } from './heavy-alloc-coord.js';
62
+ /**
63
+ * Five labelled workerd eviction reasons. Surfaced as a constant
64
+ * taxonomy in /api/_diag/memory so any consumer can count observed
65
+ * events against the well-known set.
66
+ *
67
+ */
68
+ export const WORKERD_EVICTION_LABELS = [
69
+ 'lru',
70
+ 'condemned',
71
+ 'inactive',
72
+ 'dynamic_worker',
73
+ 'dynamic_worker_banned',
74
+ ];
75
+ /**
76
+ * Supervisor allocation sites known to be unaccounted for.
77
+ *
78
+ * Empty: both former entries were the prefetch-bundle path in
79
+ * facets/manager.ts, and both are now instrumented at their own allocation
80
+ * sites rather than described from here. `buildPrefetchBundle` takes an
81
+ * `acquireSupervisorAllocation` lease and reports
82
+ * `breakdown.prefetchBundleBytes`; `prefetchBundleCache` is bounded by
83
+ * PREFETCH_CACHE_MAX_BYTES instead of by entry count and reports its live
84
+ * retained total as `breakdown.prefetchCacheBytes`.
85
+ *
86
+ * An empty list says nothing is CURRENTLY known to be missing — it is not a
87
+ * proof of completeness, which no list can be. Adding an entry is how a newly
88
+ * discovered gap stays visible in /api/_diag/memory until it is closed the
89
+ * same way: instrumented where it allocates, then folded into HeapBreakdown.
90
+ */
91
+ export const HEAP_BLIND_SPOTS = [];
92
+ // ── Architectural constants for non-counter contributors ────────────────
93
+ //
94
+ // Each constant below has a comment explaining the source-of-truth and
95
+ // when it is expected to change (typically: when a Track A' wave lands).
96
+ // They are NOT runtime-measured because the underlying values aren't
97
+ // observable from JS — they're properties of the worker bundle itself.
98
+ /**
99
+ * Static supervisor baseline. The compiled worker bundle resident in V8
100
+ * isolate memory: module sources, class definitions, top-level imports,
101
+ * lookup tables.
102
+ *
103
+ * Phase 2 A'.5 dropped the 16 MiB esbuild-wasm base64 string from the
104
+ * generated module (it now lives in env.ASSETS), shrinking the worker
105
+ * bundle by ~21 MiB UTF-16-resident. Empirical baseline post-A'.5 is
106
+ * ~9 MiB (verified via the heap-estimator probe + bundle size).
107
+ *
108
+ * This is constant across a deploy. A bundle-size shrink (removing an
109
+ * unused dependency, splitting another big constant out to assets)
110
+ * would lower this further.
111
+ */
112
+ const SUPERVISOR_BASELINE_BYTES = 9 * 1024 * 1024;
113
+ /**
114
+ * Estimate pre-bundle slice bytes resident in the supervisor heap.
115
+ *
116
+ * Pre A'.2: the supervisor builds the slice in heap (up to
117
+ * SLICE_CAP_BYTES = 28 MiB per concurrent slot) before passing to the
118
+ * pre-bundle facet. With PRE_BUNDLE_CONCURRENCY = 1 the cap is 28 MiB.
119
+ *
120
+ * Diag counters track pre-bundle attempts/completions but not
121
+ * in-flight slice bytes specifically. Until A'.2 streams the slice,
122
+ * we treat the slice as PRESENT in supervisor heap iff the pre-bundle
123
+ * phase is active AND a slot is in flight (no completion yet for the
124
+ * current attempted count).
125
+ *
126
+ * After A'.2 lands the slice flows via ReadableStream-over-RPC and
127
+ * supervisor never holds the bytes — this contribution drops to 0.
128
+ */
129
+ export function estimatePreBundleSliceBytes(c) {
130
+ // Conservative: if the most recent batch attempted >0 specs and the
131
+ // batch hasn't fully completed, assume one slice's worth of bytes
132
+ // are in flight.
133
+ const f = c.preBundleFacet;
134
+ const inFlight = f.attempted - f.bundlesCompleted - f.errors - f.skipped;
135
+ if (inFlight <= 0)
136
+ return 0;
137
+ return Math.min(inFlight, PRE_BUNDLE_CONCURRENCY) * PRE_BUNDLE_SLICE_CAP_BYTES;
138
+ }
139
+ /**
140
+ * Build a heap estimate from runtime counters + VFS inputs.
141
+ *
142
+ * Deterministic and I/O-free. Called from the /api/_diag/memory request
143
+ * handler; the allocation-budget snapshot is process-local state.
144
+ */
145
+ export function estimateSupervisorHeap(c, vfs) {
146
+ const allocationBudget = readSupervisorAllocationBudget();
147
+ const preBundleSliceBytes = estimatePreBundleSliceBytes(c);
148
+ // The prefetch build holds a lease, so its bytes are already inside
149
+ // allocationBudget.current — naming it here moves it out of the
150
+ // unattributed remainder rather than adding to the total twice.
151
+ const transientAttributedBytes = vfs.inFlightWriteBytes +
152
+ preBundleSliceBytes +
153
+ c.inFlightRpcPayloadBytes +
154
+ c.prefetchBundleBytes;
155
+ const breakdown = {
156
+ supervisorBaselineBytes: SUPERVISOR_BASELINE_BYTES,
157
+ vfsLruBytes: vfs.cacheHotBytes,
158
+ vfsInFlightBytes: vfs.inFlightWriteBytes,
159
+ preBundleSliceBytes,
160
+ streamingBuffersBytes: c.inFlightRpcPayloadBytes,
161
+ prefetchBundleBytes: c.prefetchBundleBytes,
162
+ prefetchCacheBytes: c.prefetchCacheBytes,
163
+ unattributedReservationBytes: Math.max(0, allocationBudget.current - transientAttributedBytes),
164
+ };
165
+ const estimatedBytes = breakdown.supervisorBaselineBytes +
166
+ breakdown.vfsLruBytes +
167
+ breakdown.vfsInFlightBytes +
168
+ breakdown.preBundleSliceBytes +
169
+ breakdown.streamingBuffersBytes +
170
+ breakdown.prefetchBundleBytes +
171
+ breakdown.prefetchCacheBytes +
172
+ breakdown.unattributedReservationBytes;
173
+ const percentOfCeiling = Math.round((estimatedBytes / SUPERVISOR_HEAP_CEILING_BYTES) * 1000) / 10;
174
+ return {
175
+ estimatedBytes,
176
+ ceilingBytes: SUPERVISOR_HEAP_CEILING_BYTES,
177
+ percentOfCeiling,
178
+ breakdown,
179
+ blindSpots: HEAP_BLIND_SPOTS,
180
+ blindSpotCeilingBytes: HEAP_BLIND_SPOTS.some((s) => s.capBytes === null)
181
+ ? null
182
+ : HEAP_BLIND_SPOTS.reduce((sum, s) => sum + (s.capBytes ?? 0), 0),
183
+ allocationBudget,
184
+ };
185
+ }
@@ -0,0 +1,93 @@
1
+ /**
2
+ * Supervisor-local byte-budget back-pressure for transient allocations.
3
+ *
4
+ * A DO can share its isolate's 128 MiB platform ceiling with peer DOs, so
5
+ * independent "safe" allocations cannot each assume the full ceiling.
6
+ * Module scope gives every allocator in one supervisor isolate a single FIFO
7
+ * budget. Full-budget owners are exclusive; weighted owners may overlap only
8
+ * while their retained-byte claims fit together.
9
+ */
10
+ import { type ResizableCreditLease, type WeightedCreditPoolOptions } from './weighted-credit-pool.js';
11
+ export interface SupervisorAllocationBudgetStats {
12
+ readonly capacity: number;
13
+ readonly current: number;
14
+ readonly peak: number;
15
+ readonly queued: number;
16
+ }
17
+ interface AllocationBudgetLifecycle {
18
+ onActive?: () => void;
19
+ onIdle?: () => void;
20
+ }
21
+ /**
22
+ * Reusable contract behind the supervisor singleton. A separate instance is
23
+ * useful in tests and keeps the invariant independent of Nimbus internals.
24
+ */
25
+ export declare class SupervisorAllocationBudget {
26
+ readonly capacity: number;
27
+ private readonly lifecycle;
28
+ private readonly credits;
29
+ private active;
30
+ /**
31
+ * Leases that drive the disposable-cache lifecycle. Occupancy cannot stand
32
+ * in for this: a read holds credit too, and a read must not sacrifice the
33
+ * cache it is filling.
34
+ */
35
+ private lifecycleHolders;
36
+ constructor(capacity: number, lifecycle?: AllocationBudgetLifecycle, reserve?: WeightedCreditPoolOptions);
37
+ get stats(): SupervisorAllocationBudgetStats;
38
+ /**
39
+ * Reserve bytes and, while held, mark the budget active so observers can
40
+ * free heap headroom. For owners whose payload is what the headroom is for:
41
+ * installs, clones, pre-bundles, boot payloads, streamed writes.
42
+ */
43
+ acquire(bytes: number, signal?: AbortSignal): Promise<ResizableCreditLease>;
44
+ /**
45
+ * Reserve bytes WITHOUT driving the cache lifecycle, for owners that are
46
+ * filling the disposable cache rather than competing with it. Shrinking a
47
+ * chunk cache to serve a chunk read is circular: sequentially the budget
48
+ * empties between every read, so an occupancy-edged observer fires once per
49
+ * read and the cache is pinned at its shrunk floor for the whole workload,
50
+ * never warming. The byte credit is still taken — back-pressure is what
51
+ * stops a large read overlapping a large write.
52
+ */
53
+ acquireWithoutLifecycle(bytes: number, signal?: AbortSignal): Promise<ResizableCreditLease>;
54
+ private _acquire;
55
+ }
56
+ /**
57
+ * A registered observer (typically a SqliteVFS) receives shrink/restore
58
+ * signals when the shared budget transitions idle↔active.
59
+ */
60
+ interface AllocObserver {
61
+ /** Called when budget occupancy transitions idle → active. */
62
+ onAcquire?: () => void;
63
+ /** Called when budget occupancy transitions active → idle. */
64
+ onRelease?: () => void;
65
+ }
66
+ /**
67
+ * Register an observer that fires when the shared allocation budget
68
+ * transitions idle → active and active → idle.
69
+ * Returns an unsubscribe function. Idempotent: registering the same
70
+ * observer twice is a no-op (Set semantics).
71
+ *
72
+ * NimbusSession uses this to shrink the disposable VFS LRU while transient
73
+ * payloads are admitted.
74
+ */
75
+ export declare function registerAllocObserver(o: AllocObserver): () => void;
76
+ /**
77
+ * Reserve an exact number of supervisor-resident bytes.
78
+ */
79
+ export declare function acquireSupervisorAllocation(bytes: number, signal?: AbortSignal): Promise<ResizableCreditLease>;
80
+ /**
81
+ * Reserve bytes for a filesystem READ. Takes the same byte credit as any
82
+ * other owner but does not shrink the disposable VFS cache, which reads are
83
+ * there to fill.
84
+ */
85
+ export declare function acquireSupervisorReadAllocation(bytes: number, signal?: AbortSignal): Promise<ResizableCreditLease>;
86
+ /**
87
+ * Reserve the full budget for an allocation whose retained size is not known
88
+ * accurately enough to overlap safely with other heavy work.
89
+ */
90
+ export declare function acquireHeavyAlloc(signal?: AbortSignal): Promise<() => void>;
91
+ export declare function readSupervisorAllocationBudget(): SupervisorAllocationBudgetStats;
92
+ export {};
93
+ //# sourceMappingURL=heavy-alloc-coord.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"heavy-alloc-coord.d.ts","sourceRoot":"","sources":["../src/heavy-alloc-coord.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAOH,OAAO,EAEL,KAAK,oBAAoB,EACzB,KAAK,yBAAyB,EAC/B,MAAM,2BAA2B,CAAC;AAEnC,MAAM,WAAW,+BAA+B;IAC9C,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAED,UAAU,yBAAyB;IACjC,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAC;IACtB,MAAM,CAAC,EAAE,MAAM,IAAI,CAAC;CACrB;AAED;;;GAGG;AACH,qBAAa,0BAA0B;IAWnC,QAAQ,CAAC,QAAQ,EAAE,MAAM;IACzB,OAAO,CAAC,QAAQ,CAAC,SAAS;IAX5B,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAqB;IAC7C,OAAO,CAAC,MAAM,CAAS;IACvB;;;;OAIG;IACH,OAAO,CAAC,gBAAgB,CAAK;gBAGlB,QAAQ,EAAE,MAAM,EACR,SAAS,GAAE,yBAA8B,EAC1D,OAAO,GAAE,yBAA8B;IAKzC,IAAI,KAAK,IAAI,+BAA+B,CAK3C;IAED;;;;OAIG;IACH,OAAO,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,oBAAoB,CAAC;IAI3E;;;;;;;;OAQG;IACH,uBAAuB,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,oBAAoB,CAAC;YAI7E,QAAQ;CAmCvB;AAED;;;GAGG;AACH,UAAU,aAAa;IACrB,8DAA8D;IAC9D,SAAS,CAAC,EAAE,MAAM,IAAI,CAAC;IACvB,8DAA8D;IAC9D,SAAS,CAAC,EAAE,MAAM,IAAI,CAAC;CACxB;AAiBD;;;;;;;;GAQG;AACH,wBAAgB,qBAAqB,CAAC,CAAC,EAAE,aAAa,GAAG,MAAM,IAAI,CAWlE;AAqCD;;GAEG;AACH,wBAAgB,2BAA2B,CACzC,KAAK,EAAE,MAAM,EACb,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,oBAAoB,CAAC,CAE/B;AAED;;;;GAIG;AACH,wBAAgB,+BAA+B,CAC7C,KAAK,EAAE,MAAM,EACb,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,oBAAoB,CAAC,CAE/B;AAED;;;GAGG;AACH,wBAAsB,iBAAiB,CAAC,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,MAAM,IAAI,CAAC,CAMjF;AAED,wBAAgB,8BAA8B,IAAI,+BAA+B,CAEhF"}
@@ -0,0 +1,181 @@
1
+ /**
2
+ * Supervisor-local byte-budget back-pressure for transient allocations.
3
+ *
4
+ * A DO can share its isolate's 128 MiB platform ceiling with peer DOs, so
5
+ * independent "safe" allocations cannot each assume the full ceiling.
6
+ * Module scope gives every allocator in one supervisor isolate a single FIFO
7
+ * budget. Full-budget owners are exclusive; weighted owners may overlap only
8
+ * while their retained-byte claims fit together.
9
+ */
10
+ import { CHUNK_SIZE, SUPERVISOR_IN_FLIGHT_ALLOCATION_BUDGET_BYTES, SUPERVISOR_READ_RESERVE_BYTES, } from './limits.js';
11
+ import { WeightedCreditPool, } from './weighted-credit-pool.js';
12
+ /**
13
+ * Reusable contract behind the supervisor singleton. A separate instance is
14
+ * useful in tests and keeps the invariant independent of Nimbus internals.
15
+ */
16
+ export class SupervisorAllocationBudget {
17
+ capacity;
18
+ lifecycle;
19
+ credits;
20
+ active = false;
21
+ /**
22
+ * Leases that drive the disposable-cache lifecycle. Occupancy cannot stand
23
+ * in for this: a read holds credit too, and a read must not sacrifice the
24
+ * cache it is filling.
25
+ */
26
+ lifecycleHolders = 0;
27
+ constructor(capacity, lifecycle = {}, reserve = {}) {
28
+ this.capacity = capacity;
29
+ this.lifecycle = lifecycle;
30
+ this.credits = new WeightedCreditPool(capacity, reserve);
31
+ }
32
+ get stats() {
33
+ return {
34
+ capacity: this.capacity,
35
+ ...this.credits.stats,
36
+ };
37
+ }
38
+ /**
39
+ * Reserve bytes and, while held, mark the budget active so observers can
40
+ * free heap headroom. For owners whose payload is what the headroom is for:
41
+ * installs, clones, pre-bundles, boot payloads, streamed writes.
42
+ */
43
+ acquire(bytes, signal) {
44
+ return this._acquire(bytes, signal, true);
45
+ }
46
+ /**
47
+ * Reserve bytes WITHOUT driving the cache lifecycle, for owners that are
48
+ * filling the disposable cache rather than competing with it. Shrinking a
49
+ * chunk cache to serve a chunk read is circular: sequentially the budget
50
+ * empties between every read, so an occupancy-edged observer fires once per
51
+ * read and the cache is pinned at its shrunk floor for the whole workload,
52
+ * never warming. The byte credit is still taken — back-pressure is what
53
+ * stops a large read overlapping a large write.
54
+ */
55
+ acquireWithoutLifecycle(bytes, signal) {
56
+ return this._acquire(bytes, signal, false);
57
+ }
58
+ async _acquire(bytes, signal, drivesLifecycle) {
59
+ const credit = await this.credits.acquire(bytes, signal);
60
+ if (drivesLifecycle) {
61
+ this.lifecycleHolders++;
62
+ if (!this.active) {
63
+ this.active = true;
64
+ this.lifecycle.onActive?.();
65
+ }
66
+ }
67
+ let released = false;
68
+ return {
69
+ get bytes() {
70
+ return credit.bytes;
71
+ },
72
+ shrinkTo: (nextBytes) => {
73
+ credit.shrinkTo(nextBytes);
74
+ },
75
+ release: () => {
76
+ if (released)
77
+ return;
78
+ released = true;
79
+ credit.release();
80
+ if (!drivesLifecycle)
81
+ return;
82
+ this.lifecycleHolders--;
83
+ if (this.lifecycleHolders === 0 && this.active) {
84
+ this.active = false;
85
+ this.lifecycle.onIdle?.();
86
+ }
87
+ },
88
+ };
89
+ }
90
+ }
91
+ const coord = { observers: new Set(), active: false };
92
+ function getCoord() {
93
+ return coord;
94
+ }
95
+ function errorMessage(error) {
96
+ return error instanceof Error ? error.message : String(error);
97
+ }
98
+ /**
99
+ * Register an observer that fires when the shared allocation budget
100
+ * transitions idle → active and active → idle.
101
+ * Returns an unsubscribe function. Idempotent: registering the same
102
+ * observer twice is a no-op (Set semantics).
103
+ *
104
+ * NimbusSession uses this to shrink the disposable VFS LRU while transient
105
+ * payloads are admitted.
106
+ */
107
+ export function registerAllocObserver(o) {
108
+ const c = getCoord();
109
+ const isNew = !c.observers.has(o);
110
+ c.observers.add(o);
111
+ if (isNew && c.active) {
112
+ try {
113
+ o.onAcquire?.();
114
+ }
115
+ catch (error) {
116
+ // eslint-disable-next-line no-console
117
+ console.error('[heavy-alloc-coord] observer.onAcquire threw:', errorMessage(error));
118
+ }
119
+ }
120
+ return () => { c.observers.delete(o); };
121
+ }
122
+ function fireOnAcquire() {
123
+ const c = getCoord();
124
+ c.active = true;
125
+ for (const o of c.observers) {
126
+ try {
127
+ o.onAcquire?.();
128
+ }
129
+ catch (error) {
130
+ // Observer errors must NOT break the heavy-alloc protocol.
131
+ // Log and continue.
132
+ // eslint-disable-next-line no-console
133
+ console.error('[heavy-alloc-coord] observer.onAcquire threw:', errorMessage(error));
134
+ }
135
+ }
136
+ }
137
+ function fireOnRelease() {
138
+ const c = getCoord();
139
+ c.active = false;
140
+ for (const o of c.observers) {
141
+ try {
142
+ o.onRelease?.();
143
+ }
144
+ catch (error) {
145
+ // eslint-disable-next-line no-console
146
+ console.error('[heavy-alloc-coord] observer.onRelease threw:', errorMessage(error));
147
+ }
148
+ }
149
+ }
150
+ const supervisorAllocationBudget = new SupervisorAllocationBudget(SUPERVISOR_IN_FLIGHT_ALLOCATION_BUDGET_BYTES, {
151
+ onActive: fireOnAcquire,
152
+ onIdle: fireOnRelease,
153
+ },
154
+ // A chunk-sized read draws on this rather than queueing behind a
155
+ // multi-megabyte owner for a wait unrelated to its own cost.
156
+ { smallRequestBytes: CHUNK_SIZE, reserve: SUPERVISOR_READ_RESERVE_BYTES });
157
+ /**
158
+ * Reserve an exact number of supervisor-resident bytes.
159
+ */
160
+ export function acquireSupervisorAllocation(bytes, signal) {
161
+ return supervisorAllocationBudget.acquire(bytes, signal);
162
+ }
163
+ /**
164
+ * Reserve bytes for a filesystem READ. Takes the same byte credit as any
165
+ * other owner but does not shrink the disposable VFS cache, which reads are
166
+ * there to fill.
167
+ */
168
+ export function acquireSupervisorReadAllocation(bytes, signal) {
169
+ return supervisorAllocationBudget.acquireWithoutLifecycle(bytes, signal);
170
+ }
171
+ /**
172
+ * Reserve the full budget for an allocation whose retained size is not known
173
+ * accurately enough to overlap safely with other heavy work.
174
+ */
175
+ export async function acquireHeavyAlloc(signal) {
176
+ const lease = await acquireSupervisorAllocation(SUPERVISOR_IN_FLIGHT_ALLOCATION_BUDGET_BYTES, signal);
177
+ return lease.release;
178
+ }
179
+ export function readSupervisorAllocationBudget() {
180
+ return supervisorAllocationBudget.stats;
181
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * @nimbus-sh/platform — the measured Cloudflare platform truths Nimbus is
3
+ * built on: limits tables, the error taxonomy for isolates that die without
4
+ * throwing, the supervisor budget machinery, and wire-format primitives.
5
+ *
6
+ * A zero-dependency leaf: core, fabric, and the worker all consume this
7
+ * package; nothing here imports from any of them.
8
+ */
9
+ export * from './diag-counters.js';
10
+ export * from './heap-estimate.js';
11
+ export * from './heavy-alloc-coord.js';
12
+ export * from './install-phase.js';
13
+ export * from './limits.js';
14
+ export * from './oom-classify.js';
15
+ export * from './oom-discriminator.js';
16
+ export * from './rpc-dispose.js';
17
+ export * from './w7-frame.js';
18
+ export * from './weighted-credit-pool.js';
19
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,cAAc,oBAAoB,CAAC;AACnC,cAAc,oBAAoB,CAAC;AACnC,cAAc,wBAAwB,CAAC;AACvC,cAAc,oBAAoB,CAAC;AACnC,cAAc,aAAa,CAAC;AAC5B,cAAc,mBAAmB,CAAC;AAClC,cAAc,wBAAwB,CAAC;AACvC,cAAc,kBAAkB,CAAC;AACjC,cAAc,eAAe,CAAC;AAC9B,cAAc,2BAA2B,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,18 @@
1
+ /**
2
+ * @nimbus-sh/platform — the measured Cloudflare platform truths Nimbus is
3
+ * built on: limits tables, the error taxonomy for isolates that die without
4
+ * throwing, the supervisor budget machinery, and wire-format primitives.
5
+ *
6
+ * A zero-dependency leaf: core, fabric, and the worker all consume this
7
+ * package; nothing here imports from any of them.
8
+ */
9
+ export * from './diag-counters.js';
10
+ export * from './heap-estimate.js';
11
+ export * from './heavy-alloc-coord.js';
12
+ export * from './install-phase.js';
13
+ export * from './limits.js';
14
+ export * from './oom-classify.js';
15
+ export * from './oom-discriminator.js';
16
+ export * from './rpc-dispose.js';
17
+ export * from './w7-frame.js';
18
+ export * from './weighted-credit-pool.js';
@@ -0,0 +1,47 @@
1
+ /**
2
+ * _shared/install-phase.ts — single source of truth for the npm-install
3
+ * state-machine phase enum.
4
+ *
5
+ * History: CLN-1 (2026-05-11) — consolidated from two diverged copies.
6
+ * - `npm/installer.ts:93-95` was the progress-event variant. Included
7
+ * 'lock-check' (a real progress event emitted while parsing
8
+ * package-lock.json) but lacked 'idle'.
9
+ * - `observability/diag-counters.ts:33-35` was the counter-state variant.
10
+ * Included 'idle' (the resting state when no install is in flight)
11
+ * but lacked 'lock-check'. The diag-counter's `setInstallPhase()`
12
+ * was actually called with 'idle' (installer.ts:184,201) — passing
13
+ * a value not declared in installer.ts's local type. The type check
14
+ * fell through to diag-counters' type because installer.ts imports
15
+ * setInstallPhase from there; the local `InstallPhase` definition
16
+ * in installer.ts was only used by `InstallProgress.phase`.
17
+ *
18
+ * Going forward both consumers import from this module; any new phase
19
+ * value added here is automatically available everywhere.
20
+ *
21
+ * Phase semantics (chronological order for a single install run):
22
+ *
23
+ * idle resting state, no install in flight
24
+ * ↓
25
+ * lock-check parsing existing package-lock.json (if present)
26
+ * ↓
27
+ * resolve walking the dep tree, fetching packuments
28
+ * ↓
29
+ * hoist hoisting peer-deps + flattening node_modules
30
+ * ↓
31
+ * diff comparing target tree against current install state
32
+ * ↓
33
+ * fetch downloading missing tarballs
34
+ * ↓
35
+ * write extracting tarballs into the VFS
36
+ * ↓
37
+ * link-bins creating bin/ symlinks
38
+ * ↓
39
+ * bundle (post-install) pre-bundling for esbuild fast path
40
+ * ↓
41
+ * done install complete; about to return to 'idle'
42
+ *
43
+ * The phase strings are surfaced via /api/_diag/install-pipeline and
44
+ * displayed in the install-progress UI; treat them as a public API.
45
+ */
46
+ export type InstallPhase = 'idle' | 'lock-check' | 'resolve' | 'hoist' | 'diff' | 'fetch' | 'write' | 'link-bins' | 'bundle' | 'done';
47
+ //# sourceMappingURL=install-phase.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"install-phase.d.ts","sourceRoot":"","sources":["../src/install-phase.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4CG;AACH,MAAM,MAAM,YAAY,GACpB,MAAM,GACN,YAAY,GACZ,SAAS,GACT,OAAO,GACP,MAAM,GACN,OAAO,GACP,OAAO,GACP,WAAW,GACX,QAAQ,GACR,MAAM,CAAC"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,83 @@
1
+ /**
2
+ * limits.ts — the measured Cloudflare platform limits Nimbus is built
3
+ * against.
4
+ *
5
+ * Every constant here is a fact about the platform — a hard ceiling, or a
6
+ * measured envelope proven safe against one — rather than a Nimbus policy
7
+ * choice. Policy constants (versions, defaults, feature bounds) live in
8
+ * `@nimbus-sh/core/constants.js`; this table is what they are derived from.
9
+ */
10
+ /**
11
+ * The storage/wire chunk quantum: bytes per content chunk in the SQLite
12
+ * VFS schema, and therefore the unit every transaction bound and streamed
13
+ * write frame is denominated in. SQLite's per-ROW value cap is 2 MB (key
14
+ * length included, measured single-value ceiling 2,199,981 B), so 64 KiB
15
+ * keeps a chunk row far under it while amortizing per-row overhead.
16
+ */
17
+ export declare const CHUNK_SIZE = 65536;
18
+ export declare const MAX_TX_BLOB_BYTES: number;
19
+ export declare const MAX_TX_LOGICAL_ROWS = 256;
20
+ export declare const MAX_TX_SQL_EXECS = 64;
21
+ /**
22
+ * Storage per SQLite-backed Durable Object: 10 decimal GB documented
23
+ * (Workers Paid), shared by the root object, every facet beneath it, and
24
+ * every clone — a copy-on-write clone consumes its FULL logical bytes with
25
+ * no CoW credit. Probed window: 10.58e9 logical bytes fit, 11.6e9 failed —
26
+ * 10 GiB (10,737,418,240) falls INSIDE that window and is not a number to
27
+ * design to. At the wall, ordinary writes fail catchably as SQLITE_FULL
28
+ * ('database or disk is full') while reads and DELETEs keep working, so the
29
+ * recovery is to drain; a facet CLONE over the wall is an uncatchable reset
30
+ * that empties the destination, so clone admission is decided BEFORE the
31
+ * clone, with reserve.
32
+ */
33
+ export declare const DO_STORAGE_LIMIT_BYTES = 10000000000;
34
+ /**
35
+ * Maximum SQL statement text per exec. Documented "100 KB"; read as binary
36
+ * KiB because the value is SQLite's compile-time SQLITE_MAX_SQL_LENGTH
37
+ * rather than a billing quantity (unverified reading — reached by generated
38
+ * statements, a batched multi-VALUES insert being the realistic breach).
39
+ */
40
+ export declare const SQLITE_MAX_STATEMENT_BYTES: number;
41
+ /**
42
+ * Maximum bound parameters per query. The easiest cap to hit accidentally:
43
+ * a batched insert of more than 100/columns rows in one statement breaches
44
+ * it.
45
+ */
46
+ export declare const SQLITE_MAX_BOUND_PARAMETERS = 100;
47
+ /**
48
+ * Maximum bytes of one string, BLOB, or table ROW — the bound is per ROW,
49
+ * key length included, not per value. The measured single-value ceiling is
50
+ * 2,199,981 bytes, ABOVE this constant: budgeting each value against the
51
+ * row bound is conservative and correct, budgeting a row against the value
52
+ * ceiling is not. Writes over it fail; reads and deletes keep working.
53
+ */
54
+ export declare const SQLITE_MAX_ROW_BYTES = 2000000;
55
+ /**
56
+ * A `blockConcurrencyWhile()` callback still pending this long is
57
+ * cancelled and the Durable Object is RESET — every event queued behind
58
+ * the gate dies with it. Proven by probe (reset observed at 31 s against a
59
+ * 31 s-busy neighbour; cold activations track a busy neighbour 1:1), and
60
+ * partyserver runs `onStart()` inside the gate, so ordinary traffic can
61
+ * reach this. Anything on that path must be bounded well below this;
62
+ * fabric defers async reconciliation off the gate entirely (`onColdStart`).
63
+ */
64
+ export declare const BLOCK_CONCURRENCY_CANCEL_MS = 30000;
65
+ /**
66
+ * Bytes one hibernatable WebSocket attachment may serialize to. Verified in
67
+ * workerd source at the pinned version (v1.20260603.1,
68
+ * src/workerd/api/web-socket.h: `MAX_ATTACHMENT_SIZE = 1024 * 16`). The
69
+ * bound is on the SERIALIZED bytes — workerd re-serializes on every
70
+ * `serializeAttachment` call to check it — not on UTF-8 JSON text, so a
71
+ * JSON-length measurement is an approximation and only the platform's own
72
+ * refusal ("A WebSocket 'attachment' cannot be larger than 16384 bytes.")
73
+ * is the ceiling.
74
+ */
75
+ export declare const WS_ATTACHMENT_LIMIT_BYTES = 16384;
76
+ export declare const MAX_RPC_SAFE_PAYLOAD_BYTES: number;
77
+ export declare const SUPERVISOR_HEAP_CEILING_BYTES: number;
78
+ export declare const SUPERVISOR_IN_FLIGHT_ALLOCATION_BUDGET_BYTES: number;
79
+ export declare const SUPERVISOR_READ_RESERVE_BYTES: number;
80
+ export declare const MAX_GLOBAL_WRITE_STREAM_CREDIT_BYTES: number;
81
+ export declare const PRE_BUNDLE_SLICE_CAP_BYTES: number;
82
+ export declare const PRE_BUNDLE_CONCURRENCY = 1;
83
+ //# sourceMappingURL=limits.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"limits.d.ts","sourceRoot":"","sources":["../src/limits.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AASH;;;;;;GAMG;AACH,eAAO,MAAM,UAAU,QAAS,CAAC;AAEjC,eAAO,MAAM,iBAAiB,QAAkB,CAAC;AACjD,eAAO,MAAM,mBAAmB,MAAM,CAAC;AACvC,eAAO,MAAM,gBAAgB,KAAK,CAAC;AAInC;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,sBAAsB,cAAiB,CAAC;AAErD;;;;;GAKG;AACH,eAAO,MAAM,0BAA0B,QAAa,CAAC;AAErD;;;;GAIG;AACH,eAAO,MAAM,2BAA2B,MAAM,CAAC;AAE/C;;;;;;GAMG;AACH,eAAO,MAAM,oBAAoB,UAAY,CAAC;AAI9C;;;;;;;;GAQG;AACH,eAAO,MAAM,2BAA2B,QAAS,CAAC;AAIlD;;;;;;;;;GASG;AACH,eAAO,MAAM,yBAAyB,QAAS,CAAC;AAOhD,eAAO,MAAM,0BAA0B,QAAmB,CAAC;AA6B3D,eAAO,MAAM,6BAA6B,QAAmB,CAAC;AAO9D,eAAO,MAAM,4CAA4C,QAAmB,CAAC;AAY7E,eAAO,MAAM,6BAA6B,QAAc,CAAC;AAKzD,eAAO,MAAM,oCAAoC,QAAkB,CAAC;AAQpE,eAAO,MAAM,0BAA0B,QAA6B,CAAC;AACrE,eAAO,MAAM,sBAAsB,IAAI,CAAC"}