@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/client.js ADDED
@@ -0,0 +1,854 @@
1
+ //#region src/image-layout.ts
2
+ /**
3
+ * Leaf name of the packed base image: one gzip member holding the ustar archive.
4
+ * The app build writes it beside the page and the page's boot fetches it from
5
+ * there, so the extension is part of what a deployment serves.
6
+ */
7
+ const IMAGE_FILE_NAME = "vfs-image.tar.gz";
8
+ //#endregion
9
+ //#region src/fixture-manifest.ts
10
+ /** Browser-readable catalog of built-in Preview filesystem overlays. */
11
+ /** Manifest format version emitted beside the base VFS image. */
12
+ const PREVIEW_FIXTURE_MANIFEST_VERSION = 1;
13
+ /** Leaf name resolved beside the base image. */
14
+ const PREVIEW_FIXTURE_MANIFEST_FILE = "fixtures.json";
15
+ function recordOf(value) {
16
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
17
+ }
18
+ /**
19
+ * Validate the static fixture catalog before it controls Worker fetches.
20
+ * @param value - Parsed JSON response.
21
+ * @returns A detached manifest with unique ids and non-empty overlay lists.
22
+ */
23
+ function parsePreviewFixtureManifest(value) {
24
+ const record = recordOf(value);
25
+ if (record?.version !== 1 || !Array.isArray(record.fixtures)) throw new Error(`preview fixture manifest must use version ${String(1)}`);
26
+ const fixtures = [];
27
+ const ids = /* @__PURE__ */ new Set();
28
+ for (const value of record.fixtures) {
29
+ const fixture = recordOf(value);
30
+ const id = fixture?.id;
31
+ const label = fixture?.label;
32
+ const description = fixture?.description;
33
+ const overlays = fixture?.overlays;
34
+ const overlayUrls = Array.isArray(overlays) ? overlays.filter((overlay) => typeof overlay === "string" && overlay.length > 0) : [];
35
+ 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");
36
+ if (ids.has(id)) throw new Error(`preview fixture manifest repeats id "${id}"`);
37
+ ids.add(id);
38
+ fixtures.push({
39
+ id,
40
+ label,
41
+ description,
42
+ overlays: overlayUrls
43
+ });
44
+ }
45
+ const defaultFixture = record.defaultFixture;
46
+ if (defaultFixture !== null && (typeof defaultFixture !== "string" || !ids.has(defaultFixture))) throw new Error("preview fixture manifest defaultFixture does not name a fixture");
47
+ return {
48
+ version: 1,
49
+ defaultFixture,
50
+ fixtures
51
+ };
52
+ }
53
+ //#endregion
54
+ //#region src/client/client.ts
55
+ /** Error carrying stream semantics across independently bundled Client code. */
56
+ var TunnelLogicalStreamError = class extends Error {
57
+ dshRemoteStreamFailure;
58
+ constructor(failure, options) {
59
+ super(failure.message, options);
60
+ this.name = "TunnelLogicalStreamError";
61
+ this.dshRemoteStreamFailure = failure.kind === "remote" ? {
62
+ kind: "remote",
63
+ code: failure.code,
64
+ details: failure.details
65
+ } : { kind: "carrier" };
66
+ }
67
+ };
68
+ var LogicalStreamInbox = class {
69
+ frames = [];
70
+ wake;
71
+ failed = false;
72
+ failure;
73
+ push(frame) {
74
+ if (this.failed) return;
75
+ this.frames.push(frame);
76
+ this.wake?.();
77
+ this.wake = void 0;
78
+ }
79
+ fail(reason) {
80
+ if (this.failed) return;
81
+ this.failed = true;
82
+ this.failure = reason;
83
+ this.frames.length = 0;
84
+ this.wake?.();
85
+ this.wake = void 0;
86
+ }
87
+ async next() {
88
+ while (this.frames.length === 0) {
89
+ if (this.failed) throw this.failure;
90
+ await new Promise((resolve) => {
91
+ this.wake = resolve;
92
+ });
93
+ }
94
+ return this.frames.shift();
95
+ }
96
+ };
97
+ /**
98
+ * Statuses the worker only produces when the host refused the exchange rather than
99
+ * answered it; a route's own 4xx is the tree talking and stays silent here.
100
+ */
101
+ const REFUSAL_STATUS = 500;
102
+ const encoder = new TextEncoder();
103
+ const SOURCE_MAP_TRAILER = /\/\/# sourceMappingURL=([^\r\n]+)\s*$/;
104
+ const BASE64_CHUNK_BYTES = 32 * 1024;
105
+ /** Encode UTF-8 text for an inline data URL without a call-stack-sized spread. */
106
+ function base64(value) {
107
+ const bytes = encoder.encode(value);
108
+ let binary = "";
109
+ for (let offset = 0; offset < bytes.length; offset += BASE64_CHUNK_BYTES) binary += String.fromCharCode(...bytes.subarray(offset, offset + BASE64_CHUNK_BYTES));
110
+ return btoa(binary);
111
+ }
112
+ /** Replace a tunnel-only map reference with a self-contained Base64 data URL. */
113
+ async function localizeSourceMap(source, bundleUrl, fetch) {
114
+ const match = SOURCE_MAP_TRAILER.exec(source);
115
+ if (match?.[1] === void 0) return source;
116
+ try {
117
+ const response = await fetch(new URL(match[1], new URL(bundleUrl, globalThis.location.origin)));
118
+ if (!response.ok) return source.replace(SOURCE_MAP_TRAILER, "");
119
+ const dataUrl = `data:application/json;charset=utf-8;base64,${base64(await response.text())}`;
120
+ return source.replace(SOURCE_MAP_TRAILER, `//# sourceMappingURL=${dataUrl}`);
121
+ } catch {
122
+ return source.replace(SOURCE_MAP_TRAILER, "");
123
+ }
124
+ }
125
+ /** Keep opaque Blobs and transferable streams intact; normalize other bodies to bytes. */
126
+ function toTunnelBody(body) {
127
+ if (body === void 0 || body === null) return void 0;
128
+ if (typeof body === "string") return encoder.encode(body).buffer;
129
+ if (body instanceof Blob) return body;
130
+ if (body instanceof ReadableStream) return body;
131
+ if (body instanceof ArrayBuffer) return body;
132
+ if (ArrayBuffer.isView(body)) return body.buffer.slice(body.byteOffset, body.byteOffset + body.byteLength);
133
+ throw new Error(`web-preview tunnel: unsupported request body ${Object.prototype.toString.call(body)}`);
134
+ }
135
+ /** Statuses whose Response must carry a null body. */
136
+ const NULL_BODY_STATUS = new Set([
137
+ 101,
138
+ 204,
139
+ 205,
140
+ 304
141
+ ]);
142
+ /** The page half of the tunnel: one `fetch`-shaped face over `postMessage`. */
143
+ var WorkerTunnel = class {
144
+ worker;
145
+ nextId = 1;
146
+ unary = /* @__PURE__ */ new Map();
147
+ bodyStreams = /* @__PURE__ */ new Map();
148
+ logicalStreams = /* @__PURE__ */ new Map();
149
+ /**
150
+ * In-flight request descriptions, so a refusal names what was refused.
151
+ *
152
+ * A tunnel failure and a failure inside the host tree look identical from the
153
+ * page — both surface as one rejected fetch — and the acceptance run keeps the
154
+ * page console but not the frames. Warning here separates the two without
155
+ * recording anything on the normal path, where no refusal frame ever arrives.
156
+ */
157
+ inFlight = /* @__PURE__ */ new Map();
158
+ /** Body-phase abort listeners, released when their stream settles. */
159
+ releases = /* @__PURE__ */ new Map();
160
+ /**
161
+ * Attach to a spawned worker and start consuming response frames.
162
+ * @param worker - the host worker.
163
+ */
164
+ constructor(worker) {
165
+ this.worker = worker;
166
+ worker.addEventListener("message", (event) => {
167
+ this.receive(event.data);
168
+ });
169
+ worker.addEventListener("error", (event) => {
170
+ const reason = /* @__PURE__ */ new Error(`web-preview tunnel: worker failed: ${event.message}`);
171
+ for (const id of this.inFlight.keys()) this.warnRefusal(id, `worker failed: ${event.message}`);
172
+ this.inFlight.clear();
173
+ for (const pending of this.unary.values()) pending.reject(reason);
174
+ this.unary.clear();
175
+ for (const controller of this.bodyStreams.values()) controller.error(reason);
176
+ this.bodyStreams.clear();
177
+ const failure = new TunnelLogicalStreamError({
178
+ kind: "carrier",
179
+ message: `web-preview tunnel: worker failed: ${event.message}`
180
+ }, { cause: reason });
181
+ for (const inbox of this.logicalStreams.values()) inbox.fail(failure);
182
+ this.logicalStreams.clear();
183
+ for (const release of this.releases.values()) release();
184
+ this.releases.clear();
185
+ });
186
+ }
187
+ /**
188
+ * Open the tunnel: the worker assembles its host from this frame.
189
+ * @param image - VFS image URL the worker fetches.
190
+ * @param overlays - Ordered data overlay URLs applied before boot.
191
+ */
192
+ init(image, overlays = []) {
193
+ this.worker.postMessage({
194
+ t: "init",
195
+ image,
196
+ overlays
197
+ });
198
+ }
199
+ /** Fetch-shaped entry: one request frame, one Response (streamed when the worker streams). */
200
+ fetch = async (input, init) => {
201
+ const signal = init?.signal;
202
+ if (signal?.aborted === true) throw new DOMException("The operation was aborted.", "AbortError");
203
+ const id = this.nextId++;
204
+ const body = init?.body === void 0 || init.body === null ? void 0 : toTunnelBody(init.body);
205
+ const frame = {
206
+ t: "req",
207
+ id,
208
+ method: init?.method ?? "GET",
209
+ url: new URL(input, globalThis.location.origin).toString(),
210
+ headers: Object.fromEntries(new Headers(init?.headers).entries()),
211
+ ...body === void 0 ? {} : { body }
212
+ };
213
+ const response = new Promise((resolve, reject) => {
214
+ this.unary.set(id, {
215
+ resolve,
216
+ reject
217
+ });
218
+ });
219
+ this.inFlight.set(id, `${frame.method} ${frame.url}`);
220
+ if (body instanceof ReadableStream) this.worker.postMessage(frame, [body]);
221
+ else this.worker.postMessage(frame);
222
+ if (signal === void 0 || signal === null) return await response;
223
+ const raced = this.rejectOnAbort(id, signal);
224
+ try {
225
+ const settled = await Promise.race([response, raced.rejected]);
226
+ if (this.bodyStreams.has(id)) this.observeStreamAbort(id, signal);
227
+ return settled;
228
+ } finally {
229
+ raced.release();
230
+ }
231
+ };
232
+ /**
233
+ * Open one decoded Gateway Remote stream over the worker-local carrier.
234
+ * @param endpoint - canonical Gateway Remote endpoint.
235
+ * @param payload - decoded endpoint payload.
236
+ * @param signal - logical-stream cancellation.
237
+ * @returns decoded stream values from the worker Host.
238
+ */
239
+ async *open(endpoint, payload, signal) {
240
+ signal.throwIfAborted();
241
+ const id = this.nextId++;
242
+ const inbox = new LogicalStreamInbox();
243
+ let opened = false;
244
+ let terminal = false;
245
+ const onAbort = () => {
246
+ inbox.fail(signal.reason);
247
+ };
248
+ signal.addEventListener("abort", onAbort, { once: true });
249
+ this.logicalStreams.set(id, inbox);
250
+ this.inFlight.set(id, `STREAM ${endpoint}`);
251
+ try {
252
+ const frame = {
253
+ t: "stream-open",
254
+ id,
255
+ endpoint,
256
+ payload
257
+ };
258
+ try {
259
+ this.worker.postMessage(frame);
260
+ opened = true;
261
+ } catch (cause) {
262
+ throw new TunnelLogicalStreamError({
263
+ kind: "carrier",
264
+ message: `web-preview tunnel: failed to open Remote stream ${endpoint}`
265
+ }, { cause });
266
+ }
267
+ while (true) {
268
+ const response = await inbox.next();
269
+ signal.throwIfAborted();
270
+ if (response.t === "stream-item") {
271
+ yield response.value;
272
+ continue;
273
+ }
274
+ terminal = true;
275
+ if (response.t === "stream-error") throw new TunnelLogicalStreamError(response.failure);
276
+ return;
277
+ }
278
+ } finally {
279
+ signal.removeEventListener("abort", onAbort);
280
+ this.logicalStreams.delete(id);
281
+ this.inFlight.delete(id);
282
+ if (opened && !terminal) this.abortWorkerOperation(id);
283
+ }
284
+ }
285
+ /**
286
+ * Read the pre-cordis boot payload (the injection table).
287
+ * @returns The payload the page applies before the client tree loads.
288
+ */
289
+ async bootPayload() {
290
+ const response = await this.fetch("/__boot__");
291
+ if (!response.ok) throw new Error(`web-preview tunnel: boot payload failed with HTTP ${String(response.status)}: ${await response.text()}`);
292
+ return await response.json();
293
+ }
294
+ /**
295
+ * `loadBundle` seam: take one client bundle through the tunnel and execute it
296
+ * as a classic script, exactly like the shell's same-origin `<script src>`.
297
+ * The image packs each bundle with a trailing `sourceURL` naming its image
298
+ * path, so the blob shows under that name in the debugger instead of as an
299
+ * anonymous blob entry.
300
+ * @param url - Graph combo URL (`/plugins/??<id>/client.js&rev=...`).
301
+ */
302
+ async loadBundle(url) {
303
+ const response = await this.fetch(url);
304
+ if (!response.ok) throw new Error(`web-preview tunnel: bundle ${url} failed with HTTP ${String(response.status)}`);
305
+ const source = await localizeSourceMap(await response.text(), url, this.fetch);
306
+ const blob = URL.createObjectURL(new Blob([source], { type: "text/javascript" }));
307
+ try {
308
+ await new Promise((resolve, reject) => {
309
+ const el = document.createElement("script");
310
+ el.src = blob;
311
+ el.addEventListener("load", () => {
312
+ el.remove();
313
+ resolve();
314
+ }, { once: true });
315
+ el.addEventListener("error", () => {
316
+ el.remove();
317
+ reject(/* @__PURE__ */ new Error(`web-preview tunnel: bundle ${url} failed to execute`));
318
+ }, { once: true });
319
+ document.head.append(el);
320
+ });
321
+ } finally {
322
+ URL.revokeObjectURL(blob);
323
+ }
324
+ }
325
+ rejectOnAbort(id, signal) {
326
+ let release = () => {};
327
+ return {
328
+ rejected: new Promise((_resolve, reject) => {
329
+ const fail = () => {
330
+ reject(this.abortRequest(id));
331
+ };
332
+ if (signal.aborted) {
333
+ fail();
334
+ return;
335
+ }
336
+ signal.addEventListener("abort", fail, { once: true });
337
+ release = () => {
338
+ signal.removeEventListener("abort", fail);
339
+ };
340
+ }),
341
+ release
342
+ };
343
+ }
344
+ /**
345
+ * Tear down one request the page abandoned: the maps forget it, the worker
346
+ * is told, and a live body stream errors for its reader.
347
+ * @param id - request id being abandoned.
348
+ * @returns The abort error the caller surfaces.
349
+ */
350
+ abortRequest(id) {
351
+ this.unary.delete(id);
352
+ const controller = this.bodyStreams.get(id);
353
+ this.bodyStreams.delete(id);
354
+ this.inFlight.delete(id);
355
+ this.releases.delete(id);
356
+ this.abortWorkerOperation(id);
357
+ const reason = new DOMException("The operation was aborted.", "AbortError");
358
+ controller?.error(reason);
359
+ return reason;
360
+ }
361
+ /**
362
+ * Hold the caller's signal over the body phase: the head settled, so
363
+ * {@link rejectOnAbort}'s listener is about to go, but a stop must still
364
+ * end the stream. Released when the stream settles.
365
+ * @param id - request id whose body is still crossing.
366
+ * @param signal - the caller's signal.
367
+ */
368
+ observeStreamAbort(id, signal) {
369
+ const onAbort = () => {
370
+ this.abortRequest(id);
371
+ };
372
+ signal.addEventListener("abort", onAbort, { once: true });
373
+ this.releases.set(id, () => {
374
+ signal.removeEventListener("abort", onAbort);
375
+ });
376
+ }
377
+ /** Release a body-phase abort listener a settled stream no longer needs. */
378
+ releaseSignal(id) {
379
+ const release = this.releases.get(id);
380
+ this.releases.delete(id);
381
+ release?.();
382
+ }
383
+ /** Cancel a stream the consumer stopped reading (the head already resolved). */
384
+ cancelStream(id) {
385
+ this.releaseSignal(id);
386
+ this.bodyStreams.delete(id);
387
+ this.inFlight.delete(id);
388
+ this.abortWorkerOperation(id);
389
+ }
390
+ /** Best-effort cancellation: a failed worker cannot receive the frame anyway. */
391
+ abortWorkerOperation(id) {
392
+ const abort = {
393
+ t: "abort",
394
+ id
395
+ };
396
+ try {
397
+ this.worker.postMessage(abort);
398
+ } catch {}
399
+ }
400
+ /**
401
+ * Report a refusal on the page console, where the acceptance run already keeps it.
402
+ *
403
+ * The prefix names the reporter, not the culprit: a 5xx can equally come from a
404
+ * handler inside the host tree. The message text decides — the worker expands
405
+ * nested causes into it, and its deepest layer is where the failure was thrown.
406
+ * @param id - request id the frame answers.
407
+ * @param outcome - what came back instead of a reply.
408
+ */
409
+ warnRefusal(id, outcome) {
410
+ console.warn(`web-preview tunnel: request ${String(id)} ${this.inFlight.get(id) ?? "(unknown request)"} → ${outcome}`);
411
+ }
412
+ receive(frame) {
413
+ switch (frame.t) {
414
+ case "res": {
415
+ const pending = this.unary.get(frame.id);
416
+ if (pending === void 0) return;
417
+ if (frame.status >= REFUSAL_STATUS) this.warnRefusal(frame.id, `HTTP ${String(frame.status)}${frame.message === void 0 ? "" : `: ${frame.message}`}`);
418
+ this.unary.delete(frame.id);
419
+ this.inFlight.delete(frame.id);
420
+ const body = NULL_BODY_STATUS.has(frame.status) ? null : frame.body ?? frame.message ?? null;
421
+ pending.resolve(new Response(body, {
422
+ status: frame.status,
423
+ headers: frame.headers
424
+ }));
425
+ return;
426
+ }
427
+ case "res-head": {
428
+ const pending = this.unary.get(frame.id);
429
+ if (pending === void 0) return;
430
+ this.unary.delete(frame.id);
431
+ const stream = new ReadableStream({
432
+ start: (controller) => {
433
+ this.bodyStreams.set(frame.id, controller);
434
+ },
435
+ cancel: () => {
436
+ this.cancelStream(frame.id);
437
+ }
438
+ });
439
+ pending.resolve(new Response(stream, {
440
+ status: frame.status,
441
+ headers: frame.headers
442
+ }));
443
+ return;
444
+ }
445
+ case "res-chunk":
446
+ this.bodyStreams.get(frame.id)?.enqueue(new Uint8Array(frame.chunk));
447
+ return;
448
+ case "res-end": {
449
+ const controller = this.bodyStreams.get(frame.id);
450
+ if (controller === void 0) return;
451
+ this.bodyStreams.delete(frame.id);
452
+ this.inFlight.delete(frame.id);
453
+ this.releaseSignal(frame.id);
454
+ controller.close();
455
+ return;
456
+ }
457
+ case "res-err": {
458
+ const reason = /* @__PURE__ */ new Error(`web-preview tunnel: ${frame.message}`);
459
+ this.warnRefusal(frame.id, `res-err: ${frame.message}`);
460
+ const pending = this.unary.get(frame.id);
461
+ this.inFlight.delete(frame.id);
462
+ if (pending !== void 0) {
463
+ this.unary.delete(frame.id);
464
+ pending.reject(reason);
465
+ return;
466
+ }
467
+ const controller = this.bodyStreams.get(frame.id);
468
+ if (controller === void 0) return;
469
+ this.bodyStreams.delete(frame.id);
470
+ this.releaseSignal(frame.id);
471
+ controller.error(reason);
472
+ return;
473
+ }
474
+ case "stream-item":
475
+ case "stream-end":
476
+ case "stream-error":
477
+ this.logicalStreams.get(frame.id)?.push(frame);
478
+ return;
479
+ default: throw new Error(`web-preview tunnel: unknown frame ${JSON.stringify(frame)}`);
480
+ }
481
+ }
482
+ };
483
+ //#endregion
484
+ //#region src/client/apply-injections.ts
485
+ function assertNever(row) {
486
+ throw new Error(`webworker-runtime: unknown index injection row ${JSON.stringify(row)}`);
487
+ }
488
+ /**
489
+ * Execute every row in table order.
490
+ * @param rows - Injection table from the boot payload.
491
+ * @param loadScript - Executes one script-src row; the tunnel's `loadBundle`,
492
+ * because the row URLs (`/plugins/...`) resolve only through the worker.
493
+ */
494
+ async function applyIndexInjections(rows, loadScript) {
495
+ for (const row of rows) switch (row.kind) {
496
+ case "global":
497
+ globalThis[row.name] = row.value;
498
+ break;
499
+ case "script": {
500
+ const el = document.createElement("script");
501
+ el.textContent = row.text;
502
+ (row.placement === "head" ? document.head : document.body).append(el);
503
+ break;
504
+ }
505
+ case "script-src":
506
+ await loadScript(row.src);
507
+ break;
508
+ case "script-preload": break;
509
+ case "style": {
510
+ const el = document.createElement("style");
511
+ el.textContent = row.text;
512
+ document.head.append(el);
513
+ break;
514
+ }
515
+ case "html":
516
+ (row.placement === "head" ? document.head : document.body).insertAdjacentHTML("beforeend", row.html);
517
+ break;
518
+ default: assertNever(row);
519
+ }
520
+ }
521
+ //#endregion
522
+ //#region src/client/source-chooser.ts
523
+ /** Pre-boot filesystem-source chooser for static WebWorker previews. */
524
+ const EMPTY_SOURCE = "none";
525
+ const WEBFS_SOURCE = "webfs";
526
+ const PREVIEW_FIXTURE_QUERY = "preview-fixture";
527
+ const CHOOSER_STYLE = `
528
+ [data-preview-source-chooser] {
529
+ position: fixed;
530
+ inset: 0;
531
+ z-index: 1200;
532
+ display: grid;
533
+ place-items: center;
534
+ overflow: auto;
535
+ padding: 24px;
536
+ box-sizing: border-box;
537
+ color: #0f1115;
538
+ background: #fff;
539
+ font-size: 14px;
540
+ line-height: 22px;
541
+ }
542
+ [data-preview-source-card] {
543
+ width: min(600px, 100%);
544
+ max-height: calc(100dvh - 48px);
545
+ box-sizing: border-box;
546
+ padding: 28px;
547
+ overflow-y: auto;
548
+ border: 1px solid transparent;
549
+ border-radius: 24px;
550
+ background: #fff;
551
+ box-shadow: 0 0 1px rgb(0 0 0 / 20%), 0 12px 32px rgb(0 0 0 / 8%);
552
+ }
553
+ [data-preview-source-card] h1 {
554
+ margin: 0;
555
+ font-size: 20px;
556
+ line-height: 28px;
557
+ font-weight: 500;
558
+ }
559
+ [data-preview-source-card] > p {
560
+ margin: 8px 0 0;
561
+ color: #61666b;
562
+ }
563
+ [data-preview-source-card] fieldset {
564
+ display: flex;
565
+ flex-direction: column;
566
+ gap: 1px;
567
+ margin: 24px 0 0;
568
+ padding: 0;
569
+ border: 0;
570
+ }
571
+ [data-preview-source-card] legend {
572
+ margin: 0 0 8px;
573
+ padding: 0 4px;
574
+ color: #61666b;
575
+ font-size: 13px;
576
+ line-height: 20px;
577
+ font-weight: 500;
578
+ }
579
+ [data-preview-source-option] {
580
+ position: relative;
581
+ display: flex;
582
+ align-items: flex-start;
583
+ gap: 8px;
584
+ min-height: 56px;
585
+ padding: 8px 12px 8px 8px;
586
+ box-sizing: border-box;
587
+ border: 1px solid transparent;
588
+ border-radius: 12px;
589
+ background: transparent;
590
+ cursor: pointer;
591
+ transition: background-color 120ms ease, border-color 120ms ease;
592
+ }
593
+ [data-preview-source-option]:hover:not(:has(input:disabled)),
594
+ [data-preview-source-option]:has(input:checked) {
595
+ background: rgb(38 49 72 / 6%);
596
+ }
597
+ [data-preview-source-option]:has(input:checked) {
598
+ border-color: rgb(0 0 0 / 10%);
599
+ }
600
+ [data-preview-source-option]:has(input:disabled) {
601
+ cursor: default;
602
+ opacity: 0.4;
603
+ }
604
+ [data-preview-source-option] input {
605
+ flex: none;
606
+ width: 16px;
607
+ height: 16px;
608
+ margin: 4px 0 0;
609
+ accent-color: #0f1115;
610
+ }
611
+ [data-preview-source-option] > span { flex: 1; min-width: 0; }
612
+ [data-preview-source-option] strong {
613
+ display: block;
614
+ font-size: 14px;
615
+ line-height: 24px;
616
+ font-weight: 500;
617
+ }
618
+ [data-preview-source-option] strong + span {
619
+ display: block;
620
+ color: #81858c;
621
+ font-size: 14px;
622
+ line-height: 24px;
623
+ }
624
+ [data-preview-source-submit] {
625
+ display: block;
626
+ min-width: 120px;
627
+ height: 36px;
628
+ margin: 24px 0 0 auto;
629
+ padding: 0 14px;
630
+ border: 0;
631
+ border-radius: 18px;
632
+ color: #fff;
633
+ background: #0f1115;
634
+ font-size: 14px;
635
+ line-height: 22px;
636
+ cursor: pointer;
637
+ transition: background-color 120ms ease;
638
+ }
639
+ [data-preview-source-submit]:hover:not(:disabled) {
640
+ background: #43454a;
641
+ }
642
+ [data-preview-source-submit]:focus-visible {
643
+ outline: 2px solid rgb(0 0 0 / 16%);
644
+ outline-offset: 2px;
645
+ }
646
+ [data-preview-source-submit]:disabled { cursor: not-allowed; opacity: 0.5; }
647
+ @media (prefers-color-scheme: dark) {
648
+ [data-preview-source-chooser] {
649
+ color: #f9fafb;
650
+ background: #151517;
651
+ }
652
+ [data-preview-source-card] { border-color: rgb(255 255 255 / 6%); background: #2c2c2e; }
653
+ [data-preview-source-card] > p, [data-preview-source-card] legend { color: #cfd3d6; }
654
+ [data-preview-source-option] strong + span { color: #adb2b8; }
655
+ [data-preview-source-option]:hover:not(:has(input:disabled)),
656
+ [data-preview-source-option]:has(input:checked) { background: rgb(255 255 255 / 8%); }
657
+ [data-preview-source-option]:has(input:checked) { border-color: rgb(255 255 255 / 12%); }
658
+ [data-preview-source-option] input { accent-color: #f9fafb; }
659
+ [data-preview-source-submit] { color: #0f1115; background: #f9fafb; }
660
+ [data-preview-source-submit]:hover:not(:disabled) { background: #ebeef2; }
661
+ [data-preview-source-submit]:focus-visible { outline-color: rgb(255 255 255 / 20%); }
662
+ }
663
+ @media (max-width: 560px) {
664
+ [data-preview-source-card] { padding: 24px; }
665
+ [data-preview-source-submit] { width: 100%; }
666
+ }
667
+ @media (prefers-reduced-motion: reduce) {
668
+ [data-preview-source-option], [data-preview-source-submit] { transition: none; }
669
+ }
670
+ `;
671
+ const ENTITIES = {
672
+ "&": "&amp;",
673
+ "<": "&lt;",
674
+ ">": "&gt;",
675
+ "\"": "&quot;",
676
+ "'": "&#39;"
677
+ };
678
+ function escapeMarkup(value) {
679
+ return value.replace(/[&<>"']/g, (character) => ENTITIES[character] ?? character);
680
+ }
681
+ function optionMarkup(choice, selected) {
682
+ return `<label data-preview-source-option>
683
+ <input type="radio" name="preview-source" value="${choice.id}"${choice.id === selected ? " checked" : ""}${choice.disabled === true ? " disabled" : ""}>
684
+ <span>
685
+ <strong>${escapeMarkup(choice.label)}</strong>
686
+ <span>${escapeMarkup(choice.description)}</span>
687
+ </span>
688
+ </label>`;
689
+ }
690
+ function fixtureChoices(entries, manifestUrl) {
691
+ return entries.map((entry) => ({
692
+ id: entry.id,
693
+ label: entry.label,
694
+ description: entry.description,
695
+ overlays: entry.overlays.map((overlay) => new URL(overlay, manifestUrl))
696
+ }));
697
+ }
698
+ /**
699
+ * Render the source chooser and wait for an enabled selection.
700
+ * @param manifestUrl - Built-in fixture catalog URL.
701
+ * @returns Ordered overlay URLs selected for the Worker mount.
702
+ */
703
+ async function choosePreviewSource(manifestUrl) {
704
+ const requested = new URL(location.href).searchParams.get(PREVIEW_FIXTURE_QUERY);
705
+ if (requested === EMPTY_SOURCE) return [];
706
+ const response = await fetch(manifestUrl);
707
+ if (!response.ok) throw new Error(`preview source chooser: fixture manifest returned ${String(response.status)}`);
708
+ const manifest = parsePreviewFixtureManifest(await response.json());
709
+ const choices = [
710
+ {
711
+ id: EMPTY_SOURCE,
712
+ label: "Empty environment",
713
+ description: "Load only the base runtime to verify first launch and workspace creation.",
714
+ overlays: []
715
+ },
716
+ ...fixtureChoices(manifest.fixtures, manifestUrl),
717
+ {
718
+ id: WEBFS_SOURCE,
719
+ label: "WebFS directory",
720
+ description: "Requires directory access and will be available after the WebFS provider lands.",
721
+ overlays: [],
722
+ disabled: true
723
+ }
724
+ ];
725
+ if (requested !== null) {
726
+ const requestedChoice = choices.find((choice) => choice.id === requested && choice.disabled !== true);
727
+ if (requestedChoice === void 0) throw new Error(`preview source chooser: unknown or interactive source "${requested}"`);
728
+ return requestedChoice.overlays;
729
+ }
730
+ const root = document.getElementById("root");
731
+ if (root === null) throw new Error("preview source chooser: missing #root");
732
+ const selected = manifest.defaultFixture ?? EMPTY_SOURCE;
733
+ const style = document.createElement("style");
734
+ style.dataset.previewSourceStyle = "";
735
+ style.textContent = CHOOSER_STYLE;
736
+ document.head.append(style);
737
+ const chooser = document.createElement("main");
738
+ chooser.dataset.previewSourceChooser = "";
739
+ chooser.innerHTML = `<form data-preview-source-card aria-labelledby="preview-source-title">
740
+ <h1 id="preview-source-title">Choose Preview data</h1>
741
+ <p>Data mounts before the Worker and application start. Refresh to choose again.</p>
742
+ <fieldset>
743
+ <legend>Filesystem source</legend>
744
+ ${choices.map((choice) => optionMarkup(choice, selected)).join("")}
745
+ </fieldset>
746
+ <button data-preview-source-submit type="submit">Start Preview</button>
747
+ </form>`;
748
+ root.prepend(chooser);
749
+ const form = chooser.querySelector("[data-preview-source-card]");
750
+ if (form === null) throw new Error("preview source chooser: form was not rendered");
751
+ const sourceId = await new Promise((resolve, reject) => {
752
+ form.addEventListener("submit", (event) => {
753
+ event.preventDefault();
754
+ const value = new FormData(form).get("preview-source");
755
+ if (typeof value === "string") resolve(value);
756
+ else reject(/* @__PURE__ */ new Error("preview source chooser: no source selected"));
757
+ }, { once: true });
758
+ });
759
+ const choice = choices.find((candidate) => candidate.id === sourceId && candidate.disabled !== true);
760
+ if (choice === void 0) throw new Error(`preview source chooser: unavailable source "${sourceId}"`);
761
+ chooser.remove();
762
+ style.remove();
763
+ return choice.overlays;
764
+ }
765
+ //#endregion
766
+ //#region src/client/index.ts
767
+ /**
768
+ * Page half: everything a deployment needs to reach a worker-hosted harness.
769
+ *
770
+ * This is **pre-Cordis glue, not a client plugin**: it installs the transport
771
+ * global and executes the boot injection table that the client plugin graph
772
+ * is later loaded through, so it cannot itself be a graph row. A page imports
773
+ * it directly and decides where the worker bundle and image live; nothing
774
+ * here mounts into a shipped roster.
775
+ * @module @deepseek-ai/dsh-experimental-webworker-runtime/client
776
+ */
777
+ function bootReadyGate() {
778
+ return globalThis.__DSH_BOOT_READY__ ??= Promise.withResolvers();
779
+ }
780
+ /**
781
+ * Install the page boot barrier before an asynchronous source chooser waits
782
+ * for user input. The later {@link connectWorkerHost} call settles the same
783
+ * barrier.
784
+ */
785
+ function holdWorkerHostBoot() {
786
+ bootReadyGate().promise.catch(() => {});
787
+ }
788
+ /**
789
+ * Run the optional pre-boot source-selection stage. Calling this stage holds
790
+ * the stock shell until the caller passes its result to {@link connectWorkerHost};
791
+ * callers that need no chooser call `connectWorkerHost` directly and receive
792
+ * the base image with an empty overlay list.
793
+ * @param options - Base image and optional fixture-catalog locations.
794
+ * @returns The ordered overlays selected by the user.
795
+ */
796
+ async function chooseWorkerHostSource(options = {}) {
797
+ holdWorkerHostBoot();
798
+ const image = new URL(options.image ?? "vfs-image.tar.gz", document.baseURI);
799
+ const manifest = new URL(options.fixtureManifest ?? "fixtures.json", image);
800
+ try {
801
+ return { overlays: await choosePreviewSource(manifest) };
802
+ } catch (reason) {
803
+ bootReadyGate().reject(reason);
804
+ throw reason;
805
+ }
806
+ }
807
+ /**
808
+ * Connect a spawned host worker and complete the pre-Cordis handshake.
809
+ *
810
+ * The caller constructs the Worker so its bundler resolves the bundle URL
811
+ * statically; the opening `init` frame then carries the base image and ordered
812
+ * overlay locations.
813
+ *
814
+ * Order is fixed by the web boot protocol: the transport global must exist
815
+ * before any bundle executes; the injection table then reproduces the served
816
+ * boot rows — the `__ModuleLoader__` registration queue, the parser-preload
817
+ * bundles, `__DSH_BOOT__`, the theme bootstrap — in table order. The
818
+ * boot-readiness deferred (`__DSH_BOOT_READY__`) is installed before the
819
+ * first await and settles with the handshake, so a client entry evaluating
820
+ * concurrently in the same document holds at its pre-boot await until every
821
+ * row has taken effect, and surfaces a failed handshake instead of
822
+ * proceeding on missing globals.
823
+ * @param worker - The host worker.
824
+ * @param options - Base-image and overlay location overrides.
825
+ * @returns The connection; hand `loadBundle` to the shell entry's boot seam.
826
+ */
827
+ async function connectWorkerHost(worker, options) {
828
+ const ready = bootReadyGate();
829
+ ready.promise.catch(() => {});
830
+ try {
831
+ const tunnel = new WorkerTunnel(worker);
832
+ tunnel.init(new URL(options?.image ?? "vfs-image.tar.gz", document.baseURI).href, (options?.overlays ?? []).map((overlay) => new URL(overlay, document.baseURI).href));
833
+ const payload = await tunnel.bootPayload();
834
+ globalThis.__DSH_TRANSPORT__ = {
835
+ fetch: (input, init) => tunnel.fetch(input, init),
836
+ openStream: (endpoint, payload, signal) => tunnel.open(endpoint, payload, signal),
837
+ loadBundle: (url) => tunnel.loadBundle(url),
838
+ ownsHost: true
839
+ };
840
+ globalThis.__DSH_FILE_UPLOAD__ = { fetch: (input, init) => tunnel.fetch(input, init) };
841
+ await applyIndexInjections(payload.injections, (src) => tunnel.loadBundle(src));
842
+ ready.resolve();
843
+ return {
844
+ worker,
845
+ tunnel,
846
+ loadBundle: (url) => tunnel.loadBundle(url)
847
+ };
848
+ } catch (reason) {
849
+ ready.reject(reason);
850
+ throw reason;
851
+ }
852
+ }
853
+ //#endregion
854
+ export { IMAGE_FILE_NAME, PREVIEW_FIXTURE_MANIFEST_FILE, PREVIEW_FIXTURE_MANIFEST_VERSION, WorkerTunnel, applyIndexInjections, chooseWorkerHostSource, connectWorkerHost, parsePreviewFixtureManifest };