@nimbus-sh/core 0.3.0 → 0.5.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 (155) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +140 -0
  3. package/dist/_shared/error-text.d.ts +15 -0
  4. package/dist/_shared/error-text.d.ts.map +1 -0
  5. package/dist/_shared/error-text.js +21 -0
  6. package/dist/_shared/exports-resolver.d.ts +19 -7
  7. package/dist/_shared/exports-resolver.d.ts.map +1 -1
  8. package/dist/_shared/exports-resolver.js +1 -1
  9. package/dist/_shared/retry.d.ts.map +1 -1
  10. package/dist/_shared/retry.js +3 -1
  11. package/dist/_shared/tarball-stream.d.ts +93 -0
  12. package/dist/_shared/tarball-stream.d.ts.map +1 -0
  13. package/dist/_shared/tarball-stream.js +235 -0
  14. package/dist/_shared/tarball.d.ts +17 -0
  15. package/dist/_shared/tarball.d.ts.map +1 -0
  16. package/dist/_shared/tarball.js +39 -0
  17. package/dist/_shared/w7-frame.js +2 -1
  18. package/dist/constants.d.ts.map +1 -1
  19. package/dist/constants.js +30 -16
  20. package/dist/observability/oom-classify.d.ts +1 -1
  21. package/dist/observability/oom-classify.d.ts.map +1 -1
  22. package/dist/observability/oom-classify.js +7 -0
  23. package/dist/observability/oom-discriminator.d.ts +1 -1
  24. package/dist/runtime/clang-runner.d.ts +38 -0
  25. package/dist/runtime/clang-runner.d.ts.map +1 -0
  26. package/dist/runtime/clang-runner.js +866 -0
  27. package/dist/runtime/esbuild-service.d.ts +4 -3
  28. package/dist/runtime/esbuild-service.d.ts.map +1 -1
  29. package/dist/runtime/esbuild-service.js +9 -4
  30. package/dist/runtime/facet-host.d.ts +12 -0
  31. package/dist/runtime/facet-host.d.ts.map +1 -1
  32. package/dist/runtime/installed-runtimes.d.ts +4 -3
  33. package/dist/runtime/installed-runtimes.d.ts.map +1 -1
  34. package/dist/runtime/local-facet-host.d.ts.map +1 -1
  35. package/dist/runtime/local-facet-host.js +29 -9
  36. package/dist/runtime/long-running-handle.d.ts +3 -3
  37. package/dist/runtime/long-running-handle.d.ts.map +1 -1
  38. package/dist/runtime/os-contracts.d.ts +7 -5
  39. package/dist/runtime/os-contracts.d.ts.map +1 -1
  40. package/dist/runtime/port-registry.d.ts +28 -0
  41. package/dist/runtime/port-registry.d.ts.map +1 -1
  42. package/dist/runtime/port-registry.js +79 -9
  43. package/dist/runtime/process-logs.d.ts +9 -1
  44. package/dist/runtime/process-logs.d.ts.map +1 -1
  45. package/dist/runtime/project-detect.d.ts +6 -1
  46. package/dist/runtime/project-detect.d.ts.map +1 -1
  47. package/dist/runtime/require-resolver.d.ts.map +1 -1
  48. package/dist/runtime/require-resolver.js +3 -4
  49. package/dist/runtime/ruby-gems.d.ts +30 -0
  50. package/dist/runtime/ruby-gems.d.ts.map +1 -0
  51. package/dist/runtime/ruby-gems.js +636 -0
  52. package/dist/runtime/ruby-runner.d.ts +127 -0
  53. package/dist/runtime/ruby-runner.d.ts.map +1 -0
  54. package/dist/runtime/ruby-runner.js +1357 -0
  55. package/dist/runtime/runtime-registry.d.ts +4 -3
  56. package/dist/runtime/runtime-registry.d.ts.map +1 -1
  57. package/dist/runtime/runtime-registry.js +2 -1
  58. package/dist/runtime/session-process-supervisor.d.ts +15 -0
  59. package/dist/runtime/session-process-supervisor.d.ts.map +1 -1
  60. package/dist/runtime/session-process-supervisor.js +30 -0
  61. package/dist/runtime/sqlite-runtime-fs-bridge.js +1 -5
  62. package/dist/runtime/virtual-socket-kernel.d.ts.map +1 -1
  63. package/dist/runtime/virtual-socket-kernel.generated.d.ts +1 -1
  64. package/dist/runtime/virtual-socket-kernel.generated.d.ts.map +1 -1
  65. package/dist/runtime/virtual-socket-kernel.generated.js +2 -2
  66. package/dist/runtime/virtual-socket-kernel.js +10 -5
  67. package/dist/runtime/wasm-runner.d.ts.map +1 -1
  68. package/dist/runtime/wasm-runner.js +22 -14
  69. package/dist/shell/features.d.ts.map +1 -1
  70. package/dist/shell/features.js +5 -4
  71. package/dist/shell/unix-commands.d.ts +14 -1
  72. package/dist/shell/unix-commands.d.ts.map +1 -1
  73. package/dist/shell/unix-commands.js +115 -69
  74. package/dist/substrate/lifo/commands/net/ip.d.ts.map +1 -1
  75. package/dist/substrate/lifo/commands/system/ps.d.ts +2 -1
  76. package/dist/substrate/lifo/commands/system/ps.d.ts.map +1 -1
  77. package/dist/substrate/lifo/kernel/ServiceManager.d.ts.map +1 -1
  78. package/dist/substrate/lifo/kernel/ServiceManager.js +0 -2
  79. package/dist/substrate/lifo/kernel/network/NetworkStack.d.ts +22 -11
  80. package/dist/substrate/lifo/kernel/network/NetworkStack.d.ts.map +1 -1
  81. package/dist/substrate/lifo/kernel/network/NetworkStack.js +0 -2
  82. package/dist/substrate/lifo/kernel/network/tunnel/WebSocketTunnel.d.ts +2 -1
  83. package/dist/substrate/lifo/kernel/network/tunnel/WebSocketTunnel.d.ts.map +1 -1
  84. package/dist/substrate/lifo/kernel/network/tunnel/WebSocketTunnel.js +3 -4
  85. package/dist/substrate/lifo/kernel/vfs/VFS.d.ts +17 -0
  86. package/dist/substrate/lifo/kernel/vfs/VFS.d.ts.map +1 -1
  87. package/dist/substrate/lifo/kernel/vfs/VFS.js +33 -0
  88. package/dist/substrate/lifo/node-compat/esbuild.d.ts.map +1 -1
  89. package/dist/substrate/lifo/node-compat/esbuild.js +2 -4
  90. package/dist/substrate/lifo/node-compat/index.d.ts.map +1 -1
  91. package/dist/substrate/lifo/node-compat/index.js +3 -2
  92. package/dist/substrate/lifo/node-compat/loose-equality.d.ts +19 -0
  93. package/dist/substrate/lifo/node-compat/loose-equality.d.ts.map +1 -0
  94. package/dist/substrate/lifo/node-compat/loose-equality.js +91 -0
  95. package/dist/substrate/lifo/node-compat/process.d.ts.map +1 -1
  96. package/dist/substrate/lifo/node-compat/process.js +6 -3
  97. package/dist/substrate/lifo/shell/Shell.d.ts +19 -0
  98. package/dist/substrate/lifo/shell/Shell.d.ts.map +1 -1
  99. package/dist/substrate/lifo/shell/Shell.js +86 -75
  100. package/dist/substrate/lifo/shell/lexer.d.ts +9 -0
  101. package/dist/substrate/lifo/shell/lexer.d.ts.map +1 -1
  102. package/dist/substrate/lifo/shell/lexer.js +40 -0
  103. package/dist/vfs/seed-project.d.ts.map +1 -1
  104. package/dist/vfs/seed-project.js +8 -2
  105. package/dist/vfs/sqlite-vfs.d.ts +61 -1
  106. package/dist/vfs/sqlite-vfs.d.ts.map +1 -1
  107. package/dist/vfs/sqlite-vfs.js +199 -35
  108. package/dist/workspace/nimbus-workspace.d.ts.map +1 -1
  109. package/dist/workspace/nimbus-workspace.js +9 -2
  110. package/package.json +4 -2
  111. package/src/_shared/error-text.ts +21 -0
  112. package/src/_shared/exports-resolver.ts +24 -4
  113. package/src/_shared/retry.ts +4 -2
  114. package/src/_shared/tarball-stream.ts +263 -0
  115. package/src/_shared/tarball.ts +46 -0
  116. package/src/_shared/w7-frame.ts +2 -1
  117. package/src/constants.ts +30 -16
  118. package/src/observability/oom-classify.ts +7 -0
  119. package/src/observability/oom-discriminator.ts +1 -1
  120. package/src/runtime/clang-runner.ts +924 -0
  121. package/src/runtime/esbuild-service.ts +16 -11
  122. package/src/runtime/facet-host.ts +12 -0
  123. package/src/runtime/installed-runtimes.ts +4 -3
  124. package/src/runtime/local-facet-host.ts +28 -8
  125. package/src/runtime/long-running-handle.ts +3 -3
  126. package/src/runtime/os-contracts.ts +7 -5
  127. package/src/runtime/port-registry.ts +97 -9
  128. package/src/runtime/process-logs.ts +12 -3
  129. package/src/runtime/project-detect.ts +7 -1
  130. package/src/runtime/require-resolver.ts +7 -7
  131. package/src/runtime/ruby-gems.ts +682 -0
  132. package/src/runtime/ruby-runner.ts +1484 -0
  133. package/src/runtime/runtime-registry.ts +9 -7
  134. package/src/runtime/session-process-supervisor.ts +27 -0
  135. package/src/runtime/sqlite-runtime-fs-bridge.ts +9 -6
  136. package/src/runtime/virtual-socket-kernel.generated.ts +2 -2
  137. package/src/runtime/virtual-socket-kernel.ts +10 -5
  138. package/src/runtime/wasm-runner.ts +55 -29
  139. package/src/shell/features.ts +5 -4
  140. package/src/shell/unix-commands.ts +273 -128
  141. package/src/substrate/lifo/commands/net/ip.ts +15 -15
  142. package/src/substrate/lifo/commands/system/ps.ts +2 -1
  143. package/src/substrate/lifo/kernel/ServiceManager.ts +0 -2
  144. package/src/substrate/lifo/kernel/network/NetworkStack.ts +24 -17
  145. package/src/substrate/lifo/kernel/network/tunnel/WebSocketTunnel.ts +37 -12
  146. package/src/substrate/lifo/kernel/vfs/VFS.ts +34 -0
  147. package/src/substrate/lifo/node-compat/esbuild.ts +16 -8
  148. package/src/substrate/lifo/node-compat/index.ts +3 -2
  149. package/src/substrate/lifo/node-compat/loose-equality.ts +81 -0
  150. package/src/substrate/lifo/node-compat/process.ts +5 -2
  151. package/src/substrate/lifo/shell/Shell.ts +92 -70
  152. package/src/substrate/lifo/shell/lexer.ts +38 -0
  153. package/src/vfs/seed-project.ts +10 -3
  154. package/src/vfs/sqlite-vfs.ts +209 -36
  155. package/src/workspace/nimbus-workspace.ts +10 -1
@@ -0,0 +1,263 @@
1
+ /**
2
+ * tarball-stream.ts — pure streaming tar primitives.
3
+ *
4
+ * A leaf with no imports at all, deliberately: `bundle-facet-workers.mjs`
5
+ * esbuilds this file into a string constant the loader pool injects into
6
+ * dynamic workers, and a facet isolate resolves no specifier. Anything this
7
+ * file imported would have to travel with it.
8
+ *
9
+ * Zero dependencies. Works identically on the supervisor and inside a
10
+ * facet isolate. Never buffers the full decompressed tarball — peak
11
+ * transient heap is one file's bytes plus a 512-byte carry.
12
+ */
13
+
14
+ /**
15
+ * Maximum size of a single file inside a tarball. Larger entries are skipped.
16
+ *
17
+ * History: 5 MB was too low — it silently dropped `esbuild-wasm/esbuild.wasm`
18
+ * (11.35 MB on v0.24.2), which made Nimbus-in-Nimbus `npm run dev` fail with
19
+ * `No such module "esbuild-wasm/esbuild.wasm"` since the missing file caused
20
+ * esbuild's VFS plugin to mark the import `external`, and workerd's LOADER
21
+ * has no entry for that specifier. 20 MB covers esbuild-wasm with headroom
22
+ * while keeping per-facet peak heap bounded for the streaming extractor.
23
+ */
24
+ export const MAX_FILE_BYTES = 20_000_000;
25
+
26
+ /**
27
+ * Read one tar header (USTAR) out of `block`. Returns parsed fields or
28
+ * `null` for an end-of-archive block (all zeros).
29
+ */
30
+ /**
31
+ * Collapse "."/".." segments in a tar entry's package-relative path.
32
+ * Returns the canonical relative path, or '' when the entry escapes its
33
+ * package root (a leading ".." that pops above the root) — the caller
34
+ * treats '' as a no-name entry and skips it. Mirrors the segment logic in
35
+ * w7-frame's canonicalPath so joined write paths are always accepted.
36
+ */
37
+ export function canonicalTarName(name: string): string {
38
+ const out: string[] = [];
39
+ for (const seg of name.split('/')) {
40
+ if (seg === '..') {
41
+ if (out.length === 0) return '';
42
+ out.pop();
43
+ } else if (seg !== '' && seg !== '.') {
44
+ out.push(seg);
45
+ }
46
+ }
47
+ return out.join('/');
48
+ }
49
+
50
+ export function parseTarHeader(block: Uint8Array): { name: string; size: number; typeFlag: number } | null {
51
+ if (block[0] === 0) return null;
52
+
53
+ let name = '';
54
+ for (let i = 0; i < 100 && block[i] !== 0; i++) {
55
+ name += String.fromCharCode(block[i]);
56
+ }
57
+ let prefix = '';
58
+ for (let i = 345; i < 500 && block[i] !== 0; i++) {
59
+ prefix += String.fromCharCode(block[i]);
60
+ }
61
+ if (prefix) name = prefix + '/' + name;
62
+ // Strip the npm `package/` convention.
63
+ name = name.replace(/^package\//, '');
64
+ // Canonicalize the entry-relative path. npm tarballs legitimately carry
65
+ // entries like "./dist/index.js" (agent-base, http-proxy-agent,
66
+ // protobufjs, ...); left as-is the "./" survives into the VFS write path
67
+ // and the w7-frame writer rejects the noncanonical path, failing the
68
+ // whole shared install wave and dropping shard-mates' completion markers.
69
+ // Collapsing here (the one place entry names are assembled) keeps every
70
+ // downstream join canonical. An entry that escapes its package root via
71
+ // ".." is dropped to '' → skipped as a no-name entry.
72
+ name = canonicalTarName(name);
73
+
74
+ let sizeStr = '';
75
+ for (let i = 124; i < 136 && block[i] !== 0; i++) {
76
+ sizeStr += String.fromCharCode(block[i]);
77
+ }
78
+ const size = parseInt(sizeStr.trim(), 8) || 0;
79
+ const typeFlag = block[156];
80
+ return { name, size, typeFlag };
81
+ }
82
+
83
+ /**
84
+ * Wrap a `ReadableStream<Uint8Array>` as an async iterable. Workerd and
85
+ * Node both support `Symbol.asyncIterator` on ReadableStream, but we
86
+ * spell the reader loop out so we don't depend on ambient lib typings.
87
+ */
88
+ export async function* readableStreamToAsyncIterable(
89
+ rs: ReadableStream<Uint8Array>,
90
+ ): AsyncGenerator<Uint8Array, void, undefined> {
91
+ const reader = rs.getReader();
92
+ try {
93
+ while (true) {
94
+ const { value, done } = await reader.read();
95
+ if (done) return;
96
+ if (value && value.length > 0) yield value;
97
+ }
98
+ } finally {
99
+ try { reader.releaseLock(); } catch { /* ignore */ }
100
+ }
101
+ }
102
+
103
+ /**
104
+ * Reason a tar entry was skipped and never yielded. Surfaced to callers
105
+ * via the optional `onSkip` callback on `streamTarEntries`.
106
+ *
107
+ * - 'too-large': size > MAX_FILE_BYTES. The most common reason Nimbus
108
+ * cares about — it's what caused esbuild-wasm/esbuild.wasm to vanish
109
+ * silently in Nimbus-in-Nimbus before the cap was raised.
110
+ * - 'non-regular': typeFlag indicates a symlink / hardlink / directory /
111
+ * PaxHeader / GNU LongName etc. These aren't files we stage.
112
+ * - 'no-name': header parsed but name was empty (malformed or a PaxHeader
113
+ * that our parser didn't recognize as non-regular).
114
+ */
115
+ export type TarSkipReason = 'too-large' | 'non-regular' | 'no-name';
116
+
117
+ /**
118
+ * Optional skip-observer passed to `streamTarEntries`. Called ONCE per
119
+ * skipped entry, with the declared name (may be empty for 'no-name'
120
+ * skips) and the declared size in bytes.
121
+ *
122
+ * Consumers typically push these into a per-package warnings array so
123
+ * users see what wasn't installed. The callback is synchronous and
124
+ * must not throw — thrown errors are swallowed to keep the extractor
125
+ * best-effort.
126
+ */
127
+ export type TarSkipCallback = (
128
+ name: string,
129
+ size: number,
130
+ reason: TarSkipReason,
131
+ ) => void;
132
+
133
+ /**
134
+ * Streaming tar extractor.
135
+ *
136
+ * Consumes an async iterable of Uint8Array chunks (the decompressed tar
137
+ * byte stream) and yields one `{ name, data }` entry per regular file,
138
+ * as each file completes.
139
+ *
140
+ * Memory invariant: holds at most one pending file's bytes (≤ MAX_FILE_BYTES)
141
+ * plus a small carry buffer for the tar header being assembled.
142
+ *
143
+ * Skips: symlinks, directories, hardlinks, long-name extensions (PaxHeader),
144
+ * and any file whose declared size exceeds MAX_FILE_BYTES.
145
+ *
146
+ * If `onSkip` is provided, it is invoked for each skipped entry with the
147
+ * name, declared size, and reason code. Callers that need to surface
148
+ * dropped-file warnings to users should pass one; legacy callers that
149
+ * omit the arg still behave exactly as before (silent skip).
150
+ */
151
+ export async function* streamTarEntries(
152
+ source: AsyncIterable<Uint8Array>,
153
+ onSkip?: TarSkipCallback,
154
+ ): AsyncGenerator<{ name: string; data: Uint8Array }, void, undefined> {
155
+ let carry: Uint8Array<ArrayBufferLike> = new Uint8Array(0);
156
+
157
+ type State =
158
+ | { kind: 'header' }
159
+ | { kind: 'file'; name: string; remaining: number; fileBuf: Uint8Array; fileOffset: number; pad: number; skip: boolean }
160
+ | { kind: 'skip'; remaining: number };
161
+ let state: State = { kind: 'header' };
162
+
163
+ function concat(a: Uint8Array, b: Uint8Array): Uint8Array {
164
+ if (a.length === 0) return b;
165
+ if (b.length === 0) return a;
166
+ const out = new Uint8Array(a.length + b.length);
167
+ out.set(a, 0);
168
+ out.set(b, a.length);
169
+ return out;
170
+ }
171
+
172
+ for await (const chunkRaw of source) {
173
+ let buf = concat(carry, chunkRaw);
174
+ let cursor = 0;
175
+
176
+ while (true) {
177
+ if (state.kind === 'header') {
178
+ if (buf.length - cursor < 512) break;
179
+ const header = buf.subarray(cursor, cursor + 512);
180
+ const parsed = parseTarHeader(header);
181
+ cursor += 512;
182
+ if (!parsed) return; // end-of-archive
183
+ const { name, size, typeFlag } = parsed;
184
+ const pad = size === 0 ? 0 : (512 - (size % 512)) % 512;
185
+ const isRegularFile = (typeFlag === 48 /* '0' */ || typeFlag === 0);
186
+ if (size === 0) {
187
+ if (isRegularFile && name) {
188
+ yield { name, data: new Uint8Array(0) };
189
+ }
190
+ state = { kind: 'header' };
191
+ continue;
192
+ }
193
+ if (!isRegularFile || !name || size > MAX_FILE_BYTES) {
194
+ if (onSkip) {
195
+ // Classify before entering the skip state so the reason is
196
+ // exact. Precedence matches the condition order above:
197
+ // non-regular first (directories/symlinks/PaxHeaders are
198
+ // skipped regardless of size), then no-name, then too-large.
199
+ const reason: TarSkipReason = !isRegularFile
200
+ ? 'non-regular'
201
+ : !name
202
+ ? 'no-name'
203
+ : 'too-large';
204
+ try { onSkip(name, size, reason); } catch { /* best-effort */ }
205
+ }
206
+ state = { kind: 'skip', remaining: size + pad };
207
+ continue;
208
+ }
209
+ state = {
210
+ kind: 'file',
211
+ name,
212
+ remaining: size,
213
+ fileBuf: new Uint8Array(size),
214
+ fileOffset: 0,
215
+ pad,
216
+ skip: false,
217
+ };
218
+ continue;
219
+ }
220
+
221
+ if (state.kind === 'file') {
222
+ const avail = buf.length - cursor;
223
+ if (avail === 0) break;
224
+ if (state.remaining > 0) {
225
+ const take = Math.min(state.remaining, avail);
226
+ state.fileBuf.set(buf.subarray(cursor, cursor + take), state.fileOffset);
227
+ state.fileOffset += take;
228
+ state.remaining -= take;
229
+ cursor += take;
230
+ if (state.remaining > 0) break;
231
+ }
232
+ if (state.pad > 0) {
233
+ const avail2 = buf.length - cursor;
234
+ if (avail2 === 0) break;
235
+ const take = Math.min(state.pad, avail2);
236
+ state.pad -= take;
237
+ cursor += take;
238
+ if (state.pad > 0) break;
239
+ }
240
+ yield { name: state.name, data: state.fileBuf };
241
+ state = { kind: 'header' };
242
+ continue;
243
+ }
244
+
245
+ // state.kind === 'skip'
246
+ const avail = buf.length - cursor;
247
+ if (avail === 0) break;
248
+ const take = Math.min(state.remaining, avail);
249
+ cursor += take;
250
+ state.remaining -= take;
251
+ if (state.remaining > 0) break;
252
+ state = { kind: 'header' };
253
+ }
254
+
255
+ if (cursor >= buf.length) {
256
+ carry = new Uint8Array(0);
257
+ } else if (cursor === 0) {
258
+ carry = buf;
259
+ } else {
260
+ carry = buf.slice(cursor);
261
+ }
262
+ }
263
+ }
@@ -0,0 +1,46 @@
1
+ /**
2
+ * tarball.ts — a whole tarball, in memory, as a path→bytes map.
3
+ *
4
+ * The streaming primitives it walks with (`parseTarHeader`, `streamTarEntries`,
5
+ * `readableStreamToAsyncIterable`) live in `./tarball-stream.ts` — a
6
+ * dependency-free leaf, because `bundle-facet-workers.mjs` esbuilds that file
7
+ * into a string the loader pool injects into dynamic workers, where an import
8
+ * would not resolve.
9
+ *
10
+ * This half is for a caller that already holds the bytes: `gem install`, which
11
+ * fetches a `.gem` (a tar holding a gzipped tar) and needs both layers open at
12
+ * once. An installer streaming a tarball it is still downloading should drive
13
+ * `streamTarEntries` itself and never hold the whole thing.
14
+ */
15
+
16
+ import {
17
+ streamTarEntries,
18
+ readableStreamToAsyncIterable,
19
+ } from './tarball-stream.js';
20
+
21
+ /** Extract every regular file. Gzipped input is decompressed first. */
22
+ export async function extractTarball(
23
+ tarball: ArrayBuffer,
24
+ ): Promise<Map<string, Uint8Array>> {
25
+ const files = new Map<string, Uint8Array>();
26
+ const raw = new Uint8Array(tarball);
27
+
28
+ // Adapter: wrap the single buffer as an async iterable. If gzipped, pipe
29
+ // through DecompressionStream so the streaming parser still sees tar bytes.
30
+ let source: AsyncIterable<Uint8Array>;
31
+ if (raw[0] === 0x1f && raw[1] === 0x8b) {
32
+ const rs = new Blob([tarball]).stream().pipeThrough(new DecompressionStream('gzip'));
33
+ source = readableStreamToAsyncIterable(rs);
34
+ } else {
35
+ source = (async function* () { yield raw; })();
36
+ }
37
+
38
+ try {
39
+ for await (const entry of streamTarEntries(source)) {
40
+ files.set(entry.name, entry.data);
41
+ }
42
+ } catch {
43
+ return files;
44
+ }
45
+ return files;
46
+ }
@@ -893,7 +893,8 @@ const CRC_SEED = 0xffff_ffff;
893
893
  let crcTable: Uint32Array | null = null;
894
894
 
895
895
  function crc32Update(check: number, bytes: Uint8Array): number {
896
- const table = crcTable ??= createCrcTable();
896
+ crcTable ??= createCrcTable();
897
+ const table = crcTable;
897
898
  let value = check;
898
899
  for (const byte of bytes) value = table[(value ^ byte) & 0xff] ^ (value >>> 8);
899
900
  return value >>> 0;
package/src/constants.ts CHANGED
@@ -142,10 +142,13 @@ export const FACET_TIMEOUT_MS = 30_000; // 30s execution timeout
142
142
  // JavaScript, so workerd's per-module text-size limit applies to the
143
143
  // JSON-escaped form (each `\n` / `\"` / `\u` adds bytes, plus the
144
144
  // per-key string-quote overhead).
145
- // raw → boots, 8 MiB raw fails. Encoded as JSON that's roughly 18-25 MiB
146
- // of module text. We target 22 MiB encoded as the hard ceiling, leaving
147
- // ~2-3 MiB of headroom for the rest of the worker module (shims, runner
148
- // boot code) and any minor drift in the eviction loop's accounting.
145
+ // Measured: 8 MiB raw failed to boot; the passing raw bound was measured
146
+ // too, but the number was lost to comment truncation before it ever
147
+ // reached git, so only the failing side is known. Encoded as JSON that's
148
+ // roughly 18-25 MiB of module text. We target 22 MiB encoded as the hard
149
+ // ceiling, leaving ~2-3 MiB of headroom for the rest of the worker module
150
+ // (shims, runner boot code) and any minor drift in the eviction loop's
151
+ // accounting.
149
152
  //
150
153
  // facet-manager.ts:buildPrefetchBundle uses TextEncoder().encode().length
151
154
  // to measure exact UTF-8 bytes (not JS string .length, which counts UTF-16
@@ -224,20 +227,31 @@ export const CF_COMPAT_DATE = '2026-04-01';
224
227
 
225
228
  // ── Supervisor heap budget [C'.1] ───────────────────────────────────────
226
229
  //
227
- // The supervisor isolate's 128 MiB workerd cap is a HARD platform ceiling
228
- // (per docs/research/cf-primitives-dossier.md §6 invariant I1 — 128 MiB
229
- // per V8 isolate, may be shared across same-class peer DOs co-tenanting in
230
- // one process). Nimbus targets HALF of that as a self-imposed soft ceiling
231
- // so the supervisor always has runway when workerd LRU-evicts neighbours
232
- // or AIR (Asynchronous Isolate Recreation) folds growing isolates.
230
+ // Three distinct memory regimes, not one shared pool:
233
231
  //
234
- // 64 MiB is a budget, not a measurement, and nothing enforces it.
232
+ // - Supervisor DO isolate: 128 MiB HARD platform ceiling ("Each isolate
233
+ // can consume up to 128 MB of memory" —
234
+ // https://developers.cloudflare.com/workers/platform/limits/).
235
+ // - Facets: ~208-256 MiB EACH, measured — memory independent of the
236
+ // supervisor and of each other (1,664 MiB live across 8 facets +
237
+ // parent) but CPU SHARED across the actor thread. See
238
+ // packages/worker/src/loaders/process-fabric.ts.
239
+ // - Peer DOs: independent memory AND CPU budgets, measured 1.2 GiB
240
+ // live across 8 peers.
241
+ //
242
+ // Nimbus targets HALF of the supervisor's 128 MiB as a self-imposed soft
243
+ // ceiling so the supervisor always has runway when workerd LRU-evicts
244
+ // neighbours or AIR (Asynchronous Isolate Recreation) folds growing
245
+ // isolates.
246
+ //
247
+ // 64 MiB is a soft admission budget, not a measurement — the right value
248
+ // is unmeasured — and nothing enforces it directly.
235
249
  // src/observability/heap-estimate.ts sums the INSTRUMENTED contributors —
236
- // the supervisor baseline, VFS LRU and in-flight writes, pre-bundle slices,
237
- // and streaming RPC buffers which is a lower bound, not full coverage.
238
- // The prefetch-bundle path in facets/manager.ts allocates against this
239
- // budget without accounting for it; see HEAP_BLIND_SPOTS for the current
240
- // gap. Read a low percentOfCeiling accordingly.
250
+ // the supervisor baseline, VFS LRU and in-flight writes, pre-bundle
251
+ // slices, streaming RPC buffers, and the prefetch-bundle build lease and
252
+ // cache gauge which is a lower bound, not full coverage; allocation
253
+ // sites known to be missing are listed in HEAP_BLIND_SPOTS. Read a low
254
+ // percentOfCeiling accordingly.
241
255
  export const SUPERVISOR_HEAP_CEILING_BYTES = 64 * 1024 * 1024;
242
256
 
243
257
  // Shared allowance for transient allocations in the supervisor DO. With the
@@ -68,6 +68,7 @@ const OOM_CAUSES = [
68
68
  'clone_refused',
69
69
  'rpc_timeout',
70
70
  'subrequest_cap',
71
+ 'dynamic_worker_cap',
71
72
  'condemnation',
72
73
  'hard_evict',
73
74
  'unknown',
@@ -117,6 +118,12 @@ export function classifyMessage(msg: string): OomCause {
117
118
  // Subrequest cap (Cloudflare platform)
118
119
  if (m.includes('too many subrequests')) return 'subrequest_cap';
119
120
 
121
+ // Worker Loader concurrency cap (Cloudflare platform): a Durable Object
122
+ // admits ~5-6 concurrent dynamic workers, and loader-cache entries are
123
+ // never released — every distinct loader.get(id) permanently consumes a
124
+ // slot, so this cap recurs until the DO itself is replaced.
125
+ if (m.includes('too many concurrent dynamic workers')) return 'dynamic_worker_cap';
126
+
120
127
  // Memory exhaustion. workerd trace outcome `exceededMemory`; in-band it
121
128
  // arrives as a `broken.exceededMemory` actor break, a "…exceeded [its]
122
129
  // memory limit" kill, or the "Memory limit exceeded" RangeError.
@@ -128,7 +128,7 @@ export interface DiagRecoveryEvent {
128
128
  toState: SessionState;
129
129
  /** Human-readable trigger ('ws-close', 'isolate-evicted', etc.). */
130
130
  trigger: string;
131
- /** _w9IsolateGen at the time of the event. Lets a probe detect the
131
+ /** _isolateGen at the time of the event. Lets a probe detect the
132
132
  * difference between an in-isolate transition and a cross-isolate one
133
133
  * (the latter implies workerd recycled the DO). */
134
134
  isolateGen: number;