-16
package-lock.json
-16
package-lock.json
···
22
"react": "19.2.3",
23
"react-dom": "19.2.3",
24
"react-server-dom-webpack": "19.2.3",
25
-
"text-encoding": "^0.7.0",
26
"web-streams-polyfill": "^4.2.0"
27
},
28
"devDependencies": {
···
33
"@types/node": "^25.0.2",
34
"@types/react": "^19.2.7",
35
"@types/react-dom": "^19.2.3",
36
-
"@types/text-encoding": "^0.0.40",
37
"@vitejs/plugin-react": "^5.1.2",
38
"@vitest/browser": "^4.0.15",
39
"@vitest/browser-playwright": "^4.0.15",
···
1876
"peerDependencies": {
1877
"@types/react": "^19.2.0"
1878
}
1879
-
},
1880
-
"node_modules/@types/text-encoding": {
1881
-
"version": "0.0.40",
1882
-
"resolved": "https://registry.npmjs.org/@types/text-encoding/-/text-encoding-0.0.40.tgz",
1883
-
"integrity": "sha512-dHzoIdwBfY7jcSTTt6XBkaeiuFQAQD7r/7aJySKDdHkYBCDOvs9jPVt4NYXuwBMn89PP6gSd29WubIS19wTiXg==",
1884
-
"dev": true,
1885
-
"license": "MIT"
1886
},
1887
"node_modules/@typescript-eslint/eslint-plugin": {
1888
"version": "8.50.0",
···
5924
"optional": true
5925
}
5926
}
5927
-
},
5928
-
"node_modules/text-encoding": {
5929
-
"version": "0.7.0",
5930
-
"resolved": "https://registry.npmjs.org/text-encoding/-/text-encoding-0.7.0.tgz",
5931
-
"integrity": "sha512-oJQ3f1hrOnbRLOcwKz0Liq2IcrvDeZRHXhd9RgLrsT+DjWY/nty1Hi7v3dtkaEYbPYe0mUoOfzRrMwfXXwgPUA==",
5932
-
"deprecated": "no longer maintained",
5933
-
"license": "(Unlicense OR Apache-2.0)"
5934
},
5935
"node_modules/through": {
5936
"version": "2.3.8",
···
22
"react": "19.2.3",
23
"react-dom": "19.2.3",
24
"react-server-dom-webpack": "19.2.3",
25
"web-streams-polyfill": "^4.2.0"
26
},
27
"devDependencies": {
···
32
"@types/node": "^25.0.2",
33
"@types/react": "^19.2.7",
34
"@types/react-dom": "^19.2.3",
35
"@vitejs/plugin-react": "^5.1.2",
36
"@vitest/browser": "^4.0.15",
37
"@vitest/browser-playwright": "^4.0.15",
···
1874
"peerDependencies": {
1875
"@types/react": "^19.2.0"
1876
}
1877
},
1878
"node_modules/@typescript-eslint/eslint-plugin": {
1879
"version": "8.50.0",
···
5915
"optional": true
5916
}
5917
}
5918
},
5919
"node_modules/through": {
5920
"version": "2.3.8",
-2
package.json
-2
package.json
···
35
"react": "19.2.3",
36
"react-dom": "19.2.3",
37
"react-server-dom-webpack": "19.2.3",
38
-
"text-encoding": "^0.7.0",
39
"web-streams-polyfill": "^4.2.0"
40
},
41
"overrides": {
···
49
"@types/node": "^25.0.2",
50
"@types/react": "^19.2.7",
51
"@types/react-dom": "^19.2.3",
52
-
"@types/text-encoding": "^0.0.40",
53
"@vitejs/plugin-react": "^5.1.2",
54
"@vitest/browser": "^4.0.15",
55
"@vitest/browser-playwright": "^4.0.15",
···
35
"react": "19.2.3",
36
"react-dom": "19.2.3",
37
"react-server-dom-webpack": "19.2.3",
38
"web-streams-polyfill": "^4.2.0"
39
},
40
"overrides": {
···
48
"@types/node": "^25.0.2",
49
"@types/react": "^19.2.7",
50
"@types/react-dom": "^19.2.3",
51
"@vitejs/plugin-react": "^5.1.2",
52
"@vitest/browser": "^4.0.15",
53
"@vitest/browser-playwright": "^4.0.15",
-16
src/client/byte-stream-polyfill.ts
-16
src/client/byte-stream-polyfill.ts
···
1
-
// Safari doesn't implement ReadableByteStreamController.
2
-
// The standard web-streams-polyfill only polyfills ReadableStream, not byte streams.
3
-
// This adds the missing byte stream support.
4
-
5
-
import {
6
-
ReadableStream as PolyfillReadableStream,
7
-
ReadableByteStreamController as PolyfillReadableByteStreamController,
8
-
} from "web-streams-polyfill";
9
-
10
-
if (typeof globalThis.ReadableByteStreamController === "undefined") {
11
-
// Safari doesn't have byte stream support - use the polyfill's ReadableStream
12
-
// which includes full byte stream support
13
-
globalThis.ReadableStream = PolyfillReadableStream as typeof ReadableStream;
14
-
// @ts-expect-error: ReadableByteStreamController polyfill assignment
15
-
globalThis.ReadableByteStreamController = PolyfillReadableByteStreamController;
16
-
}
···
+2
-6
src/client/embed.tsx
+2
-6
src/client/embed.tsx
···
1
-
// Must be first - shims webpack globals for react-server-dom-webpack
2
-
import "./webpack-shim.ts";
3
-
4
-
import "./byte-stream-polyfill.ts";
5
-
import "web-streams-polyfill/polyfill";
6
-
import "text-encoding";
7
8
import React, { useState, useEffect } from "react";
9
import { createRoot } from "react-dom/client";
+2
-6
src/client/index.tsx
+2
-6
src/client/index.tsx
···
1
-
// Must be first - shims webpack globals for react-server-dom-webpack
2
-
import "./webpack-shim.ts";
3
-
4
-
import "./byte-stream-polyfill.ts";
5
-
import "web-streams-polyfill/polyfill";
6
-
import "text-encoding";
7
8
import { createRoot } from "react-dom/client";
9
import { App } from "./ui/App.tsx";
-272
src/client/server-worker.ts
-272
src/client/server-worker.ts
···
1
-
import workerUrl from "../server/worker.ts?rolldown-worker";
2
-
import type { ClientManifest } from "../shared/compiler.ts";
3
-
4
-
const randomUUID: () => string =
5
-
crypto.randomUUID?.bind(crypto) ??
6
-
function (): string {
7
-
const bytes = crypto.getRandomValues(new Uint8Array(16));
8
-
bytes[6] = (bytes[6]! & 0x0f) | 0x40; // version 4
9
-
bytes[8] = (bytes[8]! & 0x3f) | 0x80; // variant 1
10
-
const hex = [...bytes].map((b) => b.toString(16).padStart(2, "0"));
11
-
return `${hex.slice(0, 4).join("")}-${hex.slice(4, 6).join("")}-${hex.slice(6, 8).join("")}-${hex.slice(8, 10).join("")}-${hex.slice(10).join("")}`;
12
-
};
13
-
14
-
type EncodedArgsFormData = {
15
-
type: "formdata";
16
-
data: string;
17
-
};
18
-
19
-
type EncodedArgsString = {
20
-
type: "string";
21
-
data: string;
22
-
};
23
-
24
-
type EncodedArgsTransfer = EncodedArgsFormData | EncodedArgsString;
25
-
26
-
function serializeForTransfer(encoded: FormData | string): EncodedArgsTransfer {
27
-
if (encoded instanceof FormData) {
28
-
return {
29
-
type: "formdata",
30
-
data: new URLSearchParams(encoded as unknown as Record<string, string>).toString(),
31
-
};
32
-
}
33
-
return { type: "string", data: encoded };
34
-
}
35
-
36
-
type WorkerReadyMessage = {
37
-
type: "ready";
38
-
};
39
-
40
-
type WorkerStreamStartMessage = {
41
-
type: "stream-start";
42
-
requestId: string;
43
-
};
44
-
45
-
type WorkerStreamChunkMessage = {
46
-
type: "stream-chunk";
47
-
requestId: string;
48
-
chunk: Uint8Array;
49
-
};
50
-
51
-
type WorkerStreamEndMessage = {
52
-
type: "stream-end";
53
-
requestId: string;
54
-
};
55
-
56
-
type WorkerStreamErrorMessage = {
57
-
type: "stream-error";
58
-
requestId: string;
59
-
error: { message: string };
60
-
};
61
-
62
-
type WorkerDeployedMessage = {
63
-
type: "deployed";
64
-
requestId: string;
65
-
};
66
-
67
-
type WorkerErrorMessage = {
68
-
type: "error";
69
-
requestId: string;
70
-
error: { message: string; stack?: string };
71
-
};
72
-
73
-
type WorkerMessage =
74
-
| WorkerReadyMessage
75
-
| WorkerStreamStartMessage
76
-
| WorkerStreamChunkMessage
77
-
| WorkerStreamEndMessage
78
-
| WorkerStreamErrorMessage
79
-
| WorkerDeployedMessage
80
-
| WorkerErrorMessage;
81
-
82
-
type PendingRequest = {
83
-
resolve: (value: unknown) => void;
84
-
reject: (error: Error) => void;
85
-
};
86
-
87
-
export class ServerWorker {
88
-
private worker: Worker;
89
-
private pending: Map<string, PendingRequest> = new Map();
90
-
private streams: Map<string, ReadableStreamDefaultController<Uint8Array>> = new Map();
91
-
private readyPromise: Promise<void>;
92
-
private readyResolve!: () => void;
93
-
94
-
constructor() {
95
-
this.worker = new Worker(workerUrl);
96
-
this.readyPromise = new Promise((resolve) => {
97
-
this.readyResolve = resolve;
98
-
});
99
-
100
-
this.worker.onmessage = this.handleMessage.bind(this);
101
-
this.worker.onerror = this.handleError.bind(this);
102
-
}
103
-
104
-
private handleMessage(event: MessageEvent<WorkerMessage>): void {
105
-
const { type } = event.data;
106
-
107
-
if (type === "ready") {
108
-
this.readyResolve();
109
-
return;
110
-
}
111
-
112
-
if (type === "stream-start") {
113
-
const { requestId } = event.data;
114
-
const pending = this.pending.get(requestId);
115
-
if (!pending) return;
116
-
this.pending.delete(requestId);
117
-
118
-
let controller: ReadableStreamDefaultController<Uint8Array>;
119
-
const stream = new ReadableStream<Uint8Array>({
120
-
start: (c) => {
121
-
controller = c;
122
-
},
123
-
});
124
-
this.streams.set(requestId, controller!);
125
-
pending.resolve(stream);
126
-
return;
127
-
}
128
-
129
-
if (type === "stream-chunk") {
130
-
const { requestId, chunk } = event.data;
131
-
const controller = this.streams.get(requestId);
132
-
if (controller) controller.enqueue(chunk);
133
-
return;
134
-
}
135
-
136
-
if (type === "stream-end") {
137
-
const { requestId } = event.data;
138
-
const controller = this.streams.get(requestId);
139
-
if (controller) {
140
-
controller.close();
141
-
this.streams.delete(requestId);
142
-
}
143
-
return;
144
-
}
145
-
146
-
if (type === "stream-error") {
147
-
const { requestId, error } = event.data;
148
-
const controller = this.streams.get(requestId);
149
-
if (controller) {
150
-
controller.error(new Error(error.message));
151
-
this.streams.delete(requestId);
152
-
}
153
-
return;
154
-
}
155
-
156
-
if (type === "deployed") {
157
-
const { requestId } = event.data as WorkerDeployedMessage;
158
-
const pending = this.pending.get(requestId);
159
-
if (!pending) {
160
-
console.warn(`No pending request for ${requestId}`);
161
-
return;
162
-
}
163
-
this.pending.delete(requestId);
164
-
pending.resolve(undefined);
165
-
return;
166
-
}
167
-
168
-
if (type === "error") {
169
-
const { requestId, error } = event.data as WorkerErrorMessage;
170
-
const pending = this.pending.get(requestId);
171
-
if (!pending) {
172
-
console.warn(`No pending request for ${requestId}`);
173
-
return;
174
-
}
175
-
this.pending.delete(requestId);
176
-
const err = new Error(error.message);
177
-
if (error.stack) {
178
-
err.stack = error.stack;
179
-
}
180
-
pending.reject(err);
181
-
}
182
-
}
183
-
184
-
private handleError(event: ErrorEvent): void {
185
-
const errorMsg = event.message || "Unknown worker error";
186
-
console.error(`Worker error: ${errorMsg}`);
187
-
188
-
for (const [, pending] of this.pending) {
189
-
pending.reject(new Error(`Worker error: ${errorMsg}`));
190
-
}
191
-
this.pending.clear();
192
-
}
193
-
194
-
async deploy({
195
-
compiledCode,
196
-
manifest,
197
-
actionNames,
198
-
}: {
199
-
compiledCode: string;
200
-
manifest: ClientManifest;
201
-
actionNames: string[];
202
-
}): Promise<void> {
203
-
await this.readyPromise;
204
-
const requestId = randomUUID();
205
-
206
-
return new Promise((resolve, reject) => {
207
-
this.pending.set(requestId, { resolve: resolve as (value: unknown) => void, reject });
208
-
this.worker.postMessage({
209
-
type: "deploy",
210
-
requestId,
211
-
compiledCode,
212
-
manifest,
213
-
actionNames,
214
-
});
215
-
});
216
-
}
217
-
218
-
async render(): Promise<ReadableStream<Uint8Array>> {
219
-
await this.readyPromise;
220
-
const requestId = randomUUID();
221
-
222
-
return new Promise((resolve, reject) => {
223
-
this.pending.set(requestId, { resolve: resolve as (value: unknown) => void, reject });
224
-
this.worker.postMessage({ type: "render", requestId });
225
-
});
226
-
}
227
-
228
-
async callAction(
229
-
actionId: string,
230
-
encodedArgs: FormData | string,
231
-
): Promise<ReadableStream<Uint8Array>> {
232
-
await this.readyPromise;
233
-
const requestId = randomUUID();
234
-
235
-
return new Promise((resolve, reject) => {
236
-
this.pending.set(requestId, { resolve: resolve as (value: unknown) => void, reject });
237
-
this.worker.postMessage({
238
-
type: "action",
239
-
requestId,
240
-
actionId,
241
-
encodedArgs: serializeForTransfer(encodedArgs),
242
-
});
243
-
});
244
-
}
245
-
246
-
async callActionRaw(actionId: string, rawPayload: string): Promise<ReadableStream<Uint8Array>> {
247
-
await this.readyPromise;
248
-
const requestId = randomUUID();
249
-
250
-
return new Promise((resolve, reject) => {
251
-
this.pending.set(requestId, { resolve: resolve as (value: unknown) => void, reject });
252
-
this.worker.postMessage({
253
-
type: "action",
254
-
requestId,
255
-
actionId,
256
-
encodedArgs: { type: "formdata", data: rawPayload },
257
-
});
258
-
});
259
-
}
260
-
261
-
terminate(): void {
262
-
this.worker.terminate();
263
-
for (const [, pending] of this.pending) {
264
-
pending.reject(new Error("Worker terminated"));
265
-
}
266
-
this.pending.clear();
267
-
for (const [, controller] of this.streams) {
268
-
controller.error(new Error("Worker terminated"));
269
-
}
270
-
this.streams.clear();
271
-
}
272
-
}
···
+21
-14
src/client/ui/Workspace.tsx
+21
-14
src/client/ui/Workspace.tsx
···
7
evaluateClientModule,
8
type CallServerCallback,
9
} from "../runtime/index.ts";
10
-
import { ServerWorker } from "../server-worker.ts";
11
import {
12
parseClientModule,
13
parseServerActions,
···
35
}: WorkspaceProps): React.ReactElement {
36
const [serverCode, setServerCode] = useState(initialServerCode);
37
const [clientCode, setClientCode] = useState(initialClientCode);
38
-
const [serverWorker] = useState(() => new ServerWorker());
39
const [timeline] = useState(() => new Timeline());
40
const [callServerRef] = useState<CallServerRef>({ current: null });
41
42
const snapshot = useSyncExternalStore(timeline.subscribe, timeline.getSnapshot);
···
67
68
const handleAddRawAction = useCallback(
69
async (actionName: string, rawPayload: string) => {
70
try {
71
-
const responseRaw = await serverWorker.callActionRaw(actionName, rawPayload);
72
const streamOptions = callServerRef.current ? { callServer: callServerRef.current } : {};
73
const stream = new SteppableStream(responseRaw, streamOptions);
74
await stream.waitForBuffer();
···
77
console.error("[raw action] Failed:", err);
78
}
79
},
80
-
[serverWorker, timeline, callServerRef],
81
);
82
83
const compile = useCallback(
84
async (sCode: string, cCode: string) => {
85
try {
86
setError(null);
87
timeline.clear();
···
96
const compiledServer = compileToCommonJS(sCode);
97
setAvailableActions(actionNames);
98
99
-
await serverWorker.deploy({
100
-
compiledCode: compiledServer,
101
-
manifest,
102
-
actionNames,
103
-
});
104
105
const callServer: CallServerCallback | null =
106
actionNames.length > 0
···
114
encodedArgs as unknown as Record<string, string>,
115
).toString();
116
117
-
const responseRaw = await serverWorker.callAction(actionName, encodedArgs);
118
const stream = new SteppableStream(responseRaw, {
119
callServer: callServer as CallServerCallback,
120
});
···
126
127
callServerRef.current = callServer;
128
129
-
const renderRaw = await serverWorker.render();
130
const renderStreamOptions = callServer ? { callServer } : {};
131
const renderStream = new SteppableStream(renderRaw, renderStreamOptions);
132
await renderStream.waitForBuffer();
···
140
setClientModuleReady(false);
141
}
142
},
143
-
[timeline, serverWorker, callServerRef],
144
);
145
146
const handleReset = useCallback(() => {
···
157
}, [serverCode, clientCode, compile]);
158
159
useEffect(() => {
160
-
return () => serverWorker.terminate();
161
-
}, [serverWorker]);
162
163
return (
164
<main>
···
7
evaluateClientModule,
8
type CallServerCallback,
9
} from "../runtime/index.ts";
10
+
import { WorkerClient, encodeArgs } from "../worker-client.ts";
11
import {
12
parseClientModule,
13
parseServerActions,
···
35
}: WorkspaceProps): React.ReactElement {
36
const [serverCode, setServerCode] = useState(initialServerCode);
37
const [clientCode, setClientCode] = useState(initialClientCode);
38
const [timeline] = useState(() => new Timeline());
39
+
const [workerClient, setWorkerClient] = useState<WorkerClient | null>(null);
40
const [callServerRef] = useState<CallServerRef>({ current: null });
41
42
const snapshot = useSyncExternalStore(timeline.subscribe, timeline.getSnapshot);
···
67
68
const handleAddRawAction = useCallback(
69
async (actionName: string, rawPayload: string) => {
70
+
if (!workerClient) throw new Error("Worker not initialized");
71
try {
72
+
const responseRaw = await workerClient.callAction(actionName, {
73
+
type: "formdata",
74
+
data: rawPayload,
75
+
});
76
const streamOptions = callServerRef.current ? { callServer: callServerRef.current } : {};
77
const stream = new SteppableStream(responseRaw, streamOptions);
78
await stream.waitForBuffer();
···
81
console.error("[raw action] Failed:", err);
82
}
83
},
84
+
[workerClient, timeline, callServerRef],
85
);
86
87
const compile = useCallback(
88
async (sCode: string, cCode: string) => {
89
+
if (!workerClient) throw new Error("Worker not initialized");
90
try {
91
setError(null);
92
timeline.clear();
···
101
const compiledServer = compileToCommonJS(sCode);
102
setAvailableActions(actionNames);
103
104
+
await workerClient.deploy(compiledServer, manifest, actionNames);
105
106
const callServer: CallServerCallback | null =
107
actionNames.length > 0
···
115
encodedArgs as unknown as Record<string, string>,
116
).toString();
117
118
+
const responseRaw = await workerClient.callAction(
119
+
actionName,
120
+
encodeArgs(encodedArgs),
121
+
);
122
const stream = new SteppableStream(responseRaw, {
123
callServer: callServer as CallServerCallback,
124
});
···
130
131
callServerRef.current = callServer;
132
133
+
const renderRaw = await workerClient.render();
134
const renderStreamOptions = callServer ? { callServer } : {};
135
const renderStream = new SteppableStream(renderRaw, renderStreamOptions);
136
await renderStream.waitForBuffer();
···
144
setClientModuleReady(false);
145
}
146
},
147
+
[timeline, workerClient, callServerRef],
148
);
149
150
const handleReset = useCallback(() => {
···
161
}, [serverCode, clientCode, compile]);
162
163
useEffect(() => {
164
+
const client = new WorkerClient();
165
+
// eslint-disable-next-line react-hooks/set-state-in-effect
166
+
setWorkerClient(client);
167
+
return () => client.terminate();
168
+
}, []);
169
170
return (
171
<main>
-76
src/client/webpack-shim.ts
-76
src/client/webpack-shim.ts
···
1
-
// Shim webpack globals for react-server-dom-webpack/client in browser context
2
-
3
-
type ModuleFactory = {
4
-
(module: { exports: unknown }): void;
5
-
};
6
-
7
-
type WebpackRequire = {
8
-
(moduleId: string): unknown;
9
-
m: Record<string, ModuleFactory>;
10
-
c: Record<string, { exports: unknown } | unknown>;
11
-
d: (exports: object, definition: Record<string, () => unknown>) => void;
12
-
r: (exports: object) => void;
13
-
o: (obj: object, prop: string) => boolean;
14
-
e: (chunkId: string) => Promise<void>;
15
-
p: string;
16
-
};
17
-
18
-
const clientModuleCache: Record<string, { exports: unknown }> = {};
19
-
const clientModuleFactories: Record<string, ModuleFactory> = {};
20
-
21
-
window.__webpack_module_cache__ = clientModuleCache;
22
-
window.__webpack_modules__ = clientModuleFactories;
23
-
24
-
const clientWebpackRequire: WebpackRequire = function (moduleId: string): unknown {
25
-
const cached = clientModuleCache[moduleId];
26
-
if (cached) {
27
-
return cached.exports ?? cached;
28
-
}
29
-
const factory = clientModuleFactories[moduleId];
30
-
if (factory) {
31
-
const module: { exports: unknown } = { exports: {} };
32
-
factory(module);
33
-
clientModuleCache[moduleId] = module;
34
-
return module.exports;
35
-
}
36
-
throw new Error(`Module ${moduleId} not found in webpack shim`);
37
-
} as WebpackRequire;
38
-
39
-
clientWebpackRequire.m = clientModuleFactories;
40
-
clientWebpackRequire.c = clientModuleCache;
41
-
clientWebpackRequire.d = function (
42
-
exports: object,
43
-
definition: Record<string, () => unknown>,
44
-
): void {
45
-
for (const key in definition) {
46
-
const getter = definition[key];
47
-
if (
48
-
getter &&
49
-
Object.prototype.hasOwnProperty.call(definition, key) &&
50
-
!Object.prototype.hasOwnProperty.call(exports, key)
51
-
) {
52
-
Object.defineProperty(exports, key, { enumerable: true, get: getter });
53
-
}
54
-
}
55
-
};
56
-
clientWebpackRequire.r = function (exports: object): void {
57
-
if (typeof Symbol !== "undefined" && Symbol.toStringTag) {
58
-
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
59
-
}
60
-
Object.defineProperty(exports, "__esModule", { value: true });
61
-
};
62
-
clientWebpackRequire.o = function (obj: object, prop: string): boolean {
63
-
return Object.prototype.hasOwnProperty.call(obj, prop);
64
-
};
65
-
clientWebpackRequire.e = function (_chunkId: string): Promise<void> {
66
-
return Promise.resolve();
67
-
};
68
-
clientWebpackRequire.p = "/";
69
-
70
-
window.__webpack_require__ = clientWebpackRequire;
71
-
72
-
window.__webpack_chunk_load__ = function (_chunkId: string): Promise<void> {
73
-
return Promise.resolve();
74
-
};
75
-
76
-
export {};
···
+109
src/client/worker-client.ts
+109
src/client/worker-client.ts
···
···
1
+
import workerUrl from "../server/worker-server.ts?rolldown-worker";
2
+
import type { Response, EncodedArgs, Deploy, Render, CallAction } from "../server/worker-server.ts";
3
+
import type { ClientManifest } from "../shared/compiler.ts";
4
+
5
+
export type { EncodedArgs, ClientManifest };
6
+
7
+
export function encodeArgs(encoded: FormData | string): EncodedArgs {
8
+
if (encoded instanceof FormData) {
9
+
return {
10
+
type: "formdata",
11
+
data: new URLSearchParams(encoded as unknown as Record<string, string>).toString(),
12
+
};
13
+
}
14
+
return { type: "string", data: encoded };
15
+
}
16
+
17
+
export class WorkerClient {
18
+
private worker: Worker;
19
+
private requests = new Map<string, ReadableStreamDefaultController<Uint8Array>>();
20
+
private readyPromise: Promise<void>;
21
+
private readyResolve!: () => void;
22
+
23
+
constructor() {
24
+
this.worker = new Worker(workerUrl);
25
+
this.readyPromise = new Promise((resolve) => {
26
+
this.readyResolve = resolve;
27
+
});
28
+
this.worker.onmessage = this.handleMessage.bind(this);
29
+
this.worker.onerror = (e) => {
30
+
const err = new Error(e.message || "Worker error");
31
+
for (const controller of this.requests.values()) {
32
+
controller.error(err);
33
+
}
34
+
this.requests.clear();
35
+
};
36
+
}
37
+
38
+
private handleMessage(event: MessageEvent<Response>): void {
39
+
const msg = event.data;
40
+
41
+
if (msg.type === "ready") {
42
+
this.readyResolve();
43
+
return;
44
+
}
45
+
46
+
const controller = this.requests.get(msg.requestId);
47
+
if (!controller) throw new Error(`Unknown request: ${msg.requestId}`);
48
+
49
+
switch (msg.type) {
50
+
case "next":
51
+
controller.enqueue(msg.value);
52
+
break;
53
+
54
+
case "done":
55
+
controller.close();
56
+
this.requests.delete(msg.requestId);
57
+
break;
58
+
59
+
case "throw": {
60
+
const err = new Error(msg.error);
61
+
if (msg.stack) {
62
+
err.stack = msg.stack;
63
+
}
64
+
controller.error(err);
65
+
this.requests.delete(msg.requestId);
66
+
break;
67
+
}
68
+
}
69
+
}
70
+
71
+
private nextRequestId = 0;
72
+
73
+
private request(body: Record<string, unknown>): ReadableStream<Uint8Array> {
74
+
const requestId = String(this.nextRequestId++);
75
+
let controller!: ReadableStreamDefaultController<Uint8Array>;
76
+
const stream = new ReadableStream<Uint8Array>({
77
+
start: (c) => {
78
+
controller = c;
79
+
},
80
+
});
81
+
this.requests.set(requestId, controller);
82
+
this.worker.postMessage({ ...body, requestId });
83
+
return stream;
84
+
}
85
+
86
+
terminate(): void {
87
+
this.worker.terminate();
88
+
const err = new Error("Worker terminated");
89
+
for (const controller of this.requests.values()) {
90
+
controller.error(err);
91
+
}
92
+
this.requests.clear();
93
+
}
94
+
95
+
async deploy(...args: Parameters<Deploy>): Promise<ReturnType<Deploy>> {
96
+
await this.readyPromise;
97
+
return this.request({ method: "deploy", args });
98
+
}
99
+
100
+
async render(...args: Parameters<Render>): Promise<ReturnType<Render>> {
101
+
await this.readyPromise;
102
+
return this.request({ method: "render", args });
103
+
}
104
+
105
+
async callAction(...args: Parameters<CallAction>): ReturnType<CallAction> {
106
+
await this.readyPromise;
107
+
return this.request({ method: "action", args });
108
+
}
109
+
}
-68
src/server/webpack-shim.ts
-68
src/server/webpack-shim.ts
···
1
-
// Shim webpack globals for react-server-dom-webpack/server in worker context
2
-
// Uses self instead of window since this runs in a Web Worker
3
-
4
-
type WebpackRequire = {
5
-
(moduleId: string): unknown;
6
-
m: Record<string, (module: { exports: unknown }) => void>;
7
-
c: Record<string, unknown>;
8
-
d: (exports: object, definition: Record<string, () => unknown>) => void;
9
-
r: (exports: object) => void;
10
-
o: (obj: object, prop: string) => boolean;
11
-
e: (chunkId: string) => Promise<void>;
12
-
p: string;
13
-
};
14
-
15
-
type WorkerSelf = DedicatedWorkerGlobalScope & {
16
-
__webpack_require__: WebpackRequire;
17
-
__webpack_chunk_load__: (chunkId: string) => Promise<void>;
18
-
};
19
-
20
-
const workerSelf = self as unknown as WorkerSelf;
21
-
22
-
const moduleCache: Record<string, unknown> = {};
23
-
24
-
const webpackRequire: WebpackRequire = function (moduleId: string): unknown {
25
-
if (moduleCache[moduleId]) {
26
-
return moduleCache[moduleId];
27
-
}
28
-
throw new Error(`Module ${moduleId} not found in webpack shim`);
29
-
} as WebpackRequire;
30
-
31
-
webpackRequire.m = {};
32
-
webpackRequire.c = moduleCache;
33
-
webpackRequire.d = function (exports: object, definition: Record<string, () => unknown>): void {
34
-
for (const key in definition) {
35
-
const getter = definition[key];
36
-
if (
37
-
getter &&
38
-
Object.prototype.hasOwnProperty.call(definition, key) &&
39
-
!Object.prototype.hasOwnProperty.call(exports, key)
40
-
) {
41
-
Object.defineProperty(exports, key, {
42
-
enumerable: true,
43
-
get: getter,
44
-
});
45
-
}
46
-
}
47
-
};
48
-
webpackRequire.r = function (exports: object): void {
49
-
if (typeof Symbol !== "undefined" && Symbol.toStringTag) {
50
-
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
51
-
}
52
-
Object.defineProperty(exports, "__esModule", { value: true });
53
-
};
54
-
webpackRequire.o = function (obj: object, prop: string): boolean {
55
-
return Object.prototype.hasOwnProperty.call(obj, prop);
56
-
};
57
-
webpackRequire.e = function (_chunkId: string): Promise<void> {
58
-
return Promise.resolve();
59
-
};
60
-
webpackRequire.p = "/";
61
-
62
-
workerSelf.__webpack_require__ = webpackRequire;
63
-
64
-
workerSelf.__webpack_chunk_load__ = function (_chunkId: string): Promise<void> {
65
-
return Promise.resolve();
66
-
};
67
-
68
-
export {};
···
+168
src/server/worker-server.ts
+168
src/server/worker-server.ts
···
···
1
+
// Server Worker - RSC server simulation
2
+
3
+
import "../shared/webpack-shim.ts";
4
+
import "../shared/polyfill.ts";
5
+
6
+
import {
7
+
renderToReadableStream,
8
+
registerServerReference,
9
+
createClientModuleProxy,
10
+
decodeReply,
11
+
} from "react-server-dom-webpack/server";
12
+
import React from "react";
13
+
import type { ClientManifest } from "../shared/compiler.ts";
14
+
15
+
declare const self: DedicatedWorkerGlobalScope;
16
+
17
+
// --- Types ---
18
+
19
+
export type EncodedArgs = {
20
+
type: "formdata" | "string";
21
+
data: string;
22
+
};
23
+
24
+
export type Response =
25
+
| { type: "ready" }
26
+
| { type: "next"; requestId: string; value: Uint8Array }
27
+
| { type: "done"; requestId: string }
28
+
| { type: "throw"; requestId: string; error: string; stack?: string };
29
+
30
+
// --- State ---
31
+
32
+
type ServerModule = {
33
+
default?: React.ComponentType | React.ReactNode;
34
+
[key: string]: unknown;
35
+
};
36
+
37
+
let deployed: { manifest: ClientManifest; module: ServerModule } | null = null;
38
+
39
+
// --- Response helpers ---
40
+
41
+
async function sendStream(
42
+
requestId: string,
43
+
getStream: () => ReadableStream<Uint8Array> | Promise<ReadableStream<Uint8Array>>,
44
+
): Promise<void> {
45
+
try {
46
+
const stream = await getStream();
47
+
const reader = stream.getReader();
48
+
while (true) {
49
+
const { done, value } = await reader.read();
50
+
if (done) break;
51
+
self.postMessage({ type: "next", requestId, value });
52
+
}
53
+
self.postMessage({ type: "done", requestId });
54
+
} catch (err) {
55
+
const error = err instanceof Error ? err : new Error(String(err));
56
+
const msg: Response = { type: "throw", requestId, error: error.message };
57
+
if (error.stack) {
58
+
msg.stack = error.stack;
59
+
}
60
+
self.postMessage(msg);
61
+
}
62
+
}
63
+
64
+
// --- RPC handlers ---
65
+
66
+
function deploy(
67
+
compiledCode: string,
68
+
manifest: ClientManifest,
69
+
actionNames: string[],
70
+
): ReadableStream<Uint8Array> {
71
+
const clientModule = createClientModuleProxy("client");
72
+
const modules: Record<string, unknown> = {
73
+
react: React,
74
+
"./client": clientModule,
75
+
};
76
+
77
+
let code = compiledCode;
78
+
if (actionNames.length > 0) {
79
+
code +=
80
+
"\n" +
81
+
actionNames
82
+
.map(
83
+
(name) =>
84
+
`__registerServerReference(${name}, "${name}", "${name}"); exports.${name} = ${name};`,
85
+
)
86
+
.join("\n");
87
+
}
88
+
89
+
const module: { exports: ServerModule } = { exports: {} };
90
+
const require = (id: string): unknown => {
91
+
if (!modules[id]) throw new Error(`Module "${id}" not found`);
92
+
return modules[id];
93
+
};
94
+
95
+
new Function("module", "exports", "require", "React", "__registerServerReference", code)(
96
+
module,
97
+
module.exports,
98
+
require,
99
+
React,
100
+
registerServerReference,
101
+
);
102
+
103
+
deployed = { manifest, module: module.exports };
104
+
return new ReadableStream({ start: (c) => c.close() });
105
+
}
106
+
export type Deploy = typeof deploy;
107
+
108
+
function render(): ReadableStream<Uint8Array> {
109
+
if (!deployed) throw new Error("No code deployed");
110
+
const App = deployed.module.default as React.ComponentType;
111
+
return renderToReadableStream(React.createElement(App), deployed.manifest);
112
+
}
113
+
export type Render = typeof render;
114
+
115
+
async function callAction(
116
+
actionId: string,
117
+
encodedArgs: EncodedArgs,
118
+
): Promise<ReadableStream<Uint8Array>> {
119
+
if (!deployed) throw new Error("No code deployed");
120
+
if (!Object.hasOwn(deployed.module, actionId)) {
121
+
throw new Error(`Action "${actionId}" not found`);
122
+
}
123
+
const actionFn = deployed.module[actionId] as Function;
124
+
125
+
let body: FormData | string;
126
+
if (encodedArgs.type === "formdata") {
127
+
body = new FormData();
128
+
for (const [key, value] of new URLSearchParams(encodedArgs.data)) {
129
+
body.append(key, value);
130
+
}
131
+
} else {
132
+
body = encodedArgs.data;
133
+
}
134
+
135
+
const decoded = await decodeReply(body, {});
136
+
const args = Array.isArray(decoded) ? decoded : [decoded];
137
+
const result = await actionFn(...args);
138
+
139
+
return renderToReadableStream(result, deployed.manifest);
140
+
}
141
+
export type CallAction = typeof callAction;
142
+
143
+
// --- Message dispatch ---
144
+
145
+
self.onmessage = (
146
+
event: MessageEvent<
147
+
{ requestId: string } & (
148
+
| { method: "deploy"; args: Parameters<Deploy> }
149
+
| { method: "render"; args: Parameters<Render> }
150
+
| { method: "action"; args: Parameters<CallAction> }
151
+
)
152
+
>,
153
+
) => {
154
+
const req = event.data;
155
+
switch (req.method) {
156
+
case "deploy":
157
+
sendStream(req.requestId, () => deploy(...req.args));
158
+
break;
159
+
case "render":
160
+
sendStream(req.requestId, () => render(...req.args));
161
+
break;
162
+
case "action":
163
+
sendStream(req.requestId, () => callAction(...req.args));
164
+
break;
165
+
}
166
+
};
167
+
168
+
self.postMessage({ type: "ready" });
-273
src/server/worker.ts
-273
src/server/worker.ts
···
1
-
// Server Worker - RSC server simulation
2
-
//
3
-
// Models a real server: deploy code once, then handle requests against it.
4
-
// - `deploy`: Store compiled code, manifest, etc. (like deploying to production)
5
-
// - `render`/`action`: Execute against deployed code
6
-
7
-
import "./webpack-shim.ts";
8
-
import "../client/byte-stream-polyfill.ts";
9
-
import "text-encoding";
10
-
11
-
import {
12
-
renderToReadableStream,
13
-
registerServerReference,
14
-
createClientModuleProxy,
15
-
decodeReply,
16
-
type ClientManifest,
17
-
} from "react-server-dom-webpack/server";
18
-
import React from "react";
19
-
20
-
declare const self: DedicatedWorkerGlobalScope;
21
-
22
-
type DeployMessage = {
23
-
type: "deploy";
24
-
requestId: string;
25
-
compiledCode: string;
26
-
manifest: ClientManifest;
27
-
actionNames: string[];
28
-
};
29
-
30
-
type RenderMessage = {
31
-
type: "render";
32
-
requestId: string;
33
-
};
34
-
35
-
type ActionMessage = {
36
-
type: "action";
37
-
requestId: string;
38
-
actionId: string;
39
-
encodedArgs: EncodedArgs;
40
-
};
41
-
42
-
type WorkerMessage = DeployMessage | RenderMessage | ActionMessage;
43
-
44
-
type EncodedArgs = {
45
-
type: "formdata" | "string";
46
-
data: string;
47
-
};
48
-
49
-
type ErrorResponse = {
50
-
type: "error";
51
-
requestId: string;
52
-
error: { message: string; stack?: string };
53
-
};
54
-
55
-
type DeployedResponse = {
56
-
type: "deployed";
57
-
requestId: string;
58
-
};
59
-
60
-
type StreamStartResponse = {
61
-
type: "stream-start";
62
-
requestId: string;
63
-
};
64
-
65
-
type StreamChunkResponse = {
66
-
type: "stream-chunk";
67
-
requestId: string;
68
-
chunk: Uint8Array;
69
-
};
70
-
71
-
type StreamEndResponse = {
72
-
type: "stream-end";
73
-
requestId: string;
74
-
};
75
-
76
-
type StreamErrorResponse = {
77
-
type: "stream-error";
78
-
requestId: string;
79
-
error: { message: string };
80
-
};
81
-
82
-
type ReadyResponse = {
83
-
type: "ready";
84
-
};
85
-
86
-
type ServerModule = {
87
-
default?: React.ComponentType | React.ReactNode;
88
-
[key: string]: unknown;
89
-
};
90
-
91
-
type DeployedState = {
92
-
manifest: ClientManifest;
93
-
serverModule: ServerModule;
94
-
actionNames: string[];
95
-
};
96
-
97
-
let deployed: DeployedState | null = null;
98
-
99
-
// Safari doesn't support transferable streams
100
-
async function streamToMain(stream: ReadableStream<Uint8Array>, requestId: string): Promise<void> {
101
-
const reader = stream.getReader();
102
-
try {
103
-
while (true) {
104
-
const { done, value } = await reader.read();
105
-
if (done) {
106
-
self.postMessage({ type: "stream-end", requestId } satisfies StreamEndResponse);
107
-
break;
108
-
}
109
-
self.postMessage({
110
-
type: "stream-chunk",
111
-
requestId,
112
-
chunk: value,
113
-
} satisfies StreamChunkResponse);
114
-
}
115
-
} catch (err) {
116
-
const error = err instanceof Error ? err : new Error(String(err));
117
-
self.postMessage({
118
-
type: "stream-error",
119
-
requestId,
120
-
error: { message: error.message },
121
-
} satisfies StreamErrorResponse);
122
-
}
123
-
}
124
-
125
-
self.onmessage = async (event: MessageEvent<WorkerMessage>) => {
126
-
const { type, requestId } = event.data;
127
-
128
-
try {
129
-
switch (type) {
130
-
case "deploy":
131
-
handleDeploy(event.data);
132
-
break;
133
-
case "render":
134
-
await handleRender(event.data);
135
-
break;
136
-
case "action":
137
-
await handleAction(event.data);
138
-
break;
139
-
default: {
140
-
const _exhaustive: never = type;
141
-
throw new Error(`Unknown message type: ${_exhaustive}`);
142
-
}
143
-
}
144
-
} catch (error) {
145
-
const err = error instanceof Error ? error : new Error(String(error));
146
-
const errorPayload: { message: string; stack?: string } = { message: err.message };
147
-
if (err.stack) {
148
-
errorPayload.stack = err.stack;
149
-
}
150
-
self.postMessage({
151
-
type: "error",
152
-
requestId,
153
-
error: errorPayload,
154
-
} satisfies ErrorResponse);
155
-
}
156
-
};
157
-
158
-
function handleDeploy({ compiledCode, manifest, actionNames, requestId }: DeployMessage): void {
159
-
const clientModule = createClientModuleProxy("client");
160
-
const modules: Record<string, unknown> = { react: React, "./client": clientModule };
161
-
const serverModule = evalModule(compiledCode, modules, actionNames);
162
-
163
-
deployed = { manifest, serverModule, actionNames };
164
-
165
-
self.postMessage({ type: "deployed", requestId } satisfies DeployedResponse);
166
-
}
167
-
168
-
function requireDeployed(): DeployedState {
169
-
if (!deployed) throw new Error("No code deployed");
170
-
return deployed;
171
-
}
172
-
173
-
async function handleRender({ requestId }: RenderMessage): Promise<void> {
174
-
const { manifest, serverModule } = requireDeployed();
175
-
176
-
const App = serverModule.default ?? serverModule;
177
-
const element =
178
-
typeof App === "function"
179
-
? React.createElement(App as React.ComponentType)
180
-
: (App as React.ReactNode);
181
-
182
-
const flightStream = renderToReadableStream(element, manifest, {
183
-
onError: (error: unknown) => {
184
-
if (error instanceof Error) return error.message;
185
-
return String(error);
186
-
},
187
-
});
188
-
189
-
self.postMessage({ type: "stream-start", requestId } satisfies StreamStartResponse);
190
-
streamToMain(flightStream, requestId);
191
-
}
192
-
193
-
async function handleAction({ actionId, encodedArgs, requestId }: ActionMessage): Promise<void> {
194
-
const { manifest, serverModule } = requireDeployed();
195
-
196
-
const actionFn = serverModule[actionId];
197
-
if (typeof actionFn !== "function") {
198
-
throw new Error(`Action "${actionId}" not found`);
199
-
}
200
-
201
-
const toDecode = reconstructEncodedArgs(encodedArgs);
202
-
const args = await decodeReply(toDecode, {});
203
-
const argsArray = Array.isArray(args) ? args : [args];
204
-
const result = (await (actionFn as (...args: unknown[]) => Promise<unknown>)(
205
-
...argsArray,
206
-
)) as React.ReactNode;
207
-
208
-
const flightStream = renderToReadableStream(result, manifest, {
209
-
onError: (error: unknown) => {
210
-
if (error instanceof Error) return error.message;
211
-
return String(error);
212
-
},
213
-
});
214
-
215
-
self.postMessage({ type: "stream-start", requestId } satisfies StreamStartResponse);
216
-
streamToMain(flightStream, requestId);
217
-
}
218
-
219
-
function reconstructEncodedArgs(encodedArgs: EncodedArgs): FormData | string {
220
-
if (encodedArgs.type === "formdata") {
221
-
const formData = new FormData();
222
-
for (const [key, value] of new URLSearchParams(encodedArgs.data)) {
223
-
formData.append(key, value);
224
-
}
225
-
return formData;
226
-
}
227
-
return encodedArgs.data;
228
-
}
229
-
230
-
function evalModule(
231
-
code: string,
232
-
modules: Record<string, unknown>,
233
-
actionNames: string[] | undefined,
234
-
): ServerModule {
235
-
let finalCode = code;
236
-
if (actionNames && actionNames.length > 0) {
237
-
finalCode +=
238
-
"\n" +
239
-
actionNames
240
-
.map(
241
-
(name) =>
242
-
`__registerServerReference(${name}, "${name}", "${name}"); exports.${name} = ${name};`,
243
-
)
244
-
.join("\n");
245
-
}
246
-
247
-
const module: { exports: ServerModule } = { exports: {} };
248
-
const require = (id: string): unknown => {
249
-
if (!modules[id]) throw new Error(`Module "${id}" not found`);
250
-
return modules[id];
251
-
};
252
-
253
-
const fn = new Function(
254
-
"module",
255
-
"exports",
256
-
"require",
257
-
"React",
258
-
"__registerServerReference",
259
-
finalCode,
260
-
) as (
261
-
module: { exports: ServerModule },
262
-
exports: ServerModule,
263
-
require: (id: string) => unknown,
264
-
ReactLib: typeof React,
265
-
registerServerRef: typeof registerServerReference,
266
-
) => void;
267
-
268
-
fn(module, module.exports, require, React, registerServerReference);
269
-
270
-
return module.exports;
271
-
}
272
-
273
-
self.postMessage({ type: "ready" } satisfies ReadyResponse);
···
-19
types/web-streams-polyfill.d.ts
-19
types/web-streams-polyfill.d.ts
···
1
-
// Type declarations for web-streams-polyfill specific exports
2
-
3
-
declare module "web-streams-polyfill" {
4
-
export const ReadableStream: {
5
-
new <R = unknown>(
6
-
underlyingSource?: UnderlyingSource<R>,
7
-
strategy?: QueuingStrategy<R>,
8
-
): ReadableStream<R>;
9
-
prototype: ReadableStream;
10
-
};
11
-
// ReadableByteStreamController is an internal class, we type it loosely
12
-
export const ReadableByteStreamController: unknown;
13
-
export const WritableStream: typeof globalThis.WritableStream;
14
-
export const TransformStream: typeof globalThis.TransformStream;
15
-
}
16
-
17
-
declare module "web-streams-polyfill/polyfill" {
18
-
// Side-effect only import that polyfills globals
19
-
}
···