@run0/jiki 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (152) hide show
  1. package/dist/browser-bundle.d.ts +40 -0
  2. package/dist/builtins.d.ts +22 -0
  3. package/dist/code-transform.d.ts +7 -0
  4. package/dist/config/cdn.d.ts +13 -0
  5. package/dist/container.d.ts +101 -0
  6. package/dist/dev-server.d.ts +69 -0
  7. package/dist/errors.d.ts +19 -0
  8. package/dist/frameworks/code-transforms.d.ts +32 -0
  9. package/dist/frameworks/next-api-handler.d.ts +72 -0
  10. package/dist/frameworks/next-dev-server.d.ts +141 -0
  11. package/dist/frameworks/next-html-generator.d.ts +36 -0
  12. package/dist/frameworks/next-route-resolver.d.ts +19 -0
  13. package/dist/frameworks/next-shims.d.ts +78 -0
  14. package/dist/frameworks/remix-dev-server.d.ts +47 -0
  15. package/dist/frameworks/sveltekit-dev-server.d.ts +43 -0
  16. package/dist/frameworks/vite-dev-server.d.ts +50 -0
  17. package/dist/fs-errors.d.ts +36 -0
  18. package/dist/index.cjs +14916 -0
  19. package/dist/index.cjs.map +1 -0
  20. package/dist/index.d.ts +61 -0
  21. package/dist/index.mjs +14898 -0
  22. package/dist/index.mjs.map +1 -0
  23. package/dist/kernel.d.ts +48 -0
  24. package/dist/memfs.d.ts +144 -0
  25. package/dist/metrics.d.ts +78 -0
  26. package/dist/module-resolver.d.ts +60 -0
  27. package/dist/network-interceptor.d.ts +71 -0
  28. package/dist/npm/cache.d.ts +76 -0
  29. package/dist/npm/index.d.ts +60 -0
  30. package/dist/npm/lockfile-reader.d.ts +32 -0
  31. package/dist/npm/pnpm.d.ts +18 -0
  32. package/dist/npm/registry.d.ts +45 -0
  33. package/dist/npm/resolver.d.ts +39 -0
  34. package/dist/npm/sync-installer.d.ts +18 -0
  35. package/dist/npm/tarball.d.ts +4 -0
  36. package/dist/npm/workspaces.d.ts +46 -0
  37. package/dist/persistence.d.ts +94 -0
  38. package/dist/plugin.d.ts +156 -0
  39. package/dist/polyfills/assert.d.ts +30 -0
  40. package/dist/polyfills/child_process.d.ts +116 -0
  41. package/dist/polyfills/chokidar.d.ts +18 -0
  42. package/dist/polyfills/crypto.d.ts +49 -0
  43. package/dist/polyfills/events.d.ts +28 -0
  44. package/dist/polyfills/fs.d.ts +82 -0
  45. package/dist/polyfills/http.d.ts +147 -0
  46. package/dist/polyfills/module.d.ts +29 -0
  47. package/dist/polyfills/net.d.ts +53 -0
  48. package/dist/polyfills/os.d.ts +91 -0
  49. package/dist/polyfills/path.d.ts +96 -0
  50. package/dist/polyfills/perf_hooks.d.ts +21 -0
  51. package/dist/polyfills/process.d.ts +99 -0
  52. package/dist/polyfills/querystring.d.ts +15 -0
  53. package/dist/polyfills/readdirp.d.ts +18 -0
  54. package/dist/polyfills/readline.d.ts +32 -0
  55. package/dist/polyfills/stream.d.ts +106 -0
  56. package/dist/polyfills/stubs.d.ts +737 -0
  57. package/dist/polyfills/tty.d.ts +25 -0
  58. package/dist/polyfills/url.d.ts +41 -0
  59. package/dist/polyfills/util.d.ts +61 -0
  60. package/dist/polyfills/v8.d.ts +43 -0
  61. package/dist/polyfills/vm.d.ts +76 -0
  62. package/dist/polyfills/worker-threads.d.ts +77 -0
  63. package/dist/polyfills/ws.d.ts +32 -0
  64. package/dist/polyfills/zlib.d.ts +87 -0
  65. package/dist/runtime-helpers.d.ts +4 -0
  66. package/dist/runtime-interface.d.ts +39 -0
  67. package/dist/sandbox.d.ts +69 -0
  68. package/dist/server-bridge.d.ts +55 -0
  69. package/dist/shell-commands.d.ts +2 -0
  70. package/dist/shell.d.ts +101 -0
  71. package/dist/transpiler.d.ts +47 -0
  72. package/dist/type-checker.d.ts +57 -0
  73. package/dist/types/package-json.d.ts +17 -0
  74. package/dist/utils/binary-encoding.d.ts +4 -0
  75. package/dist/utils/hash.d.ts +6 -0
  76. package/dist/utils/safe-path.d.ts +6 -0
  77. package/dist/worker-runtime.d.ts +34 -0
  78. package/package.json +59 -0
  79. package/src/browser-bundle.ts +498 -0
  80. package/src/builtins.ts +222 -0
  81. package/src/code-transform.ts +183 -0
  82. package/src/config/cdn.ts +17 -0
  83. package/src/container.ts +343 -0
  84. package/src/dev-server.ts +322 -0
  85. package/src/errors.ts +604 -0
  86. package/src/frameworks/code-transforms.ts +667 -0
  87. package/src/frameworks/next-api-handler.ts +366 -0
  88. package/src/frameworks/next-dev-server.ts +1252 -0
  89. package/src/frameworks/next-html-generator.ts +585 -0
  90. package/src/frameworks/next-route-resolver.ts +521 -0
  91. package/src/frameworks/next-shims.ts +1084 -0
  92. package/src/frameworks/remix-dev-server.ts +163 -0
  93. package/src/frameworks/sveltekit-dev-server.ts +197 -0
  94. package/src/frameworks/vite-dev-server.ts +370 -0
  95. package/src/fs-errors.ts +118 -0
  96. package/src/index.ts +188 -0
  97. package/src/kernel.ts +381 -0
  98. package/src/memfs.ts +1006 -0
  99. package/src/metrics.ts +140 -0
  100. package/src/module-resolver.ts +511 -0
  101. package/src/network-interceptor.ts +143 -0
  102. package/src/npm/cache.ts +172 -0
  103. package/src/npm/index.ts +377 -0
  104. package/src/npm/lockfile-reader.ts +105 -0
  105. package/src/npm/pnpm.ts +108 -0
  106. package/src/npm/registry.ts +120 -0
  107. package/src/npm/resolver.ts +339 -0
  108. package/src/npm/sync-installer.ts +217 -0
  109. package/src/npm/tarball.ts +136 -0
  110. package/src/npm/workspaces.ts +255 -0
  111. package/src/persistence.ts +235 -0
  112. package/src/plugin.ts +293 -0
  113. package/src/polyfills/assert.ts +164 -0
  114. package/src/polyfills/child_process.ts +535 -0
  115. package/src/polyfills/chokidar.ts +52 -0
  116. package/src/polyfills/crypto.ts +433 -0
  117. package/src/polyfills/events.ts +178 -0
  118. package/src/polyfills/fs.ts +297 -0
  119. package/src/polyfills/http.ts +478 -0
  120. package/src/polyfills/module.ts +97 -0
  121. package/src/polyfills/net.ts +123 -0
  122. package/src/polyfills/os.ts +108 -0
  123. package/src/polyfills/path.ts +169 -0
  124. package/src/polyfills/perf_hooks.ts +30 -0
  125. package/src/polyfills/process.ts +349 -0
  126. package/src/polyfills/querystring.ts +66 -0
  127. package/src/polyfills/readdirp.ts +72 -0
  128. package/src/polyfills/readline.ts +80 -0
  129. package/src/polyfills/stream.ts +610 -0
  130. package/src/polyfills/stubs.ts +600 -0
  131. package/src/polyfills/tty.ts +43 -0
  132. package/src/polyfills/url.ts +97 -0
  133. package/src/polyfills/util.ts +173 -0
  134. package/src/polyfills/v8.ts +62 -0
  135. package/src/polyfills/vm.ts +111 -0
  136. package/src/polyfills/worker-threads.ts +189 -0
  137. package/src/polyfills/ws.ts +73 -0
  138. package/src/polyfills/zlib.ts +244 -0
  139. package/src/runtime-helpers.ts +83 -0
  140. package/src/runtime-interface.ts +46 -0
  141. package/src/sandbox.ts +178 -0
  142. package/src/server-bridge.ts +473 -0
  143. package/src/service-worker.ts +153 -0
  144. package/src/shell-commands.ts +708 -0
  145. package/src/shell.ts +795 -0
  146. package/src/transpiler.ts +282 -0
  147. package/src/type-checker.ts +241 -0
  148. package/src/types/package-json.ts +17 -0
  149. package/src/utils/binary-encoding.ts +38 -0
  150. package/src/utils/hash.ts +24 -0
  151. package/src/utils/safe-path.ts +38 -0
  152. package/src/worker-runtime.ts +42 -0
@@ -0,0 +1,473 @@
1
+ /**
2
+ * Server Bridge
3
+ * Connects Service Worker requests to virtual HTTP servers running in-memory.
4
+ * Provides the illusion of real HTTP by intercepting /__virtual__/{port}/* URLs.
5
+ */
6
+
7
+ import { EventEmitter } from "./polyfills/events";
8
+ import { BufferImpl as Buffer } from "./polyfills/stream";
9
+ import { uint8ToBase64 } from "./utils/binary-encoding";
10
+ import type { ResponseData } from "./dev-server";
11
+
12
+ const _encoder = new TextEncoder();
13
+
14
+ export interface IVirtualServer {
15
+ handleRequest(
16
+ method: string,
17
+ url: string,
18
+ headers: Record<string, string>,
19
+ body?: Buffer | string,
20
+ ): Promise<ResponseData>;
21
+ handleStreamingRequest?(
22
+ method: string,
23
+ url: string,
24
+ headers: Record<string, string>,
25
+ body: Buffer | undefined,
26
+ onStart: (
27
+ statusCode: number,
28
+ statusMessage: string,
29
+ headers: Record<string, string>,
30
+ ) => void,
31
+ onChunk: (chunk: string | Uint8Array) => void,
32
+ onEnd: () => void,
33
+ ): Promise<void>;
34
+ }
35
+
36
+ export interface VirtualServer {
37
+ server: IVirtualServer;
38
+ port: number;
39
+ hostname: string;
40
+ }
41
+
42
+ export interface BridgeOptions {
43
+ baseUrl?: string;
44
+ onServerReady?: (port: number, url: string) => void;
45
+ }
46
+
47
+ export interface InitServiceWorkerOptions {
48
+ swUrl?: string;
49
+ }
50
+
51
+ /** A queued request waiting for a server to become available on a port. */
52
+ interface QueuedRequest {
53
+ port: number;
54
+ method: string;
55
+ url: string;
56
+ headers: Record<string, string>;
57
+ body?: ArrayBuffer;
58
+ resolve: (response: ResponseData) => void;
59
+ reject: (error: Error) => void;
60
+ timer: ReturnType<typeof setTimeout>;
61
+ }
62
+
63
+ export class ServerBridge extends EventEmitter {
64
+ static DEBUG = false;
65
+ /** Maximum time (ms) a queued request will wait for a server to become available. */
66
+ static REQUEST_QUEUE_TIMEOUT = 10_000;
67
+ private servers: Map<number, VirtualServer> = new Map();
68
+ private baseUrl: string;
69
+ private options: BridgeOptions;
70
+ private messageChannel: MessageChannel | null = null;
71
+ private serviceWorkerReady = false;
72
+ private keepaliveInterval: ReturnType<typeof setInterval> | null = null;
73
+ /** Requests queued while waiting for a server to register on a given port. */
74
+ private requestQueue: QueuedRequest[] = [];
75
+
76
+ constructor(options: BridgeOptions = {}) {
77
+ super();
78
+ this.options = options;
79
+
80
+ if (typeof location !== "undefined") {
81
+ this.baseUrl =
82
+ options.baseUrl || `${location.protocol}//${location.host}`;
83
+ } else {
84
+ this.baseUrl = options.baseUrl || "http://localhost";
85
+ }
86
+ }
87
+
88
+ registerServer(
89
+ server: IVirtualServer,
90
+ port: number,
91
+ hostname = "0.0.0.0",
92
+ ): void {
93
+ this.servers.set(port, { server, port, hostname });
94
+
95
+ const url = this.getServerUrl(port);
96
+ this.emit("server-ready", port, url);
97
+ this.options.onServerReady?.(port, url);
98
+
99
+ this.notifyServiceWorker("server-registered", { port, hostname });
100
+
101
+ // Drain queued requests waiting for this port
102
+ this.drainQueue(port);
103
+ }
104
+
105
+ /**
106
+ * Process any queued requests waiting for a server on the given port.
107
+ */
108
+ private drainQueue(port: number): void {
109
+ const pending = this.requestQueue.filter(q => q.port === port);
110
+ this.requestQueue = this.requestQueue.filter(q => q.port !== port);
111
+
112
+ for (const req of pending) {
113
+ clearTimeout(req.timer);
114
+ this.handleRequest(req.port, req.method, req.url, req.headers, req.body)
115
+ .then(req.resolve)
116
+ .catch(req.reject);
117
+ }
118
+ }
119
+
120
+ unregisterServer(port: number): void {
121
+ this.servers.delete(port);
122
+ this.notifyServiceWorker("server-unregistered", { port });
123
+ }
124
+
125
+ getServerUrl(port: number): string {
126
+ return `${this.baseUrl}/__virtual__/${port}`;
127
+ }
128
+
129
+ getServerPorts(): number[] {
130
+ return [...this.servers.keys()];
131
+ }
132
+
133
+ async handleRequest(
134
+ port: number,
135
+ method: string,
136
+ url: string,
137
+ headers: Record<string, string>,
138
+ body?: ArrayBuffer,
139
+ ): Promise<ResponseData> {
140
+ const entry = this.servers.get(port);
141
+ if (!entry) {
142
+ // Queue the request and wait for a server to register on this port
143
+ return new Promise<ResponseData>((resolve, reject) => {
144
+ const timer = setTimeout(() => {
145
+ // Remove from queue on timeout
146
+ this.requestQueue = this.requestQueue.filter(q => q !== queued);
147
+ resolve({
148
+ statusCode: 503,
149
+ statusMessage: "Service Unavailable",
150
+ headers: { "Content-Type": "text/plain" },
151
+ body: Buffer.from(
152
+ `No server listening on port ${port} (timed out after ${ServerBridge.REQUEST_QUEUE_TIMEOUT}ms)`,
153
+ ),
154
+ });
155
+ }, ServerBridge.REQUEST_QUEUE_TIMEOUT);
156
+
157
+ const queued: QueuedRequest = {
158
+ port,
159
+ method,
160
+ url,
161
+ headers,
162
+ body,
163
+ resolve,
164
+ reject,
165
+ timer,
166
+ };
167
+ this.requestQueue.push(queued);
168
+ });
169
+ }
170
+
171
+ try {
172
+ const bodyBuffer = body ? Buffer.from(new Uint8Array(body)) : undefined;
173
+ return await entry.server.handleRequest(method, url, headers, bodyBuffer);
174
+ } catch (error) {
175
+ const msg =
176
+ error instanceof Error ? error.message : "Internal Server Error";
177
+ return {
178
+ statusCode: 500,
179
+ statusMessage: "Internal Server Error",
180
+ headers: { "Content-Type": "text/plain" },
181
+ body: Buffer.from(msg),
182
+ };
183
+ }
184
+ }
185
+
186
+ async initServiceWorker(options?: InitServiceWorkerOptions): Promise<void> {
187
+ if (typeof navigator === "undefined" || !("serviceWorker" in navigator)) {
188
+ throw new Error("Service Workers not supported");
189
+ }
190
+
191
+ const swUrl = options?.swUrl ?? "/__sw__.js";
192
+
193
+ const controllerReady = navigator.serviceWorker.controller
194
+ ? Promise.resolve()
195
+ : new Promise<void>(resolve => {
196
+ navigator.serviceWorker.addEventListener(
197
+ "controllerchange",
198
+ () => resolve(),
199
+ { once: true },
200
+ );
201
+ });
202
+
203
+ const registration = await navigator.serviceWorker.register(swUrl, {
204
+ scope: "/",
205
+ });
206
+
207
+ const sw =
208
+ registration.active || registration.waiting || registration.installing;
209
+ if (!sw) throw new Error("Service Worker registration failed");
210
+
211
+ await new Promise<void>(resolve => {
212
+ if (sw.state === "activated") {
213
+ resolve();
214
+ } else {
215
+ const handler = () => {
216
+ if (sw.state === "activated") {
217
+ sw.removeEventListener("statechange", handler);
218
+ resolve();
219
+ }
220
+ };
221
+ sw.addEventListener("statechange", handler);
222
+ }
223
+ });
224
+
225
+ this.messageChannel = new MessageChannel();
226
+ this.messageChannel.port1.onmessage =
227
+ this.handleServiceWorkerMessage.bind(this);
228
+
229
+ sw.postMessage({ type: "init", port: this.messageChannel.port2 }, [
230
+ this.messageChannel.port2,
231
+ ]);
232
+
233
+ await controllerReady;
234
+
235
+ const reinit = () => {
236
+ if (navigator.serviceWorker.controller) {
237
+ this.messageChannel = new MessageChannel();
238
+ this.messageChannel.port1.onmessage =
239
+ this.handleServiceWorkerMessage.bind(this);
240
+ navigator.serviceWorker.controller.postMessage(
241
+ { type: "init", port: this.messageChannel.port2 },
242
+ [this.messageChannel.port2],
243
+ );
244
+ }
245
+ };
246
+ navigator.serviceWorker.addEventListener("controllerchange", reinit);
247
+ navigator.serviceWorker.addEventListener("message", event => {
248
+ if (event.data?.type === "sw-needs-init") reinit();
249
+ });
250
+
251
+ this.keepaliveInterval = setInterval(() => {
252
+ this.messageChannel?.port1.postMessage({ type: "keepalive" });
253
+ }, 20_000);
254
+
255
+ this.serviceWorkerReady = true;
256
+ this.emit("sw-ready");
257
+ }
258
+
259
+ private async handleServiceWorkerMessage(event: MessageEvent): Promise<void> {
260
+ const { type, id, data } = event.data;
261
+
262
+ if (type === "request") {
263
+ const { port, method, url, headers, body, streaming } = data;
264
+ try {
265
+ if (streaming) {
266
+ await this.handleStreamingBridge(
267
+ id,
268
+ port,
269
+ method,
270
+ url,
271
+ headers,
272
+ body,
273
+ );
274
+ } else {
275
+ const response = await this.handleRequest(
276
+ port,
277
+ method,
278
+ url,
279
+ headers,
280
+ body,
281
+ );
282
+ let bodyBase64 = "";
283
+ if (response.body && response.body.length > 0) {
284
+ const bytes =
285
+ response.body instanceof Uint8Array
286
+ ? response.body
287
+ : new Uint8Array(0);
288
+ bodyBase64 = uint8ToBase64(bytes);
289
+ }
290
+ this.messageChannel?.port1.postMessage({
291
+ type: "response",
292
+ id,
293
+ data: {
294
+ statusCode: response.statusCode,
295
+ statusMessage: response.statusMessage,
296
+ headers: response.headers,
297
+ bodyBase64,
298
+ },
299
+ });
300
+ }
301
+ } catch (error) {
302
+ this.messageChannel?.port1.postMessage({
303
+ type: "response",
304
+ id,
305
+ error: error instanceof Error ? error.message : "Unknown error",
306
+ });
307
+ }
308
+ }
309
+ }
310
+
311
+ private async handleStreamingBridge(
312
+ id: number,
313
+ port: number,
314
+ method: string,
315
+ url: string,
316
+ headers: Record<string, string>,
317
+ body?: ArrayBuffer,
318
+ ): Promise<void> {
319
+ const entry = this.servers.get(port);
320
+ if (!entry) {
321
+ this.messageChannel?.port1.postMessage({
322
+ type: "stream-start",
323
+ id,
324
+ data: {
325
+ statusCode: 503,
326
+ statusMessage: "Service Unavailable",
327
+ headers: {},
328
+ },
329
+ });
330
+ this.messageChannel?.port1.postMessage({ type: "stream-end", id });
331
+ return;
332
+ }
333
+
334
+ const bodyBuffer = body ? Buffer.from(new Uint8Array(body)) : undefined;
335
+
336
+ if (typeof entry.server.handleStreamingRequest === "function") {
337
+ await entry.server.handleStreamingRequest(
338
+ method,
339
+ url,
340
+ headers,
341
+ bodyBuffer,
342
+ (statusCode, statusMessage, respHeaders) => {
343
+ this.messageChannel?.port1.postMessage({
344
+ type: "stream-start",
345
+ id,
346
+ data: { statusCode, statusMessage, headers: respHeaders },
347
+ });
348
+ },
349
+ chunk => {
350
+ const bytes =
351
+ typeof chunk === "string" ? _encoder.encode(chunk) : chunk;
352
+ this.messageChannel?.port1.postMessage({
353
+ type: "stream-chunk",
354
+ id,
355
+ data: { chunkBase64: uint8ToBase64(bytes) },
356
+ });
357
+ },
358
+ () => {
359
+ this.messageChannel?.port1.postMessage({
360
+ type: "stream-end",
361
+ id,
362
+ });
363
+ },
364
+ );
365
+ } else {
366
+ const response = await entry.server.handleRequest(
367
+ method,
368
+ url,
369
+ headers,
370
+ bodyBuffer,
371
+ );
372
+ this.messageChannel?.port1.postMessage({
373
+ type: "stream-start",
374
+ id,
375
+ data: {
376
+ statusCode: response.statusCode,
377
+ statusMessage: response.statusMessage,
378
+ headers: response.headers,
379
+ },
380
+ });
381
+ if (response.body && response.body.length > 0) {
382
+ const bytes =
383
+ response.body instanceof Uint8Array
384
+ ? response.body
385
+ : new Uint8Array(0);
386
+ this.messageChannel?.port1.postMessage({
387
+ type: "stream-chunk",
388
+ id,
389
+ data: { chunkBase64: uint8ToBase64(bytes) },
390
+ });
391
+ }
392
+ this.messageChannel?.port1.postMessage({ type: "stream-end", id });
393
+ }
394
+ }
395
+
396
+ createFetchHandler(): (request: Request) => Promise<Response> {
397
+ return async (request: Request): Promise<Response> => {
398
+ const url = new URL(request.url);
399
+ const match = url.pathname.match(/^\/__virtual__\/(\d+)(\/.*)?$/);
400
+ if (!match) throw new Error("Not a virtual server request");
401
+
402
+ const port = parseInt(match[1], 10);
403
+ const path = match[2] || "/";
404
+
405
+ const headers: Record<string, string> = {};
406
+ request.headers.forEach((value, key) => {
407
+ headers[key] = value;
408
+ });
409
+
410
+ let body: ArrayBuffer | undefined;
411
+ if (request.method !== "GET" && request.method !== "HEAD") {
412
+ body = await request.arrayBuffer();
413
+ }
414
+
415
+ const response = await this.handleRequest(
416
+ port,
417
+ request.method,
418
+ path + url.search,
419
+ headers,
420
+ body,
421
+ );
422
+
423
+ return new Response(response.body, {
424
+ status: response.statusCode,
425
+ statusText: response.statusMessage,
426
+ headers: response.headers,
427
+ });
428
+ };
429
+ }
430
+
431
+ private notifyServiceWorker(type: string, data: unknown): void {
432
+ if (this.serviceWorkerReady && this.messageChannel) {
433
+ this.messageChannel.port1.postMessage({ type, data });
434
+ }
435
+ }
436
+
437
+ dispose(): void {
438
+ if (this.keepaliveInterval) {
439
+ clearInterval(this.keepaliveInterval);
440
+ this.keepaliveInterval = null;
441
+ }
442
+ // Clear any queued requests
443
+ for (const req of this.requestQueue) {
444
+ clearTimeout(req.timer);
445
+ req.resolve({
446
+ statusCode: 503,
447
+ statusMessage: "Service Unavailable",
448
+ headers: { "Content-Type": "text/plain" },
449
+ body: Buffer.from("Server bridge disposed"),
450
+ });
451
+ }
452
+ this.requestQueue = [];
453
+ this.servers.clear();
454
+ this.serviceWorkerReady = false;
455
+ this.messageChannel = null;
456
+ }
457
+ }
458
+
459
+ let globalBridge: ServerBridge | null = null;
460
+
461
+ export function getServerBridge(options?: BridgeOptions): ServerBridge {
462
+ if (!globalBridge) {
463
+ globalBridge = new ServerBridge(options);
464
+ }
465
+ return globalBridge;
466
+ }
467
+
468
+ export function resetServerBridge(): void {
469
+ if (globalBridge) {
470
+ globalBridge.dispose();
471
+ globalBridge = null;
472
+ }
473
+ }
@@ -0,0 +1,153 @@
1
+ /**
2
+ * Service Worker for jiki virtual server bridge.
3
+ * Intercepts fetch requests to /__virtual__/{port}/* and routes them
4
+ * to the main thread via MessageChannel.
5
+ *
6
+ * Build this file separately and serve it at the swUrl configured
7
+ * in ServerBridge.initServiceWorker() (default: /__sw__.js).
8
+ */
9
+
10
+ declare const self: ServiceWorkerGlobalScope;
11
+
12
+ let mainPort: MessagePort | null = null;
13
+ let requestId = 0;
14
+ const pendingRequests = new Map<
15
+ number,
16
+ {
17
+ resolve: (resp: Response) => void;
18
+ reject: (err: Error) => void;
19
+ controller?: ReadableStreamDefaultController<Uint8Array>;
20
+ }
21
+ >();
22
+
23
+ const VIRTUAL_PREFIX = "/__virtual__/";
24
+
25
+ function base64ToUint8(base64: string): Uint8Array {
26
+ const binary = atob(base64);
27
+ const bytes = new Uint8Array(binary.length);
28
+ for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
29
+ return bytes;
30
+ }
31
+
32
+ self.addEventListener("install", () => {
33
+ (self as unknown as { skipWaiting(): Promise<void> }).skipWaiting();
34
+ });
35
+
36
+ self.addEventListener("activate", event => {
37
+ event.waitUntil((self as unknown as { clients: Clients }).clients.claim());
38
+ });
39
+
40
+ self.addEventListener("message", (event: ExtendableMessageEvent) => {
41
+ if (event.data?.type === "init" && event.data.port) {
42
+ mainPort = event.data.port as MessagePort;
43
+ mainPort.onmessage = handleMainMessage;
44
+ }
45
+ if (event.data?.type === "keepalive") {
46
+ /* keep-alive ping, nothing to do */
47
+ }
48
+ });
49
+
50
+ function handleMainMessage(event: MessageEvent): void {
51
+ const { type, id, data, error } = event.data;
52
+
53
+ const pending = pendingRequests.get(id);
54
+ if (!pending) return;
55
+
56
+ if (type === "response") {
57
+ if (error) {
58
+ pending.reject(new Error(error));
59
+ pendingRequests.delete(id);
60
+ return;
61
+ }
62
+ const { statusCode, statusMessage, headers, bodyBase64 } = data;
63
+ const body = bodyBase64 ? base64ToUint8(bodyBase64) : new Uint8Array(0);
64
+ pending.resolve(
65
+ new Response(body, {
66
+ status: statusCode,
67
+ statusText: statusMessage,
68
+ headers,
69
+ }),
70
+ );
71
+ pendingRequests.delete(id);
72
+ } else if (type === "stream-start") {
73
+ const { statusCode, statusMessage, headers: respHeaders } = data;
74
+ const stream = new ReadableStream<Uint8Array>({
75
+ start(controller) {
76
+ pending.controller = controller;
77
+ },
78
+ });
79
+ pending.resolve(
80
+ new Response(stream, {
81
+ status: statusCode,
82
+ statusText: statusMessage,
83
+ headers: respHeaders,
84
+ }),
85
+ );
86
+ } else if (type === "stream-chunk") {
87
+ const chunk = base64ToUint8(data.chunkBase64);
88
+ pending.controller?.enqueue(chunk);
89
+ } else if (type === "stream-end") {
90
+ pending.controller?.close();
91
+ pendingRequests.delete(id);
92
+ }
93
+ }
94
+
95
+ self.addEventListener("fetch", (event: FetchEvent) => {
96
+ const url = new URL(event.request.url);
97
+ if (!url.pathname.startsWith(VIRTUAL_PREFIX)) return;
98
+
99
+ if (!mainPort) {
100
+ (self as unknown as { clients: Clients }).clients
101
+ .matchAll()
102
+ .then((clients: readonly Client[]) => {
103
+ for (const client of clients) {
104
+ client.postMessage({ type: "sw-needs-init" });
105
+ }
106
+ });
107
+ event.respondWith(
108
+ new Response("Service Worker not connected", { status: 503 }),
109
+ );
110
+ return;
111
+ }
112
+
113
+ const match = url.pathname.match(/^\/__virtual__\/(\d+)(\/.*)?$/);
114
+ if (!match) return;
115
+
116
+ const port = parseInt(match[1], 10);
117
+ const path = (match[2] || "/") + url.search;
118
+
119
+ const headers: Record<string, string> = {};
120
+ event.request.headers.forEach((value, key) => {
121
+ headers[key] = value;
122
+ });
123
+
124
+ const isStreamable =
125
+ headers.accept?.includes("text/event-stream") ||
126
+ headers.accept?.includes("application/x-ndjson");
127
+
128
+ event.respondWith(
129
+ (async () => {
130
+ const id = ++requestId;
131
+ let body: ArrayBuffer | undefined;
132
+ if (event.request.method !== "GET" && event.request.method !== "HEAD") {
133
+ body = await event.request.arrayBuffer();
134
+ }
135
+
136
+ return new Promise<Response>((resolve, reject) => {
137
+ pendingRequests.set(id, { resolve, reject });
138
+ mainPort!.postMessage({
139
+ type: "request",
140
+ id,
141
+ data: {
142
+ port,
143
+ method: event.request.method,
144
+ url: path,
145
+ headers,
146
+ body,
147
+ streaming: isStreamable,
148
+ },
149
+ });
150
+ });
151
+ })(),
152
+ );
153
+ });