@deepseek-ai/dsh-experimental-webworker-runtime 0.1.6-alpha.1

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 (87) hide show
  1. package/LICENSE +21 -0
  2. package/README.i18n.yaml +6 -0
  3. package/README.md +75 -0
  4. package/README.zh.md +75 -0
  5. package/lib/client.js +854 -0
  6. package/lib/index.js +3506 -0
  7. package/lib/types/client/apply-injections.d.ts +15 -0
  8. package/lib/types/client/client.d.ts +102 -0
  9. package/lib/types/client/index.d.ts +66 -0
  10. package/lib/types/client/source-chooser.d.ts +8 -0
  11. package/lib/types/compile/transform.d.ts +39 -0
  12. package/lib/types/fixture-manifest.d.ts +26 -0
  13. package/lib/types/image-layout.d.ts +43 -0
  14. package/lib/types/index.d.ts +23 -0
  15. package/lib/types/module-proxies.d.ts +25 -0
  16. package/lib/types/module-system/module-loader.d.ts +177 -0
  17. package/lib/types/module-system/posix-path.d.ts +98 -0
  18. package/lib/types/node/builtin_modules/implemented/abort-error.d.ts +11 -0
  19. package/lib/types/node/builtin_modules/implemented/async_hooks.d.ts +174 -0
  20. package/lib/types/node/builtin_modules/implemented/buffer.d.ts +28 -0
  21. package/lib/types/node/builtin_modules/implemented/child_process.d.ts +178 -0
  22. package/lib/types/node/builtin_modules/implemented/crypto.d.ts +53 -0
  23. package/lib/types/node/builtin_modules/implemented/events.d.ts +84 -0
  24. package/lib/types/node/builtin_modules/implemented/fs/promises.d.ts +39 -0
  25. package/lib/types/node/builtin_modules/implemented/fs-watch.d.ts +119 -0
  26. package/lib/types/node/builtin_modules/implemented/fs.d.ts +502 -0
  27. package/lib/types/node/builtin_modules/implemented/http.d.ts +113 -0
  28. package/lib/types/node/builtin_modules/implemented/module.d.ts +50 -0
  29. package/lib/types/node/builtin_modules/implemented/os.d.ts +115 -0
  30. package/lib/types/node/builtin_modules/implemented/path.d.ts +138 -0
  31. package/lib/types/node/builtin_modules/implemented/perf_hooks.d.ts +13 -0
  32. package/lib/types/node/builtin_modules/implemented/stream.d.ts +22 -0
  33. package/lib/types/node/builtin_modules/implemented/timers/promises.d.ts +36 -0
  34. package/lib/types/node/builtin_modules/implemented/tty.d.ts +18 -0
  35. package/lib/types/node/builtin_modules/implemented/url.d.ts +61 -0
  36. package/lib/types/node/builtin_modules/implemented/util/types.d.ts +12 -0
  37. package/lib/types/node/builtin_modules/implemented/util.d.ts +95 -0
  38. package/lib/types/node/builtin_modules/implemented/zlib.d.ts +65 -0
  39. package/lib/types/node/builtin_modules/mock/dns/promises.d.ts +10 -0
  40. package/lib/types/node/builtin_modules/mock/net.d.ts +61 -0
  41. package/lib/types/node/builtin_modules/mock/sqlite.d.ts +19 -0
  42. package/lib/types/node/builtin_modules/mock/vm.d.ts +25 -0
  43. package/lib/types/node/builtin_modules/mock/worker_threads.d.ts +34 -0
  44. package/lib/types/node/builtins.d.ts +13 -0
  45. package/lib/types/node/external_packages/koffi.d.ts +63 -0
  46. package/lib/types/node/external_packages/node-addon-system-flock.d.ts +19 -0
  47. package/lib/types/node/external_packages/node-pty.d.ts +13 -0
  48. package/lib/types/node/external_packages/pi-ai.d.ts +51 -0
  49. package/lib/types/node/external_packages/replaced-externals.d.ts +9 -0
  50. package/lib/types/node/external_packages/ripgrep.d.ts +16 -0
  51. package/lib/types/node/external_packages/sharp.d.ts +6 -0
  52. package/lib/types/node/external_packages/ws.d.ts +38 -0
  53. package/lib/types/node/globals/crypto.d.ts +3 -0
  54. package/lib/types/node/globals/process.d.ts +78 -0
  55. package/lib/types/node/globals/timers.d.ts +10 -0
  56. package/lib/types/node/notImplementedFail.d.ts +34 -0
  57. package/lib/types/node/process-table.d.ts +50 -0
  58. package/lib/types/polyfill/async-context/als-runtime.d.ts +45 -0
  59. package/lib/types/polyfill/async-context/async-context-hooks.d.ts +6 -0
  60. package/lib/types/shell/ast.d.ts +23 -0
  61. package/lib/types/shell/expand.d.ts +65 -0
  62. package/lib/types/shell/fs-access.d.ts +43 -0
  63. package/lib/types/shell/interpret.d.ts +54 -0
  64. package/lib/types/shell/process/child.d.ts +26 -0
  65. package/lib/types/shell/process/host.d.ts +58 -0
  66. package/lib/types/shell/process/landlock.d.ts +33 -0
  67. package/lib/types/shell/process/protocol.d.ts +74 -0
  68. package/lib/types/shell/process/virtual-executables.d.ts +49 -0
  69. package/lib/types/shell/programs/builtins.d.ts +9 -0
  70. package/lib/types/shell/programs/files.d.ts +11 -0
  71. package/lib/types/shell/programs/index.d.ts +14 -0
  72. package/lib/types/shell/programs/options.d.ts +40 -0
  73. package/lib/types/shell/programs/text.d.ts +9 -0
  74. package/lib/types/shell/types.d.ts +138 -0
  75. package/lib/types/storage/active.d.ts +18 -0
  76. package/lib/types/storage/image-gzip.d.ts +39 -0
  77. package/lib/types/storage/memory.d.ts +308 -0
  78. package/lib/types/storage/paths.d.ts +18 -0
  79. package/lib/types/storage/tar.d.ts +41 -0
  80. package/lib/types/storage/types.d.ts +279 -0
  81. package/lib/types/transport/frames.d.ts +108 -0
  82. package/lib/types/transport/synthetic-http.d.ts +50 -0
  83. package/lib/types/transport/tunnel.d.ts +132 -0
  84. package/lib/types/worker-host.d.ts +165 -0
  85. package/lib/types/worker.d.ts +2 -0
  86. package/lib/worker.js +21879 -0
  87. package/package.json +73 -0
package/lib/index.js ADDED
@@ -0,0 +1,3506 @@
1
+ import { parse } from "acorn";
2
+ //#region \0rolldown/runtime.js
3
+ var __defProp = Object.defineProperty;
4
+ var __exportAll = (all, no_symbols) => {
5
+ let target = {};
6
+ for (var name in all) __defProp(target, name, {
7
+ get: all[name],
8
+ enumerable: true
9
+ });
10
+ if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
11
+ return target;
12
+ };
13
+ //#endregion
14
+ //#region src/polyfill/async-context/als-runtime.ts
15
+ /**
16
+ * Build the runtime the rewritten code calls.
17
+ * @param causality - Snapshot face from the `node:async_hooks` proxy; omitted
18
+ * leaves the rewrite inert (it still hops a microtask, but moves no state).
19
+ * @returns Runtime object passed to every module wrapper.
20
+ */
21
+ function createAlsRuntime(causality) {
22
+ const snapshot = () => causality?.snapshot();
23
+ const restore = (value) => {
24
+ causality?.restore(value);
25
+ };
26
+ return {
27
+ snapshot,
28
+ pause: (value) => {
29
+ const captured = snapshot();
30
+ return Promise.resolve(value).then((settled) => ({
31
+ ok: true,
32
+ value: settled,
33
+ snapshot: captured
34
+ }), (error) => ({
35
+ ok: false,
36
+ error,
37
+ snapshot: captured
38
+ }));
39
+ },
40
+ resume: (token) => {
41
+ restore(token.snapshot);
42
+ if (token.ok) return token.value;
43
+ throw token.error;
44
+ },
45
+ afterYield: (captured, sent) => {
46
+ restore(captured);
47
+ return sent;
48
+ },
49
+ iterator: (value) => {
50
+ const source = value;
51
+ const asyncFactory = source[Symbol.asyncIterator];
52
+ if (typeof asyncFactory === "function") return asyncFactory.call(source);
53
+ const syncFactory = source[Symbol.iterator];
54
+ if (typeof syncFactory !== "function") throw new TypeError("webworker als: for-await source is neither async nor sync iterable");
55
+ const inner = syncFactory.call(source);
56
+ return {
57
+ next: async (...args) => {
58
+ const step = inner.next(...args);
59
+ return {
60
+ done: step.done ?? false,
61
+ value: await step.value
62
+ };
63
+ },
64
+ return: async (sent) => {
65
+ const step = inner.return?.(sent) ?? {
66
+ done: true,
67
+ value: void 0
68
+ };
69
+ return {
70
+ done: step.done ?? true,
71
+ value: await step.value
72
+ };
73
+ }
74
+ };
75
+ },
76
+ close: async (iterator) => {
77
+ try {
78
+ return await iterator.return?.(void 0);
79
+ } catch {
80
+ return;
81
+ }
82
+ }
83
+ };
84
+ }
85
+ //#endregion
86
+ //#region src/transport/frames.ts
87
+ /**
88
+ * Validate a `postMessage` payload as a tunnel frame.
89
+ * @param data - Message data received by the worker.
90
+ * @returns The frame.
91
+ */
92
+ function parseInboundFrame(data) {
93
+ if (typeof data !== "object" || data === null) throw new Error(`webworker tunnel: message is not a frame: ${String(data)}`);
94
+ const frame = data;
95
+ if (frame.t === "init") {
96
+ if (typeof frame.image !== "string") throw new Error("webworker tunnel: init frame needs a string image url");
97
+ if (!Array.isArray(frame.overlays) || frame.overlays.some((overlay) => typeof overlay !== "string")) throw new Error("webworker tunnel: init frame needs an array of string overlay urls");
98
+ return {
99
+ t: "init",
100
+ image: frame.image,
101
+ overlays: frame.overlays
102
+ };
103
+ }
104
+ const id = frame.id;
105
+ if (typeof id !== "string" && typeof id !== "number") throw new Error(`webworker tunnel: frame has no usable id: ${JSON.stringify(frame.id)}`);
106
+ if (frame.t === "abort") return {
107
+ t: "abort",
108
+ id
109
+ };
110
+ if (frame.t === "stream-open") {
111
+ if (typeof frame.endpoint !== "string" || frame.endpoint.length === 0) throw new Error(`webworker tunnel: stream ${String(id)} needs a non-empty endpoint`);
112
+ return {
113
+ t: "stream-open",
114
+ id,
115
+ endpoint: frame.endpoint,
116
+ payload: frame.payload
117
+ };
118
+ }
119
+ if (frame.t !== "req") throw new Error(`webworker tunnel: unknown frame type ${JSON.stringify(frame.t)}`);
120
+ if (typeof frame.method !== "string" || typeof frame.url !== "string") throw new Error(`webworker tunnel: request ${String(id)} needs string method and url`);
121
+ if (typeof frame.headers !== "object" || frame.headers === null) throw new Error(`webworker tunnel: request ${String(id)} needs a headers object`);
122
+ const headers = {};
123
+ for (const [key, value] of Object.entries(frame.headers)) if (typeof value === "string") headers[key.toLowerCase()] = value;
124
+ const body = frame.body;
125
+ if (body !== void 0 && !(body instanceof ArrayBuffer) && !(body instanceof Blob) && !(body instanceof ReadableStream)) throw new Error(`webworker tunnel: request ${String(id)} body must be an ArrayBuffer, Blob, or ReadableStream`);
126
+ return {
127
+ t: "req",
128
+ id,
129
+ method: frame.method,
130
+ url: frame.url,
131
+ headers,
132
+ body
133
+ };
134
+ }
135
+ //#endregion
136
+ //#region src/module-system/posix-path.ts
137
+ var posix_path_exports = /* @__PURE__ */ __exportAll({
138
+ SEP: () => "/",
139
+ basename: () => basename,
140
+ dirname: () => dirname,
141
+ extname: () => extname,
142
+ fileUrlToPath: () => fileUrlToPath,
143
+ isAbsolute: () => isAbsolute,
144
+ join: () => join,
145
+ normalize: () => normalize,
146
+ parse: () => parse$1,
147
+ pathToFileUrl: () => pathToFileUrl,
148
+ relative: () => relative,
149
+ resolve: () => resolve,
150
+ toNamespacedPath: () => toNamespacedPath
151
+ });
152
+ /**
153
+ * Collapse `.` and `..` segments.
154
+ * @param path - Path with any number of separators.
155
+ * @returns Normalized path; a relative input keeps leading `..` segments.
156
+ */
157
+ function normalize(path) {
158
+ const absolute = path.startsWith("/");
159
+ const trailing = path.length > 1 && path.endsWith("/");
160
+ const out = [];
161
+ for (const segment of path.split("/")) {
162
+ if (segment === "" || segment === ".") continue;
163
+ if (segment === ".." && out.length > 0 && out[out.length - 1] !== "..") {
164
+ out.pop();
165
+ continue;
166
+ }
167
+ if (segment === ".." && absolute) continue;
168
+ out.push(segment);
169
+ }
170
+ const body = out.join("/");
171
+ if (absolute) return "/" + body + (trailing && body !== "" ? "/" : "");
172
+ if (body === "") return trailing ? "./" : ".";
173
+ return body + (trailing ? "/" : "");
174
+ }
175
+ /**
176
+ * Join segments and normalize the result.
177
+ * @param segments - Path segments.
178
+ * @returns Joined path, `.` when nothing remains.
179
+ */
180
+ function join(...segments) {
181
+ const joined = segments.filter((segment) => segment !== "").join("/");
182
+ return joined === "" ? "." : normalize(joined);
183
+ }
184
+ /**
185
+ * Resolve segments right to left against a base directory.
186
+ * @param segments - Path segments; the first absolute one wins.
187
+ * @returns Absolute normalized path.
188
+ */
189
+ function resolve(...segments) {
190
+ let path = "";
191
+ for (const segment of [...segments].reverse()) {
192
+ if (segment === "") continue;
193
+ path = path === "" ? segment : `${segment}/${path}`;
194
+ if (segment.startsWith("/")) break;
195
+ }
196
+ return normalize(path.startsWith("/") ? path : `/${path}`);
197
+ }
198
+ /**
199
+ * Directory part of a path, after normalization (see the module note).
200
+ * @param path - Path to inspect.
201
+ * @returns Parent path; `/` for root children and `.` for bare names.
202
+ */
203
+ function dirname(path) {
204
+ const normalized = normalize(path).replace(/\/+$/, "");
205
+ const index = normalized.lastIndexOf("/");
206
+ if (index < 0) return ".";
207
+ if (index === 0) return "/";
208
+ return normalized.slice(0, index);
209
+ }
210
+ /**
211
+ * Last segment of a path, after normalization (see the module note).
212
+ * @param path - Path to inspect.
213
+ * @param suffix - Optional suffix to strip.
214
+ * @returns Final segment.
215
+ */
216
+ function basename(path, suffix) {
217
+ const normalized = normalize(path).replace(/\/+$/, "");
218
+ const name = normalized.slice(normalized.lastIndexOf("/") + 1);
219
+ if (suffix !== void 0 && suffix !== name && name.endsWith(suffix)) return name.slice(0, -suffix.length);
220
+ return name;
221
+ }
222
+ /**
223
+ * Extension of the last segment, dot included.
224
+ * @param path - Path to inspect.
225
+ * @returns Extension, or an empty string when there is none.
226
+ */
227
+ function extname(path) {
228
+ const name = basename(path);
229
+ const index = name.lastIndexOf(".");
230
+ return index <= 0 ? "" : name.slice(index);
231
+ }
232
+ /**
233
+ * Report whether a path starts at the root.
234
+ * @param path - Path to inspect.
235
+ * @returns True for absolute paths.
236
+ */
237
+ function isAbsolute(path) {
238
+ return path.startsWith("/");
239
+ }
240
+ /**
241
+ * Relative path from one absolute path to another.
242
+ * @param from - Source directory.
243
+ * @param to - Target path.
244
+ * @returns Relative path using `..` segments.
245
+ */
246
+ function relative(from, to) {
247
+ const source = resolve(from).split("/").filter((segment) => segment !== "");
248
+ const target = resolve(to).split("/").filter((segment) => segment !== "");
249
+ let shared = 0;
250
+ while (shared < source.length && shared < target.length && source[shared] === target[shared]) shared += 1;
251
+ return [...new Array(source.length - shared).fill(".."), ...target.slice(shared)].join("/");
252
+ }
253
+ /**
254
+ * Split a path into components, after normalization (see the module note).
255
+ * @param path - Path to split.
256
+ * @returns Root, directory, base name, extension, and stem.
257
+ */
258
+ function parse$1(path) {
259
+ const root = isAbsolute(path) ? "/" : "";
260
+ const base = basename(path);
261
+ const ext = extname(path);
262
+ return {
263
+ root,
264
+ dir: dirname(path),
265
+ base,
266
+ ext,
267
+ name: ext === "" ? base : base.slice(0, -ext.length)
268
+ };
269
+ }
270
+ /**
271
+ * Node's Windows-only namespaced-path conversion.
272
+ * @param path - the path to convert.
273
+ * @returns The path unchanged; namespaced paths are a Windows concept.
274
+ */
275
+ function toNamespacedPath(path) {
276
+ return path;
277
+ }
278
+ /**
279
+ * Convert a VFS path into a `file:` URL string.
280
+ * @param path - Absolute VFS path.
281
+ * @returns URL text with each segment percent-encoded.
282
+ */
283
+ function pathToFileUrl(path) {
284
+ return `file://${resolve(path).split("/").map((segment) => encodeURIComponent(segment)).join("/")}`;
285
+ }
286
+ /**
287
+ * Convert a `file:` URL back into a VFS path.
288
+ * @param url - URL text or URL instance.
289
+ * @returns Absolute VFS path.
290
+ */
291
+ function fileUrlToPath(url) {
292
+ const text = typeof url === "string" ? url : url.href;
293
+ if (!text.startsWith("file://")) throw new Error(`webworker vfs: not a file URL: ${text}`);
294
+ return decodeURIComponent(text.slice(7).replace(/[?#].*$/, "")) || "/";
295
+ }
296
+ //#endregion
297
+ //#region src/image-layout.ts
298
+ /**
299
+ * Image layout contract shared by the packer and the worker host: the virtual
300
+ * root, where the composed config and the manifest sit inside the image, and
301
+ * the working directories every image carries empty. One definition, two
302
+ * consumers — the packer writes this layout, the worker host mounts it.
303
+ */
304
+ /** Default virtual root; the runtime mounts the image here unless told otherwise. */
305
+ const DEFAULT_ROOT = "/dsh";
306
+ /**
307
+ * Leaf name of the packed base image: one gzip member holding the ustar archive.
308
+ * The app build writes it beside the page and the page's boot fetches it from
309
+ * there, so the extension is part of what a deployment serves.
310
+ */
311
+ const IMAGE_FILE_NAME = "vfs-image.tar.gz";
312
+ /** Image path the composed profile is written to; the runtime's Loader reads it. */
313
+ const IMAGE_CONFIG_PATH = "config/cordis.yml";
314
+ /** Image path of the manifest the runtime reads before it wraps a single module. */
315
+ const IMAGE_MANIFEST_PATH = "config/vfs-manifest.json";
316
+ /** Home directory under the root; the process shim's `DSH_HOME`/`HOME` default. */
317
+ const IMAGE_HOME_DIRECTORY = "home";
318
+ /** Working directories the host tree expects to exist, empty. */
319
+ const IMAGE_EMPTY_DIRECTORIES = [
320
+ "home/",
321
+ "workspace/",
322
+ "tmp/"
323
+ ];
324
+ /**
325
+ * Top-level directories an overlay archive may populate. Runtime code,
326
+ * configuration, and the lowering manifest remain owned by the base image.
327
+ */
328
+ const IMAGE_OVERLAY_DIRECTORIES = ["home", "workspace"];
329
+ /**
330
+ * Identity of the lowered code shape, recorded in the image manifest by the
331
+ * packer and required by the worker host: an image lowered by an older transform
332
+ * would otherwise run against newer wrapper semantics. Bump on any change to
333
+ * emitted code or to {@link WRAPPER_PARAMS}.
334
+ */
335
+ const LOWERING_VERSION = "dsh-worker-transform/1";
336
+ /**
337
+ * Free variables a lowered body expects from its wrapper, in order.
338
+ *
339
+ * Part of the image layout rather than of the transform, because the loader
340
+ * wraps bodies it never parses: the packer emits against these names and the
341
+ * worker binds them, with no compiler in the worker bundle to agree with.
342
+ */
343
+ const WRAPPER_PARAMS = [
344
+ "exports",
345
+ "require",
346
+ "module",
347
+ "__filename",
348
+ "__dirname",
349
+ "__dsh$meta",
350
+ "__als"
351
+ ];
352
+ //#endregion
353
+ //#region src/module-system/module-loader.ts
354
+ /**
355
+ * CommonJS module loader over the worker VFS. It fills the `loader.internal`
356
+ * seam Cordis uses for every entry import, and backs the `node:module`
357
+ * `createRequire` proxy that `typert-loader`, `client-modules`, and the plugin
358
+ * package inventory resolve package metadata through.
359
+ *
360
+ * Resolution is a narrowed Node `require` algorithm: `exports` walk with a
361
+ * fixed condition order, extension probing, and one cache keyed by resolved
362
+ * absolute path. Module bodies are wrapped as the image holds them: lowering is
363
+ * the packer's job, so nothing here parses JavaScript.
364
+ * @module @deepseek-ai/dsh-experimental-webworker-runtime/src/module-system/module-loader
365
+ */
366
+ /** Condition keys honoured in `exports`, in order; `node` is deliberately absent. */
367
+ const DEFAULT_CONDITIONS = [
368
+ "browser",
369
+ "require",
370
+ "import",
371
+ "default"
372
+ ];
373
+ /** Extensions probed when a specifier has no usable one. */
374
+ const EXTENSIONS = [
375
+ ".js",
376
+ ".json",
377
+ ".mjs",
378
+ ".cjs"
379
+ ];
380
+ function isRecord(value) {
381
+ return typeof value === "object" && value !== null && !Array.isArray(value);
382
+ }
383
+ /** Loader for one VFS mount; construct once per worker. */
384
+ var WorkerModuleLoader = class {
385
+ vfs;
386
+ root;
387
+ staticModules;
388
+ staticPrefixes;
389
+ conditions;
390
+ als;
391
+ modules = /* @__PURE__ */ new Map();
392
+ manifests = /* @__PURE__ */ new Map();
393
+ stack = [];
394
+ /**
395
+ * The Cordis module seam. `parentURL` positions relative specifiers;
396
+ * import attributes are ignored, as the client implementation does.
397
+ */
398
+ internal;
399
+ constructor(options) {
400
+ this.vfs = options.vfs;
401
+ this.root = options.root ?? "/dsh";
402
+ this.staticModules = new Map(Object.entries(options.staticModules));
403
+ this.staticPrefixes = Object.entries(options.staticModulePrefixes ?? {}).sort(([left], [right]) => right.length - left.length);
404
+ this.conditions = new Set(options.conditions ?? DEFAULT_CONDITIONS);
405
+ this.als = createAlsRuntime(options.alsCausality);
406
+ const resolveInternal = (specifier, parentURL) => {
407
+ const from = parentURL === void 0 ? this.root : this.baseDirectoryOf(parentURL);
408
+ const resolution = this.resolve(specifier, from);
409
+ if (resolution.kind === "static") return {
410
+ format: "builtin",
411
+ url: resolution.specifier
412
+ };
413
+ return {
414
+ format: resolution.path.endsWith(".json") ? "json" : "commonjs",
415
+ url: pathToFileUrl(resolution.path)
416
+ };
417
+ };
418
+ this.internal = {
419
+ version: "worker",
420
+ import: async (specifier, parentURL) => {
421
+ const from = parentURL === void 0 ? this.root : this.baseDirectoryOf(parentURL);
422
+ return this.load(this.resolve(specifier, from));
423
+ },
424
+ resolve: async (specifier, parentURL) => resolveInternal(specifier, parentURL),
425
+ resolveSync: resolveInternal
426
+ };
427
+ }
428
+ fail(detail) {
429
+ const chain = this.stack.length === 0 ? "" : ` (importer chain: ${this.stack.join(" -> ")})`;
430
+ throw new Error(`webworker modules: ${detail}${chain}`);
431
+ }
432
+ /** @returns Directory a base path or URL resolves specifiers from. */
433
+ baseDirectoryOf(base) {
434
+ const text = typeof base === "string" ? base : base.href;
435
+ const path = text.startsWith("file://") ? fileUrlToPath(text) : text;
436
+ if (path.endsWith("/")) return resolve(path);
437
+ return this.vfs.existsSync(path) && this.vfs.statSync(path).isDirectory() ? resolve(path) : dirname(path);
438
+ }
439
+ manifestOf(directory) {
440
+ const cached = this.manifests.get(directory);
441
+ if (cached !== void 0) return cached;
442
+ const path = join(directory, "package.json");
443
+ const text = this.vfs.readFileSync(path, "utf8");
444
+ let parsed;
445
+ try {
446
+ parsed = JSON.parse(text);
447
+ } catch (reason) {
448
+ this.fail(`${path} is not valid JSON: ${reason.message}`);
449
+ }
450
+ if (!isRecord(parsed)) this.fail(`${path} does not hold an object`);
451
+ const manifest = parsed;
452
+ this.manifests.set(directory, manifest);
453
+ return manifest;
454
+ }
455
+ /** Walk one `exports` value against the condition set and requested subpath. */
456
+ selectExport(field, subpath, packageName) {
457
+ if (field === null) return void 0;
458
+ if (typeof field === "string") return subpath === "." ? field : void 0;
459
+ if (Array.isArray(field)) {
460
+ for (const candidate of field) {
461
+ const picked = this.selectExport(candidate, subpath, packageName);
462
+ if (picked !== void 0) return picked;
463
+ }
464
+ return;
465
+ }
466
+ const entries = Object.entries(field);
467
+ if (!entries.some(([key]) => key === "." || key.startsWith("./"))) {
468
+ if (subpath !== ".") return void 0;
469
+ return this.selectCondition(field, packageName);
470
+ }
471
+ for (const [key, value] of entries) if (key === subpath) return typeof value === "string" ? value : this.selectCondition(value, packageName, subpath);
472
+ for (const [key, value] of entries) {
473
+ const star = key.indexOf("*");
474
+ if (star < 0) continue;
475
+ const prefix = key.slice(0, star);
476
+ const suffix = key.slice(star + 1);
477
+ if (!subpath.startsWith(prefix) || !subpath.endsWith(suffix)) continue;
478
+ const captured = subpath.slice(prefix.length, subpath.length - suffix.length);
479
+ const target = typeof value === "string" ? value : this.selectCondition(value, packageName, subpath);
480
+ if (target !== void 0) return target.replaceAll("*", captured);
481
+ }
482
+ }
483
+ /** Pick the first condition branch this runtime satisfies. */
484
+ selectCondition(field, packageName, subpath = ".") {
485
+ if (field === null) return void 0;
486
+ if (typeof field === "string") return field;
487
+ if (Array.isArray(field)) {
488
+ for (const candidate of field) {
489
+ const picked = this.selectCondition(candidate, packageName, subpath);
490
+ if (picked !== void 0) return picked;
491
+ }
492
+ return;
493
+ }
494
+ for (const [key, value] of Object.entries(field)) {
495
+ if (!this.conditions.has(key)) continue;
496
+ const picked = this.selectCondition(value, packageName, subpath);
497
+ if (picked !== void 0) return picked;
498
+ }
499
+ }
500
+ /** Extension and directory probing for a concrete path. */
501
+ probe(path, specifier) {
502
+ const candidates = [path, ...EXTENSIONS.map((extension) => path + extension)];
503
+ for (const candidate of candidates) if (this.vfs.existsSync(candidate) && this.vfs.statSync(candidate).isFile()) return candidate;
504
+ if (this.vfs.existsSync(path) && this.vfs.statSync(path).isDirectory()) {
505
+ if (this.vfs.existsSync(join(path, "package.json"))) {
506
+ const main = this.manifestOf(path).main;
507
+ if (main !== void 0) return this.probe(join(path, main), specifier);
508
+ }
509
+ return this.probe(join(path, "index"), specifier);
510
+ }
511
+ return this.fail(`cannot resolve "${specifier}": no file at ${candidates.join(", ")}`);
512
+ }
513
+ /** @returns The Worker-provided implementation of a static specifier. */
514
+ staticModule(specifier) {
515
+ const exact = this.staticModules.get(specifier);
516
+ if (exact !== void 0) return exact;
517
+ for (const [prefix, factory] of this.staticPrefixes) if (specifier.startsWith(prefix)) return factory;
518
+ return this.staticModules.get(`node:${specifier}`);
519
+ }
520
+ /**
521
+ * Resolve a specifier the way the module that requested it would.
522
+ * @param specifier - Bare name, relative path, absolute path, or file URL.
523
+ * @param fromDirectory - Directory of the requesting module.
524
+ * @returns Static module or the resolved VFS path.
525
+ */
526
+ resolve(specifier, fromDirectory) {
527
+ const staticModule = this.staticModule(specifier);
528
+ if (staticModule !== void 0) return {
529
+ kind: "static",
530
+ specifier,
531
+ factory: staticModule
532
+ };
533
+ if (specifier.startsWith("cordis:") || specifier.startsWith("node:")) return this.fail(`no static module is registered for "${specifier}"`);
534
+ if (specifier.startsWith("file://")) return {
535
+ kind: "file",
536
+ path: this.probe(fileUrlToPath(specifier), specifier)
537
+ };
538
+ if (specifier.startsWith(".")) return {
539
+ kind: "file",
540
+ path: this.probe(join(fromDirectory, specifier), specifier)
541
+ };
542
+ if (isAbsolute(specifier)) return {
543
+ kind: "file",
544
+ path: this.probe(specifier, specifier)
545
+ };
546
+ const segments = specifier.split("/");
547
+ const packageName = specifier.startsWith("@") ? segments.slice(0, 2).join("/") : segments[0] ?? specifier;
548
+ const rest = specifier.slice(packageName.length).replace(/^\//, "");
549
+ const packageDirectory = join(this.root, "node_modules", packageName);
550
+ if (!this.vfs.existsSync(join(packageDirectory, "package.json"))) return this.fail(`cannot resolve "${specifier}": ${packageDirectory}/package.json is not in the image`);
551
+ const manifest = this.manifestOf(packageDirectory);
552
+ const subpath = rest === "" ? "." : `./${rest}`;
553
+ if (manifest.exports !== void 0) {
554
+ const target = this.selectExport(manifest.exports, subpath, packageName);
555
+ if (target === void 0) return this.fail(`"${packageName}" does not export "${subpath}" under conditions [${[...this.conditions].join(", ")}]`);
556
+ return {
557
+ kind: "file",
558
+ path: this.probe(join(packageDirectory, target), specifier)
559
+ };
560
+ }
561
+ const legacy = subpath === "." ? manifest.main ?? "index.js" : rest;
562
+ return {
563
+ kind: "file",
564
+ path: this.probe(join(packageDirectory, legacy), specifier)
565
+ };
566
+ }
567
+ /**
568
+ * Load a resolved module, reusing the cache and tolerating cycles with
569
+ * CommonJS partial-export semantics.
570
+ * @param resolution - Result of {@link resolve}.
571
+ * @returns The module's exports.
572
+ */
573
+ load(resolution) {
574
+ if (resolution.kind === "static") return resolution.factory();
575
+ const path = resolution.path;
576
+ const cached = this.modules.get(path);
577
+ if (cached !== void 0) return cached.module.exports;
578
+ if (path.endsWith(".json")) {
579
+ const parsed = JSON.parse(this.vfs.readFileSync(path, "utf8"));
580
+ this.modules.set(path, { module: { exports: parsed } });
581
+ return parsed;
582
+ }
583
+ const record = { module: { exports: {} } };
584
+ this.modules.set(path, record);
585
+ this.stack.push(path);
586
+ try {
587
+ const source = this.vfs.readFileSync(path, "utf8");
588
+ const factory = this.compile(source, path);
589
+ const directory = dirname(path);
590
+ factory(record.module.exports, this.requireFrom(directory), record.module, path, directory, {
591
+ url: pathToFileUrl(path),
592
+ resolve: (specifier) => {
593
+ const resolution = this.resolve(specifier, directory);
594
+ return resolution.kind === "static" ? resolution.specifier : pathToFileUrl(resolution.path);
595
+ }
596
+ }, this.als);
597
+ return record.module.exports;
598
+ } catch (reason) {
599
+ this.modules.delete(path);
600
+ throw reason;
601
+ } finally {
602
+ this.stack.pop();
603
+ }
604
+ }
605
+ /**
606
+ * Compile a body the image already lowered.
607
+ *
608
+ * Module syntax reaching here means the image was packed by something other
609
+ * than the packer, or its collector missed the entry. The worker carries no
610
+ * transform to recover with, so it names the image as the thing to rebuild.
611
+ * @param code - Module body as the image holds it.
612
+ * @param path - Resolved VFS path.
613
+ * @returns The wrapper factory.
614
+ */
615
+ compile(code, path) {
616
+ try {
617
+ return new Function(...WRAPPER_PARAMS, code);
618
+ } catch (reason) {
619
+ if (reason instanceof SyntaxError && /await/i.test(reason.message)) this.fail(`${path} uses top-level await, which cannot run as CommonJS in the worker: ${reason.message}`);
620
+ if (reason instanceof SyntaxError && /import|export/i.test(reason.message)) this.fail(`${path} still carries module syntax, so the image was not lowered by the packer (${reason.message}); rebuild the image`);
621
+ this.fail(`${path} failed to compile: ${reason.message}`);
622
+ }
623
+ }
624
+ /**
625
+ * Build a `require` bound to a directory.
626
+ * @param fromDirectory - Directory relative specifiers resolve against.
627
+ * @returns Callable require with `resolve`.
628
+ */
629
+ requireFrom(fromDirectory) {
630
+ const require = (specifier) => this.load(this.resolve(specifier, fromDirectory));
631
+ const resolve$1 = ((specifier) => {
632
+ const resolution = this.resolve(specifier, fromDirectory);
633
+ if (resolution.kind === "static") return this.fail(`"${specifier}" is a worker-provided module and has no VFS path`);
634
+ return resolution.path;
635
+ });
636
+ resolve$1.paths = (specifier) => {
637
+ if (this.staticModule(specifier) !== void 0 || specifier.startsWith("node:")) return null;
638
+ if (specifier.startsWith(".")) return [resolve(fromDirectory, ".")];
639
+ return [join(this.root, "node_modules")];
640
+ };
641
+ return Object.assign(require, { resolve: resolve$1 });
642
+ }
643
+ /**
644
+ * `node:module` `createRequire` for the VFS.
645
+ * @param base - Module path, directory path, or `file:` URL.
646
+ * @returns Require bound to that base.
647
+ */
648
+ createRequire(base) {
649
+ return this.requireFrom(this.baseDirectoryOf(base));
650
+ }
651
+ /**
652
+ * Report what this loader has done, for the host's boot diagnostics.
653
+ * @returns How many module bodies it has run.
654
+ */
655
+ usage() {
656
+ return { modules: this.modules.size };
657
+ }
658
+ };
659
+ let active$1;
660
+ /**
661
+ * Publish the loader the `node:module` proxy resolves through.
662
+ * @param loader - Loader built by the worker entry.
663
+ */
664
+ function setActiveModuleLoader(loader) {
665
+ active$1 = loader;
666
+ }
667
+ /**
668
+ * Read the published loader.
669
+ * @returns The active loader.
670
+ */
671
+ function requireActiveModuleLoader() {
672
+ if (active$1 === void 0) throw new Error("webworker modules: no loader is mounted; the worker entry must call setActiveModuleLoader before any createRequire use");
673
+ return active$1;
674
+ }
675
+ //#endregion
676
+ //#region src/module-proxies.ts
677
+ /**
678
+ * The worker bundle's module proxy table: the ONLY platform fork of the host
679
+ * tree. Entries replace Node builtins, external npm packages, and the native
680
+ * flock subpath; other workspace and vendored modules are mounted as they ship.
681
+ *
682
+ * The build turns these into bundler aliases, and `node/builtins.ts` turns the
683
+ * same modules into the loader's static table — one list, two consumers.
684
+ *
685
+ * The replacement path states the classification. `./node/builtin_modules/implemented/<module>.ts`
686
+ * carries the module's real semantics over a worker-side data source (VFS, the
687
+ * tunnel, a wasm codec, a browser primitive); `./node/builtin_modules/mock/<module>.ts` is a
688
+ * structural placeholder that mounts silently and reports the missing capability
689
+ * when a call finally reaches it. External npm replacements live in
690
+ * `./externals/`, named after the package they stand in for.
691
+ * @module @deepseek-ai/dsh-experimental-webworker-runtime/src/module-proxies
692
+ */
693
+ /**
694
+ * Module proxy table — the ONLY platform fork of the worker host. Keys are
695
+ * exact module specifiers; the native system package's Landlock entry stays
696
+ * unmodified while its flock subpath is replaced.
697
+ */
698
+ const MODULE_PROXIES = {
699
+ "node:fs": "./node/builtin_modules/implemented/fs.ts",
700
+ "fs": "./node/builtin_modules/implemented/fs.ts",
701
+ "node:fs/promises": "./node/builtin_modules/implemented/fs/promises.ts",
702
+ "fs/promises": "./node/builtin_modules/implemented/fs/promises.ts",
703
+ "node:path": "./node/builtin_modules/implemented/path.ts",
704
+ "path": "./node/builtin_modules/implemented/path.ts",
705
+ "node:path/posix": "./node/builtin_modules/implemented/path.ts",
706
+ "node:os": "./node/builtin_modules/implemented/os.ts",
707
+ "os": "./node/builtin_modules/implemented/os.ts",
708
+ "node:url": "./node/builtin_modules/implemented/url.ts",
709
+ "node:module": "./node/builtin_modules/implemented/module.ts",
710
+ "node:crypto": "./node/builtin_modules/implemented/crypto.ts",
711
+ "crypto": "./node/builtin_modules/implemented/crypto.ts",
712
+ "node:buffer": "./node/builtin_modules/implemented/buffer.ts",
713
+ "node:http": "./node/builtin_modules/implemented/http.ts",
714
+ "node:async_hooks": "./node/builtin_modules/implemented/async_hooks.ts",
715
+ "node:util": "./node/builtin_modules/implemented/util.ts",
716
+ "node:util/types": "./node/builtin_modules/implemented/util/types.ts",
717
+ "node:events": "./node/builtin_modules/implemented/events.ts",
718
+ "node:timers/promises": "./node/builtin_modules/implemented/timers/promises.ts",
719
+ "node:perf_hooks": "./node/builtin_modules/implemented/perf_hooks.ts",
720
+ "node:tty": "./node/builtin_modules/implemented/tty.ts",
721
+ "tty": "./node/builtin_modules/implemented/tty.ts",
722
+ "node:zlib": "./node/builtin_modules/implemented/zlib.ts",
723
+ "node:child_process": "./node/builtin_modules/implemented/child_process.ts",
724
+ "node:dns/promises": "./node/builtin_modules/mock/dns/promises.ts",
725
+ "dns/promises": "./node/builtin_modules/mock/dns/promises.ts",
726
+ "node:net": "./node/builtin_modules/mock/net.ts",
727
+ "node:stream": "./node/builtin_modules/implemented/stream.ts",
728
+ "node:vm": "./node/builtin_modules/mock/vm.ts",
729
+ "node:worker_threads": "./node/builtin_modules/mock/worker_threads.ts",
730
+ "node:sqlite": "./node/builtin_modules/mock/sqlite.ts",
731
+ "@deepseek-ai/node-addon-system/flock": "./node/external_packages/node-addon-system-flock.ts",
732
+ "koffi": "./node/external_packages/koffi.ts",
733
+ "sharp": "./node/external_packages/sharp.ts",
734
+ "node-pty": "./node/external_packages/node-pty.ts",
735
+ "@vscode/ripgrep": "./node/external_packages/ripgrep.ts",
736
+ "@earendil-works/pi-ai": "./node/external_packages/pi-ai.ts",
737
+ "ws": "./node/external_packages/ws.ts"
738
+ };
739
+ /** pi-ai subpaths (`/providers/all`, `/api/*.lazy`) share the one structural stub. */
740
+ const MODULE_PROXY_PREFIXES = { "@earendil-works/pi-ai/": "./node/external_packages/pi-ai.ts" };
741
+ //#endregion
742
+ //#region src/node/external_packages/replaced-externals.ts
743
+ /**
744
+ * Exact package or subpath specifiers served from the worker bundle. Kept
745
+ * import-free for the runtime builtin table and the VFS image collector.
746
+ * Whole-package entries are omitted from the image; subpath entries leave
747
+ * their parent package available to other consumers.
748
+ */
749
+ /** Package or subpath specifiers served from the worker bundle instead of the VFS. */
750
+ const REPLACED_EXTERNAL_PACKAGES = [
751
+ "@earendil-works/pi-ai",
752
+ "@vscode/ripgrep",
753
+ "@deepseek-ai/node-addon-system/flock",
754
+ "koffi",
755
+ "node-pty",
756
+ "sharp",
757
+ "ws"
758
+ ];
759
+ //#endregion
760
+ //#region src/transport/synthetic-http.ts
761
+ const encoder$3 = new TextEncoder();
762
+ /**
763
+ * Build the request/response pair for one tunnel request.
764
+ *
765
+ * `res.end()` is the settle point: the captured listener returns void, so the
766
+ * response object itself reports completion. `write()` always returns true,
767
+ * which skips backpressure waiting the tunnel cannot observe anyway.
768
+ * @param frame - Validated request frame.
769
+ * @param sink - Frame emitter for the response.
770
+ * @returns The pair handed to the captured request listener.
771
+ */
772
+ function createSyntheticExchange(frame, sink) {
773
+ const listeners = /* @__PURE__ */ new Map();
774
+ let status = 200;
775
+ let headers = {};
776
+ let streaming = false;
777
+ let finished = false;
778
+ let aborted = false;
779
+ const emit = (event) => {
780
+ for (const callback of [...listeners.get(event) ?? []]) callback();
781
+ };
782
+ const req = {
783
+ url: frame.url,
784
+ method: frame.method,
785
+ headers: frame.headers,
786
+ destroy: () => {
787
+ aborted = true;
788
+ },
789
+ async *[Symbol.asyncIterator]() {
790
+ if (frame.body === void 0) return;
791
+ if (frame.body instanceof Blob) {
792
+ for await (const chunk of frame.body.stream()) {
793
+ if (aborted) return;
794
+ if (chunk.byteLength > 0) yield chunk;
795
+ }
796
+ return;
797
+ }
798
+ if (frame.body instanceof ReadableStream) {
799
+ for await (const chunk of frame.body) {
800
+ if (aborted) return;
801
+ if (!(chunk instanceof Uint8Array)) throw new TypeError("webworker tunnel: request stream produced a non-Uint8Array chunk");
802
+ if (chunk.byteLength > 0) yield chunk;
803
+ }
804
+ return;
805
+ }
806
+ if (aborted || frame.body.byteLength === 0) return;
807
+ yield new Uint8Array(frame.body);
808
+ }
809
+ };
810
+ const res = {
811
+ writeHead: (nextStatus, nextHeaders) => {
812
+ status = nextStatus;
813
+ if (nextHeaders !== void 0) {
814
+ headers = {};
815
+ for (const [key, value] of Object.entries(nextHeaders)) headers[key.toLowerCase()] = String(value);
816
+ }
817
+ return res;
818
+ },
819
+ write: (chunk) => {
820
+ if (finished || aborted) return false;
821
+ if (!streaming) {
822
+ streaming = true;
823
+ sink.head(status, headers);
824
+ }
825
+ sink.chunk(typeof chunk === "string" ? encoder$3.encode(chunk) : chunk);
826
+ return true;
827
+ },
828
+ end: (body) => {
829
+ if (finished) return res;
830
+ finished = true;
831
+ const bytes = body === void 0 ? void 0 : typeof body === "string" ? encoder$3.encode(body) : body;
832
+ if (streaming) {
833
+ if (bytes !== void 0) sink.chunk(bytes);
834
+ sink.end();
835
+ } else sink.end({
836
+ status,
837
+ headers,
838
+ body: bytes
839
+ });
840
+ emit("close");
841
+ return res;
842
+ },
843
+ destroy: () => {
844
+ if (finished) return;
845
+ finished = true;
846
+ sink.fail(`response destroyed for ${frame.method} ${frame.url}`);
847
+ emit("close");
848
+ },
849
+ on: (event, callback) => {
850
+ const set = listeners.get(event) ?? /* @__PURE__ */ new Set();
851
+ set.add(callback);
852
+ listeners.set(event, set);
853
+ return res;
854
+ },
855
+ off: (event, callback) => {
856
+ listeners.get(event)?.delete(callback);
857
+ return res;
858
+ }
859
+ };
860
+ res.once = res.on;
861
+ Object.defineProperty(res, "headersSent", { get: () => streaming });
862
+ Object.defineProperty(res, "writableEnded", { get: () => finished });
863
+ return {
864
+ req,
865
+ res,
866
+ get aborted() {
867
+ return aborted;
868
+ },
869
+ abort: () => {
870
+ if (finished) return;
871
+ aborted = true;
872
+ finished = true;
873
+ emit("aborted");
874
+ emit("close");
875
+ }
876
+ };
877
+ }
878
+ //#endregion
879
+ //#region src/compile/transform.ts
880
+ /**
881
+ * The worker's module transform: one acorn parse turns an ES module into a
882
+ * CommonJS body **and** routes every suspension point through the ambient-store
883
+ * protocol.
884
+ *
885
+ * Both jobs live in one pass because they are two edits over one syntax tree;
886
+ * running a lexer first and a parser second meant two scanners, two sets of
887
+ * blind spots, and a second pass reading the first pass's output. Editing is
888
+ * interval-based — the original text is sliced and spliced, never reprinted —
889
+ * so **line numbers survive**: a stack frame in a transformed module points at
890
+ * the same line as the built artifact it came from.
891
+ *
892
+ * The image packer is this transform's only caller: it lowers every JavaScript
893
+ * entry it packs and records `LOWERING_VERSION` in the image manifest, so the
894
+ * worker wraps those bodies without carrying a compiler of its own.
895
+ * @module @deepseek-ai/dsh-experimental-webworker-runtime/src/compile/transform
896
+ */
897
+ const HELPER_SOURCE = {
898
+ def: "const __dsh$def=(t,k,get)=>Object.defineProperty(t,k,{enumerable:true,configurable:true,get});",
899
+ default: "const __dsh$default=(m)=>(m&&m.__esModule?m.default:m);",
900
+ ns: "const __dsh$ns=(m)=>(m&&m.__esModule?m:Object.assign({},m,{default:m}));",
901
+ exportAll: "const __dsh$exportAll=(t,m)=>{for(const k of Object.keys(m))if(k!==\"default\"&&!(k in t))__dsh$def(t,k,()=>m[k]);};",
902
+ dynImport: "const __dsh$dynImport=(s)=>Promise.resolve().then(()=>__dsh$ns(require(s)));"
903
+ };
904
+ const HELPER_DEPENDENCIES = {
905
+ exportAll: ["def"],
906
+ dynImport: ["ns"]
907
+ };
908
+ /** Runtime identifier the suspension protocol reaches. */
909
+ const ALS = "__als";
910
+ /** @returns Number of line breaks in a slice. */
911
+ function countNewlines(text) {
912
+ let count = 0;
913
+ for (let index = text.indexOf("\n"); index >= 0; index = text.indexOf("\n", index + 1)) count += 1;
914
+ return count;
915
+ }
916
+ var Transformer = class {
917
+ path;
918
+ edits = [];
919
+ source;
920
+ helpers = /* @__PURE__ */ new Set();
921
+ bindings = [];
922
+ modules = 0;
923
+ temporaries = 0;
924
+ moduleSyntax = false;
925
+ moduleRequests = /* @__PURE__ */ new Set();
926
+ metaResolveRequests = /* @__PURE__ */ new Set();
927
+ createRequireBindings = /* @__PURE__ */ new Set();
928
+ constructor(source, path) {
929
+ this.path = path;
930
+ this.source = source.startsWith("#!") ? `//${source.slice(2)}` : source;
931
+ }
932
+ fail(detail, index) {
933
+ const line = this.source.slice(0, index).split("\n").length;
934
+ throw new Error(`webworker transform: ${detail} (${this.path}:${line})`);
935
+ }
936
+ helper(name) {
937
+ for (const dependency of HELPER_DEPENDENCIES[name] ?? []) this.helper(dependency);
938
+ this.helpers.add(name);
939
+ return `__dsh$${name}`;
940
+ }
941
+ moduleTemp() {
942
+ this.modules += 1;
943
+ return `__dsh$m${this.modules}`;
944
+ }
945
+ alsTemp() {
946
+ this.temporaries += 1;
947
+ return `__als$${this.temporaries}`;
948
+ }
949
+ /**
950
+ * Replace a range, keeping the module's line count.
951
+ *
952
+ * The padding is the newlines the original range held **minus** the ones the
953
+ * replacement re-emits: a rewrite that splices the original body back in
954
+ * (a desugared loop) already carries that body's newlines, and padding by the
955
+ * whole range again would push every later line down.
956
+ */
957
+ edit(start, end, build) {
958
+ const original = countNewlines(this.source.slice(start, end));
959
+ this.edits.push({
960
+ start,
961
+ end,
962
+ render: (inner) => {
963
+ const text = build(inner);
964
+ return text + "\n".repeat(Math.max(0, original - countNewlines(text)));
965
+ }
966
+ });
967
+ }
968
+ replace(start, end, text) {
969
+ this.edit(start, end, () => text);
970
+ }
971
+ insert(at, text) {
972
+ this.edits.push({
973
+ start: at,
974
+ end: at,
975
+ render: () => text
976
+ });
977
+ }
978
+ structural(start, end, render) {
979
+ this.edit(start, end, render);
980
+ }
981
+ literal(node) {
982
+ const value = node.value;
983
+ if (typeof value !== "string") this.fail("a module specifier must be a string literal", node.start);
984
+ this.moduleRequests.add(value);
985
+ return JSON.stringify(value);
986
+ }
987
+ /** @returns Static module requests the body makes, in first-appearance order. */
988
+ requests() {
989
+ return [...this.moduleRequests];
990
+ }
991
+ /** @returns Literal `import.meta.resolve()` requests, in first-appearance order. */
992
+ metaRequests() {
993
+ return [...this.metaResolveRequests];
994
+ }
995
+ importDeclaration(node) {
996
+ this.moduleSyntax = true;
997
+ if (Array.isArray(node.attributes) && node.attributes.length > 0) this.fail("import attributes are not supported", node.start);
998
+ const source = node.source;
999
+ const request = `require(${this.literal(source)})`;
1000
+ const specifiers = node.specifiers;
1001
+ if (specifiers.length === 0) {
1002
+ this.replace(node.start, node.end, `${request};`);
1003
+ return;
1004
+ }
1005
+ const held = this.moduleTemp();
1006
+ const lines = [`const ${held}=${request};`];
1007
+ for (const specifier of specifiers) {
1008
+ const local = specifier.local.name;
1009
+ if (specifier.type === "ImportDefaultSpecifier") {
1010
+ lines.push(`const ${local}=${this.helper("default")}(${held});`);
1011
+ continue;
1012
+ }
1013
+ if (specifier.type === "ImportNamespaceSpecifier") {
1014
+ lines.push(`const ${local}=${this.helper("ns")}(${held});`);
1015
+ continue;
1016
+ }
1017
+ const imported = specifier.imported;
1018
+ const name = imported.type === "Identifier" ? imported.name : imported.value;
1019
+ lines.push(`const ${local}=${held}[${JSON.stringify(name)}];`);
1020
+ }
1021
+ this.replace(node.start, node.end, lines.join(""));
1022
+ }
1023
+ exportNamed(node) {
1024
+ this.moduleSyntax = true;
1025
+ const declaration = node.declaration;
1026
+ const source = node.source;
1027
+ const specifiers = node.specifiers;
1028
+ if (declaration !== null) {
1029
+ this.replace(node.start, declaration.start, "");
1030
+ for (const { exported, local } of declaredBindings(declaration, (detail) => this.fail(detail, declaration.start))) this.bindings.push({
1031
+ exported,
1032
+ local
1033
+ });
1034
+ return;
1035
+ }
1036
+ if (source !== null) {
1037
+ const held = this.moduleTemp();
1038
+ const define = this.helper("def");
1039
+ const lines = [`const ${held}=require(${this.literal(source)});`];
1040
+ for (const specifier of specifiers) {
1041
+ const local = nameOf(specifier.local);
1042
+ const exported = nameOf(specifier.exported);
1043
+ lines.push(`${define}(exports,${JSON.stringify(exported)},()=>${held}[${JSON.stringify(local)}]);`);
1044
+ }
1045
+ this.replace(node.start, node.end, lines.join(""));
1046
+ return;
1047
+ }
1048
+ for (const specifier of specifiers) this.bindings.push({
1049
+ exported: nameOf(specifier.exported),
1050
+ local: nameOf(specifier.local)
1051
+ });
1052
+ this.replace(node.start, node.end, "");
1053
+ }
1054
+ exportDefault(node) {
1055
+ this.moduleSyntax = true;
1056
+ const declaration = node.declaration;
1057
+ this.replace(node.start, declaration.start, "exports.default = ");
1058
+ }
1059
+ exportAll(node) {
1060
+ this.moduleSyntax = true;
1061
+ const request = `require(${this.literal(node.source)})`;
1062
+ const exported = node.exported;
1063
+ if (exported === null) {
1064
+ this.replace(node.start, node.end, `${this.helper("exportAll")}(exports,${request});`);
1065
+ return;
1066
+ }
1067
+ const held = this.moduleTemp();
1068
+ const define = this.helper("def");
1069
+ this.replace(node.start, node.end, `const ${held}=${this.helper("ns")}(${request});${define}(exports,${JSON.stringify(nameOf(exported))},()=>${held});`);
1070
+ }
1071
+ awaitExpression(node) {
1072
+ const keywordEnd = node.start + 5;
1073
+ if (this.source.slice(node.start, keywordEnd) !== "await") this.fail("unexpected await layout", node.start);
1074
+ this.replace(node.start, keywordEnd, `${ALS}.resume(await ${ALS}.pause(`);
1075
+ this.insert(node.end, "))");
1076
+ }
1077
+ /**
1078
+ * `for await (L of R) B` becomes an explicit loop over the same protocol.
1079
+ * `iterator.return` runs only on abrupt completion, as the language says, and
1080
+ * is awaited so teardown still orders before the loop exits.
1081
+ */
1082
+ forAwait(node) {
1083
+ const left = node.left;
1084
+ const right = node.right;
1085
+ const body = node.body;
1086
+ const iterator = this.alsTemp();
1087
+ const step = this.alsTemp();
1088
+ const exhausted = this.alsTemp();
1089
+ const binding = (inner) => {
1090
+ if (left.type !== "VariableDeclaration") return `(${inner(left.start, left.end)})=${step}.value;`;
1091
+ const declarations = left.declarations;
1092
+ const pattern = declarations[0]?.id;
1093
+ if (declarations.length !== 1 || pattern === void 0) this.fail("for-await must declare exactly one binding", left.start);
1094
+ return `${String(left.kind)} ${inner(pattern.start, pattern.end)}=${step}.value;`;
1095
+ };
1096
+ this.structural(node.start, node.end, (inner) => [
1097
+ `{const ${iterator}=${ALS}.iterator(${inner(right.start, right.end)});`,
1098
+ `let ${step};let ${exhausted}=false;`,
1099
+ `try{for(;;){${step}=${ALS}.resume(await ${ALS}.pause(${iterator}.next()));`,
1100
+ `if(${step}.done){${exhausted}=true;break}`,
1101
+ `{${binding(inner)}${body.type === "BlockStatement" ? inner(body.start, body.end) : `{${inner(body.start, body.end)}}`}}}}`,
1102
+ `finally{if(!${exhausted})${ALS}.resume(await ${ALS}.pause(${ALS}.close(${iterator})))}}`
1103
+ ].join(""));
1104
+ }
1105
+ /**
1106
+ * `yield` resumes with whatever the consumer sent, so the snapshot is taken
1107
+ * before suspending and restored when the call completes. `yield*` delegates,
1108
+ * which has no expression form here: it is desugared as a statement, and a
1109
+ * consumer's `throw()` is not forwarded into the inner iterator (`next` and
1110
+ * `return` are).
1111
+ */
1112
+ yieldExpression(node, statement) {
1113
+ if (node.delegate !== true) {
1114
+ this.insert(node.start, `${ALS}.afterYield(${ALS}.snapshot(),`);
1115
+ this.insert(node.end, ")");
1116
+ return;
1117
+ }
1118
+ const argument = node.argument;
1119
+ if (argument === null) this.fail("yield* without an operand", node.start);
1120
+ if (statement === void 0) this.fail("yield* is only supported as a statement", node.start);
1121
+ if (statement.expression !== node) this.fail("yield* is only supported as the whole statement expression", node.start);
1122
+ const iterator = this.alsTemp();
1123
+ const step = this.alsTemp();
1124
+ const sent = this.alsTemp();
1125
+ const exhausted = this.alsTemp();
1126
+ this.structural(statement.start, statement.end, (inner) => [
1127
+ `{const ${iterator}=${ALS}.iterator(${inner(argument.start, argument.end)});`,
1128
+ `let ${sent};let ${exhausted}=false;`,
1129
+ `try{for(;;){const ${step}=${ALS}.resume(await ${ALS}.pause(${iterator}.next(${sent})));`,
1130
+ `if(${step}.done){${exhausted}=true;break}`,
1131
+ `${sent}=${ALS}.afterYield(${ALS}.snapshot(),yield ${step}.value)}}`,
1132
+ `finally{if(!${exhausted})${ALS}.resume(await ${ALS}.pause(${ALS}.close(${iterator})))}}`
1133
+ ].join(""));
1134
+ }
1135
+ visit(node, context) {
1136
+ if (node === null || typeof node !== "object") return;
1137
+ if (Array.isArray(node)) {
1138
+ for (const child of node) this.visit(child, context);
1139
+ return;
1140
+ }
1141
+ const record = node;
1142
+ if (typeof record.type !== "string") return;
1143
+ let next = context;
1144
+ switch (record.type) {
1145
+ case "ImportDeclaration":
1146
+ this.importDeclaration(record);
1147
+ break;
1148
+ case "ExportNamedDeclaration":
1149
+ this.exportNamed(record);
1150
+ break;
1151
+ case "ExportDefaultDeclaration":
1152
+ this.exportDefault(record);
1153
+ break;
1154
+ case "ExportAllDeclaration":
1155
+ this.exportAll(record);
1156
+ break;
1157
+ case "ImportExpression": {
1158
+ this.moduleSyntax = true;
1159
+ if (!this.source.startsWith("import", record.start)) this.fail("unexpected dynamic import layout", record.start);
1160
+ this.replace(record.start, record.start + 6, this.helper("dynImport"));
1161
+ const argument = record.source;
1162
+ if (argument !== void 0 && typeof argument.value === "string") this.moduleRequests.add(argument.value);
1163
+ break;
1164
+ }
1165
+ case "CallExpression": {
1166
+ const callee = record.callee;
1167
+ const callArguments = record.arguments;
1168
+ if (this.isRequireCall(callee, context.moduleScope) && callArguments.length === 1 && typeof callArguments[0]?.value === "string") this.moduleRequests.add(callArguments[0].value);
1169
+ if (callee.type === "MemberExpression") {
1170
+ const object = callee.object;
1171
+ const property = callee.property;
1172
+ if (object.type === "MetaProperty" && object.meta.name === "import" && property.type === "Identifier" && property.name === "resolve" && typeof callArguments[0]?.value === "string") this.metaResolveRequests.add(callArguments[0].value);
1173
+ }
1174
+ break;
1175
+ }
1176
+ case "MetaProperty":
1177
+ if (record.meta.name === "import") {
1178
+ this.moduleSyntax = true;
1179
+ this.replace(record.start, record.end, "__dsh$meta");
1180
+ }
1181
+ break;
1182
+ case "AwaitExpression":
1183
+ if (context.functionDepth === 0) this.fail("top-level await cannot run as CommonJS in the worker", record.start);
1184
+ this.awaitExpression(record);
1185
+ break;
1186
+ case "ForOfStatement":
1187
+ if (record.await === true) {
1188
+ if (context.functionDepth === 0) this.fail("a top-level for-await loop cannot run as CommonJS", record.start);
1189
+ this.forAwait(record);
1190
+ }
1191
+ next = {
1192
+ ...next,
1193
+ moduleScope: false
1194
+ };
1195
+ break;
1196
+ case "LabeledStatement": {
1197
+ const body = record.body;
1198
+ if (body.type === "ForOfStatement" && body.await === true) this.fail("a labeled for-await loop is not supported", record.start);
1199
+ break;
1200
+ }
1201
+ case "YieldExpression":
1202
+ if (context.asyncGenerator) this.yieldExpression(record, context.statement);
1203
+ break;
1204
+ case "FunctionDeclaration":
1205
+ case "FunctionExpression":
1206
+ case "ArrowFunctionExpression":
1207
+ next = {
1208
+ asyncGenerator: record.async === true && record.generator === true,
1209
+ functionDepth: context.functionDepth + 1,
1210
+ moduleScope: false
1211
+ };
1212
+ break;
1213
+ case "BlockStatement":
1214
+ case "CatchClause":
1215
+ case "ClassBody":
1216
+ case "ForStatement":
1217
+ case "ForInStatement":
1218
+ case "SwitchStatement":
1219
+ next = {
1220
+ ...next,
1221
+ moduleScope: false
1222
+ };
1223
+ break;
1224
+ default: break;
1225
+ }
1226
+ if (record.type === "ExpressionStatement") next = {
1227
+ ...next,
1228
+ statement: record
1229
+ };
1230
+ for (const [key, value] of Object.entries(record)) {
1231
+ if (key === "type" || key === "start" || key === "end") continue;
1232
+ this.visit(value, next);
1233
+ }
1234
+ }
1235
+ isCreateRequireCall(node) {
1236
+ if (node.type !== "CallExpression") return false;
1237
+ const callee = node.callee;
1238
+ const args = node.arguments;
1239
+ if (callee.type !== "Identifier" || !this.createRequireBindings.has(nameOf(callee)) || args.length !== 1) return false;
1240
+ const base = args[0];
1241
+ if (base.type !== "MemberExpression" || base.computed === true) return false;
1242
+ const object = base.object;
1243
+ const property = base.property;
1244
+ return object.type === "MetaProperty" && object.meta.name === "import" && property.type === "Identifier" && property.name === "url";
1245
+ }
1246
+ isRequireCall(callee, moduleScope) {
1247
+ return callee.type === "Identifier" && callee.name === "require" || moduleScope && this.isCreateRequireCall(callee);
1248
+ }
1249
+ indexCreateRequireImports(program) {
1250
+ for (const statement of program.body) {
1251
+ if (statement.type !== "ImportDeclaration") continue;
1252
+ const source = statement.source;
1253
+ if (source.value !== "node:module" && source.value !== "module") continue;
1254
+ for (const specifier of statement.specifiers) {
1255
+ if (specifier.type !== "ImportSpecifier" || nameOf(specifier.imported) !== "createRequire") continue;
1256
+ this.createRequireBindings.add(nameOf(specifier.local));
1257
+ }
1258
+ }
1259
+ }
1260
+ run() {
1261
+ if (this.source.includes(`${ALS}.pause(`) || this.source.includes("__als$")) this.fail("the module is already lowered; check the image manifest wiring", 0);
1262
+ let program;
1263
+ try {
1264
+ program = parse(this.source, {
1265
+ ecmaVersion: "latest",
1266
+ sourceType: "module",
1267
+ allowAwaitOutsideFunction: true
1268
+ });
1269
+ } catch (reason) {
1270
+ this.fail(`parse failed: ${reason.message}`, 0);
1271
+ }
1272
+ this.indexCreateRequireImports(program);
1273
+ this.visit(program, {
1274
+ asyncGenerator: false,
1275
+ functionDepth: 0,
1276
+ moduleScope: true
1277
+ });
1278
+ if (this.edits.length === 0 && !this.moduleSyntax) return this.source;
1279
+ const prologue = [];
1280
+ if (this.moduleSyntax) prologue.push("\"use strict\";Object.defineProperty(exports,\"__esModule\",{value:true});");
1281
+ if (this.bindings.length > 0) this.helper("def");
1282
+ for (const [name, source] of Object.entries(HELPER_SOURCE)) if (this.helpers.has(name)) prologue.push(source);
1283
+ for (const { exported, local } of this.bindings) prologue.push(`__dsh$def(exports,${JSON.stringify(exported)},()=>${local});`);
1284
+ const sorted = [...this.edits].sort((left, right) => left.start - right.start || left.end - right.end);
1285
+ const render = (from, to) => {
1286
+ let cursor = from;
1287
+ let out = "";
1288
+ for (const edit of sorted) {
1289
+ if (edit.start < cursor || edit.end > to) continue;
1290
+ out += this.source.slice(cursor, edit.start) + edit.render(render);
1291
+ cursor = edit.end;
1292
+ }
1293
+ return out + this.source.slice(cursor, to);
1294
+ };
1295
+ const code = prologue.join("") + render(0, this.source.length);
1296
+ try {
1297
+ parse(code, {
1298
+ ecmaVersion: "latest",
1299
+ sourceType: "script",
1300
+ allowAwaitOutsideFunction: false
1301
+ });
1302
+ } catch (reason) {
1303
+ this.fail(`the transform produced code that does not parse: ${reason.message}`, 0);
1304
+ }
1305
+ return code;
1306
+ }
1307
+ };
1308
+ /** @returns The name a specifier or identifier node carries. */
1309
+ function nameOf(node) {
1310
+ return node.type === "Identifier" ? node.name : String(node.value);
1311
+ }
1312
+ /** Every binding an exported declaration introduces, including patterns. */
1313
+ function declaredBindings(declaration, fail) {
1314
+ if (declaration.type === "FunctionDeclaration" || declaration.type === "ClassDeclaration") {
1315
+ const id = declaration.id;
1316
+ if (id === null) fail("an exported declaration must be named");
1317
+ const name = id.name;
1318
+ return [{
1319
+ exported: name,
1320
+ local: name
1321
+ }];
1322
+ }
1323
+ if (declaration.type !== "VariableDeclaration") fail(`unsupported exported declaration ${declaration.type}`);
1324
+ const bindings = [];
1325
+ const collect = (pattern) => {
1326
+ switch (pattern.type) {
1327
+ case "Identifier":
1328
+ bindings.push({
1329
+ exported: pattern.name,
1330
+ local: pattern.name
1331
+ });
1332
+ return;
1333
+ case "ObjectPattern":
1334
+ for (const property of pattern.properties) collect(property.type === "RestElement" ? property.argument : property.value);
1335
+ return;
1336
+ case "ArrayPattern":
1337
+ for (const element of pattern.elements) if (element !== null) collect(element);
1338
+ return;
1339
+ case "AssignmentPattern":
1340
+ collect(pattern.left);
1341
+ return;
1342
+ case "RestElement":
1343
+ collect(pattern.argument);
1344
+ return;
1345
+ default: fail(`unsupported binding pattern ${pattern.type}`);
1346
+ }
1347
+ };
1348
+ for (const declarator of declaration.declarations) collect(declarator.id);
1349
+ return bindings;
1350
+ }
1351
+ const cache = /* @__PURE__ */ new Map();
1352
+ /**
1353
+ * Transform one module into a body for the worker wrapper.
1354
+ *
1355
+ * Results are cached by source text, so a module reached through two paths, or
1356
+ * a repeated build, parses once.
1357
+ * @param source - Module source, ESM or CommonJS.
1358
+ * @param path - Path used in diagnostics.
1359
+ * @returns The lowered body and the module requests found in it.
1360
+ */
1361
+ function transformDetailed(source, path) {
1362
+ const cached = cache.get(source);
1363
+ if (cached !== void 0) return cached;
1364
+ const transformer = new Transformer(source, path);
1365
+ const transformed = {
1366
+ code: transformer.run(),
1367
+ moduleRequests: transformer.requests(),
1368
+ metaResolveRequests: transformer.metaRequests()
1369
+ };
1370
+ cache.set(source, transformed);
1371
+ return transformed;
1372
+ }
1373
+ /**
1374
+ * Lower one module at image-pack time.
1375
+ *
1376
+ * The collector calls this for every JavaScript entry it packs and records
1377
+ * `LOWERING_VERSION` in the image manifest; the loader then wraps those entries
1378
+ * without parsing them. `lowered: false` reports that the transform would have
1379
+ * returned the input verbatim (already CommonJS, no suspension point), so the
1380
+ * entry may be packed as it is.
1381
+ *
1382
+ * Throwing is the intended failure mode: a module this transform cannot express
1383
+ * must fail the build rather than ship an image that breaks at load.
1384
+ * @param options - Virtual path inside the image and the module source.
1385
+ * @returns The code to pack and whether it changed.
1386
+ */
1387
+ function lowerModuleSource(options) {
1388
+ const { code, moduleRequests, metaResolveRequests } = transformDetailed(options.source, options.filename);
1389
+ return {
1390
+ code,
1391
+ lowered: code !== options.source,
1392
+ moduleRequests,
1393
+ metaResolveRequests
1394
+ };
1395
+ }
1396
+ //#endregion
1397
+ //#region src/transport/tunnel.ts
1398
+ /**
1399
+ * Worker end of the postMessage tunnel. It owns the dispatch lanes and the queue
1400
+ * that holds requests until the host tree is serving:
1401
+ *
1402
+ * - `GET /__boot__` answers from tunnel glue, never from the host API surface,
1403
+ * because the page needs the boot payload before its Cordis tree exists.
1404
+ * - Privileged `/api` methods take that same direct entry. The method set is not
1405
+ * restated here: a 401 or 403 from the route lane is retried on the direct
1406
+ * lane because the page owns the worker and needs no network authentication.
1407
+ * - Everything else is fed into the real webserver route table through the
1408
+ * request listener the app's fake `node:http` captured, keeping the trust
1409
+ * fences, byte limits, and status semantics intact.
1410
+ *
1411
+ * A boot failure rejects the whole queue with 503 rather than leaving the page
1412
+ * waiting.
1413
+ * @module @deepseek-ai/dsh-experimental-webworker-runtime/src/transport/tunnel
1414
+ */
1415
+ /** Prefix owning the API methods. */
1416
+ const API_PREFIX = "/api";
1417
+ /** Host header the synthesized requests carry; the API trust fence requires one. */
1418
+ const SYNTHETIC_HOST = "127.0.0.1";
1419
+ const encoder$2 = new TextEncoder();
1420
+ /**
1421
+ * Render a failure with everything nested inside it.
1422
+ *
1423
+ * A boot failure is usually an `AggregateError` of per-entry failures, each
1424
+ * wrapping the plugin's own error as `cause`; only the outermost message names
1425
+ * "loader entries failed to apply", which says nothing about which row broke.
1426
+ *
1427
+ * The page logs the rendered text verbatim for refusals; keep it stable for
1428
+ * anyone matching boot-failure output.
1429
+ * @param reason - Thrown value.
1430
+ * @returns One line per nested failure, indented by depth.
1431
+ */
1432
+ function describeFailure(reason) {
1433
+ const seen = /* @__PURE__ */ new Set();
1434
+ const lines = [];
1435
+ const walk = (value, depth) => {
1436
+ if (value === null || value === void 0 || seen.has(value) || depth > 6) return;
1437
+ seen.add(value);
1438
+ const indent = " ".repeat(depth);
1439
+ if (!(value instanceof Error)) {
1440
+ const rendered = typeof value === "string" ? value : JSON.stringify(value);
1441
+ lines.push(`${indent}${rendered ?? typeof value}`);
1442
+ return;
1443
+ }
1444
+ lines.push(`${indent}${value.name}: ${value.message}`);
1445
+ if (value instanceof AggregateError) for (const inner of value.errors) walk(inner, depth + 1);
1446
+ walk(value.cause, depth + 1);
1447
+ };
1448
+ walk(reason, 0);
1449
+ return lines.join("\n");
1450
+ }
1451
+ /**
1452
+ * Copy bytes into an exact-size ArrayBuffer so it can be transferred.
1453
+ *
1454
+ * Sliced on the ArrayBuffer, not the view: `Uint8Array.prototype.slice` copies,
1455
+ * but a Node-style Buffer overrides `slice()` with view semantics, and the fs
1456
+ * bridge hands VFS reads over as Buffer views into the whole mounted image —
1457
+ * `bytes.slice().buffer` would then post the entire image as the body.
1458
+ */
1459
+ function toTransferable(bytes) {
1460
+ return bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
1461
+ }
1462
+ /** Recorded response frames, so a route-lane authentication refusal can be discarded. */
1463
+ var BufferedSink = class {
1464
+ calls = [];
1465
+ target;
1466
+ settle;
1467
+ /** Resolves when the exchange either starts streaming or answers in one frame. */
1468
+ settled = new Promise((resolve) => {
1469
+ this.settle = resolve;
1470
+ });
1471
+ sink = {
1472
+ head: (status, headers) => {
1473
+ this.record(() => {
1474
+ this.target?.head(status, headers);
1475
+ });
1476
+ this.settle?.({
1477
+ streamed: true,
1478
+ status
1479
+ });
1480
+ },
1481
+ chunk: (bytes) => {
1482
+ this.record(() => {
1483
+ this.target?.chunk(bytes);
1484
+ });
1485
+ },
1486
+ end: (payload) => {
1487
+ this.record(() => {
1488
+ this.target?.end(payload);
1489
+ });
1490
+ this.settle?.({
1491
+ streamed: payload === void 0,
1492
+ status: payload?.status ?? 200
1493
+ });
1494
+ },
1495
+ fail: (message) => {
1496
+ this.record(() => {
1497
+ this.target?.fail(message);
1498
+ });
1499
+ this.settle?.({
1500
+ streamed: true,
1501
+ status: 500
1502
+ });
1503
+ }
1504
+ };
1505
+ record(call) {
1506
+ if (this.target === void 0) this.calls.push(call);
1507
+ else call();
1508
+ }
1509
+ /**
1510
+ * Send everything recorded so far to a real sink and pass later calls through.
1511
+ * @param target - Sink receiving the frames.
1512
+ */
1513
+ flushTo(target) {
1514
+ this.target = target;
1515
+ for (const call of this.calls.splice(0)) call();
1516
+ }
1517
+ };
1518
+ /** One tunnel per worker; wire {@link TunnelServer.handleMessage} to `onmessage` first. */
1519
+ var TunnelServer = class {
1520
+ port;
1521
+ requestListener;
1522
+ privilegedMethods;
1523
+ unaryApiLane;
1524
+ queue = [];
1525
+ inFlight = /* @__PURE__ */ new Map();
1526
+ seams;
1527
+ failure;
1528
+ listener;
1529
+ constructor(options) {
1530
+ this.port = options.port;
1531
+ this.requestListener = options.requestListener;
1532
+ this.privilegedMethods = options.privilegedMethods;
1533
+ this.unaryApiLane = options.unaryApiLane ?? "route";
1534
+ }
1535
+ /**
1536
+ * Accept one `postMessage` payload.
1537
+ * @param data - Message data from the page.
1538
+ */
1539
+ handleMessage(data) {
1540
+ const frame = parseInboundFrame(data);
1541
+ if (frame.t === "init") throw new Error("webworker tunnel: duplicate init frame; the tunnel is already open");
1542
+ if (frame.t === "abort") {
1543
+ this.inFlight.get(frame.id)?.abort();
1544
+ this.inFlight.delete(frame.id);
1545
+ const queued = this.queue.findIndex((request) => request.id === frame.id);
1546
+ if (queued !== -1) this.queue.splice(queued, 1);
1547
+ return;
1548
+ }
1549
+ if (this.failure !== void 0) {
1550
+ this.refuse(frame, this.failure);
1551
+ return;
1552
+ }
1553
+ if (this.seams === void 0) {
1554
+ this.queue.push(frame);
1555
+ return;
1556
+ }
1557
+ this.dispatchFrame(frame);
1558
+ }
1559
+ /**
1560
+ * Start serving: drains everything queued during boot.
1561
+ * @param seams - Faces that exist only after the host tree is up.
1562
+ */
1563
+ serve(seams) {
1564
+ this.seams = seams;
1565
+ console.info(`webworker tunnel: serving (unary /api lane=${this.unaryApiLane}${this.unaryApiLane === "route" ? " with 401/403 retry" : ""}, privileged set=${this.privilegedMethods === void 0 ? "none" : String(this.privilegedMethods.size)}, queued=${String(this.queue.length)})`);
1566
+ for (const frame of this.queue.splice(0)) this.dispatchFrame(frame);
1567
+ }
1568
+ /**
1569
+ * Refuse every queued and future request; the page renders this like a server
1570
+ * that failed to start.
1571
+ * @param reason - Boot failure to report.
1572
+ */
1573
+ fail(reason) {
1574
+ const message = describeFailure(reason);
1575
+ this.failure = message;
1576
+ for (const frame of this.queue.splice(0)) this.refuse(frame, message);
1577
+ }
1578
+ send(frame, transfer) {
1579
+ this.port.postMessage(frame, transfer);
1580
+ }
1581
+ refuse(frame, message) {
1582
+ if (frame.t === "stream-open") {
1583
+ this.send({
1584
+ t: "stream-error",
1585
+ id: frame.id,
1586
+ failure: {
1587
+ kind: "carrier",
1588
+ message
1589
+ }
1590
+ });
1591
+ return;
1592
+ }
1593
+ const body = toTransferable(encoder$2.encode(message));
1594
+ this.send({
1595
+ t: "res",
1596
+ id: frame.id,
1597
+ status: 503,
1598
+ headers: { "content-type": "text/plain; charset=utf-8" },
1599
+ body,
1600
+ message
1601
+ }, [body]);
1602
+ }
1603
+ dispatchFrame(frame) {
1604
+ if (frame.t === "stream-open") this.serveStream(frame);
1605
+ else this.serveRequest(frame);
1606
+ }
1607
+ async serveStream(frame) {
1608
+ if (this.seams === void 0) {
1609
+ this.refuse(frame, "webworker tunnel: Remote stream requested before the host tree is serving");
1610
+ return;
1611
+ }
1612
+ const seams = this.seams;
1613
+ const controller = new AbortController();
1614
+ this.inFlight.set(frame.id, { abort: () => {
1615
+ controller.abort();
1616
+ } });
1617
+ try {
1618
+ const source = await seams.openStream(frame.endpoint, frame.payload, controller.signal);
1619
+ for await (const value of source) {
1620
+ if (controller.signal.aborted) return;
1621
+ this.send({
1622
+ t: "stream-item",
1623
+ id: frame.id,
1624
+ value
1625
+ });
1626
+ }
1627
+ if (!controller.signal.aborted) this.send({
1628
+ t: "stream-end",
1629
+ id: frame.id
1630
+ });
1631
+ } catch (error) {
1632
+ if (!controller.signal.aborted) {
1633
+ const failure = seams.streamFailure(error);
1634
+ this.send({
1635
+ t: "stream-error",
1636
+ id: frame.id,
1637
+ failure: {
1638
+ kind: "remote",
1639
+ ...failure
1640
+ }
1641
+ });
1642
+ }
1643
+ } finally {
1644
+ this.inFlight.delete(frame.id);
1645
+ }
1646
+ }
1647
+ sinkFor(id) {
1648
+ const send = this.send.bind(this);
1649
+ const inFlight = this.inFlight;
1650
+ return {
1651
+ head(status, headers) {
1652
+ send({
1653
+ t: "res-head",
1654
+ id,
1655
+ status,
1656
+ headers
1657
+ });
1658
+ },
1659
+ chunk(bytes) {
1660
+ const buffer = toTransferable(bytes);
1661
+ send({
1662
+ t: "res-chunk",
1663
+ id,
1664
+ chunk: buffer
1665
+ }, [buffer]);
1666
+ },
1667
+ end(payload) {
1668
+ if (payload === void 0) send({
1669
+ t: "res-end",
1670
+ id
1671
+ });
1672
+ else {
1673
+ const body = payload.body === void 0 ? void 0 : toTransferable(payload.body);
1674
+ send({
1675
+ t: "res",
1676
+ id,
1677
+ status: payload.status,
1678
+ headers: payload.headers,
1679
+ body
1680
+ }, body === void 0 ? void 0 : [body]);
1681
+ }
1682
+ inFlight.delete(id);
1683
+ },
1684
+ fail(message) {
1685
+ send({
1686
+ t: "res-err",
1687
+ id,
1688
+ message
1689
+ });
1690
+ inFlight.delete(id);
1691
+ }
1692
+ };
1693
+ }
1694
+ /** The page sends an absolute URL; route handlers read `req.url` as a path. */
1695
+ pathFrame(frame) {
1696
+ const url = new URL(frame.url, `http://${SYNTHETIC_HOST}`);
1697
+ return {
1698
+ frame: {
1699
+ ...frame,
1700
+ url: `${url.pathname}${url.search}`,
1701
+ headers: {
1702
+ ...frame.headers,
1703
+ host: SYNTHETIC_HOST
1704
+ }
1705
+ },
1706
+ path: url.pathname
1707
+ };
1708
+ }
1709
+ async serveRequest(frame) {
1710
+ const sink = this.sinkFor(frame.id);
1711
+ try {
1712
+ const { frame: routed, path } = this.pathFrame(frame);
1713
+ if (path === "/__boot__") {
1714
+ this.serveBoot(frame, sink);
1715
+ return;
1716
+ }
1717
+ if (path.startsWith(`/api/`)) {
1718
+ await this.serveApi(frame, routed, path, sink);
1719
+ return;
1720
+ }
1721
+ this.dispatch(routed, sink);
1722
+ } catch (reason) {
1723
+ sink.fail(reason instanceof Error ? reason.message : String(reason));
1724
+ }
1725
+ }
1726
+ /**
1727
+ * The listener is captured once and reused, so only requests that arrive
1728
+ * before the web server binds pay an await.
1729
+ * @returns The webserver request listener.
1730
+ */
1731
+ async whenListener() {
1732
+ this.listener ??= await this.requestListener();
1733
+ return this.listener;
1734
+ }
1735
+ /** Feed the real route table through the captured listener. */
1736
+ dispatch(frame, sink, into) {
1737
+ const exchange = createSyntheticExchange(frame, into ?? sink);
1738
+ this.inFlight.set(frame.id, exchange);
1739
+ const listener = this.listener;
1740
+ if (listener !== void 0) {
1741
+ listener(exchange.req, exchange.res);
1742
+ return exchange;
1743
+ }
1744
+ this.whenListener().then((resolved) => {
1745
+ if (!exchange.aborted) resolved(exchange.req, exchange.res);
1746
+ }, (reason) => {
1747
+ sink.fail(reason instanceof Error ? reason.message : String(reason));
1748
+ });
1749
+ return exchange;
1750
+ }
1751
+ /**
1752
+ * Unary `/api`: keep the route lane's fences, but fall back to the direct lane
1753
+ * when network authentication or trust rejects the worker-owning page.
1754
+ */
1755
+ async serveApi(original, routed, path, sink) {
1756
+ const method = path.slice(5);
1757
+ if (this.unaryApiLane === "direct" || this.privilegedMethods?.has(method) === true) {
1758
+ await this.serveDirect(original, sink);
1759
+ return;
1760
+ }
1761
+ const buffered = new BufferedSink();
1762
+ const exchange = this.dispatch(routed, sink, buffered.sink);
1763
+ let settleAborted = () => {};
1764
+ const aborted = new Promise((resolve) => {
1765
+ settleAborted = () => {
1766
+ resolve("aborted");
1767
+ };
1768
+ });
1769
+ this.inFlight.set(routed.id, { abort: () => {
1770
+ exchange.abort();
1771
+ settleAborted();
1772
+ } });
1773
+ const outcome = await Promise.race([buffered.settled, aborted]);
1774
+ if (outcome === "aborted" || exchange.aborted) return;
1775
+ if (outcome.status === 401 || outcome.status === 403) {
1776
+ console.debug(`webworker tunnel: route lane refused ${method} with ${String(outcome.status)}; answering on the direct lane`);
1777
+ await this.serveDirect(original, sink);
1778
+ return;
1779
+ }
1780
+ buffered.flushTo(sink);
1781
+ }
1782
+ serveBoot(frame, sink) {
1783
+ if (this.seams === void 0) throw new Error("webworker tunnel: boot payload requested before the host tree is serving");
1784
+ if (frame.method !== "GET") {
1785
+ sink.end({
1786
+ status: 405,
1787
+ headers: { allow: "GET" }
1788
+ });
1789
+ return;
1790
+ }
1791
+ const body = encoder$2.encode(JSON.stringify(this.seams.bootPayload()));
1792
+ sink.end({
1793
+ status: 200,
1794
+ headers: {
1795
+ "content-type": "application/json; charset=utf-8",
1796
+ "cache-control": "no-store"
1797
+ },
1798
+ body
1799
+ });
1800
+ }
1801
+ async serveDirect(frame, sink) {
1802
+ if (this.seams === void 0) throw new Error("webworker tunnel: direct fetch requested before the host tree is serving");
1803
+ const controller = new AbortController();
1804
+ this.inFlight.set(frame.id, { abort: () => {
1805
+ controller.abort();
1806
+ } });
1807
+ const headers = new Headers();
1808
+ for (const [key, value] of Object.entries(frame.headers)) try {
1809
+ headers.set(key, value);
1810
+ } catch {}
1811
+ const request = new Request(new URL(frame.url, `http://${SYNTHETIC_HOST}`), {
1812
+ method: frame.method,
1813
+ headers,
1814
+ body: frame.body === void 0 || frame.method === "GET" || frame.method === "HEAD" ? null : frame.body,
1815
+ signal: controller.signal
1816
+ });
1817
+ const response = await this.seams.directFetch(request);
1818
+ const responseHeaders = {};
1819
+ response.headers.forEach((value, key) => {
1820
+ responseHeaders[key] = value;
1821
+ });
1822
+ if (!(response.body !== null && (responseHeaders["content-type"] ?? "").startsWith("text/event-stream"))) {
1823
+ const buffer = await response.arrayBuffer();
1824
+ sink.end({
1825
+ status: response.status,
1826
+ headers: responseHeaders,
1827
+ body: buffer.byteLength === 0 ? void 0 : new Uint8Array(buffer)
1828
+ });
1829
+ return;
1830
+ }
1831
+ sink.head(response.status, responseHeaders);
1832
+ const reader = response.body.getReader();
1833
+ this.inFlight.set(frame.id, { abort: () => {
1834
+ controller.abort();
1835
+ reader.cancel().catch(() => {});
1836
+ } });
1837
+ try {
1838
+ for (;;) {
1839
+ const { done, value } = await reader.read();
1840
+ if (done) break;
1841
+ sink.chunk(value);
1842
+ }
1843
+ sink.end();
1844
+ } catch (reason) {
1845
+ sink.fail(reason instanceof Error ? reason.message : String(reason));
1846
+ } finally {
1847
+ reader.releaseLock();
1848
+ }
1849
+ }
1850
+ };
1851
+ //#endregion
1852
+ //#region src/node/process-table.ts
1853
+ const entries = /* @__PURE__ */ new Map();
1854
+ /**
1855
+ * Whether a command with this pid is still running.
1856
+ * @param pid - pid to look up; a negative value addresses the group, which here
1857
+ * holds exactly the one process that leads it.
1858
+ * @returns true while the entry is in the table.
1859
+ */
1860
+ function processAlive(pid) {
1861
+ return entries.has(Math.abs(pid));
1862
+ }
1863
+ /**
1864
+ * Deliver a signal to one running command.
1865
+ *
1866
+ * `SIGKILL` stops the command whatever it is doing; every other signal asks it
1867
+ * to stop at its next command boundary. That distinction is real only for a
1868
+ * worker-backed process — see {@link RunningProcess.destroy}.
1869
+ * @param pid - pid or negative process-group id.
1870
+ * @param signal - the signal name to record and deliver.
1871
+ * @returns true when an entry received it, false when no such process exists.
1872
+ */
1873
+ function signalProcess(pid, signal) {
1874
+ const entry = entries.get(Math.abs(pid));
1875
+ if (entry === void 0) return false;
1876
+ entry.signal ??= signal;
1877
+ if (signal === "SIGKILL") entry.process?.destroy();
1878
+ else entry.process?.interrupt();
1879
+ return true;
1880
+ }
1881
+ //#endregion
1882
+ //#region src/node/globals/process.ts
1883
+ /**
1884
+ * The `process` global the worker needs before any VFS module runs. Cordis
1885
+ * reads `process.env` and `process.versions.node` while the Loader is
1886
+ * constructed, and `cordis.yml` keeps its `!!js process.*` expressions, so the
1887
+ * configuration bytes stay identical to the Node deployment. Third-party Node
1888
+ * packages use the presence of `process.title` to avoid browser-only globals.
1889
+ * @module @deepseek-ai/dsh-experimental-webworker-runtime/src/node/globals/process
1890
+ */
1891
+ /**
1892
+ * Publish `globalThis.process`.
1893
+ *
1894
+ * `versions.node` is `0.0.0` on purpose: it makes Cordis's
1895
+ * `ModuleLoader.fromInternal()` return undefined instead of reaching for Node
1896
+ * internals, which is what lets the worker install its own module seam.
1897
+ * @param options - Root, environment, and argument vector.
1898
+ * @returns The published object, for the module proxy table.
1899
+ */
1900
+ function installProcessGlobal(options) {
1901
+ const start = performance.now();
1902
+ const write = (target) => (chunk) => {
1903
+ console[target](chunk.replace(/\n$/, ""));
1904
+ return true;
1905
+ };
1906
+ const shim = {
1907
+ env: { ...options.env },
1908
+ argv: [...options.argv ?? ["node", "dsh-webworker"]],
1909
+ execArgv: [],
1910
+ execPath: "/dsh/bin/node",
1911
+ title: "dsh-webworker",
1912
+ platform: "linux",
1913
+ arch: "x64",
1914
+ pid: 1,
1915
+ version: "v0.0.0",
1916
+ versions: { node: "0.0.0" },
1917
+ cwd: () => options.cwd,
1918
+ getBuiltinModule: (id) => {
1919
+ let resolution;
1920
+ try {
1921
+ resolution = requireActiveModuleLoader().resolve(id, "/");
1922
+ } catch {
1923
+ return;
1924
+ }
1925
+ return resolution.kind === "static" ? resolution.factory() : void 0;
1926
+ },
1927
+ kill: (pid, signal = "SIGTERM") => {
1928
+ if (signal === 0) {
1929
+ if (processAlive(pid)) return true;
1930
+ const error = /* @__PURE__ */ new Error("kill ESRCH");
1931
+ error.code = "ESRCH";
1932
+ error.syscall = "kill";
1933
+ throw error;
1934
+ }
1935
+ return signalProcess(pid, signal);
1936
+ },
1937
+ nextTick: (callback, ...args) => {
1938
+ queueMicrotask(() => {
1939
+ callback(...args);
1940
+ });
1941
+ },
1942
+ stdout: { write: write("log") },
1943
+ stderr: { write: write("error") },
1944
+ on: () => shim,
1945
+ off: () => shim,
1946
+ once: () => shim,
1947
+ prependListener: () => shim,
1948
+ prependOnceListener: () => shim,
1949
+ removeListener: () => shim,
1950
+ removeAllListeners: () => shim,
1951
+ listeners: () => [],
1952
+ listenerCount: () => 0,
1953
+ setMaxListeners: () => shim,
1954
+ emit: () => false,
1955
+ hrtime: { bigint: () => BigInt(Math.round((performance.now() - start) * 1e6)) },
1956
+ uptime: () => (performance.now() - start) / 1e3,
1957
+ exit: (code) => {
1958
+ console.warn(`webworker process: exit(${String(code ?? 0)}) requested; the worker keeps running`);
1959
+ }
1960
+ };
1961
+ globalThis.process = shim;
1962
+ return shim;
1963
+ }
1964
+ //#endregion
1965
+ //#region src/storage/image-gzip.ts
1966
+ /**
1967
+ * The image byte envelope. The packer writes one gzip member holding the ustar
1968
+ * archive, and the worker inflates it with the platform's own decompressor before
1969
+ * the tar reader sees a byte — `storage/tar.ts` stays a pure ustar reader with no
1970
+ * codec in it.
1971
+ *
1972
+ * Inflation runs on the fetch stream rather than on downloaded bytes: the
1973
+ * decompressor consumes each chunk as it lands, so unpacking overlaps the
1974
+ * download instead of following it, and the compressed copy never has to be held
1975
+ * whole in memory beside the archive it produces.
1976
+ *
1977
+ * One format, no negotiation: a body that does not start a gzip member is refused
1978
+ * by name, in the stream, before the decompressor sees it. Without that check a
1979
+ * plain tar, a truncated download, or a proxy's HTML error page would reach
1980
+ * `parseTar` and fail as a corrupt header field, which says nothing about what
1981
+ * the deployment actually served.
1982
+ * @module @deepseek-ai/dsh-experimental-webworker-runtime/src/storage/image-gzip
1983
+ */
1984
+ /** gzip member identification bytes (RFC 1952 §2.3.1). */
1985
+ const GZIP_MAGIC = [31, 139];
1986
+ /** Bytes of a refused body quoted in the failure, enough to recognize text served in its place. */
1987
+ const QUOTED_BYTES = 8;
1988
+ const hex = (bytes) => [...bytes.slice(0, QUOTED_BYTES)].map((byte) => byte.toString(16).padStart(2, "0")).join(" ");
1989
+ /**
1990
+ * A pass-through that refuses a body which is not a gzip member.
1991
+ *
1992
+ * The check spans chunks: a transport may deliver the first byte alone, so the
1993
+ * head is held until it can be judged and then forwarded intact. A body that ends
1994
+ * before two bytes arrive is refused in `flush`, where "too short" is the only
1995
+ * thing left to report.
1996
+ * @param source - the image URL, or how the bytes arrived; named in a refusal.
1997
+ * @returns The transform to pipe the body through before the decompressor.
1998
+ */
1999
+ function requireGzipMember(source) {
2000
+ let head = new Uint8Array(0);
2001
+ let judged = false;
2002
+ const refuse = (read) => /* @__PURE__ */ new Error(`webworker image: ${source} is not the gzip-compressed tar this deployment serves as its image (expected a member starting 1f 8b, read ${read.byteLength === 0 ? "an empty body" : hex(read)}); a host that answered with a Content-Encoding the transport already decoded, or a build that wrote the archive uncompressed, arrives exactly this way`);
2003
+ return new TransformStream({
2004
+ transform: (chunk, controller) => {
2005
+ if (judged) {
2006
+ controller.enqueue(chunk);
2007
+ return;
2008
+ }
2009
+ const merged = new Uint8Array(head.byteLength + chunk.byteLength);
2010
+ merged.set(head);
2011
+ merged.set(chunk, head.byteLength);
2012
+ head = merged;
2013
+ if (head.byteLength < GZIP_MAGIC.length) return;
2014
+ if (GZIP_MAGIC.some((byte, at) => head[at] !== byte)) throw refuse(head);
2015
+ judged = true;
2016
+ controller.enqueue(head);
2017
+ },
2018
+ flush: () => {
2019
+ if (!judged) throw refuse(head);
2020
+ }
2021
+ });
2022
+ }
2023
+ /**
2024
+ * Inflate a packed VFS image as it arrives.
2025
+ * @param body - the image body, straight from `fetch` or wrapped around bytes.
2026
+ * @param source - the image URL, or how the bytes arrived; named in a refusal.
2027
+ * @returns the ustar archive the image carries.
2028
+ * @throws When the body does not start a gzip member, or the member is corrupt.
2029
+ */
2030
+ async function inflateImageStream(body, source) {
2031
+ const inflated = body.pipeThrough(requireGzipMember(source)).pipeThrough(new DecompressionStream("gzip"));
2032
+ return new Uint8Array(await new Response(inflated).arrayBuffer());
2033
+ }
2034
+ /**
2035
+ * Inflate a packed VFS image held in memory.
2036
+ *
2037
+ * The bytes become a body so both entries run the same stream: one decompression
2038
+ * path, one refusal, whether the image came off the network or out of a caller's
2039
+ * buffer.
2040
+ * @param bytes - the image bytes.
2041
+ * @param source - how the bytes arrived; named in a refusal.
2042
+ * @returns the ustar archive the image carries.
2043
+ * @throws When the bytes do not start a gzip member, or the member is corrupt.
2044
+ */
2045
+ async function inflateImage(bytes, source) {
2046
+ const body = new Response(bytes).body;
2047
+ if (body === null) throw new Error(`webworker image: ${source} produced no readable body`);
2048
+ return await inflateImageStream(body, source);
2049
+ }
2050
+ //#endregion
2051
+ //#region src/storage/tar.ts
2052
+ /**
2053
+ * Uncompressed ustar archive: the VFS image format. One fetch delivers the
2054
+ * whole tree, and the reader hands out subarray views into the fetched buffer,
2055
+ * so mounting copies nothing and no inflate step runs inside the worker.
2056
+ *
2057
+ * Hand-rolled on purpose: both sides need synchronous in-memory operation and
2058
+ * the reader ships inside the worker bundle, where the streaming tar packages
2059
+ * would drag Node stream shims back in. The subset is plain ustar — regular
2060
+ * files and directories, names up to 255 bytes via the name-prefix split — and
2061
+ * anything outside it fails loud on either side.
2062
+ * @module @deepseek-ai/dsh-experimental-webworker-runtime/src/storage/tar
2063
+ */
2064
+ const encoder$1 = new TextEncoder();
2065
+ const decoder$1 = new TextDecoder();
2066
+ const BLOCK = 512;
2067
+ /** Write an octal field: zero-padded digits with a terminating NUL. */
2068
+ function writeOctal(header, offset, length, value) {
2069
+ header.set(encoder$1.encode(value.toString(8).padStart(length - 1, "0")), offset);
2070
+ }
2071
+ /**
2072
+ * Split an entry name into the ustar name and prefix fields.
2073
+ * @param name - Full entry name.
2074
+ * @returns The two fields; the prefix is empty when the name fits directly.
2075
+ * @throws When no slash yields name ≤ 100 and prefix ≤ 155 bytes: the entry
2076
+ * cannot be archived and a silently truncated name would corrupt the image.
2077
+ */
2078
+ function splitName(name) {
2079
+ if (encoder$1.encode(name).length <= 100) return {
2080
+ name,
2081
+ prefix: ""
2082
+ };
2083
+ for (let index = name.length - 1; index > 0; index -= 1) {
2084
+ if (name[index] !== "/") continue;
2085
+ const prefix = name.slice(0, index);
2086
+ const rest = name.slice(index + 1);
2087
+ if (encoder$1.encode(rest).length <= 100 && encoder$1.encode(prefix).length <= 155) return {
2088
+ name: rest,
2089
+ prefix
2090
+ };
2091
+ }
2092
+ throw new Error(`vfs tar: entry name does not fit the ustar name+prefix split: ${name}`);
2093
+ }
2094
+ /**
2095
+ * Pack entries into one uncompressed ustar archive.
2096
+ *
2097
+ * Entries keep their given order; names ending in a slash become directory
2098
+ * entries. Contents are written verbatim — compression belongs to the HTTP
2099
+ * transport, not to the archive.
2100
+ * @param files - Entry name to content bytes.
2101
+ * @returns The archive bytes.
2102
+ */
2103
+ function packTar(files) {
2104
+ const chunks = [];
2105
+ for (const [entryName, bytes] of Object.entries(files)) {
2106
+ const directory = entryName.endsWith("/");
2107
+ const size = directory ? 0 : bytes.length;
2108
+ const { name, prefix } = splitName(entryName);
2109
+ const header = new Uint8Array(BLOCK);
2110
+ header.set(encoder$1.encode(name), 0);
2111
+ writeOctal(header, 100, 8, directory ? 493 : 420);
2112
+ writeOctal(header, 108, 8, 0);
2113
+ writeOctal(header, 116, 8, 0);
2114
+ writeOctal(header, 124, 12, size);
2115
+ writeOctal(header, 136, 12, 0);
2116
+ header.fill(32, 148, 156);
2117
+ header[156] = directory ? 53 : 48;
2118
+ header.set(encoder$1.encode("ustar"), 257);
2119
+ header.set(encoder$1.encode("00"), 263);
2120
+ header.set(encoder$1.encode(prefix), 345);
2121
+ let checksum = 0;
2122
+ for (const byte of header) checksum += byte;
2123
+ header.set(encoder$1.encode(checksum.toString(8).padStart(6, "0")), 148);
2124
+ header[154] = 0;
2125
+ header[155] = 32;
2126
+ chunks.push(header);
2127
+ if (size > 0) {
2128
+ chunks.push(bytes);
2129
+ const padding = size % BLOCK;
2130
+ if (padding !== 0) chunks.push(new Uint8Array(BLOCK - padding));
2131
+ }
2132
+ }
2133
+ chunks.push(new Uint8Array(BLOCK * 2));
2134
+ const total = chunks.reduce((sum, chunk) => sum + chunk.length, 0);
2135
+ const archive = new Uint8Array(total);
2136
+ let offset = 0;
2137
+ for (const chunk of chunks) {
2138
+ archive.set(chunk, offset);
2139
+ offset += chunk.length;
2140
+ }
2141
+ return archive;
2142
+ }
2143
+ /** @returns The NUL-terminated string in one header field. */
2144
+ function readField(header, offset, length) {
2145
+ let end = offset;
2146
+ while (end < offset + length && header[end] !== 0) end += 1;
2147
+ return decoder$1.decode(header.subarray(offset, end));
2148
+ }
2149
+ /**
2150
+ * Parse an uncompressed ustar archive.
2151
+ *
2152
+ * File bytes are subarray views into `archive`, not copies; callers own the
2153
+ * aliasing. Entry kinds outside the written subset (links, PAX extensions)
2154
+ * fail loud instead of being skipped.
2155
+ * @param archive - Archive bytes.
2156
+ * @returns Entries in archive order.
2157
+ */
2158
+ function parseTar(archive) {
2159
+ const entries = [];
2160
+ let offset = 0;
2161
+ while (offset + BLOCK <= archive.length) {
2162
+ const header = archive.subarray(offset, offset + BLOCK);
2163
+ if (header.every((byte) => byte === 0)) break;
2164
+ const short = readField(header, 0, 100);
2165
+ const prefix = readField(header, 345, 155);
2166
+ const name = prefix === "" ? short : `${prefix}/${short}`;
2167
+ const size = Number.parseInt(readField(header, 124, 12).trim() || "0", 8);
2168
+ const mode = Number.parseInt(readField(header, 100, 8).trim() || "0", 8) & 511;
2169
+ const typeflag = header[156];
2170
+ const directory = typeflag === 53 || name.endsWith("/");
2171
+ if (typeflag !== 48 && typeflag !== 0 && typeflag !== 53) throw new Error(`vfs tar: unsupported entry type ${String.fromCharCode(typeflag ?? 0)} for "${name}"`);
2172
+ const dataStart = offset + BLOCK;
2173
+ entries.push({
2174
+ name,
2175
+ bytes: archive.subarray(dataStart, dataStart + size),
2176
+ directory,
2177
+ mode
2178
+ });
2179
+ offset = dataStart + Math.ceil(size / BLOCK) * BLOCK;
2180
+ }
2181
+ return entries;
2182
+ }
2183
+ //#endregion
2184
+ //#region src/storage/memory.ts
2185
+ /**
2186
+ * In-memory filesystem behind the worker's `node:fs` proxy. Contents come from
2187
+ * the build-time image (see {@link loadVfsImage}); this remains the synchronous
2188
+ * authority when an asynchronous durable sink mirrors selected subtrees.
2189
+ * @module @deepseek-ai/dsh-experimental-webworker-runtime/src/storage/memory
2190
+ */
2191
+ const decoder = new TextDecoder();
2192
+ const encoder = new TextEncoder();
2193
+ /** Creation default for files, Node's `0o666` under the classic `022` umask. */
2194
+ const DEFAULT_FILE_MODE = 420;
2195
+ /** Creation default for directories, Node's `0o777` under the classic `022` umask. */
2196
+ const DEFAULT_DIRECTORY_MODE = 493;
2197
+ function fail(code, syscall, path, detail) {
2198
+ const error = /* @__PURE__ */ new Error(`${code}: ${detail ?? syscall} failed, ${syscall} '${path}'`);
2199
+ error.code = code;
2200
+ error.path = path;
2201
+ error.syscall = syscall;
2202
+ throw error;
2203
+ }
2204
+ function encodingOf(options) {
2205
+ if (options === null || options === void 0) return void 0;
2206
+ if (typeof options === "string") return options;
2207
+ return options.encoding ?? void 0;
2208
+ }
2209
+ function statsOf(size, mtimeMs, directory, ino, mode) {
2210
+ return {
2211
+ size,
2212
+ ino: Number(ino),
2213
+ mtimeMs,
2214
+ ctimeMs: mtimeMs,
2215
+ atimeMs: mtimeMs,
2216
+ birthtimeMs: mtimeMs,
2217
+ mtime: new Date(mtimeMs),
2218
+ mode: (directory ? 16384 : 32768) | mode & 511,
2219
+ isFile: () => !directory,
2220
+ isDirectory: () => directory,
2221
+ isSymbolicLink: () => false,
2222
+ isFIFO: () => false,
2223
+ isSocket: () => false,
2224
+ isBlockDevice: () => false,
2225
+ isCharacterDevice: () => false
2226
+ };
2227
+ }
2228
+ /**
2229
+ * The same entry as {@link statsOf}, in the BigInt shape.
2230
+ *
2231
+ * Timestamps carry millisecond resolution scaled to nanoseconds, which is what
2232
+ * the underlying `mtimeMs` holds; the VFS keeps that value strictly increasing
2233
+ * per entry so two writes inside one millisecond still differ.
2234
+ * @param size - Byte length; zero for a directory.
2235
+ * @param mtimeMs - Modification time the entry carries.
2236
+ * @param directory - Whether the entry is a directory.
2237
+ * @param ino - Identity of the entry at this path.
2238
+ * @param mode - Stored permission bits of the entry.
2239
+ * @returns Stats in the shape Node returns under `{ bigint: true }`.
2240
+ */
2241
+ function bigIntStatsOf(size, mtimeMs, directory, ino, mode, nlink = 1) {
2242
+ const milliseconds = BigInt(Math.trunc(mtimeMs));
2243
+ const nanoseconds = milliseconds * 1000000n;
2244
+ const time = new Date(mtimeMs);
2245
+ return {
2246
+ size: BigInt(size),
2247
+ mode: BigInt((directory ? 16384 : 32768) | mode & 511),
2248
+ dev: 1n,
2249
+ ino,
2250
+ nlink: BigInt(nlink),
2251
+ mtimeMs: milliseconds,
2252
+ mtimeNs: nanoseconds,
2253
+ ctimeMs: milliseconds,
2254
+ ctimeNs: nanoseconds,
2255
+ atimeMs: milliseconds,
2256
+ atimeNs: nanoseconds,
2257
+ birthtimeMs: milliseconds,
2258
+ birthtimeNs: nanoseconds,
2259
+ mtime: time,
2260
+ ctime: time,
2261
+ atime: time,
2262
+ birthtime: time,
2263
+ isFile: () => !directory,
2264
+ isDirectory: () => directory,
2265
+ isSymbolicLink: () => false,
2266
+ isFIFO: () => false,
2267
+ isSocket: () => false,
2268
+ isBlockDevice: () => false,
2269
+ isCharacterDevice: () => false
2270
+ };
2271
+ }
2272
+ /** Parse the Node string flags supported by the compatibility filesystem. */
2273
+ function openMode(flags) {
2274
+ const base = flags[0];
2275
+ const suffix = flags.slice(1).split("");
2276
+ const validSuffix = suffix.every((flag) => flag === "+" || flag === "x" || flag === "s");
2277
+ const uniqueSuffix = new Set(suffix).size === suffix.length;
2278
+ if (base !== "r" && base !== "w" && base !== "a" || !validSuffix || !uniqueSuffix || base === "r" && flags.includes("x")) {
2279
+ const error = /* @__PURE__ */ new TypeError(`The argument 'flags' is invalid. Received '${flags}'`);
2280
+ error.code = "ERR_INVALID_ARG_VALUE";
2281
+ throw error;
2282
+ }
2283
+ return {
2284
+ readable: base === "r" || flags.includes("+"),
2285
+ writable: base !== "r" || flags.includes("+"),
2286
+ append: base === "a",
2287
+ create: base === "w" || base === "a",
2288
+ truncate: base === "w",
2289
+ exclusive: flags.includes("x")
2290
+ };
2291
+ }
2292
+ /** Resize bytes exactly, preserving the prefix and zero-filling growth. */
2293
+ function resize(bytes, length) {
2294
+ if (!Number.isSafeInteger(length) || length < 0) {
2295
+ const error = /* @__PURE__ */ new RangeError(`The value of "len" is out of range. It must be >= 0. Received ${String(length)}`);
2296
+ error.code = "ERR_OUT_OF_RANGE";
2297
+ throw error;
2298
+ }
2299
+ const resized = new Uint8Array(length);
2300
+ resized.set(bytes.subarray(0, length));
2301
+ return resized;
2302
+ }
2303
+ /**
2304
+ * Filesystem held in two maps: one for file bytes, one for directories.
2305
+ * Every path is normalized to an absolute POSIX path without a trailing
2306
+ * separator, so callers may pass either form.
2307
+ */
2308
+ var MemoryVfs = class {
2309
+ files = /* @__PURE__ */ new Map();
2310
+ directories = new Set(["/"]);
2311
+ /** Directory permission bits; absence means {@link DEFAULT_DIRECTORY_MODE}. */
2312
+ directoryModes = /* @__PURE__ */ new Map();
2313
+ /** Directory mtimes advance when their immediate entry set changes. */
2314
+ directoryMtimes = /* @__PURE__ */ new Map();
2315
+ mutationListeners = /* @__PURE__ */ new Set();
2316
+ sink;
2317
+ temporaries = 0;
2318
+ identities = /* @__PURE__ */ new Map();
2319
+ lastIdentity = 0n;
2320
+ /**
2321
+ * Build the synchronous filesystem authority.
2322
+ * @param options - Optional durable write-behind sink.
2323
+ */
2324
+ constructor(options = {}) {
2325
+ this.sink = options.sink;
2326
+ }
2327
+ /**
2328
+ * Settle the durable sink without changing in-memory success.
2329
+ * @returns A promise that resolves when all recorded mutations are stored.
2330
+ */
2331
+ async flush() {
2332
+ await this.sink?.flush();
2333
+ }
2334
+ /**
2335
+ * Observe committed runtime mutations. Image seeding is deliberately silent.
2336
+ * @param listener - Consumer called after each successful mutation.
2337
+ * @returns A disposer that prevents future calls.
2338
+ */
2339
+ subscribe(listener) {
2340
+ this.mutationListeners.add(listener);
2341
+ return () => {
2342
+ this.mutationListeners.delete(listener);
2343
+ };
2344
+ }
2345
+ /** Publish after state changes; one faulty observer cannot roll back a write. */
2346
+ publish(mutation) {
2347
+ const observers = [...this.sink === void 0 ? [] : [(change) => {
2348
+ this.sink?.record(change);
2349
+ }], ...this.mutationListeners];
2350
+ for (const listener of observers) try {
2351
+ listener(mutation);
2352
+ } catch (error) {
2353
+ console.error("webworker vfs: mutation observer failed", error);
2354
+ }
2355
+ }
2356
+ /** Promise face mirroring `node:fs/promises` for the methods the roster uses. */
2357
+ promises = {
2358
+ readFile: async (path, options) => this.readFileSync(path, options),
2359
+ writeFile: async (path, data, options) => {
2360
+ this.writeFileSync(path, data, options);
2361
+ },
2362
+ appendFile: async (path, data) => {
2363
+ this.appendFileSync(path, data);
2364
+ },
2365
+ mkdir: async (path, options) => this.mkdirSync(path, options),
2366
+ readdir: async (path, options) => this.readdirSync(path, options),
2367
+ stat: async (path, options) => this.statSync(path, options),
2368
+ lstat: async (path, options) => this.statSync(path, options),
2369
+ realpath: async (path) => this.realpathSync(path),
2370
+ rename: async (from, to) => {
2371
+ this.renameSync(from, to);
2372
+ },
2373
+ unlink: async (path) => {
2374
+ this.unlinkSync(path);
2375
+ },
2376
+ rm: async (path, options) => {
2377
+ this.rmSync(path, options);
2378
+ },
2379
+ mkdtemp: async (prefix) => this.mkdtempSync(prefix),
2380
+ link: async (existing, next) => {
2381
+ this.linkSync(existing, next);
2382
+ },
2383
+ truncate: async (path, length) => {
2384
+ this.truncateSync(path, length);
2385
+ },
2386
+ chmod: async (path, mode) => {
2387
+ this.chmodSync(path, mode);
2388
+ },
2389
+ opendir: async (path) => this.opendir(path),
2390
+ open: async (path, flags, mode) => this.open(path, flags, mode),
2391
+ /** Resolves for any existing path: the VFS grants read and write to everything it holds. */
2392
+ access: async (path) => {
2393
+ const target = normalize(resolve(path));
2394
+ if (!this.files.has(target) && !this.directories.has(target)) fail("ENOENT", "access", target);
2395
+ }
2396
+ };
2397
+ /** @returns Absolute path with no trailing separator. */
2398
+ key(path) {
2399
+ const absolute = normalize(resolve(path));
2400
+ return absolute.length > 1 && absolute.endsWith("/") ? absolute.slice(0, -1) : absolute;
2401
+ }
2402
+ /**
2403
+ * Read a file.
2404
+ * @param path - File path.
2405
+ * @param options - `'utf8'` or `{encoding}` for text; omitted for bytes.
2406
+ * @returns Text or a copy-free view of the stored bytes.
2407
+ */
2408
+ readFileSync(path, options) {
2409
+ const target = this.key(path);
2410
+ const node = this.files.get(target);
2411
+ if (node === void 0) {
2412
+ if (this.directories.has(target)) fail("EISDIR", "read", target);
2413
+ fail("ENOENT", "open", target);
2414
+ }
2415
+ return encodingOf(options) === void 0 ? node.bytes : decoder.decode(node.bytes);
2416
+ }
2417
+ /**
2418
+ * Report whether a path exists.
2419
+ * @param path - Path to test.
2420
+ * @returns True for files and directories.
2421
+ */
2422
+ existsSync(path) {
2423
+ const target = this.key(path);
2424
+ return this.files.has(target) || this.directories.has(target);
2425
+ }
2426
+ /**
2427
+ * Stat a path.
2428
+ * @param path - Path to stat.
2429
+ * @param options - `bigint` selects the BigInt stats Node returns for it.
2430
+ * @returns Stats for the file or directory.
2431
+ */
2432
+ statSync(path, options) {
2433
+ const target = this.key(path);
2434
+ const node = this.files.get(target);
2435
+ const [size, mtimeMs, directory, mode] = node !== void 0 ? [
2436
+ node.bytes.length,
2437
+ node.mtimeMs,
2438
+ false,
2439
+ node.mode
2440
+ ] : this.directories.has(target) ? [
2441
+ 0,
2442
+ this.directoryMtimes.get(target) ?? 0,
2443
+ true,
2444
+ this.directoryModes.get(target) ?? DEFAULT_DIRECTORY_MODE
2445
+ ] : fail("ENOENT", "stat", target);
2446
+ const identity = node === void 0 ? this.identityOf(target) : this.identityOfFile(node);
2447
+ return options?.bigint === true ? bigIntStatsOf(size, mtimeMs, directory, identity, mode, node === void 0 ? 1 : this.fileLinkCount(node)) : statsOf(size, mtimeMs, directory, identity, mode);
2448
+ }
2449
+ /** @returns Stats in the plain shape, for internal callers that read `size`/`mtimeMs`. */
2450
+ plainStats(path) {
2451
+ return this.statSync(path);
2452
+ }
2453
+ /** @returns The stable identity of an existing path, assigning one on first observation. */
2454
+ identityOf(target) {
2455
+ const existing = this.identities.get(target);
2456
+ if (existing !== void 0) return existing;
2457
+ this.lastIdentity += 1n;
2458
+ this.identities.set(target, this.lastIdentity);
2459
+ return this.lastIdentity;
2460
+ }
2461
+ /** @returns The inode-like identity retained by a file node across names. */
2462
+ identityOfFile(node) {
2463
+ if (node.identity !== void 0) return node.identity;
2464
+ this.lastIdentity += 1n;
2465
+ node.identity = this.lastIdentity;
2466
+ return node.identity;
2467
+ }
2468
+ /** @returns The number of names currently linked to one file node. */
2469
+ fileLinkCount(node) {
2470
+ return typeof node.paths === "string" ? 1 : node.paths?.size ?? 0;
2471
+ }
2472
+ /** Add one map name, promoting the rare hard-link case to a Set. */
2473
+ addFilePath(node, path) {
2474
+ if (node.paths === void 0) node.paths = path;
2475
+ else if (typeof node.paths === "string") node.paths = new Set([node.paths, path]);
2476
+ else node.paths.add(path);
2477
+ }
2478
+ /** Remove one map name, collapsing a remaining single link back to a string. */
2479
+ removeFilePath(node, path) {
2480
+ if (typeof node.paths === "string") {
2481
+ node.paths = void 0;
2482
+ return;
2483
+ }
2484
+ if (node.paths === void 0) return;
2485
+ node.paths.delete(path);
2486
+ if (node.paths.size === 1) {
2487
+ const [remaining] = node.paths;
2488
+ node.paths = remaining;
2489
+ }
2490
+ }
2491
+ /** Set one file-map entry while maintaining both nodes' reverse path indexes. */
2492
+ setFile(path, node) {
2493
+ const previous = this.files.get(path);
2494
+ if (previous === node) return;
2495
+ if (previous !== void 0) this.removeFilePath(previous, path);
2496
+ this.files.set(path, node);
2497
+ this.addFilePath(node, path);
2498
+ }
2499
+ /** Delete one file-map entry while retaining an unlinked node held by a descriptor. */
2500
+ deleteFile(path) {
2501
+ const node = this.files.get(path);
2502
+ if (node === void 0) return void 0;
2503
+ this.files.delete(path);
2504
+ this.removeFilePath(node, path);
2505
+ return node;
2506
+ }
2507
+ /** Publish one linked name after a content or metadata write. */
2508
+ publishFilePath(node, path, appendedFrom) {
2509
+ this.publish({
2510
+ kind: "write",
2511
+ path,
2512
+ bytes: node.bytes,
2513
+ mode: node.mode,
2514
+ entryChanged: false,
2515
+ ...appendedFrom === void 0 ? {} : { appendedFrom }
2516
+ });
2517
+ }
2518
+ /** Publish a content or metadata write for every hard link to one node. */
2519
+ publishFile(node, appendedFrom) {
2520
+ if (typeof node.paths === "string") {
2521
+ this.publishFilePath(node, node.paths, appendedFrom);
2522
+ return;
2523
+ }
2524
+ if (node.paths === void 0) return;
2525
+ for (const path of node.paths) this.publishFilePath(node, path, appendedFrom);
2526
+ }
2527
+ /** Replace bytes on one file identity and notify all linked paths. */
2528
+ replaceFile(node, bytes, appendedFrom) {
2529
+ node.bytes = bytes;
2530
+ node.mtimeMs = this.touchNode(node);
2531
+ this.publishFile(node, appendedFrom);
2532
+ }
2533
+ /** Write at one offset, zero-filling any gap. */
2534
+ writeFileNode(node, position, data) {
2535
+ const offset = Math.max(0, position);
2536
+ const previousLength = node.bytes.length;
2537
+ const bytes = new Uint8Array(Math.max(previousLength, offset + data.length));
2538
+ bytes.set(node.bytes);
2539
+ bytes.set(data, offset);
2540
+ this.replaceFile(node, bytes, offset === previousLength ? previousLength : void 0);
2541
+ return data.length;
2542
+ }
2543
+ /** Resize one file identity and notify all linked paths. */
2544
+ truncateFile(node, length) {
2545
+ this.replaceFile(node, resize(node.bytes, length));
2546
+ }
2547
+ /** Change one file identity's permission bits and notify every linked path. */
2548
+ chmodFile(node, mode) {
2549
+ node.mode = mode & 511;
2550
+ if (typeof node.paths === "string") this.publish({
2551
+ kind: "chmod",
2552
+ path: node.paths,
2553
+ mode: node.mode
2554
+ });
2555
+ else if (node.paths !== void 0) for (const path of node.paths) this.publish({
2556
+ kind: "chmod",
2557
+ path,
2558
+ mode: node.mode
2559
+ });
2560
+ }
2561
+ /** @returns Plain stats for an open file, including after its last name is removed. */
2562
+ fileStats(node) {
2563
+ return statsOf(node.bytes.length, node.mtimeMs, false, this.identityOfFile(node), node.mode);
2564
+ }
2565
+ /** @returns BigInt stats for an open file, including its device and inode identity. */
2566
+ fileBigIntStats(node) {
2567
+ return bigIntStatsOf(node.bytes.length, node.mtimeMs, false, this.identityOfFile(node), node.mode, this.fileLinkCount(node));
2568
+ }
2569
+ /** Forget removed directory identities, so recreated paths report new ones. */
2570
+ forgetIdentity(target) {
2571
+ this.identities.delete(target);
2572
+ const prefix = `${target}/`;
2573
+ for (const known of [...this.identities.keys()]) if (known.startsWith(prefix)) this.identities.delete(known);
2574
+ }
2575
+ /**
2576
+ * Modification time for a write, strictly after the entry's previous one.
2577
+ *
2578
+ * The clock has millisecond resolution and these writes are in memory, so two
2579
+ * revisions of one file routinely land in the same millisecond. The filesystem
2580
+ * service's stale-write guard compares timestamps, so an equal one would let a
2581
+ * stale overwrite through.
2582
+ * @param target - Normalized path being written.
2583
+ * @returns Now, or one millisecond past the entry's current time.
2584
+ */
2585
+ touch(target) {
2586
+ return this.touchNode(this.files.get(target));
2587
+ }
2588
+ /** @returns A modification time strictly newer than one file node's current value. */
2589
+ touchNode(node) {
2590
+ const previous = node?.mtimeMs;
2591
+ const now = Date.now();
2592
+ return previous === void 0 ? now : Math.max(now, previous + 1);
2593
+ }
2594
+ /** Advance a directory's mtime after its immediate children change. */
2595
+ touchDirectory(target) {
2596
+ const previous = this.directoryMtimes.get(target);
2597
+ const now = Date.now();
2598
+ this.directoryMtimes.set(target, previous === void 0 ? now : Math.max(now, previous + 1));
2599
+ }
2600
+ /**
2601
+ * List a directory.
2602
+ * @param path - Directory path.
2603
+ * @param options - `withFileTypes` returns {@link VfsDirent} objects instead of names.
2604
+ * @returns Immediate entry names, or directory entries.
2605
+ */
2606
+ readdirSync(path, options) {
2607
+ const target = this.key(path);
2608
+ if (!this.directories.has(target)) {
2609
+ if (this.files.has(target)) fail("ENOTDIR", "scandir", target);
2610
+ fail("ENOENT", "scandir", target);
2611
+ }
2612
+ const prefix = target === "/" ? "/" : `${target}/`;
2613
+ const names = /* @__PURE__ */ new Set();
2614
+ for (const candidate of [...this.files.keys(), ...this.directories]) {
2615
+ if (!candidate.startsWith(prefix) || candidate === target) continue;
2616
+ const rest = candidate.slice(prefix.length);
2617
+ if (rest === "") continue;
2618
+ const [head = rest] = rest.split("/");
2619
+ names.add(head);
2620
+ }
2621
+ const sorted = [...names].sort();
2622
+ if (options?.withFileTypes !== true) return sorted;
2623
+ return sorted.map((name) => this.direntOf(target, name));
2624
+ }
2625
+ /** @returns Directory entry for one child of `directory`. */
2626
+ direntOf(directory, name) {
2627
+ const stats = this.plainStats(join(directory, name));
2628
+ return {
2629
+ name,
2630
+ parentPath: directory,
2631
+ isFile: () => stats.isFile(),
2632
+ isDirectory: () => stats.isDirectory(),
2633
+ isSymbolicLink: () => false
2634
+ };
2635
+ }
2636
+ /**
2637
+ * Resolve a path; the VFS has no symlinks, so this only normalizes.
2638
+ * @param path - Path to resolve.
2639
+ * @returns Absolute path.
2640
+ */
2641
+ realpathSync(path) {
2642
+ const target = this.key(path);
2643
+ if (!this.existsSync(target)) fail("ENOENT", "realpath", target);
2644
+ return target;
2645
+ }
2646
+ /**
2647
+ * Create a directory.
2648
+ * @param path - Directory path.
2649
+ * @param options - `recursive` creates missing parents.
2650
+ * @returns First created path when recursive, otherwise undefined.
2651
+ */
2652
+ mkdirSync(path, options) {
2653
+ const target = this.key(path);
2654
+ if (this.files.has(target)) fail("EEXIST", "mkdir", target);
2655
+ if (this.directories.has(target)) {
2656
+ if (options?.recursive === true) return void 0;
2657
+ fail("EEXIST", "mkdir", target);
2658
+ }
2659
+ const parent = dirname(target);
2660
+ if (!this.directories.has(parent)) {
2661
+ if (options?.recursive !== true) fail("ENOENT", "mkdir", target);
2662
+ this.mkdirSync(parent, options);
2663
+ }
2664
+ this.directories.add(target);
2665
+ this.touchDirectory(target);
2666
+ this.touchDirectory(parent);
2667
+ const mode = (options?.mode ?? DEFAULT_DIRECTORY_MODE) & 511;
2668
+ if (mode !== DEFAULT_DIRECTORY_MODE) this.directoryModes.set(target, mode);
2669
+ this.publish({
2670
+ kind: "mkdir",
2671
+ path: target,
2672
+ mode
2673
+ });
2674
+ return target;
2675
+ }
2676
+ /**
2677
+ * Write a file, replacing existing contents.
2678
+ * @param path - File path; its parent directory must exist.
2679
+ * @param data - Text or bytes.
2680
+ * @param options - `flag` `wx` refuses an existing file, `a` appends.
2681
+ */
2682
+ writeFileSync(path, data, options) {
2683
+ const target = this.key(path);
2684
+ if (this.directories.has(target)) fail("EISDIR", "open", target);
2685
+ if (!this.directories.has(dirname(target))) fail("ENOENT", "open", target);
2686
+ const flag = options?.flag ?? "w";
2687
+ if (flag.startsWith("wx") && this.files.has(target)) fail("EEXIST", "open", target);
2688
+ if (flag.startsWith("a")) {
2689
+ this.appendFileSync(target, data);
2690
+ return;
2691
+ }
2692
+ const previous = this.files.get(target);
2693
+ const mode = previous?.mode ?? (options?.mode !== void 0 ? options.mode & 511 : DEFAULT_FILE_MODE);
2694
+ const bytes = typeof data === "string" ? encoder.encode(data) : data;
2695
+ if (previous !== void 0) {
2696
+ this.replaceFile(previous, bytes);
2697
+ return;
2698
+ }
2699
+ const node = {
2700
+ bytes,
2701
+ mtimeMs: this.touch(target),
2702
+ mode,
2703
+ paths: void 0
2704
+ };
2705
+ this.setFile(target, node);
2706
+ this.touchDirectory(dirname(target));
2707
+ this.publish({
2708
+ kind: "write",
2709
+ path: target,
2710
+ bytes,
2711
+ mode,
2712
+ entryChanged: true
2713
+ });
2714
+ }
2715
+ /**
2716
+ * Open a directory; consumers enumerate entries or just prove it is one.
2717
+ * @param path - Directory path.
2718
+ * @returns Directory handle.
2719
+ */
2720
+ opendir(path) {
2721
+ const target = this.key(path);
2722
+ const names = this.readdirSync(target);
2723
+ let cursor = 0;
2724
+ const direntOf = (name) => this.direntOf(target, name);
2725
+ return {
2726
+ path: target,
2727
+ close: async () => {},
2728
+ read: async () => {
2729
+ const name = names[cursor];
2730
+ cursor += 1;
2731
+ return name === void 0 ? null : direntOf(name);
2732
+ },
2733
+ async *[Symbol.asyncIterator]() {
2734
+ for (const name of names) yield direntOf(name);
2735
+ }
2736
+ };
2737
+ }
2738
+ /**
2739
+ * Open a file handle.
2740
+ * @param path - File path.
2741
+ * @param flags - Node open flags; `r` requires the file, `wx` refuses an existing one.
2742
+ * @param mode - Permission bits applied when the open creates the file.
2743
+ * @returns File handle.
2744
+ */
2745
+ open(path, flags = "r", mode) {
2746
+ const target = this.key(path);
2747
+ if (this.directories.has(target)) {
2748
+ if (!flags.startsWith("r")) fail("EISDIR", "open", target);
2749
+ return {
2750
+ write: async () => fail("EISDIR", "write", target),
2751
+ writeFile: async () => fail("EISDIR", "write", target),
2752
+ readFile: async () => fail("EISDIR", "read", target),
2753
+ truncate: async () => fail("EISDIR", "ftruncate", target),
2754
+ ...this.handleTail(target)
2755
+ };
2756
+ }
2757
+ const file = this.openFileSync(target, flags, mode);
2758
+ let position = 0;
2759
+ let closed = false;
2760
+ const current = (syscall) => {
2761
+ if (closed) fail("EBADF", syscall, target);
2762
+ return file;
2763
+ };
2764
+ return {
2765
+ write: async (data) => {
2766
+ const bytes = typeof data === "string" ? encoder.encode(data) : data;
2767
+ const descriptor = current("write");
2768
+ const offset = descriptor.append ? descriptor.stat().size : position;
2769
+ const bytesWritten = descriptor.write(offset, bytes);
2770
+ position = offset + bytesWritten;
2771
+ return { bytesWritten };
2772
+ },
2773
+ writeFile: async (data) => {
2774
+ const bytes = typeof data === "string" ? encoder.encode(data) : data;
2775
+ const descriptor = current("write");
2776
+ const offset = descriptor.append ? descriptor.stat().size : position;
2777
+ position = offset + descriptor.write(offset, bytes);
2778
+ },
2779
+ readFile: async (options) => {
2780
+ const descriptor = current("read");
2781
+ const bytes = descriptor.read(position, Math.max(0, descriptor.stat().size - position));
2782
+ position += bytes.length;
2783
+ return encodingOf(options) === void 0 ? bytes : decoder.decode(bytes);
2784
+ },
2785
+ truncate: async (length = 0) => {
2786
+ current("ftruncate").truncate(length);
2787
+ },
2788
+ chmod: async (mode) => {
2789
+ current("fchmod").chmod(mode);
2790
+ },
2791
+ stat: async (options) => options?.bigint === true ? current("fstat").statBigInt() : current("fstat").stat(),
2792
+ sync: async () => {
2793
+ current("fsync");
2794
+ await this.flush();
2795
+ },
2796
+ datasync: async () => {
2797
+ current("fdatasync");
2798
+ await this.flush();
2799
+ },
2800
+ close: async () => {
2801
+ closed = true;
2802
+ }
2803
+ };
2804
+ }
2805
+ /**
2806
+ * Open one synchronous descriptor over a stable file identity.
2807
+ * @param path - File path.
2808
+ * @param flags - Node open flags.
2809
+ * @param mode - Permission bits applied only when a file is created.
2810
+ * @returns An open file that survives path rename, replacement, and unlink.
2811
+ */
2812
+ openFileSync(path, flags = "r", mode) {
2813
+ const target = this.key(path);
2814
+ const access = openMode(flags);
2815
+ const existing = this.files.get(target);
2816
+ if (this.directories.has(target)) fail("EISDIR", "open", target);
2817
+ if (access.exclusive && existing !== void 0) fail("EEXIST", "open", target);
2818
+ if (!access.create && existing === void 0) fail("ENOENT", "open", target);
2819
+ if (access.create && existing === void 0) this.writeFileSync(target, new Uint8Array(), mode === void 0 ? void 0 : { mode });
2820
+ else if (access.truncate && existing !== void 0) this.truncateFile(existing, 0);
2821
+ const node = this.files.get(target);
2822
+ if (node === void 0) fail("ENOENT", "open", target);
2823
+ return {
2824
+ readable: access.readable,
2825
+ writable: access.writable,
2826
+ append: access.append,
2827
+ read: (position, length) => {
2828
+ if (!access.readable) fail("EBADF", "read", target);
2829
+ return node.bytes.subarray(position, position + length);
2830
+ },
2831
+ write: (position, data) => {
2832
+ if (!access.writable) fail("EBADF", "write", target);
2833
+ return this.writeFileNode(node, access.append ? node.bytes.length : position, data);
2834
+ },
2835
+ truncate: (length) => {
2836
+ if (!access.writable) fail("EINVAL", "ftruncate", target);
2837
+ this.truncateFile(node, length);
2838
+ },
2839
+ chmod: (mode) => {
2840
+ this.chmodFile(node, mode);
2841
+ },
2842
+ stat: () => this.fileStats(node),
2843
+ statBigInt: () => this.fileBigIntStats(node)
2844
+ };
2845
+ }
2846
+ /**
2847
+ * Directory-handle members for metadata, durability, and release.
2848
+ * `sync`/`datasync` settle an attached durable sink; an ephemeral filesystem
2849
+ * resolves immediately and `close` releases nothing.
2850
+ * @param target - Normalized path the handle was opened on.
2851
+ * @returns Metadata plus the no-op durability and release calls.
2852
+ */
2853
+ handleTail(target) {
2854
+ return {
2855
+ chmod: async (mode) => {
2856
+ this.chmodSync(target, mode);
2857
+ },
2858
+ stat: async (options) => this.statSync(target, options),
2859
+ sync: async () => {
2860
+ await this.flush();
2861
+ },
2862
+ datasync: async () => {
2863
+ await this.flush();
2864
+ },
2865
+ close: async () => {}
2866
+ };
2867
+ }
2868
+ /**
2869
+ * Append to a file, creating it when absent.
2870
+ * @param path - File path.
2871
+ * @param data - Text or bytes.
2872
+ */
2873
+ appendFileSync(path, data) {
2874
+ const target = this.key(path);
2875
+ const existing = this.files.get(target);
2876
+ const addition = typeof data === "string" ? encoder.encode(data) : data;
2877
+ if (existing === void 0) {
2878
+ this.writeFileSync(target, addition);
2879
+ return;
2880
+ }
2881
+ this.writeFileNode(existing, existing.bytes.length, addition);
2882
+ }
2883
+ /**
2884
+ * Move a file or directory subtree.
2885
+ * @param from - Source path.
2886
+ * @param to - Destination path.
2887
+ */
2888
+ renameSync(from, to) {
2889
+ const source = this.key(from);
2890
+ const destination = this.key(to);
2891
+ if (source === destination) return;
2892
+ const node = this.files.get(source);
2893
+ if (node !== void 0) {
2894
+ if (this.directories.has(destination)) fail("EISDIR", "rename", destination);
2895
+ if (!this.directories.has(dirname(destination))) fail("ENOENT", "rename", destination);
2896
+ if (this.files.get(destination) === node) return;
2897
+ this.deleteFile(source);
2898
+ this.setFile(destination, node);
2899
+ this.forgetIdentity(source);
2900
+ this.forgetIdentity(destination);
2901
+ this.touchDirectory(dirname(source));
2902
+ this.touchDirectory(dirname(destination));
2903
+ this.publish({
2904
+ kind: "remove",
2905
+ path: source
2906
+ });
2907
+ this.publish({
2908
+ kind: "write",
2909
+ path: destination,
2910
+ bytes: node.bytes,
2911
+ mode: node.mode,
2912
+ entryChanged: true
2913
+ });
2914
+ return;
2915
+ }
2916
+ if (!this.directories.has(source)) fail("ENOENT", "rename", source);
2917
+ if (this.files.has(destination)) fail("ENOTDIR", "rename", destination);
2918
+ if (!this.directories.has(dirname(destination))) fail("ENOENT", "rename", destination);
2919
+ if (this.directories.has(destination)) {
2920
+ if (this.readdirSync(destination).length > 0) fail("ENOTEMPTY", "rename", destination);
2921
+ this.directories.delete(destination);
2922
+ this.directoryModes.delete(destination);
2923
+ this.directoryMtimes.delete(destination);
2924
+ }
2925
+ const prefix = `${source}/`;
2926
+ const movedFiles = [];
2927
+ for (const [candidate, value] of [...this.files]) {
2928
+ if (!candidate.startsWith(prefix)) continue;
2929
+ this.deleteFile(candidate);
2930
+ const target = join(destination, candidate.slice(prefix.length));
2931
+ this.setFile(target, value);
2932
+ movedFiles.push({
2933
+ path: target,
2934
+ bytes: value.bytes,
2935
+ mode: value.mode
2936
+ });
2937
+ }
2938
+ const movedDirectories = [];
2939
+ for (const candidate of [...this.directories]) {
2940
+ if (!candidate.startsWith(prefix) && candidate !== source) continue;
2941
+ const moved = candidate === source ? destination : join(destination, candidate.slice(prefix.length));
2942
+ this.directories.delete(candidate);
2943
+ this.directories.add(moved);
2944
+ const bits = this.directoryModes.get(candidate);
2945
+ this.directoryModes.delete(candidate);
2946
+ if (bits !== void 0) this.directoryModes.set(moved, bits);
2947
+ movedDirectories.push({
2948
+ path: moved,
2949
+ mode: bits ?? DEFAULT_DIRECTORY_MODE
2950
+ });
2951
+ const mtime = this.directoryMtimes.get(candidate);
2952
+ this.directoryMtimes.delete(candidate);
2953
+ if (mtime !== void 0) this.directoryMtimes.set(moved, mtime);
2954
+ }
2955
+ this.forgetIdentity(source);
2956
+ this.forgetIdentity(destination);
2957
+ this.touchDirectory(dirname(source));
2958
+ this.touchDirectory(dirname(destination));
2959
+ this.publish({
2960
+ kind: "remove",
2961
+ path: source
2962
+ });
2963
+ for (const directory of movedDirectories) this.publish({
2964
+ kind: "mkdir",
2965
+ path: directory.path,
2966
+ mode: directory.mode
2967
+ });
2968
+ for (const entry of movedFiles) this.publish({
2969
+ kind: "write",
2970
+ path: entry.path,
2971
+ bytes: entry.bytes,
2972
+ mode: entry.mode,
2973
+ entryChanged: true
2974
+ });
2975
+ }
2976
+ /**
2977
+ * Give existing bytes a second name.
2978
+ *
2979
+ * Both names retain one file identity, so writes and metadata changes through
2980
+ * either name remain visible through the other until that name is removed.
2981
+ * @param existing - Source file path.
2982
+ * @param next - Additional path; its parent must exist and it must be free.
2983
+ */
2984
+ linkSync(existing, next) {
2985
+ const source = this.key(existing);
2986
+ const target = this.key(next);
2987
+ const node = this.files.get(source);
2988
+ if (node === void 0) fail("ENOENT", "link", source);
2989
+ if (this.files.has(target) || this.directories.has(target)) fail("EEXIST", "link", target);
2990
+ if (!this.directories.has(dirname(target))) fail("ENOENT", "link", target);
2991
+ this.setFile(target, node);
2992
+ this.touchDirectory(dirname(target));
2993
+ this.publish({
2994
+ kind: "write",
2995
+ path: target,
2996
+ bytes: node.bytes,
2997
+ mode: node.mode,
2998
+ entryChanged: true
2999
+ });
3000
+ }
3001
+ /**
3002
+ * Shorten a file.
3003
+ * @param path - File path.
3004
+ * @param length - Byte length to keep; defaults to zero.
3005
+ */
3006
+ truncateSync(path, length = 0) {
3007
+ const target = this.key(path);
3008
+ const node = this.files.get(target);
3009
+ if (node === void 0) fail("ENOENT", "truncate", target);
3010
+ this.truncateFile(node, length);
3011
+ }
3012
+ /**
3013
+ * Change an entry's permission bits; stat reads back exactly what was set.
3014
+ * @param path - File or directory path.
3015
+ * @param mode - New permission bits (`0o777` mask).
3016
+ */
3017
+ chmodSync(path, mode) {
3018
+ const target = this.key(path);
3019
+ const node = this.files.get(target);
3020
+ if (node !== void 0) {
3021
+ this.chmodFile(node, mode);
3022
+ return;
3023
+ }
3024
+ if (this.directories.has(target)) {
3025
+ const bits = mode & 511;
3026
+ this.directoryModes.set(target, bits);
3027
+ this.publish({
3028
+ kind: "chmod",
3029
+ path: target,
3030
+ mode: bits
3031
+ });
3032
+ return;
3033
+ }
3034
+ fail("ENOENT", "chmod", target);
3035
+ }
3036
+ /**
3037
+ * Remove a file.
3038
+ * @param path - File path.
3039
+ */
3040
+ unlinkSync(path) {
3041
+ const target = this.key(path);
3042
+ if (this.deleteFile(target) === void 0) fail("ENOENT", "unlink", target);
3043
+ this.forgetIdentity(target);
3044
+ this.touchDirectory(dirname(target));
3045
+ this.publish({
3046
+ kind: "remove",
3047
+ path: target
3048
+ });
3049
+ }
3050
+ /**
3051
+ * Remove a file or directory.
3052
+ * @param path - Path to remove.
3053
+ * @param options - `recursive` removes subtrees, `force` ignores absence.
3054
+ */
3055
+ rmSync(path, options) {
3056
+ const target = this.key(path);
3057
+ if (this.deleteFile(target) !== void 0) {
3058
+ this.forgetIdentity(target);
3059
+ this.touchDirectory(dirname(target));
3060
+ this.publish({
3061
+ kind: "remove",
3062
+ path: target
3063
+ });
3064
+ return;
3065
+ }
3066
+ if (this.directories.has(target)) {
3067
+ if (options?.recursive !== true) fail("ERR_FS_EISDIR", "rm", target);
3068
+ const prefix = `${target}/`;
3069
+ for (const candidate of [...this.files.keys()]) if (candidate.startsWith(prefix)) this.deleteFile(candidate);
3070
+ for (const candidate of [...this.directories]) {
3071
+ if (!candidate.startsWith(prefix)) continue;
3072
+ this.directories.delete(candidate);
3073
+ this.directoryModes.delete(candidate);
3074
+ this.directoryMtimes.delete(candidate);
3075
+ }
3076
+ this.directories.delete(target);
3077
+ this.directoryModes.delete(target);
3078
+ this.directoryMtimes.delete(target);
3079
+ this.forgetIdentity(target);
3080
+ this.touchDirectory(dirname(target));
3081
+ this.publish({
3082
+ kind: "remove",
3083
+ path: target
3084
+ });
3085
+ return;
3086
+ }
3087
+ if (options?.force !== true) fail("ENOENT", "rm", target);
3088
+ }
3089
+ /**
3090
+ * Create a uniquely named directory beside `prefix`, as `fs.mkdtempSync` does.
3091
+ * @param prefix - Path prefix; the suffix is appended without a separator.
3092
+ * @returns The created directory path.
3093
+ */
3094
+ mkdtempSync(prefix) {
3095
+ this.temporaries += 1;
3096
+ const target = `${prefix}${Date.now().toString(36)}${this.temporaries.toString(36)}`;
3097
+ this.mkdirSync(target, { recursive: true });
3098
+ return this.key(target);
3099
+ }
3100
+ /**
3101
+ * Seed a file and its parent directories, for image loading and tests.
3102
+ * @param path - File path.
3103
+ * @param data - Text or bytes.
3104
+ * @param options - Permission bits and modification time supplied by the image or durable store.
3105
+ */
3106
+ seed(path, data, options = {}) {
3107
+ const target = this.key(path);
3108
+ this.seedDirectory(dirname(target));
3109
+ this.setFile(target, {
3110
+ bytes: typeof data === "string" ? encoder.encode(data) : data,
3111
+ mtimeMs: options.mtimeMs ?? this.touch(target),
3112
+ mode: (options.mode ?? DEFAULT_FILE_MODE) & 511,
3113
+ paths: void 0
3114
+ });
3115
+ this.touchDirectory(dirname(target));
3116
+ }
3117
+ /**
3118
+ * Create a directory and its parents.
3119
+ * @param path - Directory path.
3120
+ * @param options - Permission bits and modification time supplied by the image or durable store.
3121
+ */
3122
+ seedDirectory(path, options = {}) {
3123
+ const target = this.key(path);
3124
+ if (!this.directories.has(target)) {
3125
+ const parent = dirname(target);
3126
+ if (parent !== target) this.seedDirectory(parent);
3127
+ if (this.files.has(target)) fail("EEXIST", "mkdir", target);
3128
+ this.directories.add(target);
3129
+ this.directoryMtimes.set(target, options.mtimeMs ?? Date.now());
3130
+ this.touchDirectory(parent);
3131
+ }
3132
+ if (options.mode !== void 0) this.directoryModes.set(target, options.mode & 511);
3133
+ if (options.mtimeMs !== void 0) this.directoryMtimes.set(target, options.mtimeMs);
3134
+ }
3135
+ /**
3136
+ * Report what this filesystem holds, for the host's boot diagnostics.
3137
+ * @returns File count, directory count, and total byte size.
3138
+ */
3139
+ usage() {
3140
+ let bytes = 0;
3141
+ for (const node of this.files.values()) bytes += node.bytes.length;
3142
+ return {
3143
+ files: this.files.size,
3144
+ directories: this.directories.size,
3145
+ bytes
3146
+ };
3147
+ }
3148
+ };
3149
+ /**
3150
+ * Mount a tar image produced by the build-time collector.
3151
+ *
3152
+ * Entry names are relative to `root` (`node_modules/...`, `config/cordis.yml`);
3153
+ * an absolute entry name is a collector defect and fails loud. File contents
3154
+ * stay views into `image` — nothing is copied at mount time.
3155
+ * @param image - The ustar archive, as `inflateImage` produces it from the fetched image.
3156
+ * @param root - Virtual root the entries mount under.
3157
+ * @param vfs - Filesystem to fill; a fresh one by default.
3158
+ * @returns The filled filesystem.
3159
+ */
3160
+ function loadVfsImage(image, root = "/dsh", vfs = new MemoryVfs()) {
3161
+ vfs.seedDirectory(root);
3162
+ for (const entry of parseTar(image)) {
3163
+ const relativeName = entry.name.startsWith("./") ? entry.name.slice(2) : entry.name;
3164
+ if (relativeName.startsWith("/")) throw new Error(`webworker vfs: image entry must be relative to ${root}, received "${entry.name}"`);
3165
+ const target = join(root, relativeName);
3166
+ if (entry.directory) {
3167
+ vfs.seedDirectory(target, { mode: entry.mode });
3168
+ continue;
3169
+ }
3170
+ vfs.seed(target, entry.bytes, { mode: entry.mode });
3171
+ }
3172
+ return vfs;
3173
+ }
3174
+ /**
3175
+ * Apply one ordered data overlay to an already mounted base image.
3176
+ *
3177
+ * Overlay entries may replace files only under the layout's data directories;
3178
+ * module code, configuration, and the lowering manifest cannot be shadowed.
3179
+ * Paths containing traversal segments are refused before normalization. Later
3180
+ * overlays win for files, while file/directory type conflicts fail loud.
3181
+ * @param image - Uncompressed ustar overlay archive.
3182
+ * @param root - Virtual root shared with the base image.
3183
+ * @param vfs - Mounted filesystem to update.
3184
+ * @returns The same filesystem after applying the overlay.
3185
+ */
3186
+ function loadVfsOverlay(image, root, vfs) {
3187
+ for (const entry of parseTar(image)) {
3188
+ const relativeName = entry.name.startsWith("./") ? entry.name.slice(2) : entry.name;
3189
+ const path = relativeName.endsWith("/") ? relativeName.slice(0, -1) : relativeName;
3190
+ const segments = path.split("/");
3191
+ if (path === "" || relativeName.startsWith("/") || segments.some((segment) => segment === "" || segment === "." || segment === "..") || !IMAGE_OVERLAY_DIRECTORIES.includes(segments[0] ?? "")) throw new Error(`webworker vfs: overlay entry must stay under ${IMAGE_OVERLAY_DIRECTORIES.join("/ or ")}, received "${entry.name}"`);
3192
+ const target = join(root, path);
3193
+ if (entry.directory) {
3194
+ vfs.seedDirectory(target, { mode: entry.mode });
3195
+ continue;
3196
+ }
3197
+ if (vfs.existsSync(target) && vfs.statSync(target).isDirectory()) throw new Error(`webworker vfs: overlay file cannot replace directory "${target}"`);
3198
+ vfs.seed(target, entry.bytes, { mode: entry.mode });
3199
+ }
3200
+ return vfs;
3201
+ }
3202
+ //#endregion
3203
+ //#region src/storage/active.ts
3204
+ let active;
3205
+ /**
3206
+ * Publish the filesystem the `node:fs` proxy reads.
3207
+ * @param vfs - Filesystem mounted by the worker entry.
3208
+ */
3209
+ function setActiveVfs(vfs) {
3210
+ active = vfs;
3211
+ }
3212
+ /**
3213
+ * Read the mounted filesystem.
3214
+ * @returns The active filesystem.
3215
+ */
3216
+ function requireActiveVfs() {
3217
+ if (active === void 0) throw new Error("webworker vfs: no filesystem is mounted; the worker entry must call setActiveVfs before any node:fs access");
3218
+ return active;
3219
+ }
3220
+ function requireGlobalPort(channel) {
3221
+ if (channel !== void 0) return channel;
3222
+ const post = globalThis.postMessage;
3223
+ if (typeof post !== "function") throw new Error("webworker host: no channel; pass options.channel outside a dedicated worker");
3224
+ return { postMessage: (message, transfer) => {
3225
+ post(message, transfer);
3226
+ } };
3227
+ }
3228
+ async function readImage(image) {
3229
+ if (typeof image !== "string") return await inflateImage(image, "the image bytes given to createWorkerHost");
3230
+ const response = await fetch(image);
3231
+ if (!response.ok) throw new Error(`webworker host: image fetch failed with ${String(response.status)} for ${image}`);
3232
+ if (response.body === null) throw new Error(`webworker host: image response for ${image} carried no body`);
3233
+ return await inflateImageStream(response.body, image);
3234
+ }
3235
+ /**
3236
+ * Build the worker host without touching the network or the image.
3237
+ * @param options - Assembly inputs.
3238
+ * @returns Handle whose `handleMessage` is ready immediately.
3239
+ */
3240
+ function createWorkerHost(options) {
3241
+ const root = options.root ?? "/dsh";
3242
+ const configPath = options.configPath ?? join(root, "config/cordis.yml");
3243
+ const port = options.port ?? 3080;
3244
+ const tunnel = new TunnelServer({
3245
+ port: requireGlobalPort(options.channel),
3246
+ requestListener: options.requestListener,
3247
+ ...options.privilegedMethods === void 0 ? {} : { privilegedMethods: options.privilegedMethods },
3248
+ ...options.unaryApiLane === void 0 ? {} : { unaryApiLane: options.unaryApiLane }
3249
+ });
3250
+ let vfs;
3251
+ let modules;
3252
+ let context;
3253
+ const start = async () => {
3254
+ try {
3255
+ const home = join(root, IMAGE_HOME_DIRECTORY);
3256
+ installProcessGlobal({
3257
+ cwd: root,
3258
+ env: {
3259
+ DSH_HOME: home,
3260
+ HOME: home,
3261
+ ...options.env
3262
+ }
3263
+ });
3264
+ const [bytes, overlays] = await Promise.all([readImage(options.image), Promise.all((options.overlays ?? []).map(readImage))]);
3265
+ const mounted = loadVfsImage(bytes, root);
3266
+ for (const overlay of overlays) loadVfsOverlay(overlay, root, mounted);
3267
+ for (const directory of IMAGE_EMPTY_DIRECTORIES) mounted.seedDirectory(join(root, directory.replace(/\/$/, "")));
3268
+ setActiveVfs(mounted);
3269
+ vfs = mounted;
3270
+ requireLoweredImage(mounted, options.manifestPath ?? join(root, "config/vfs-manifest.json"));
3271
+ const staticModules = { ...options.staticModules };
3272
+ for (const key of ["node:process", "process"]) staticModules[key] ??= () => globalThis.process;
3273
+ const loader = new WorkerModuleLoader({
3274
+ vfs: mounted,
3275
+ root,
3276
+ staticModules,
3277
+ ...options.staticModulePrefixes === void 0 ? {} : { staticModulePrefixes: options.staticModulePrefixes },
3278
+ ...options.alsCausality === void 0 ? {} : { alsCausality: options.alsCausality }
3279
+ });
3280
+ setActiveModuleLoader(loader);
3281
+ modules = loader;
3282
+ const require = loader.requireFrom(dirname(configPath));
3283
+ const appBoot = require("@deepseek-ai/dsh-app-boot");
3284
+ const cmdline = require("@deepseek-ai/dsh-cmdline");
3285
+ const { patches, presetOverlay } = bootPatches(loader, mounted, configPath, root);
3286
+ const ctx = await appBoot.boot("dsh-webworker", configPath, patches, (hostCtx) => {
3287
+ hostCtx.loader.internal = loader.internal;
3288
+ installLogSink(hostCtx, require);
3289
+ cmdline.provideCmdline(hostCtx, {
3290
+ args: [...options.cmdlineArgs ?? [
3291
+ "--host",
3292
+ "127.0.0.1",
3293
+ "--port",
3294
+ String(port),
3295
+ "--no-open"
3296
+ ]],
3297
+ exit: (code) => {
3298
+ console.warn(`webworker host: tree requested exit(${String(code)})`);
3299
+ }
3300
+ });
3301
+ });
3302
+ context = ctx;
3303
+ const connection = ctx.get("connection");
3304
+ if (connection === void 0) throw new Error("webworker host: the tree activated without a Connection service");
3305
+ const typertGateway = ctx.get("typertGateway");
3306
+ if (typertGateway === void 0) throw new Error("webworker host: the tree activated without a typertGateway service");
3307
+ const handler = connection.createSharedFetchHandler("/api");
3308
+ const usage = loader.usage();
3309
+ console.info(`webworker host: tree active (modules=${String(usage.modules)}, data overlays=${String(overlays.length)}, preset root overlay=${presetOverlay ? "applied" : "already in roster"}, direct lane=connection.createSharedFetchHandler, als causality=${options.alsCausality === void 0 ? "inert" : "snapshot/restore"}, image lowering=${LOWERING_VERSION})`);
3310
+ tunnel.serve({
3311
+ directFetch: (request) => handler.fetch(request),
3312
+ bootPayload: () => readBootPayload(ctx),
3313
+ openStream: typertGateway.wireStream.open,
3314
+ streamFailure: typertGateway.wireStream.failure
3315
+ });
3316
+ } catch (reason) {
3317
+ tunnel.fail(reason);
3318
+ throw reason;
3319
+ }
3320
+ };
3321
+ return {
3322
+ handleMessage: (data) => {
3323
+ tunnel.handleMessage(data);
3324
+ },
3325
+ start,
3326
+ stop: async () => {
3327
+ tunnel.fail(/* @__PURE__ */ new Error("webworker host: the tree was disposed"));
3328
+ await context?.fiber.dispose();
3329
+ },
3330
+ get vfs() {
3331
+ return vfs;
3332
+ },
3333
+ get modules() {
3334
+ return modules;
3335
+ }
3336
+ };
3337
+ }
3338
+ /**
3339
+ * Send the tree's own warnings and errors to the worker console.
3340
+ *
3341
+ * Cordis's `LoggerService` always exists and always accepts messages, but with
3342
+ * no exporter mounted it only fills a ring buffer — and no profile in this
3343
+ * repository mounts one, so `ctx.logger.warn(...)` reaches nothing. A provider
3344
+ * that fails and is skipped (the skill registry logs exactly that) then looks
3345
+ * identical to one that found nothing, which is how an empty skill catalog hid a
3346
+ * filesystem fault twice.
3347
+ *
3348
+ * Warnings and errors only: `info`/`debug` from 131 plugin rows would bury the
3349
+ * page console, and this exists to make failures visible rather than to trace.
3350
+ * @param ctx - Host context, before any entry mounts.
3351
+ * @param require - Image resolver, for cordis's own message renderer.
3352
+ */
3353
+ function installLogSink(ctx, require) {
3354
+ const { Logger } = require("@deepseek-ai/cordis");
3355
+ const exporter = {
3356
+ colors: false,
3357
+ levels: { default: 2 },
3358
+ export: (message) => {
3359
+ if (message.type !== "warn" && message.type !== "error") return;
3360
+ const line = `${message.name}: ${Logger.format(exporter, message)}`;
3361
+ if (message.type === "error") console.error(line);
3362
+ else console.warn(line);
3363
+ }
3364
+ };
3365
+ ctx.logger.exporter(exporter);
3366
+ }
3367
+ /**
3368
+ * Require the mounted image to carry bodies this build can wrap.
3369
+ *
3370
+ * The manifest the packer writes is the single source of truth: the worker holds
3371
+ * no transform, so an image that was never lowered — or was lowered against
3372
+ * different wrapper semantics — cannot be recovered at load and must be rebuilt.
3373
+ * @param vfs - Mounted filesystem.
3374
+ * @param path - Manifest path inside the image.
3375
+ * @throws When the manifest is missing, unreadable, or names another contract.
3376
+ */
3377
+ function requireLoweredImage(vfs, path) {
3378
+ if (!vfs.existsSync(path)) throw new Error(`webworker host: ${path} is missing, so the image records no lowering; rebuild the image`);
3379
+ const parsed = JSON.parse(vfs.readFileSync(path, "utf8"));
3380
+ if (typeof parsed !== "object" || parsed === null) throw new Error(`webworker host: ${path} does not hold an object`);
3381
+ const lowered = parsed.lowered;
3382
+ if (lowered !== "dsh-worker-transform/1") throw new Error(`webworker host: image was lowered by ${String(lowered)}, this build runs ${LOWERING_VERSION}; rebuild the image`);
3383
+ }
3384
+ /**
3385
+ * The shipped preset root, as the application layer that owns the composition
3386
+ * supplies it.
3387
+ *
3388
+ * A launcher appends this root itself rather than writing it into the roster —
3389
+ * `apps/cli` does it in `composeProfile` (`profile-boot.ts:159-166`) because only
3390
+ * the application knows where its own presets sit. The worker's presets travel
3391
+ * in the image, so the same overlay names their virtual path. Patching replaces
3392
+ * a row's whole `config`, so the current one is read and spread, and a roster
3393
+ * that already names roots keeps them.
3394
+ * @param loader - Module loader, for the image's YAML reader.
3395
+ * @param vfs - Filesystem holding the composed configuration.
3396
+ * @param configPath - Composed configuration path.
3397
+ * @param root - Virtual root.
3398
+ * @returns Boot patches (preset root overlay, frontend serving off) and
3399
+ * whether the preset overlay was applied.
3400
+ */
3401
+ function bootPatches(loader, vfs, configPath, root) {
3402
+ const text = vfs.readFileSync(configPath, "utf8");
3403
+ let rows;
3404
+ if (configPath.endsWith(".json")) rows = JSON.parse(text);
3405
+ else {
3406
+ const include = loader.load(loader.resolve("@deepseek-ai/cordis-plugin-include", root));
3407
+ rows = loader.load(loader.resolve("js-yaml", root)).load(text, { schema: include.entryListSchema });
3408
+ }
3409
+ const find = (entries, id) => {
3410
+ if (!Array.isArray(entries)) return void 0;
3411
+ for (const entry of entries) {
3412
+ if (entry.id === id) return entry;
3413
+ const nested = find(entry.config, id);
3414
+ if (nested !== void 0) return nested;
3415
+ }
3416
+ };
3417
+ const configOf = (row) => typeof row.config === "object" && row.config !== null && !Array.isArray(row.config) ? row.config : {};
3418
+ const patches = [];
3419
+ let presetOverlay = false;
3420
+ const presets = find(rows, "agent-presets");
3421
+ if (presets !== void 0 && configOf(presets).roots === void 0) {
3422
+ presetOverlay = true;
3423
+ patches.push({
3424
+ id: "agent-presets",
3425
+ config: {
3426
+ ...configOf(presets),
3427
+ roots: [{
3428
+ path: join(root, "config/agent-presets"),
3429
+ trust: "system"
3430
+ }]
3431
+ }
3432
+ });
3433
+ }
3434
+ const jsonl = find(rows, "session-persistence-jsonl");
3435
+ if (jsonl !== void 0) patches.push({
3436
+ id: "session-persistence-jsonl",
3437
+ config: {
3438
+ ...configOf(jsonl),
3439
+ compression: "none"
3440
+ }
3441
+ });
3442
+ return {
3443
+ patches,
3444
+ presetOverlay
3445
+ };
3446
+ }
3447
+ /**
3448
+ * Assemble the payload the page's pre-Cordis bootstrap needs: the structured
3449
+ * index injection table the served form renders into index.html. Collected
3450
+ * from the in-process webserver service, never from the API surface, because
3451
+ * the page has no Cordis tree yet.
3452
+ * @param ctx - Booted host context.
3453
+ * @returns Boot payload for `GET /__boot__`.
3454
+ */
3455
+ function readBootPayload(ctx) {
3456
+ const webServer = ctx.get("webServer");
3457
+ if (webServer === void 0) throw new Error("webworker host: no webServer service, so the page cannot receive its boot injections");
3458
+ return { injections: webServer.collectIndexInjections() };
3459
+ }
3460
+ //#endregion
3461
+ //#region src/fixture-manifest.ts
3462
+ /** Browser-readable catalog of built-in Preview filesystem overlays. */
3463
+ /** Manifest format version emitted beside the base VFS image. */
3464
+ const PREVIEW_FIXTURE_MANIFEST_VERSION = 1;
3465
+ /** Leaf name resolved beside the base image. */
3466
+ const PREVIEW_FIXTURE_MANIFEST_FILE = "fixtures.json";
3467
+ function recordOf(value) {
3468
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
3469
+ }
3470
+ /**
3471
+ * Validate the static fixture catalog before it controls Worker fetches.
3472
+ * @param value - Parsed JSON response.
3473
+ * @returns A detached manifest with unique ids and non-empty overlay lists.
3474
+ */
3475
+ function parsePreviewFixtureManifest(value) {
3476
+ const record = recordOf(value);
3477
+ if (record?.version !== 1 || !Array.isArray(record.fixtures)) throw new Error(`preview fixture manifest must use version ${String(1)}`);
3478
+ const fixtures = [];
3479
+ const ids = /* @__PURE__ */ new Set();
3480
+ for (const value of record.fixtures) {
3481
+ const fixture = recordOf(value);
3482
+ const id = fixture?.id;
3483
+ const label = fixture?.label;
3484
+ const description = fixture?.description;
3485
+ const overlays = fixture?.overlays;
3486
+ const overlayUrls = Array.isArray(overlays) ? overlays.filter((overlay) => typeof overlay === "string" && overlay.length > 0) : [];
3487
+ if (typeof id !== "string" || !/^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/.test(id) || id === "none" || id === "webfs" || typeof label !== "string" || label.length === 0 || typeof description !== "string" || description.length === 0 || !Array.isArray(overlays) || overlays.length === 0 || overlayUrls.length !== overlays.length) throw new Error("preview fixture manifest contains an invalid fixture entry");
3488
+ if (ids.has(id)) throw new Error(`preview fixture manifest repeats id "${id}"`);
3489
+ ids.add(id);
3490
+ fixtures.push({
3491
+ id,
3492
+ label,
3493
+ description,
3494
+ overlays: overlayUrls
3495
+ });
3496
+ }
3497
+ const defaultFixture = record.defaultFixture;
3498
+ if (defaultFixture !== null && (typeof defaultFixture !== "string" || !ids.has(defaultFixture))) throw new Error("preview fixture manifest defaultFixture does not name a fixture");
3499
+ return {
3500
+ version: 1,
3501
+ defaultFixture,
3502
+ fixtures
3503
+ };
3504
+ }
3505
+ //#endregion
3506
+ export { API_PREFIX, DEFAULT_CONDITIONS, DEFAULT_ROOT, IMAGE_CONFIG_PATH, IMAGE_EMPTY_DIRECTORIES, IMAGE_FILE_NAME, IMAGE_HOME_DIRECTORY, IMAGE_MANIFEST_PATH, IMAGE_OVERLAY_DIRECTORIES, LOWERING_VERSION, MODULE_PROXIES, MODULE_PROXY_PREFIXES, MemoryVfs, PREVIEW_FIXTURE_MANIFEST_FILE, PREVIEW_FIXTURE_MANIFEST_VERSION, REPLACED_EXTERNAL_PACKAGES, SYNTHETIC_HOST, TunnelServer, WRAPPER_PARAMS, WorkerModuleLoader, createAlsRuntime, createSyntheticExchange, createWorkerHost, inflateImage, inflateImageStream, installProcessGlobal, loadVfsImage, loadVfsOverlay, lowerModuleSource, packTar, parseInboundFrame, parsePreviewFixtureManifest, parseTar, posix_path_exports as posixPath, requireActiveModuleLoader, requireActiveVfs, setActiveModuleLoader, setActiveVfs };