export-runtime 0.0.5 → 0.0.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/generate-types.mjs +87 -3
- package/client.js +73 -199
- package/entry.js +3 -2
- package/handler.js +104 -358
- package/package.json +4 -1
- package/rpc.js +161 -0
- package/shared-do.js +25 -0
package/bin/generate-types.mjs
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
import { parseSync } from "oxc-parser";
|
|
4
|
+
import { minifySync } from "oxc-minify";
|
|
5
|
+
import crypto from "crypto";
|
|
4
6
|
import fs from "fs";
|
|
5
7
|
import path from "path";
|
|
8
|
+
import { fileURLToPath } from "url";
|
|
6
9
|
|
|
7
10
|
const cwd = process.cwd();
|
|
8
11
|
|
|
@@ -217,7 +220,88 @@ lines.push("}>;");
|
|
|
217
220
|
|
|
218
221
|
const typeDefinitions = lines.join("\n");
|
|
219
222
|
|
|
220
|
-
//
|
|
223
|
+
// --- Minify core modules ---
|
|
224
|
+
|
|
225
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
226
|
+
const { CORE_CODE, SHARED_CORE_CODE } = await import(path.join(__dirname, "..", "client.js"));
|
|
227
|
+
|
|
228
|
+
const minified = minifySync("_core.js", CORE_CODE);
|
|
229
|
+
if (minified.errors?.length) {
|
|
230
|
+
console.error("Minification errors (core):", minified.errors);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const minifiedShared = minifySync("_core-shared.js", SHARED_CORE_CODE);
|
|
234
|
+
if (minifiedShared.errors?.length) {
|
|
235
|
+
console.error("Minification errors (shared core):", minifiedShared.errors);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Generate a unique ID per build for cache-busting the core module path
|
|
239
|
+
const coreId = crypto.randomUUID();
|
|
240
|
+
|
|
241
|
+
// Write as a JS module
|
|
221
242
|
const outPath = path.join(cwd, ".export-types.js");
|
|
222
|
-
fs.writeFileSync(outPath,
|
|
223
|
-
|
|
243
|
+
fs.writeFileSync(outPath, [
|
|
244
|
+
`export default ${JSON.stringify(typeDefinitions)};`,
|
|
245
|
+
`export const minifiedCore = ${JSON.stringify(minified.code)};`,
|
|
246
|
+
`export const minifiedSharedCore = ${JSON.stringify(minifiedShared.code)};`,
|
|
247
|
+
`export const coreId = ${JSON.stringify(coreId)};`,
|
|
248
|
+
].join("\n") + "\n");
|
|
249
|
+
|
|
250
|
+
// Generate Worker-side shared import module (.export-shared.js)
|
|
251
|
+
const exportNames = [];
|
|
252
|
+
for (const node of program.body) {
|
|
253
|
+
if (node.type !== "ExportNamedDeclaration" || !node.declaration) continue;
|
|
254
|
+
const decl = node.declaration;
|
|
255
|
+
if (decl.id?.name) exportNames.push(decl.id.name);
|
|
256
|
+
else if (decl.declarations) {
|
|
257
|
+
for (const d of decl.declarations) {
|
|
258
|
+
if (d.id?.name) exportNames.push(d.id.name);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const sharedModulePath = path.join(cwd, ".export-shared.js");
|
|
264
|
+
const sharedModuleLines = [
|
|
265
|
+
`import { env } from "cloudflare:workers";`,
|
|
266
|
+
``,
|
|
267
|
+
`const getStub = (room = "default") =>`,
|
|
268
|
+
` env.SHARED_EXPORT.get(env.SHARED_EXPORT.idFromName(room));`,
|
|
269
|
+
``,
|
|
270
|
+
`const createSharedInstanceProxy = (stub, instanceId, path = []) =>`,
|
|
271
|
+
` new Proxy(function(){}, {`,
|
|
272
|
+
` get(_, prop) {`,
|
|
273
|
+
` if (prop === "then" || prop === Symbol.toStringTag) return undefined;`,
|
|
274
|
+
` if (prop === Symbol.dispose || prop === Symbol.asyncDispose || prop === "[release]")`,
|
|
275
|
+
` return () => stub.rpcRelease(instanceId);`,
|
|
276
|
+
` return createSharedInstanceProxy(stub, instanceId, [...path, prop]);`,
|
|
277
|
+
` },`,
|
|
278
|
+
` async apply(_, __, args) {`,
|
|
279
|
+
` const r = await stub.rpcInstanceCall(instanceId, path, args);`,
|
|
280
|
+
` return r.value;`,
|
|
281
|
+
` },`,
|
|
282
|
+
` });`,
|
|
283
|
+
``,
|
|
284
|
+
`const createSharedProxy = (stub, path = []) =>`,
|
|
285
|
+
` new Proxy(function(){}, {`,
|
|
286
|
+
` get(_, prop) {`,
|
|
287
|
+
` if (prop === "then" || prop === Symbol.toStringTag) return undefined;`,
|
|
288
|
+
` return createSharedProxy(stub, [...path, prop]);`,
|
|
289
|
+
` },`,
|
|
290
|
+
` async apply(_, __, args) {`,
|
|
291
|
+
` const r = await stub.rpcCall(path, args);`,
|
|
292
|
+
` return r.value;`,
|
|
293
|
+
` },`,
|
|
294
|
+
` async construct(_, args) {`,
|
|
295
|
+
` const r = await stub.rpcConstruct(path, args);`,
|
|
296
|
+
` return createSharedInstanceProxy(stub, r.instanceId);`,
|
|
297
|
+
` },`,
|
|
298
|
+
` });`,
|
|
299
|
+
``,
|
|
300
|
+
`const _stub = getStub();`,
|
|
301
|
+
...exportNames.map(n => `export const ${n} = createSharedProxy(_stub, [${JSON.stringify(n)}]);`),
|
|
302
|
+
`export { getStub };`,
|
|
303
|
+
];
|
|
304
|
+
fs.writeFileSync(sharedModulePath, sharedModuleLines.join("\n") + "\n");
|
|
305
|
+
|
|
306
|
+
console.log("Generated type definitions + minified core →", outPath);
|
|
307
|
+
console.log("Generated shared import module →", sharedModulePath);
|
package/client.js
CHANGED
|
@@ -1,26 +1,19 @@
|
|
|
1
|
-
//
|
|
2
|
-
|
|
1
|
+
// Core module template. __WS_SUFFIX__ is replaced: "./" for normal, "./?shared" for shared.
|
|
2
|
+
const CORE_TEMPLATE = `
|
|
3
3
|
const stringify = (value) => {
|
|
4
4
|
const stringified = [];
|
|
5
5
|
const indexes = new Map();
|
|
6
6
|
let p = 0;
|
|
7
|
-
|
|
8
7
|
const flatten = (thing) => {
|
|
9
|
-
if (typeof thing === 'function')
|
|
10
|
-
throw new Error('Cannot stringify a function');
|
|
11
|
-
}
|
|
12
|
-
|
|
8
|
+
if (typeof thing === 'function') throw new Error('Cannot stringify a function');
|
|
13
9
|
if (indexes.has(thing)) return indexes.get(thing);
|
|
14
|
-
|
|
15
10
|
if (thing === undefined) return -1;
|
|
16
11
|
if (Number.isNaN(thing)) return -3;
|
|
17
12
|
if (thing === Infinity) return -4;
|
|
18
13
|
if (thing === -Infinity) return -5;
|
|
19
14
|
if (thing === 0 && 1 / thing < 0) return -6;
|
|
20
|
-
|
|
21
15
|
const index = p++;
|
|
22
16
|
indexes.set(thing, index);
|
|
23
|
-
|
|
24
17
|
if (typeof thing === 'boolean' || typeof thing === 'number' || typeof thing === 'string' || thing === null) {
|
|
25
18
|
stringified[index] = thing;
|
|
26
19
|
} else if (thing instanceof Date) {
|
|
@@ -37,76 +30,37 @@ const stringify = (value) => {
|
|
|
37
30
|
stringified[index] = ['Set', ...[...thing].map(flatten)];
|
|
38
31
|
} else if (thing instanceof Map) {
|
|
39
32
|
stringified[index] = ['Map', ...[...thing].map(([k, v]) => [flatten(k), flatten(v)])];
|
|
40
|
-
} else if (thing
|
|
41
|
-
stringified[index] = [
|
|
42
|
-
} else if (thing instanceof Uint8Array) {
|
|
43
|
-
stringified[index] = ['Uint8Array', ...[...thing].map(flatten)];
|
|
44
|
-
} else if (thing instanceof Uint8ClampedArray) {
|
|
45
|
-
stringified[index] = ['Uint8ClampedArray', ...[...thing].map(flatten)];
|
|
46
|
-
} else if (thing instanceof Int16Array) {
|
|
47
|
-
stringified[index] = ['Int16Array', ...[...thing].map(flatten)];
|
|
48
|
-
} else if (thing instanceof Uint16Array) {
|
|
49
|
-
stringified[index] = ['Uint16Array', ...[...thing].map(flatten)];
|
|
50
|
-
} else if (thing instanceof Int32Array) {
|
|
51
|
-
stringified[index] = ['Int32Array', ...[...thing].map(flatten)];
|
|
52
|
-
} else if (thing instanceof Uint32Array) {
|
|
53
|
-
stringified[index] = ['Uint32Array', ...[...thing].map(flatten)];
|
|
54
|
-
} else if (thing instanceof Float32Array) {
|
|
55
|
-
stringified[index] = ['Float32Array', ...[...thing].map(flatten)];
|
|
56
|
-
} else if (thing instanceof Float64Array) {
|
|
57
|
-
stringified[index] = ['Float64Array', ...[...thing].map(flatten)];
|
|
58
|
-
} else if (thing instanceof BigInt64Array) {
|
|
59
|
-
stringified[index] = ['BigInt64Array', ...[...thing].map(flatten)];
|
|
60
|
-
} else if (thing instanceof BigUint64Array) {
|
|
61
|
-
stringified[index] = ['BigUint64Array', ...[...thing].map(flatten)];
|
|
33
|
+
} else if (ArrayBuffer.isView(thing)) {
|
|
34
|
+
stringified[index] = [thing[Symbol.toStringTag], ...[...thing].map(flatten)];
|
|
62
35
|
} else if (thing instanceof ArrayBuffer) {
|
|
63
36
|
stringified[index] = ['ArrayBuffer', ...[...new Uint8Array(thing)].map(flatten)];
|
|
64
37
|
} else if (Array.isArray(thing)) {
|
|
65
38
|
stringified[index] = thing.map(flatten);
|
|
66
39
|
} else if (typeof thing === 'object') {
|
|
67
40
|
const obj = {};
|
|
68
|
-
for (const key of Object.keys(thing))
|
|
69
|
-
obj[key] = flatten(thing[key]);
|
|
70
|
-
}
|
|
41
|
+
for (const key of Object.keys(thing)) obj[key] = flatten(thing[key]);
|
|
71
42
|
stringified[index] = obj;
|
|
72
43
|
} else {
|
|
73
44
|
throw new Error('Cannot stringify ' + typeof thing);
|
|
74
45
|
}
|
|
75
|
-
|
|
76
46
|
return index;
|
|
77
47
|
};
|
|
78
|
-
|
|
79
48
|
flatten(value);
|
|
80
49
|
return JSON.stringify(stringified);
|
|
81
50
|
};
|
|
82
|
-
`;
|
|
83
|
-
|
|
84
|
-
// Minimal devalue parse implementation (compatible with sveltejs/devalue)
|
|
85
|
-
export const DEVALUE_PARSE = `
|
|
86
|
-
const UNDEFINED = -1;
|
|
87
|
-
const HOLE = -2;
|
|
88
|
-
const NAN = -3;
|
|
89
|
-
const POSITIVE_INFINITY = -4;
|
|
90
|
-
const NEGATIVE_INFINITY = -5;
|
|
91
|
-
const NEGATIVE_ZERO = -6;
|
|
92
51
|
|
|
93
52
|
const parse = (serialized) => {
|
|
94
53
|
if (serialized === '') return undefined;
|
|
95
54
|
const values = JSON.parse(serialized);
|
|
96
55
|
const hydrated = new Array(values.length);
|
|
97
|
-
|
|
98
56
|
const hydrate = (index) => {
|
|
99
|
-
if (index ===
|
|
100
|
-
if (index ===
|
|
101
|
-
if (index ===
|
|
102
|
-
if (index ===
|
|
103
|
-
if (index ===
|
|
104
|
-
if (index === NEGATIVE_ZERO) return -0;
|
|
105
|
-
|
|
57
|
+
if (index === -1 || index === -2) return undefined;
|
|
58
|
+
if (index === -3) return NaN;
|
|
59
|
+
if (index === -4) return Infinity;
|
|
60
|
+
if (index === -5) return -Infinity;
|
|
61
|
+
if (index === -6) return -0;
|
|
106
62
|
if (hydrated[index] !== undefined) return hydrated[index];
|
|
107
|
-
|
|
108
63
|
const value = values[index];
|
|
109
|
-
|
|
110
64
|
if (typeof value === 'number' || typeof value === 'string' || typeof value === 'boolean' || value === null) {
|
|
111
65
|
hydrated[index] = value;
|
|
112
66
|
} else if (Array.isArray(value)) {
|
|
@@ -127,15 +81,16 @@ const parse = (serialized) => {
|
|
|
127
81
|
break;
|
|
128
82
|
case 'ArrayBuffer': {
|
|
129
83
|
const bytes = value.slice(1).map(hydrate);
|
|
130
|
-
const
|
|
131
|
-
new Uint8Array(
|
|
132
|
-
hydrated[index] =
|
|
84
|
+
const buf = new ArrayBuffer(bytes.length);
|
|
85
|
+
new Uint8Array(buf).set(bytes);
|
|
86
|
+
hydrated[index] = buf;
|
|
133
87
|
break;
|
|
134
88
|
}
|
|
135
|
-
default:
|
|
89
|
+
default: {
|
|
136
90
|
const arr = new Array(value.length);
|
|
137
91
|
hydrated[index] = arr;
|
|
138
92
|
for (let i = 0; i < value.length; i++) arr[i] = hydrate(value[i]);
|
|
93
|
+
}
|
|
139
94
|
}
|
|
140
95
|
} else {
|
|
141
96
|
const arr = new Array(value.length);
|
|
@@ -147,42 +102,29 @@ const parse = (serialized) => {
|
|
|
147
102
|
hydrated[index] = obj;
|
|
148
103
|
for (const key in value) obj[key] = hydrate(value[key]);
|
|
149
104
|
}
|
|
150
|
-
|
|
151
105
|
return hydrated[index];
|
|
152
106
|
};
|
|
153
|
-
|
|
154
107
|
return hydrate(0);
|
|
155
108
|
};
|
|
156
|
-
`;
|
|
157
|
-
|
|
158
|
-
export const CLIENT_CODE = `
|
|
159
|
-
__DEVALUE_STRINGIFY__
|
|
160
|
-
__DEVALUE_PARSE__
|
|
161
109
|
|
|
162
|
-
const
|
|
110
|
+
const _u = new URL("__WS_SUFFIX__", import.meta.url);
|
|
111
|
+
_u.protocol = _u.protocol === "https:" ? "wss:" : "ws:";
|
|
112
|
+
const ws = new WebSocket(_u.href);
|
|
163
113
|
const pending = new Map();
|
|
164
114
|
let nextId = 1;
|
|
165
|
-
let keepaliveInterval
|
|
115
|
+
let keepaliveInterval;
|
|
166
116
|
|
|
167
117
|
const ready = new Promise((resolve, reject) => {
|
|
168
118
|
ws.onopen = () => {
|
|
169
|
-
// Start keepalive ping every 30 seconds
|
|
170
119
|
keepaliveInterval = setInterval(() => {
|
|
171
|
-
if (ws.readyState === WebSocket.OPEN) {
|
|
172
|
-
ws.send(stringify({ type: "ping", id: 0 }));
|
|
173
|
-
}
|
|
120
|
+
if (ws.readyState === WebSocket.OPEN) ws.send(stringify({ type: "ping", id: 0 }));
|
|
174
121
|
}, 30000);
|
|
175
|
-
resolve(
|
|
122
|
+
resolve();
|
|
176
123
|
};
|
|
177
|
-
ws.onerror =
|
|
124
|
+
ws.onerror = reject;
|
|
178
125
|
});
|
|
179
126
|
|
|
180
|
-
ws.onclose = () => {
|
|
181
|
-
if (keepaliveInterval) {
|
|
182
|
-
clearInterval(keepaliveInterval);
|
|
183
|
-
keepaliveInterval = null;
|
|
184
|
-
}
|
|
185
|
-
};
|
|
127
|
+
ws.onclose = () => { clearInterval(keepaliveInterval); };
|
|
186
128
|
|
|
187
129
|
const sendRequest = async (msg) => {
|
|
188
130
|
await ready;
|
|
@@ -193,144 +135,76 @@ const sendRequest = async (msg) => {
|
|
|
193
135
|
});
|
|
194
136
|
};
|
|
195
137
|
|
|
138
|
+
const makeWritable = (writableId) => new WritableStream({
|
|
139
|
+
async write(chunk) {
|
|
140
|
+
await sendRequest({ type: "writable-write", writableId, chunk: chunk instanceof Uint8Array ? Array.from(chunk) : chunk });
|
|
141
|
+
},
|
|
142
|
+
async close() { await sendRequest({ type: "writable-close", writableId }); },
|
|
143
|
+
async abort() { await sendRequest({ type: "writable-abort", writableId }); }
|
|
144
|
+
});
|
|
145
|
+
|
|
196
146
|
ws.onmessage = (event) => {
|
|
197
147
|
const msg = parse(event.data);
|
|
198
|
-
|
|
199
|
-
// Ignore pong responses (keepalive)
|
|
200
|
-
if (msg.type === "pong") return;
|
|
201
|
-
|
|
202
148
|
const resolver = pending.get(msg.id);
|
|
203
149
|
if (!resolver) return;
|
|
150
|
+
pending.delete(msg.id);
|
|
204
151
|
|
|
205
152
|
if (msg.type === "error") {
|
|
206
153
|
resolver.reject(new Error(msg.error));
|
|
207
|
-
pending.delete(msg.id);
|
|
208
154
|
} else if (msg.type === "result") {
|
|
209
|
-
if (msg.valueType === "function")
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
// Create a ReadableStream proxy that pulls from server
|
|
226
|
-
const streamId = msg.streamId;
|
|
227
|
-
const stream = new ReadableStream({
|
|
228
|
-
async pull(controller) {
|
|
229
|
-
try {
|
|
230
|
-
const result = await sendRequest({ type: "stream-read", streamId });
|
|
231
|
-
if (result.done) {
|
|
232
|
-
controller.close();
|
|
233
|
-
} else {
|
|
234
|
-
controller.enqueue(result.value);
|
|
235
|
-
}
|
|
236
|
-
} catch (err) {
|
|
237
|
-
controller.error(err);
|
|
238
|
-
}
|
|
239
|
-
},
|
|
240
|
-
async cancel() {
|
|
241
|
-
await sendRequest({ type: "stream-cancel", streamId });
|
|
242
|
-
}
|
|
243
|
-
});
|
|
244
|
-
resolver.resolve(stream);
|
|
245
|
-
} else if (msg.valueType === "writablestream") {
|
|
246
|
-
// Create a WritableStream proxy that pushes to server
|
|
247
|
-
const writableId = msg.writableId;
|
|
248
|
-
const stream = new WritableStream({
|
|
249
|
-
async write(chunk) {
|
|
250
|
-
const data = chunk instanceof Uint8Array ? Array.from(chunk) : chunk;
|
|
251
|
-
await sendRequest({ type: "writable-write", writableId, chunk: data });
|
|
252
|
-
},
|
|
253
|
-
async close() {
|
|
254
|
-
await sendRequest({ type: "writable-close", writableId });
|
|
255
|
-
},
|
|
256
|
-
async abort(reason) {
|
|
257
|
-
await sendRequest({ type: "writable-abort", writableId });
|
|
258
|
-
}
|
|
259
|
-
});
|
|
260
|
-
resolver.resolve(stream);
|
|
261
|
-
} else {
|
|
262
|
-
resolver.resolve(msg.value);
|
|
263
|
-
}
|
|
264
|
-
pending.delete(msg.id);
|
|
155
|
+
if (msg.valueType === "function") resolver.resolve(createProxy(msg.path));
|
|
156
|
+
else if (msg.valueType === "instance") resolver.resolve(createInstanceProxy(msg.instanceId));
|
|
157
|
+
else if (msg.valueType === "asynciterator") resolver.resolve({
|
|
158
|
+
[Symbol.asyncIterator]() { return this; },
|
|
159
|
+
next: () => sendRequest({ type: "iterate-next", iteratorId: msg.iteratorId }),
|
|
160
|
+
return: () => sendRequest({ type: "iterate-return", iteratorId: msg.iteratorId })
|
|
161
|
+
});
|
|
162
|
+
else if (msg.valueType === "readablestream") resolver.resolve(new ReadableStream({
|
|
163
|
+
async pull(c) {
|
|
164
|
+
try { const r = await sendRequest({ type: "stream-read", streamId: msg.streamId }); r.done ? c.close() : c.enqueue(r.value); }
|
|
165
|
+
catch (e) { c.error(e); }
|
|
166
|
+
},
|
|
167
|
+
cancel: () => sendRequest({ type: "stream-cancel", streamId: msg.streamId })
|
|
168
|
+
}));
|
|
169
|
+
else if (msg.valueType === "writablestream") resolver.resolve(makeWritable(msg.writableId));
|
|
170
|
+
else resolver.resolve(msg.value);
|
|
265
171
|
} else if (msg.type === "iterate-result") {
|
|
266
172
|
resolver.resolve({ value: msg.value, done: msg.done });
|
|
267
|
-
pending.delete(msg.id);
|
|
268
173
|
} else if (msg.type === "stream-result") {
|
|
269
|
-
|
|
270
|
-
const value = Array.isArray(msg.value) ? new Uint8Array(msg.value) : msg.value;
|
|
271
|
-
resolver.resolve({ value, done: msg.done });
|
|
272
|
-
pending.delete(msg.id);
|
|
174
|
+
resolver.resolve({ value: Array.isArray(msg.value) ? new Uint8Array(msg.value) : msg.value, done: msg.done });
|
|
273
175
|
}
|
|
274
176
|
};
|
|
275
177
|
|
|
276
|
-
|
|
277
|
-
const createInstanceProxy = (instanceId, path = []) => {
|
|
278
|
-
const proxy = new Proxy(function(){}, {
|
|
279
|
-
get(_, prop) {
|
|
280
|
-
if (prop === "then" || prop === Symbol.toStringTag) return undefined;
|
|
281
|
-
if (prop === Symbol.dispose || prop === Symbol.asyncDispose) {
|
|
282
|
-
return () => sendRequest({ type: "release", instanceId });
|
|
283
|
-
}
|
|
284
|
-
if (prop === "[release]") {
|
|
285
|
-
return () => sendRequest({ type: "release", instanceId });
|
|
286
|
-
}
|
|
287
|
-
return createInstanceProxy(instanceId, [...path, prop]);
|
|
288
|
-
},
|
|
289
|
-
set(_, prop, value) {
|
|
290
|
-
sendRequest({ type: "set", instanceId, path: [...path, prop], args: [value] });
|
|
291
|
-
return true;
|
|
292
|
-
},
|
|
293
|
-
async apply(_, __, args) {
|
|
294
|
-
return sendRequest({ type: "call", instanceId, path, args });
|
|
295
|
-
}
|
|
296
|
-
});
|
|
297
|
-
return proxy;
|
|
298
|
-
};
|
|
299
|
-
|
|
300
|
-
// Proxy for exports (functions, classes, objects)
|
|
301
|
-
const createProxy = (path = []) => new Proxy(function(){}, {
|
|
178
|
+
const createInstanceProxy = (instanceId, path = []) => new Proxy(function(){}, {
|
|
302
179
|
get(_, prop) {
|
|
303
180
|
if (prop === "then" || prop === Symbol.toStringTag) return undefined;
|
|
304
|
-
|
|
181
|
+
if (prop === Symbol.dispose || prop === Symbol.asyncDispose || prop === "[release]")
|
|
182
|
+
return () => sendRequest({ type: "release", instanceId });
|
|
183
|
+
return createInstanceProxy(instanceId, [...path, prop]);
|
|
305
184
|
},
|
|
306
|
-
|
|
307
|
-
|
|
185
|
+
set(_, prop, value) {
|
|
186
|
+
sendRequest({ type: "set", instanceId, path: [...path, prop], args: [value] });
|
|
187
|
+
return true;
|
|
308
188
|
},
|
|
309
|
-
|
|
310
|
-
return sendRequest({ type: "
|
|
189
|
+
async apply(_, __, args) {
|
|
190
|
+
return sendRequest({ type: "call", instanceId, path, args });
|
|
311
191
|
}
|
|
312
192
|
});
|
|
313
193
|
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
await sendRequest({ type: "writable-write", writableId, chunk: data });
|
|
323
|
-
},
|
|
324
|
-
async close() {
|
|
325
|
-
return sendRequest({ type: "writable-close", writableId });
|
|
326
|
-
},
|
|
327
|
-
async abort(reason) {
|
|
328
|
-
await sendRequest({ type: "writable-abort", writableId });
|
|
329
|
-
}
|
|
330
|
-
});
|
|
194
|
+
export const createProxy = (path = []) => new Proxy(function(){}, {
|
|
195
|
+
get(_, prop) {
|
|
196
|
+
if (prop === "then" || prop === Symbol.toStringTag) return undefined;
|
|
197
|
+
return createProxy([...path, prop]);
|
|
198
|
+
},
|
|
199
|
+
async apply(_, __, args) { return sendRequest({ type: "call", path, args }); },
|
|
200
|
+
construct(_, args) { return sendRequest({ type: "construct", path, args }); }
|
|
201
|
+
});
|
|
331
202
|
|
|
332
|
-
|
|
203
|
+
export const createUploadStream = async () => {
|
|
204
|
+
const { writableId } = await sendRequest({ type: "writable-create" });
|
|
205
|
+
return { stream: makeWritable(writableId), writableId };
|
|
333
206
|
};
|
|
334
|
-
|
|
335
|
-
__NAMED_EXPORTS__
|
|
336
207
|
`;
|
|
208
|
+
|
|
209
|
+
export const CORE_CODE = CORE_TEMPLATE.replace("__WS_SUFFIX__", "./");
|
|
210
|
+
export const SHARED_CORE_CODE = CORE_TEMPLATE.replace("__WS_SUFFIX__", "./?shared");
|
package/entry.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as userExports from "__USER_MODULE__";
|
|
2
|
-
import generatedTypes from "__GENERATED_TYPES__";
|
|
2
|
+
import generatedTypes, { minifiedCore, minifiedSharedCore, coreId } from "__GENERATED_TYPES__";
|
|
3
3
|
import { createHandler } from "./handler.js";
|
|
4
|
+
export { SharedExportDO } from "./shared-do.js";
|
|
4
5
|
|
|
5
|
-
export default createHandler(userExports, generatedTypes);
|
|
6
|
+
export default createHandler(userExports, generatedTypes, minifiedCore, coreId, minifiedSharedCore);
|
package/handler.js
CHANGED
|
@@ -1,391 +1,137 @@
|
|
|
1
1
|
import { stringify, parse } from "devalue";
|
|
2
|
-
import {
|
|
2
|
+
import { CORE_CODE, SHARED_CORE_CODE } from "./client.js";
|
|
3
|
+
import { createRpcDispatcher } from "./rpc.js";
|
|
3
4
|
|
|
4
|
-
const
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
current = current[key];
|
|
9
|
-
}
|
|
10
|
-
return current;
|
|
11
|
-
};
|
|
12
|
-
|
|
13
|
-
const isAsyncIterable = (value) =>
|
|
14
|
-
value != null && typeof value[Symbol.asyncIterator] === "function";
|
|
15
|
-
|
|
16
|
-
const isReadableStream = (value) =>
|
|
17
|
-
value != null && typeof value.getReader === "function" && typeof value.pipeTo === "function";
|
|
5
|
+
const JS = "application/javascript; charset=utf-8";
|
|
6
|
+
const TS = "application/typescript; charset=utf-8";
|
|
7
|
+
const CORS = { "Access-Control-Allow-Origin": "*" };
|
|
8
|
+
const IMMUTABLE = "public, max-age=31536000, immutable";
|
|
18
9
|
|
|
19
|
-
const
|
|
20
|
-
|
|
10
|
+
const jsResponse = (body, extra = {}) =>
|
|
11
|
+
new Response(body, { headers: { "Content-Type": JS, ...CORS, ...extra } });
|
|
21
12
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
const lines = [
|
|
25
|
-
"// Auto-generated type definitions",
|
|
26
|
-
"// All functions are async over the network",
|
|
27
|
-
"",
|
|
28
|
-
];
|
|
13
|
+
const tsResponse = (body, status = 200) =>
|
|
14
|
+
new Response(body, { status, headers: { "Content-Type": TS, ...CORS, "Cache-Control": "no-cache" } });
|
|
29
15
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
// Extract class method names
|
|
33
|
-
const proto = value.prototype;
|
|
34
|
-
const methodNames = Object.getOwnPropertyNames(proto).filter(
|
|
35
|
-
(n) => n !== "constructor" && typeof proto[n] === "function"
|
|
36
|
-
);
|
|
16
|
+
export const createHandler = (exports, generatedTypes, minifiedCore, coreId, minifiedSharedCore) => {
|
|
17
|
+
const exportKeys = Object.keys(exports);
|
|
37
18
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
} else {
|
|
76
|
-
lines.push(`${indent}export declare const ${name}: any;`);
|
|
19
|
+
const coreModuleCode = minifiedCore || CORE_CODE;
|
|
20
|
+
const sharedCoreModuleCode = minifiedSharedCore || SHARED_CORE_CODE;
|
|
21
|
+
const corePath = `/${coreId || crypto.randomUUID()}.js`;
|
|
22
|
+
const sharedCorePath = corePath.replace(".js", "-shared.js");
|
|
23
|
+
|
|
24
|
+
// Pre-generate the named exports string (same for shared and normal, only import source differs)
|
|
25
|
+
const namedExportsCode = exportKeys
|
|
26
|
+
.map((key) => `export const ${key} = createProxy([${JSON.stringify(key)}]);`)
|
|
27
|
+
.join("\n");
|
|
28
|
+
|
|
29
|
+
const buildIndexModule = (cpath) =>
|
|
30
|
+
`import { createProxy, createUploadStream } from ".${cpath}";\n${namedExportsCode}\nexport { createUploadStream };`;
|
|
31
|
+
|
|
32
|
+
const buildExportModule = (cpath, name) =>
|
|
33
|
+
`import { createProxy } from ".${cpath}";\nconst _export = createProxy([${JSON.stringify(name)}]);\nexport default _export;\nexport { _export as ${name} };`;
|
|
34
|
+
|
|
35
|
+
// Dispatch a parsed devalue message to an RPC dispatcher
|
|
36
|
+
const dispatchMessage = async (dispatcher, msg) => {
|
|
37
|
+
const { type, path = [], args = [], instanceId, iteratorId, streamId, writableId, chunk } = msg;
|
|
38
|
+
switch (type) {
|
|
39
|
+
case "ping": return { type: "pong" };
|
|
40
|
+
case "call":
|
|
41
|
+
return instanceId !== undefined
|
|
42
|
+
? dispatcher.rpcInstanceCall(instanceId, path, args)
|
|
43
|
+
: dispatcher.rpcCall(path, args);
|
|
44
|
+
case "construct": return dispatcher.rpcConstruct(path, args);
|
|
45
|
+
case "get": return dispatcher.rpcGet(instanceId, path);
|
|
46
|
+
case "set": return dispatcher.rpcSet(instanceId, path, args[0]);
|
|
47
|
+
case "release": return dispatcher.rpcRelease(instanceId);
|
|
48
|
+
case "iterate-next": return dispatcher.rpcIterateNext(iteratorId);
|
|
49
|
+
case "iterate-return": return dispatcher.rpcIterateReturn(iteratorId);
|
|
50
|
+
case "stream-read": return dispatcher.rpcStreamRead(streamId);
|
|
51
|
+
case "stream-cancel": return dispatcher.rpcStreamCancel(streamId);
|
|
52
|
+
case "writable-create": return dispatcher.rpcWritableCreate();
|
|
53
|
+
case "writable-write": return dispatcher.rpcWritableWrite(writableId, chunk);
|
|
54
|
+
case "writable-close": return dispatcher.rpcWritableClose(writableId);
|
|
55
|
+
case "writable-abort": return dispatcher.rpcWritableAbort(writableId);
|
|
77
56
|
}
|
|
78
57
|
};
|
|
79
58
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
export const createHandler = (exports, generatedTypes) => {
|
|
95
|
-
const exportKeys = Object.keys(exports);
|
|
96
|
-
const iteratorStore = new Map();
|
|
97
|
-
const instanceStore = new Map();
|
|
98
|
-
const streamStore = new Map();
|
|
99
|
-
const writableStreamStore = new Map();
|
|
100
|
-
let nextIteratorId = 1;
|
|
101
|
-
let nextInstanceId = 1;
|
|
102
|
-
let nextStreamId = 1;
|
|
103
|
-
|
|
104
|
-
const send = (ws, data) => {
|
|
105
|
-
ws.send(stringify(data));
|
|
59
|
+
const wireWebSocket = (server, dispatcher, onClose) => {
|
|
60
|
+
server.addEventListener("message", async (event) => {
|
|
61
|
+
let id;
|
|
62
|
+
try {
|
|
63
|
+
const msg = parse(event.data);
|
|
64
|
+
id = msg.id;
|
|
65
|
+
const result = await dispatchMessage(dispatcher, msg);
|
|
66
|
+
if (result) server.send(stringify({ ...result, id }));
|
|
67
|
+
} catch (err) {
|
|
68
|
+
if (id !== undefined) server.send(stringify({ type: "error", id, error: String(err) }));
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
if (onClose) server.addEventListener("close", onClose);
|
|
106
72
|
};
|
|
107
73
|
|
|
108
74
|
return {
|
|
109
|
-
async fetch(request) {
|
|
75
|
+
async fetch(request, env) {
|
|
110
76
|
const url = new URL(request.url);
|
|
111
|
-
const
|
|
77
|
+
const isShared = url.searchParams.has("shared");
|
|
112
78
|
|
|
113
|
-
|
|
79
|
+
// --- WebSocket upgrade ---
|
|
80
|
+
if (request.headers.get("Upgrade") === "websocket") {
|
|
114
81
|
const pair = new WebSocketPair();
|
|
115
82
|
const [client, server] = Object.values(pair);
|
|
116
|
-
|
|
117
83
|
server.accept();
|
|
118
84
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
return;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
if (type === "construct") {
|
|
131
|
-
// Class instantiation
|
|
132
|
-
try {
|
|
133
|
-
const Ctor = getByPath(exports, path);
|
|
134
|
-
if (!isClass(Ctor)) {
|
|
135
|
-
send(server, { type: "error", id, error: `${path.join(".")} is not a class` });
|
|
136
|
-
return;
|
|
137
|
-
}
|
|
138
|
-
const instance = new Ctor(...args);
|
|
139
|
-
const instId = nextInstanceId++;
|
|
140
|
-
instanceStore.set(instId, instance);
|
|
141
|
-
send(server, { type: "result", id, instanceId: instId, valueType: "instance" });
|
|
142
|
-
} catch (err) {
|
|
143
|
-
send(server, { type: "error", id, error: String(err) });
|
|
144
|
-
}
|
|
145
|
-
} else if (type === "call") {
|
|
146
|
-
try {
|
|
147
|
-
let target;
|
|
148
|
-
let thisArg;
|
|
149
|
-
|
|
150
|
-
if (instanceId !== undefined) {
|
|
151
|
-
// Method call on instance
|
|
152
|
-
const instance = instanceStore.get(instanceId);
|
|
153
|
-
if (!instance) {
|
|
154
|
-
send(server, { type: "error", id, error: "Instance not found" });
|
|
155
|
-
return;
|
|
156
|
-
}
|
|
157
|
-
target = getByPath(instance, path);
|
|
158
|
-
thisArg = path.length > 1 ? getByPath(instance, path.slice(0, -1)) : instance;
|
|
159
|
-
} else {
|
|
160
|
-
// Regular function call
|
|
161
|
-
target = getByPath(exports, path);
|
|
162
|
-
thisArg = path.length > 1 ? getByPath(exports, path.slice(0, -1)) : undefined;
|
|
163
|
-
}
|
|
85
|
+
if (isShared && env?.SHARED_EXPORT) {
|
|
86
|
+
const room = url.searchParams.get("room") || "default";
|
|
87
|
+
const stub = env.SHARED_EXPORT.get(env.SHARED_EXPORT.idFromName(room));
|
|
88
|
+
wireWebSocket(server, stub);
|
|
89
|
+
} else {
|
|
90
|
+
const dispatcher = createRpcDispatcher(exports);
|
|
91
|
+
wireWebSocket(server, dispatcher, () => dispatcher.clearAll());
|
|
92
|
+
}
|
|
164
93
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
return;
|
|
168
|
-
}
|
|
94
|
+
return new Response(null, { status: 101, webSocket: client });
|
|
95
|
+
}
|
|
169
96
|
|
|
170
|
-
|
|
171
|
-
|
|
97
|
+
// --- HTTP routing ---
|
|
98
|
+
const pathname = url.pathname;
|
|
172
99
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
send(server, { type: "result", id, streamId, valueType: "readablestream" });
|
|
177
|
-
} else if (isAsyncIterable(result)) {
|
|
178
|
-
const iterId = nextIteratorId++;
|
|
179
|
-
iteratorStore.set(iterId, result[Symbol.asyncIterator]());
|
|
180
|
-
send(server, { type: "result", id, iteratorId: iterId, valueType: "asynciterator" });
|
|
181
|
-
} else if (typeof result === "function") {
|
|
182
|
-
send(server, { type: "result", id, path: [...path], valueType: "function" });
|
|
183
|
-
} else {
|
|
184
|
-
send(server, { type: "result", id, value: result });
|
|
185
|
-
}
|
|
186
|
-
} catch (err) {
|
|
187
|
-
send(server, { type: "error", id, error: String(err) });
|
|
188
|
-
}
|
|
189
|
-
} else if (type === "get") {
|
|
190
|
-
// Property access on instance
|
|
191
|
-
try {
|
|
192
|
-
const instance = instanceStore.get(instanceId);
|
|
193
|
-
if (!instance) {
|
|
194
|
-
send(server, { type: "error", id, error: "Instance not found" });
|
|
195
|
-
return;
|
|
196
|
-
}
|
|
197
|
-
const value = getByPath(instance, path);
|
|
198
|
-
if (typeof value === "function") {
|
|
199
|
-
send(server, { type: "result", id, valueType: "function" });
|
|
200
|
-
} else {
|
|
201
|
-
send(server, { type: "result", id, value });
|
|
202
|
-
}
|
|
203
|
-
} catch (err) {
|
|
204
|
-
send(server, { type: "error", id, error: String(err) });
|
|
205
|
-
}
|
|
206
|
-
} else if (type === "set") {
|
|
207
|
-
// Property assignment on instance
|
|
208
|
-
try {
|
|
209
|
-
const instance = instanceStore.get(instanceId);
|
|
210
|
-
if (!instance) {
|
|
211
|
-
send(server, { type: "error", id, error: "Instance not found" });
|
|
212
|
-
return;
|
|
213
|
-
}
|
|
214
|
-
const parent = path.length > 1 ? getByPath(instance, path.slice(0, -1)) : instance;
|
|
215
|
-
const prop = path[path.length - 1];
|
|
216
|
-
parent[prop] = args[0];
|
|
217
|
-
send(server, { type: "result", id, value: true });
|
|
218
|
-
} catch (err) {
|
|
219
|
-
send(server, { type: "error", id, error: String(err) });
|
|
220
|
-
}
|
|
221
|
-
} else if (type === "release") {
|
|
222
|
-
// Release instance
|
|
223
|
-
instanceStore.delete(instanceId);
|
|
224
|
-
send(server, { type: "result", id, value: true });
|
|
225
|
-
} else if (type === "iterate-next") {
|
|
226
|
-
const iter = iteratorStore.get(iteratorId);
|
|
227
|
-
if (!iter) {
|
|
228
|
-
send(server, { type: "error", id, error: "Iterator not found" });
|
|
229
|
-
return;
|
|
230
|
-
}
|
|
231
|
-
try {
|
|
232
|
-
const { value, done } = await iter.next();
|
|
233
|
-
if (done) iteratorStore.delete(iteratorId);
|
|
234
|
-
send(server, { type: "iterate-result", id, value, done: !!done });
|
|
235
|
-
} catch (err) {
|
|
236
|
-
send(server, { type: "error", id, error: String(err) });
|
|
237
|
-
}
|
|
238
|
-
} else if (type === "iterate-return") {
|
|
239
|
-
const iter = iteratorStore.get(iteratorId);
|
|
240
|
-
if (iter?.return) await iter.return(undefined);
|
|
241
|
-
iteratorStore.delete(iteratorId);
|
|
242
|
-
send(server, { type: "iterate-result", id, value: undefined, done: true });
|
|
243
|
-
} else if (type === "stream-read") {
|
|
244
|
-
// ReadableStream chunk read
|
|
245
|
-
const { streamId } = msg;
|
|
246
|
-
const entry = streamStore.get(streamId);
|
|
247
|
-
if (!entry) {
|
|
248
|
-
send(server, { type: "error", id, error: "Stream not found" });
|
|
249
|
-
return;
|
|
250
|
-
}
|
|
251
|
-
try {
|
|
252
|
-
// Get or create reader for this stream
|
|
253
|
-
let reader = entry.reader;
|
|
254
|
-
if (!reader) {
|
|
255
|
-
reader = entry.stream.getReader();
|
|
256
|
-
entry.reader = reader;
|
|
257
|
-
}
|
|
258
|
-
const { value, done } = await reader.read();
|
|
259
|
-
if (done) {
|
|
260
|
-
streamStore.delete(streamId);
|
|
261
|
-
}
|
|
262
|
-
// Convert Uint8Array to array for devalue serialization
|
|
263
|
-
const serializedValue = value instanceof Uint8Array ? Array.from(value) : value;
|
|
264
|
-
send(server, { type: "stream-result", id, value: serializedValue, done: !!done });
|
|
265
|
-
} catch (err) {
|
|
266
|
-
streamStore.delete(streamId);
|
|
267
|
-
send(server, { type: "error", id, error: String(err) });
|
|
268
|
-
}
|
|
269
|
-
} else if (type === "stream-cancel") {
|
|
270
|
-
// Cancel ReadableStream
|
|
271
|
-
const { streamId } = msg;
|
|
272
|
-
const entry = streamStore.get(streamId);
|
|
273
|
-
if (entry) {
|
|
274
|
-
try {
|
|
275
|
-
if (entry.reader) {
|
|
276
|
-
await entry.reader.cancel();
|
|
277
|
-
} else {
|
|
278
|
-
await entry.stream.cancel();
|
|
279
|
-
}
|
|
280
|
-
} catch (e) { /* ignore */ }
|
|
281
|
-
streamStore.delete(streamId);
|
|
282
|
-
}
|
|
283
|
-
send(server, { type: "result", id, value: true });
|
|
284
|
-
} else if (type === "writable-create") {
|
|
285
|
-
// Create a WritableStream on server side
|
|
286
|
-
const { targetPath, targetInstanceId } = msg;
|
|
287
|
-
let chunks = [];
|
|
288
|
-
const writableId = nextStreamId++;
|
|
100
|
+
// Core modules (cached immutably)
|
|
101
|
+
if (pathname === corePath) return jsResponse(coreModuleCode, { "Cache-Control": IMMUTABLE });
|
|
102
|
+
if (pathname === sharedCorePath) return jsResponse(sharedCoreModuleCode, { "Cache-Control": IMMUTABLE });
|
|
289
103
|
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
}
|
|
300
|
-
});
|
|
104
|
+
// Type definitions
|
|
105
|
+
if (url.searchParams.has("types")) {
|
|
106
|
+
if (pathname === "/") return tsResponse(generatedTypes || "");
|
|
107
|
+
const name = pathname.slice(1);
|
|
108
|
+
return exportKeys.includes(name)
|
|
109
|
+
? tsResponse(`export { ${name} as default, ${name} } from "./?types";`)
|
|
110
|
+
: tsResponse("// Export not found", 404);
|
|
111
|
+
}
|
|
112
|
+
if (pathname.endsWith(".d.ts")) return tsResponse(generatedTypes || "");
|
|
301
113
|
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
} else if (type === "writable-write") {
|
|
305
|
-
// Write chunk to WritableStream
|
|
306
|
-
const { writableId, chunk } = msg;
|
|
307
|
-
const entry = writableStreamStore.get(writableId);
|
|
308
|
-
if (!entry) {
|
|
309
|
-
send(server, { type: "error", id, error: "WritableStream not found" });
|
|
310
|
-
return;
|
|
311
|
-
}
|
|
312
|
-
try {
|
|
313
|
-
// Convert array back to Uint8Array if needed
|
|
314
|
-
const data = Array.isArray(chunk) ? new Uint8Array(chunk) : chunk;
|
|
315
|
-
entry.chunks.push(data);
|
|
316
|
-
send(server, { type: "result", id, value: true });
|
|
317
|
-
} catch (err) {
|
|
318
|
-
send(server, { type: "error", id, error: String(err) });
|
|
319
|
-
}
|
|
320
|
-
} else if (type === "writable-close") {
|
|
321
|
-
// Close WritableStream and return collected chunks
|
|
322
|
-
const { writableId } = msg;
|
|
323
|
-
const entry = writableStreamStore.get(writableId);
|
|
324
|
-
if (!entry) {
|
|
325
|
-
send(server, { type: "error", id, error: "WritableStream not found" });
|
|
326
|
-
return;
|
|
327
|
-
}
|
|
328
|
-
writableStreamStore.delete(writableId);
|
|
329
|
-
// Return the collected data
|
|
330
|
-
send(server, { type: "result", id, value: entry.chunks });
|
|
331
|
-
} else if (type === "writable-abort") {
|
|
332
|
-
// Abort WritableStream
|
|
333
|
-
const { writableId } = msg;
|
|
334
|
-
writableStreamStore.delete(writableId);
|
|
335
|
-
send(server, { type: "result", id, value: true });
|
|
336
|
-
}
|
|
337
|
-
} catch (err) {
|
|
338
|
-
console.error("WebSocket message error:", err);
|
|
339
|
-
}
|
|
340
|
-
});
|
|
114
|
+
const baseUrl = `${url.protocol}//${url.host}`;
|
|
115
|
+
const cpath = isShared ? sharedCorePath : corePath;
|
|
341
116
|
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
117
|
+
// Root module
|
|
118
|
+
if (pathname === "/") {
|
|
119
|
+
return jsResponse(buildIndexModule(cpath), {
|
|
120
|
+
"Cache-Control": "no-cache",
|
|
121
|
+
"X-TypeScript-Types": `${baseUrl}/?types`,
|
|
347
122
|
});
|
|
348
|
-
|
|
349
|
-
return new Response(null, { status: 101, webSocket: client });
|
|
350
123
|
}
|
|
351
124
|
|
|
352
|
-
|
|
353
|
-
const
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
return new Response(typeDefinitions, {
|
|
359
|
-
headers: {
|
|
360
|
-
"Content-Type": "application/typescript; charset=utf-8",
|
|
361
|
-
"Access-Control-Allow-Origin": "*",
|
|
362
|
-
"Cache-Control": "no-cache",
|
|
363
|
-
},
|
|
125
|
+
// Per-export module
|
|
126
|
+
const exportName = pathname.slice(1);
|
|
127
|
+
if (exportKeys.includes(exportName)) {
|
|
128
|
+
return jsResponse(buildExportModule(cpath, exportName), {
|
|
129
|
+
"Cache-Control": "no-cache",
|
|
130
|
+
"X-TypeScript-Types": `${baseUrl}/${exportName}?types`,
|
|
364
131
|
});
|
|
365
132
|
}
|
|
366
133
|
|
|
367
|
-
|
|
368
|
-
const namedExports = exportKeys
|
|
369
|
-
.map((key) => `export const ${key} = createProxy([${JSON.stringify(key)}]);`)
|
|
370
|
-
.join("\n");
|
|
371
|
-
|
|
372
|
-
const clientCode = CLIENT_CODE
|
|
373
|
-
.replace("__WS_URL__", JSON.stringify(wsUrl))
|
|
374
|
-
.replace("__DEVALUE_STRINGIFY__", DEVALUE_STRINGIFY)
|
|
375
|
-
.replace("__DEVALUE_PARSE__", DEVALUE_PARSE)
|
|
376
|
-
.replace("__NAMED_EXPORTS__", namedExports);
|
|
377
|
-
|
|
378
|
-
// Build types URL for X-TypeScript-Types header
|
|
379
|
-
const typesUrl = `${url.protocol}//${url.host}${url.pathname}?types`;
|
|
380
|
-
|
|
381
|
-
return new Response(clientCode, {
|
|
382
|
-
headers: {
|
|
383
|
-
"Content-Type": "application/javascript; charset=utf-8",
|
|
384
|
-
"Access-Control-Allow-Origin": "*",
|
|
385
|
-
"Cache-Control": "no-cache",
|
|
386
|
-
"X-TypeScript-Types": typesUrl,
|
|
387
|
-
},
|
|
388
|
-
});
|
|
134
|
+
return new Response("Not found", { status: 404 });
|
|
389
135
|
},
|
|
390
136
|
};
|
|
391
137
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "export-runtime",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.7",
|
|
4
4
|
"description": "Cloudflare Workers ESM Export Framework Runtime",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cloudflare",
|
|
@@ -29,10 +29,13 @@
|
|
|
29
29
|
"entry.js",
|
|
30
30
|
"handler.js",
|
|
31
31
|
"client.js",
|
|
32
|
+
"rpc.js",
|
|
33
|
+
"shared-do.js",
|
|
32
34
|
"bin/generate-types.mjs"
|
|
33
35
|
],
|
|
34
36
|
"dependencies": {
|
|
35
37
|
"devalue": "^5.1.1",
|
|
38
|
+
"oxc-minify": "^0.121.0",
|
|
36
39
|
"oxc-parser": "^0.121.0"
|
|
37
40
|
}
|
|
38
41
|
}
|
package/rpc.js
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
export const getByPath = (obj, path) => {
|
|
2
|
+
let current = obj;
|
|
3
|
+
for (const key of path) {
|
|
4
|
+
if (current == null) return undefined;
|
|
5
|
+
current = current[key];
|
|
6
|
+
}
|
|
7
|
+
return current;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export const isAsyncIterable = (value) =>
|
|
11
|
+
value != null && typeof value[Symbol.asyncIterator] === "function";
|
|
12
|
+
|
|
13
|
+
export const isReadableStream = (value) =>
|
|
14
|
+
value != null && typeof value.getReader === "function" && typeof value.pipeTo === "function";
|
|
15
|
+
|
|
16
|
+
export const isClass = (fn) =>
|
|
17
|
+
typeof fn === "function" && /^class\s/.test(Function.prototype.toString.call(fn));
|
|
18
|
+
|
|
19
|
+
export const RPC_METHODS = [
|
|
20
|
+
"rpcCall", "rpcConstruct", "rpcInstanceCall", "rpcGet", "rpcSet", "rpcRelease",
|
|
21
|
+
"rpcIterateNext", "rpcIterateReturn", "rpcStreamRead", "rpcStreamCancel",
|
|
22
|
+
"rpcWritableCreate", "rpcWritableWrite", "rpcWritableClose", "rpcWritableAbort",
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
export function createRpcDispatcher(exports) {
|
|
26
|
+
const instances = new Map();
|
|
27
|
+
const iterators = new Map();
|
|
28
|
+
const streams = new Map();
|
|
29
|
+
const writables = new Map();
|
|
30
|
+
let nextId = 1;
|
|
31
|
+
|
|
32
|
+
const requireInstance = (id) => {
|
|
33
|
+
const inst = instances.get(id);
|
|
34
|
+
if (!inst) throw new Error("Instance not found");
|
|
35
|
+
return inst;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const wrapResult = (result, path) => {
|
|
39
|
+
if (isReadableStream(result)) {
|
|
40
|
+
const id = nextId++;
|
|
41
|
+
streams.set(id, { stream: result, reader: null });
|
|
42
|
+
return { type: "result", streamId: id, valueType: "readablestream" };
|
|
43
|
+
}
|
|
44
|
+
if (isAsyncIterable(result)) {
|
|
45
|
+
const id = nextId++;
|
|
46
|
+
iterators.set(id, result[Symbol.asyncIterator]());
|
|
47
|
+
return { type: "result", iteratorId: id, valueType: "asynciterator" };
|
|
48
|
+
}
|
|
49
|
+
if (typeof result === "function") {
|
|
50
|
+
return { type: "result", path: [...path], valueType: "function" };
|
|
51
|
+
}
|
|
52
|
+
return { type: "result", value: result };
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const callTarget = async (obj, path, args) => {
|
|
56
|
+
const target = getByPath(obj, path);
|
|
57
|
+
const thisArg = path.length > 1 ? getByPath(obj, path.slice(0, -1)) : (obj === exports ? undefined : obj);
|
|
58
|
+
if (typeof target !== "function") throw new Error(`${path.join(".")} is not a function`);
|
|
59
|
+
return wrapResult(await target.apply(thisArg, args), path);
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
return {
|
|
63
|
+
rpcCall: (path, args = []) => callTarget(exports, path, args),
|
|
64
|
+
|
|
65
|
+
async rpcConstruct(path, args = []) {
|
|
66
|
+
const Ctor = getByPath(exports, path);
|
|
67
|
+
if (!isClass(Ctor)) throw new Error(`${path.join(".")} is not a class`);
|
|
68
|
+
const id = nextId++;
|
|
69
|
+
instances.set(id, new Ctor(...args));
|
|
70
|
+
return { type: "result", instanceId: id, valueType: "instance" };
|
|
71
|
+
},
|
|
72
|
+
|
|
73
|
+
rpcInstanceCall: (instanceId, path, args = []) =>
|
|
74
|
+
callTarget(requireInstance(instanceId), path, args),
|
|
75
|
+
|
|
76
|
+
async rpcGet(instanceId, path) {
|
|
77
|
+
const value = getByPath(requireInstance(instanceId), path);
|
|
78
|
+
return typeof value === "function"
|
|
79
|
+
? { type: "result", valueType: "function" }
|
|
80
|
+
: { type: "result", value };
|
|
81
|
+
},
|
|
82
|
+
|
|
83
|
+
async rpcSet(instanceId, path, value) {
|
|
84
|
+
const inst = requireInstance(instanceId);
|
|
85
|
+
const parent = path.length > 1 ? getByPath(inst, path.slice(0, -1)) : inst;
|
|
86
|
+
parent[path.at(-1)] = value;
|
|
87
|
+
return { type: "result", value: true };
|
|
88
|
+
},
|
|
89
|
+
|
|
90
|
+
async rpcRelease(instanceId) {
|
|
91
|
+
instances.delete(instanceId);
|
|
92
|
+
return { type: "result", value: true };
|
|
93
|
+
},
|
|
94
|
+
|
|
95
|
+
async rpcIterateNext(iteratorId) {
|
|
96
|
+
const iter = iterators.get(iteratorId);
|
|
97
|
+
if (!iter) throw new Error("Iterator not found");
|
|
98
|
+
const { value, done } = await iter.next();
|
|
99
|
+
if (done) iterators.delete(iteratorId);
|
|
100
|
+
return { type: "iterate-result", value, done: !!done };
|
|
101
|
+
},
|
|
102
|
+
|
|
103
|
+
async rpcIterateReturn(iteratorId) {
|
|
104
|
+
const iter = iterators.get(iteratorId);
|
|
105
|
+
if (iter?.return) await iter.return(undefined);
|
|
106
|
+
iterators.delete(iteratorId);
|
|
107
|
+
return { type: "iterate-result", value: undefined, done: true };
|
|
108
|
+
},
|
|
109
|
+
|
|
110
|
+
async rpcStreamRead(streamId) {
|
|
111
|
+
const entry = streams.get(streamId);
|
|
112
|
+
if (!entry) throw new Error("Stream not found");
|
|
113
|
+
if (!entry.reader) entry.reader = entry.stream.getReader();
|
|
114
|
+
const { value, done } = await entry.reader.read();
|
|
115
|
+
if (done) streams.delete(streamId);
|
|
116
|
+
const v = value instanceof Uint8Array ? Array.from(value) : value;
|
|
117
|
+
return { type: "stream-result", value: v, done: !!done };
|
|
118
|
+
},
|
|
119
|
+
|
|
120
|
+
async rpcStreamCancel(streamId) {
|
|
121
|
+
const entry = streams.get(streamId);
|
|
122
|
+
if (entry) {
|
|
123
|
+
try { await (entry.reader || entry.stream).cancel(); } catch {}
|
|
124
|
+
streams.delete(streamId);
|
|
125
|
+
}
|
|
126
|
+
return { type: "result", value: true };
|
|
127
|
+
},
|
|
128
|
+
|
|
129
|
+
async rpcWritableCreate() {
|
|
130
|
+
const id = nextId++;
|
|
131
|
+
writables.set(id, { chunks: [] });
|
|
132
|
+
return { type: "result", writableId: id, valueType: "writablestream" };
|
|
133
|
+
},
|
|
134
|
+
|
|
135
|
+
async rpcWritableWrite(writableId, chunk) {
|
|
136
|
+
const entry = writables.get(writableId);
|
|
137
|
+
if (!entry) throw new Error("WritableStream not found");
|
|
138
|
+
entry.chunks.push(Array.isArray(chunk) ? new Uint8Array(chunk) : chunk);
|
|
139
|
+
return { type: "result", value: true };
|
|
140
|
+
},
|
|
141
|
+
|
|
142
|
+
async rpcWritableClose(writableId) {
|
|
143
|
+
const entry = writables.get(writableId);
|
|
144
|
+
if (!entry) throw new Error("WritableStream not found");
|
|
145
|
+
writables.delete(writableId);
|
|
146
|
+
return { type: "result", value: entry.chunks };
|
|
147
|
+
},
|
|
148
|
+
|
|
149
|
+
async rpcWritableAbort(writableId) {
|
|
150
|
+
writables.delete(writableId);
|
|
151
|
+
return { type: "result", value: true };
|
|
152
|
+
},
|
|
153
|
+
|
|
154
|
+
clearAll() {
|
|
155
|
+
instances.clear();
|
|
156
|
+
iterators.clear();
|
|
157
|
+
streams.clear();
|
|
158
|
+
writables.clear();
|
|
159
|
+
},
|
|
160
|
+
};
|
|
161
|
+
}
|
package/shared-do.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { DurableObject } from "cloudflare:workers";
|
|
2
|
+
import * as userExports from "__USER_MODULE__";
|
|
3
|
+
import { createRpcDispatcher } from "./rpc.js";
|
|
4
|
+
|
|
5
|
+
export class SharedExportDO extends DurableObject {
|
|
6
|
+
#d;
|
|
7
|
+
constructor(ctx, env) {
|
|
8
|
+
super(ctx, env);
|
|
9
|
+
this.#d = createRpcDispatcher(userExports);
|
|
10
|
+
}
|
|
11
|
+
rpcCall(p, a) { return this.#d.rpcCall(p, a); }
|
|
12
|
+
rpcConstruct(p, a) { return this.#d.rpcConstruct(p, a); }
|
|
13
|
+
rpcInstanceCall(i, p, a) { return this.#d.rpcInstanceCall(i, p, a); }
|
|
14
|
+
rpcGet(i, p) { return this.#d.rpcGet(i, p); }
|
|
15
|
+
rpcSet(i, p, v) { return this.#d.rpcSet(i, p, v); }
|
|
16
|
+
rpcRelease(i) { return this.#d.rpcRelease(i); }
|
|
17
|
+
rpcIterateNext(i) { return this.#d.rpcIterateNext(i); }
|
|
18
|
+
rpcIterateReturn(i) { return this.#d.rpcIterateReturn(i); }
|
|
19
|
+
rpcStreamRead(s) { return this.#d.rpcStreamRead(s); }
|
|
20
|
+
rpcStreamCancel(s) { return this.#d.rpcStreamCancel(s); }
|
|
21
|
+
rpcWritableCreate() { return this.#d.rpcWritableCreate(); }
|
|
22
|
+
rpcWritableWrite(w, c) { return this.#d.rpcWritableWrite(w, c); }
|
|
23
|
+
rpcWritableClose(w) { return this.#d.rpcWritableClose(w); }
|
|
24
|
+
rpcWritableAbort(w) { return this.#d.rpcWritableAbort(w); }
|
|
25
|
+
}
|