@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.
- package/LICENSE +21 -0
- package/README.md +140 -0
- package/dist/_shared/error-text.d.ts +15 -0
- package/dist/_shared/error-text.d.ts.map +1 -0
- package/dist/_shared/error-text.js +21 -0
- package/dist/_shared/exports-resolver.d.ts +19 -7
- package/dist/_shared/exports-resolver.d.ts.map +1 -1
- package/dist/_shared/exports-resolver.js +1 -1
- package/dist/_shared/retry.d.ts.map +1 -1
- package/dist/_shared/retry.js +3 -1
- package/dist/_shared/tarball-stream.d.ts +93 -0
- package/dist/_shared/tarball-stream.d.ts.map +1 -0
- package/dist/_shared/tarball-stream.js +235 -0
- package/dist/_shared/tarball.d.ts +17 -0
- package/dist/_shared/tarball.d.ts.map +1 -0
- package/dist/_shared/tarball.js +39 -0
- package/dist/_shared/w7-frame.js +2 -1
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +30 -16
- package/dist/observability/oom-classify.d.ts +1 -1
- package/dist/observability/oom-classify.d.ts.map +1 -1
- package/dist/observability/oom-classify.js +7 -0
- package/dist/observability/oom-discriminator.d.ts +1 -1
- package/dist/runtime/clang-runner.d.ts +38 -0
- package/dist/runtime/clang-runner.d.ts.map +1 -0
- package/dist/runtime/clang-runner.js +866 -0
- package/dist/runtime/esbuild-service.d.ts +4 -3
- package/dist/runtime/esbuild-service.d.ts.map +1 -1
- package/dist/runtime/esbuild-service.js +9 -4
- package/dist/runtime/facet-host.d.ts +12 -0
- package/dist/runtime/facet-host.d.ts.map +1 -1
- package/dist/runtime/installed-runtimes.d.ts +4 -3
- package/dist/runtime/installed-runtimes.d.ts.map +1 -1
- package/dist/runtime/local-facet-host.d.ts.map +1 -1
- package/dist/runtime/local-facet-host.js +29 -9
- package/dist/runtime/long-running-handle.d.ts +3 -3
- package/dist/runtime/long-running-handle.d.ts.map +1 -1
- package/dist/runtime/os-contracts.d.ts +7 -5
- package/dist/runtime/os-contracts.d.ts.map +1 -1
- package/dist/runtime/port-registry.d.ts +28 -0
- package/dist/runtime/port-registry.d.ts.map +1 -1
- package/dist/runtime/port-registry.js +79 -9
- package/dist/runtime/process-logs.d.ts +9 -1
- package/dist/runtime/process-logs.d.ts.map +1 -1
- package/dist/runtime/project-detect.d.ts +6 -1
- package/dist/runtime/project-detect.d.ts.map +1 -1
- package/dist/runtime/require-resolver.d.ts.map +1 -1
- package/dist/runtime/require-resolver.js +3 -4
- package/dist/runtime/ruby-gems.d.ts +30 -0
- package/dist/runtime/ruby-gems.d.ts.map +1 -0
- package/dist/runtime/ruby-gems.js +636 -0
- package/dist/runtime/ruby-runner.d.ts +127 -0
- package/dist/runtime/ruby-runner.d.ts.map +1 -0
- package/dist/runtime/ruby-runner.js +1357 -0
- package/dist/runtime/runtime-registry.d.ts +4 -3
- package/dist/runtime/runtime-registry.d.ts.map +1 -1
- package/dist/runtime/runtime-registry.js +2 -1
- package/dist/runtime/session-process-supervisor.d.ts +15 -0
- package/dist/runtime/session-process-supervisor.d.ts.map +1 -1
- package/dist/runtime/session-process-supervisor.js +30 -0
- package/dist/runtime/sqlite-runtime-fs-bridge.js +1 -5
- package/dist/runtime/virtual-socket-kernel.d.ts.map +1 -1
- package/dist/runtime/virtual-socket-kernel.generated.d.ts +1 -1
- package/dist/runtime/virtual-socket-kernel.generated.d.ts.map +1 -1
- package/dist/runtime/virtual-socket-kernel.generated.js +2 -2
- package/dist/runtime/virtual-socket-kernel.js +10 -5
- package/dist/runtime/wasm-runner.d.ts.map +1 -1
- package/dist/runtime/wasm-runner.js +22 -14
- package/dist/shell/features.d.ts.map +1 -1
- package/dist/shell/features.js +5 -4
- package/dist/shell/unix-commands.d.ts +14 -1
- package/dist/shell/unix-commands.d.ts.map +1 -1
- package/dist/shell/unix-commands.js +115 -69
- package/dist/substrate/lifo/commands/net/ip.d.ts.map +1 -1
- package/dist/substrate/lifo/commands/system/ps.d.ts +2 -1
- package/dist/substrate/lifo/commands/system/ps.d.ts.map +1 -1
- package/dist/substrate/lifo/kernel/ServiceManager.d.ts.map +1 -1
- package/dist/substrate/lifo/kernel/ServiceManager.js +0 -2
- package/dist/substrate/lifo/kernel/network/NetworkStack.d.ts +22 -11
- package/dist/substrate/lifo/kernel/network/NetworkStack.d.ts.map +1 -1
- package/dist/substrate/lifo/kernel/network/NetworkStack.js +0 -2
- package/dist/substrate/lifo/kernel/network/tunnel/WebSocketTunnel.d.ts +2 -1
- package/dist/substrate/lifo/kernel/network/tunnel/WebSocketTunnel.d.ts.map +1 -1
- package/dist/substrate/lifo/kernel/network/tunnel/WebSocketTunnel.js +3 -4
- package/dist/substrate/lifo/kernel/vfs/VFS.d.ts +17 -0
- package/dist/substrate/lifo/kernel/vfs/VFS.d.ts.map +1 -1
- package/dist/substrate/lifo/kernel/vfs/VFS.js +33 -0
- package/dist/substrate/lifo/node-compat/esbuild.d.ts.map +1 -1
- package/dist/substrate/lifo/node-compat/esbuild.js +2 -4
- package/dist/substrate/lifo/node-compat/index.d.ts.map +1 -1
- package/dist/substrate/lifo/node-compat/index.js +3 -2
- package/dist/substrate/lifo/node-compat/loose-equality.d.ts +19 -0
- package/dist/substrate/lifo/node-compat/loose-equality.d.ts.map +1 -0
- package/dist/substrate/lifo/node-compat/loose-equality.js +91 -0
- package/dist/substrate/lifo/node-compat/process.d.ts.map +1 -1
- package/dist/substrate/lifo/node-compat/process.js +6 -3
- package/dist/substrate/lifo/shell/Shell.d.ts +19 -0
- package/dist/substrate/lifo/shell/Shell.d.ts.map +1 -1
- package/dist/substrate/lifo/shell/Shell.js +86 -75
- package/dist/substrate/lifo/shell/lexer.d.ts +9 -0
- package/dist/substrate/lifo/shell/lexer.d.ts.map +1 -1
- package/dist/substrate/lifo/shell/lexer.js +40 -0
- package/dist/vfs/seed-project.d.ts.map +1 -1
- package/dist/vfs/seed-project.js +8 -2
- package/dist/vfs/sqlite-vfs.d.ts +61 -1
- package/dist/vfs/sqlite-vfs.d.ts.map +1 -1
- package/dist/vfs/sqlite-vfs.js +199 -35
- package/dist/workspace/nimbus-workspace.d.ts.map +1 -1
- package/dist/workspace/nimbus-workspace.js +9 -2
- package/package.json +4 -2
- package/src/_shared/error-text.ts +21 -0
- package/src/_shared/exports-resolver.ts +24 -4
- package/src/_shared/retry.ts +4 -2
- package/src/_shared/tarball-stream.ts +263 -0
- package/src/_shared/tarball.ts +46 -0
- package/src/_shared/w7-frame.ts +2 -1
- package/src/constants.ts +30 -16
- package/src/observability/oom-classify.ts +7 -0
- package/src/observability/oom-discriminator.ts +1 -1
- package/src/runtime/clang-runner.ts +924 -0
- package/src/runtime/esbuild-service.ts +16 -11
- package/src/runtime/facet-host.ts +12 -0
- package/src/runtime/installed-runtimes.ts +4 -3
- package/src/runtime/local-facet-host.ts +28 -8
- package/src/runtime/long-running-handle.ts +3 -3
- package/src/runtime/os-contracts.ts +7 -5
- package/src/runtime/port-registry.ts +97 -9
- package/src/runtime/process-logs.ts +12 -3
- package/src/runtime/project-detect.ts +7 -1
- package/src/runtime/require-resolver.ts +7 -7
- package/src/runtime/ruby-gems.ts +682 -0
- package/src/runtime/ruby-runner.ts +1484 -0
- package/src/runtime/runtime-registry.ts +9 -7
- package/src/runtime/session-process-supervisor.ts +27 -0
- package/src/runtime/sqlite-runtime-fs-bridge.ts +9 -6
- package/src/runtime/virtual-socket-kernel.generated.ts +2 -2
- package/src/runtime/virtual-socket-kernel.ts +10 -5
- package/src/runtime/wasm-runner.ts +55 -29
- package/src/shell/features.ts +5 -4
- package/src/shell/unix-commands.ts +273 -128
- package/src/substrate/lifo/commands/net/ip.ts +15 -15
- package/src/substrate/lifo/commands/system/ps.ts +2 -1
- package/src/substrate/lifo/kernel/ServiceManager.ts +0 -2
- package/src/substrate/lifo/kernel/network/NetworkStack.ts +24 -17
- package/src/substrate/lifo/kernel/network/tunnel/WebSocketTunnel.ts +37 -12
- package/src/substrate/lifo/kernel/vfs/VFS.ts +34 -0
- package/src/substrate/lifo/node-compat/esbuild.ts +16 -8
- package/src/substrate/lifo/node-compat/index.ts +3 -2
- package/src/substrate/lifo/node-compat/loose-equality.ts +81 -0
- package/src/substrate/lifo/node-compat/process.ts +5 -2
- package/src/substrate/lifo/shell/Shell.ts +92 -70
- package/src/substrate/lifo/shell/lexer.ts +38 -0
- package/src/vfs/seed-project.ts +10 -3
- package/src/vfs/sqlite-vfs.ts +209 -36
- 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
|
+
}
|
package/src/_shared/w7-frame.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
//
|
|
146
|
-
//
|
|
147
|
-
//
|
|
148
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
|
237
|
-
//
|
|
238
|
-
//
|
|
239
|
-
//
|
|
240
|
-
//
|
|
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
|
-
/**
|
|
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;
|