export-runtime 0.0.6 → 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 +68 -5
- package/client.js +70 -181
- package/entry.js +3 -2
- package/handler.js +93 -355
- package/package.json +3 -1
- package/rpc.js +161 -0
- package/shared-do.js +25 -0
package/bin/generate-types.mjs
CHANGED
|
@@ -220,25 +220,88 @@ lines.push("}>;");
|
|
|
220
220
|
|
|
221
221
|
const typeDefinitions = lines.join("\n");
|
|
222
222
|
|
|
223
|
-
// --- Minify core
|
|
223
|
+
// --- Minify core modules ---
|
|
224
224
|
|
|
225
225
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
226
|
-
const { CORE_CODE } = await import(path.join(__dirname, "..", "client.js"));
|
|
226
|
+
const { CORE_CODE, SHARED_CORE_CODE } = await import(path.join(__dirname, "..", "client.js"));
|
|
227
227
|
|
|
228
|
-
// CORE_CODE uses import.meta.url for WS URL — no placeholders needed.
|
|
229
228
|
const minified = minifySync("_core.js", CORE_CODE);
|
|
230
229
|
if (minified.errors?.length) {
|
|
231
|
-
console.error("Minification errors:", minified.errors);
|
|
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);
|
|
232
236
|
}
|
|
233
237
|
|
|
234
238
|
// Generate a unique ID per build for cache-busting the core module path
|
|
235
239
|
const coreId = crypto.randomUUID();
|
|
236
240
|
|
|
237
|
-
// Write as a JS module
|
|
241
|
+
// Write as a JS module
|
|
238
242
|
const outPath = path.join(cwd, ".export-types.js");
|
|
239
243
|
fs.writeFileSync(outPath, [
|
|
240
244
|
`export default ${JSON.stringify(typeDefinitions)};`,
|
|
241
245
|
`export const minifiedCore = ${JSON.stringify(minified.code)};`,
|
|
246
|
+
`export const minifiedSharedCore = ${JSON.stringify(minifiedShared.code)};`,
|
|
242
247
|
`export const coreId = ${JSON.stringify(coreId)};`,
|
|
243
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
|
+
|
|
244
306
|
console.log("Generated type definitions + minified core →", outPath);
|
|
307
|
+
console.log("Generated shared import module →", sharedModulePath);
|
package/client.js
CHANGED
|
@@ -1,27 +1,19 @@
|
|
|
1
|
-
// Core module
|
|
2
|
-
|
|
3
|
-
export const CORE_CODE = `
|
|
1
|
+
// Core module template. __WS_SUFFIX__ is replaced: "./" for normal, "./?shared" for shared.
|
|
2
|
+
const CORE_TEMPLATE = `
|
|
4
3
|
const stringify = (value) => {
|
|
5
4
|
const stringified = [];
|
|
6
5
|
const indexes = new Map();
|
|
7
6
|
let p = 0;
|
|
8
|
-
|
|
9
7
|
const flatten = (thing) => {
|
|
10
|
-
if (typeof thing === 'function')
|
|
11
|
-
throw new Error('Cannot stringify a function');
|
|
12
|
-
}
|
|
13
|
-
|
|
8
|
+
if (typeof thing === 'function') throw new Error('Cannot stringify a function');
|
|
14
9
|
if (indexes.has(thing)) return indexes.get(thing);
|
|
15
|
-
|
|
16
10
|
if (thing === undefined) return -1;
|
|
17
11
|
if (Number.isNaN(thing)) return -3;
|
|
18
12
|
if (thing === Infinity) return -4;
|
|
19
13
|
if (thing === -Infinity) return -5;
|
|
20
14
|
if (thing === 0 && 1 / thing < 0) return -6;
|
|
21
|
-
|
|
22
15
|
const index = p++;
|
|
23
16
|
indexes.set(thing, index);
|
|
24
|
-
|
|
25
17
|
if (typeof thing === 'boolean' || typeof thing === 'number' || typeof thing === 'string' || thing === null) {
|
|
26
18
|
stringified[index] = thing;
|
|
27
19
|
} else if (thing instanceof Date) {
|
|
@@ -38,73 +30,37 @@ const stringify = (value) => {
|
|
|
38
30
|
stringified[index] = ['Set', ...[...thing].map(flatten)];
|
|
39
31
|
} else if (thing instanceof Map) {
|
|
40
32
|
stringified[index] = ['Map', ...[...thing].map(([k, v]) => [flatten(k), flatten(v)])];
|
|
41
|
-
} else if (thing
|
|
42
|
-
stringified[index] = [
|
|
43
|
-
} else if (thing instanceof Uint8Array) {
|
|
44
|
-
stringified[index] = ['Uint8Array', ...[...thing].map(flatten)];
|
|
45
|
-
} else if (thing instanceof Uint8ClampedArray) {
|
|
46
|
-
stringified[index] = ['Uint8ClampedArray', ...[...thing].map(flatten)];
|
|
47
|
-
} else if (thing instanceof Int16Array) {
|
|
48
|
-
stringified[index] = ['Int16Array', ...[...thing].map(flatten)];
|
|
49
|
-
} else if (thing instanceof Uint16Array) {
|
|
50
|
-
stringified[index] = ['Uint16Array', ...[...thing].map(flatten)];
|
|
51
|
-
} else if (thing instanceof Int32Array) {
|
|
52
|
-
stringified[index] = ['Int32Array', ...[...thing].map(flatten)];
|
|
53
|
-
} else if (thing instanceof Uint32Array) {
|
|
54
|
-
stringified[index] = ['Uint32Array', ...[...thing].map(flatten)];
|
|
55
|
-
} else if (thing instanceof Float32Array) {
|
|
56
|
-
stringified[index] = ['Float32Array', ...[...thing].map(flatten)];
|
|
57
|
-
} else if (thing instanceof Float64Array) {
|
|
58
|
-
stringified[index] = ['Float64Array', ...[...thing].map(flatten)];
|
|
59
|
-
} else if (thing instanceof BigInt64Array) {
|
|
60
|
-
stringified[index] = ['BigInt64Array', ...[...thing].map(flatten)];
|
|
61
|
-
} else if (thing instanceof BigUint64Array) {
|
|
62
|
-
stringified[index] = ['BigUint64Array', ...[...thing].map(flatten)];
|
|
33
|
+
} else if (ArrayBuffer.isView(thing)) {
|
|
34
|
+
stringified[index] = [thing[Symbol.toStringTag], ...[...thing].map(flatten)];
|
|
63
35
|
} else if (thing instanceof ArrayBuffer) {
|
|
64
36
|
stringified[index] = ['ArrayBuffer', ...[...new Uint8Array(thing)].map(flatten)];
|
|
65
37
|
} else if (Array.isArray(thing)) {
|
|
66
38
|
stringified[index] = thing.map(flatten);
|
|
67
39
|
} else if (typeof thing === 'object') {
|
|
68
40
|
const obj = {};
|
|
69
|
-
for (const key of Object.keys(thing))
|
|
70
|
-
obj[key] = flatten(thing[key]);
|
|
71
|
-
}
|
|
41
|
+
for (const key of Object.keys(thing)) obj[key] = flatten(thing[key]);
|
|
72
42
|
stringified[index] = obj;
|
|
73
43
|
} else {
|
|
74
44
|
throw new Error('Cannot stringify ' + typeof thing);
|
|
75
45
|
}
|
|
76
|
-
|
|
77
46
|
return index;
|
|
78
47
|
};
|
|
79
|
-
|
|
80
48
|
flatten(value);
|
|
81
49
|
return JSON.stringify(stringified);
|
|
82
50
|
};
|
|
83
51
|
|
|
84
|
-
const UNDEFINED = -1;
|
|
85
|
-
const HOLE = -2;
|
|
86
|
-
const NAN = -3;
|
|
87
|
-
const POSITIVE_INFINITY = -4;
|
|
88
|
-
const NEGATIVE_INFINITY = -5;
|
|
89
|
-
const NEGATIVE_ZERO = -6;
|
|
90
|
-
|
|
91
52
|
const parse = (serialized) => {
|
|
92
53
|
if (serialized === '') return undefined;
|
|
93
54
|
const values = JSON.parse(serialized);
|
|
94
55
|
const hydrated = new Array(values.length);
|
|
95
|
-
|
|
96
56
|
const hydrate = (index) => {
|
|
97
|
-
if (index ===
|
|
98
|
-
if (index ===
|
|
99
|
-
if (index ===
|
|
100
|
-
if (index ===
|
|
101
|
-
if (index ===
|
|
102
|
-
if (index === NEGATIVE_ZERO) return -0;
|
|
103
|
-
|
|
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;
|
|
104
62
|
if (hydrated[index] !== undefined) return hydrated[index];
|
|
105
|
-
|
|
106
63
|
const value = values[index];
|
|
107
|
-
|
|
108
64
|
if (typeof value === 'number' || typeof value === 'string' || typeof value === 'boolean' || value === null) {
|
|
109
65
|
hydrated[index] = value;
|
|
110
66
|
} else if (Array.isArray(value)) {
|
|
@@ -125,15 +81,16 @@ const parse = (serialized) => {
|
|
|
125
81
|
break;
|
|
126
82
|
case 'ArrayBuffer': {
|
|
127
83
|
const bytes = value.slice(1).map(hydrate);
|
|
128
|
-
const
|
|
129
|
-
new Uint8Array(
|
|
130
|
-
hydrated[index] =
|
|
84
|
+
const buf = new ArrayBuffer(bytes.length);
|
|
85
|
+
new Uint8Array(buf).set(bytes);
|
|
86
|
+
hydrated[index] = buf;
|
|
131
87
|
break;
|
|
132
88
|
}
|
|
133
|
-
default:
|
|
89
|
+
default: {
|
|
134
90
|
const arr = new Array(value.length);
|
|
135
91
|
hydrated[index] = arr;
|
|
136
92
|
for (let i = 0; i < value.length; i++) arr[i] = hydrate(value[i]);
|
|
93
|
+
}
|
|
137
94
|
}
|
|
138
95
|
} else {
|
|
139
96
|
const arr = new Array(value.length);
|
|
@@ -145,38 +102,29 @@ const parse = (serialized) => {
|
|
|
145
102
|
hydrated[index] = obj;
|
|
146
103
|
for (const key in value) obj[key] = hydrate(value[key]);
|
|
147
104
|
}
|
|
148
|
-
|
|
149
105
|
return hydrated[index];
|
|
150
106
|
};
|
|
151
|
-
|
|
152
107
|
return hydrate(0);
|
|
153
108
|
};
|
|
154
109
|
|
|
155
|
-
const _u = new URL("
|
|
110
|
+
const _u = new URL("__WS_SUFFIX__", import.meta.url);
|
|
156
111
|
_u.protocol = _u.protocol === "https:" ? "wss:" : "ws:";
|
|
157
112
|
const ws = new WebSocket(_u.href);
|
|
158
113
|
const pending = new Map();
|
|
159
114
|
let nextId = 1;
|
|
160
|
-
let keepaliveInterval
|
|
115
|
+
let keepaliveInterval;
|
|
161
116
|
|
|
162
117
|
const ready = new Promise((resolve, reject) => {
|
|
163
118
|
ws.onopen = () => {
|
|
164
119
|
keepaliveInterval = setInterval(() => {
|
|
165
|
-
if (ws.readyState === WebSocket.OPEN) {
|
|
166
|
-
ws.send(stringify({ type: "ping", id: 0 }));
|
|
167
|
-
}
|
|
120
|
+
if (ws.readyState === WebSocket.OPEN) ws.send(stringify({ type: "ping", id: 0 }));
|
|
168
121
|
}, 30000);
|
|
169
|
-
resolve(
|
|
122
|
+
resolve();
|
|
170
123
|
};
|
|
171
|
-
ws.onerror =
|
|
124
|
+
ws.onerror = reject;
|
|
172
125
|
});
|
|
173
126
|
|
|
174
|
-
ws.onclose = () => {
|
|
175
|
-
if (keepaliveInterval) {
|
|
176
|
-
clearInterval(keepaliveInterval);
|
|
177
|
-
keepaliveInterval = null;
|
|
178
|
-
}
|
|
179
|
-
};
|
|
127
|
+
ws.onclose = () => { clearInterval(keepaliveInterval); };
|
|
180
128
|
|
|
181
129
|
const sendRequest = async (msg) => {
|
|
182
130
|
await ready;
|
|
@@ -187,135 +135,76 @@ const sendRequest = async (msg) => {
|
|
|
187
135
|
});
|
|
188
136
|
};
|
|
189
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
|
+
|
|
190
146
|
ws.onmessage = (event) => {
|
|
191
147
|
const msg = parse(event.data);
|
|
192
|
-
|
|
193
|
-
if (msg.type === "pong") return;
|
|
194
|
-
|
|
195
148
|
const resolver = pending.get(msg.id);
|
|
196
149
|
if (!resolver) return;
|
|
150
|
+
pending.delete(msg.id);
|
|
197
151
|
|
|
198
152
|
if (msg.type === "error") {
|
|
199
153
|
resolver.reject(new Error(msg.error));
|
|
200
|
-
pending.delete(msg.id);
|
|
201
154
|
} else if (msg.type === "result") {
|
|
202
|
-
if (msg.valueType === "function")
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
const streamId = msg.streamId;
|
|
219
|
-
const stream = new ReadableStream({
|
|
220
|
-
async pull(controller) {
|
|
221
|
-
try {
|
|
222
|
-
const result = await sendRequest({ type: "stream-read", streamId });
|
|
223
|
-
if (result.done) {
|
|
224
|
-
controller.close();
|
|
225
|
-
} else {
|
|
226
|
-
controller.enqueue(result.value);
|
|
227
|
-
}
|
|
228
|
-
} catch (err) {
|
|
229
|
-
controller.error(err);
|
|
230
|
-
}
|
|
231
|
-
},
|
|
232
|
-
async cancel() {
|
|
233
|
-
await sendRequest({ type: "stream-cancel", streamId });
|
|
234
|
-
}
|
|
235
|
-
});
|
|
236
|
-
resolver.resolve(stream);
|
|
237
|
-
} else if (msg.valueType === "writablestream") {
|
|
238
|
-
const writableId = msg.writableId;
|
|
239
|
-
const stream = new WritableStream({
|
|
240
|
-
async write(chunk) {
|
|
241
|
-
const data = chunk instanceof Uint8Array ? Array.from(chunk) : chunk;
|
|
242
|
-
await sendRequest({ type: "writable-write", writableId, chunk: data });
|
|
243
|
-
},
|
|
244
|
-
async close() {
|
|
245
|
-
await sendRequest({ type: "writable-close", writableId });
|
|
246
|
-
},
|
|
247
|
-
async abort(reason) {
|
|
248
|
-
await sendRequest({ type: "writable-abort", writableId });
|
|
249
|
-
}
|
|
250
|
-
});
|
|
251
|
-
resolver.resolve(stream);
|
|
252
|
-
} else {
|
|
253
|
-
resolver.resolve(msg.value);
|
|
254
|
-
}
|
|
255
|
-
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);
|
|
256
171
|
} else if (msg.type === "iterate-result") {
|
|
257
172
|
resolver.resolve({ value: msg.value, done: msg.done });
|
|
258
|
-
pending.delete(msg.id);
|
|
259
173
|
} else if (msg.type === "stream-result") {
|
|
260
|
-
|
|
261
|
-
resolver.resolve({ value, done: msg.done });
|
|
262
|
-
pending.delete(msg.id);
|
|
174
|
+
resolver.resolve({ value: Array.isArray(msg.value) ? new Uint8Array(msg.value) : msg.value, done: msg.done });
|
|
263
175
|
}
|
|
264
176
|
};
|
|
265
177
|
|
|
266
|
-
const createInstanceProxy = (instanceId, path = []) => {
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
},
|
|
282
|
-
async apply(_, __, args) {
|
|
283
|
-
return sendRequest({ type: "call", instanceId, path, args });
|
|
284
|
-
}
|
|
285
|
-
});
|
|
286
|
-
return proxy;
|
|
287
|
-
};
|
|
178
|
+
const createInstanceProxy = (instanceId, path = []) => new Proxy(function(){}, {
|
|
179
|
+
get(_, prop) {
|
|
180
|
+
if (prop === "then" || prop === Symbol.toStringTag) return undefined;
|
|
181
|
+
if (prop === Symbol.dispose || prop === Symbol.asyncDispose || prop === "[release]")
|
|
182
|
+
return () => sendRequest({ type: "release", instanceId });
|
|
183
|
+
return createInstanceProxy(instanceId, [...path, prop]);
|
|
184
|
+
},
|
|
185
|
+
set(_, prop, value) {
|
|
186
|
+
sendRequest({ type: "set", instanceId, path: [...path, prop], args: [value] });
|
|
187
|
+
return true;
|
|
188
|
+
},
|
|
189
|
+
async apply(_, __, args) {
|
|
190
|
+
return sendRequest({ type: "call", instanceId, path, args });
|
|
191
|
+
}
|
|
192
|
+
});
|
|
288
193
|
|
|
289
194
|
export const createProxy = (path = []) => new Proxy(function(){}, {
|
|
290
195
|
get(_, prop) {
|
|
291
196
|
if (prop === "then" || prop === Symbol.toStringTag) return undefined;
|
|
292
197
|
return createProxy([...path, prop]);
|
|
293
198
|
},
|
|
294
|
-
async apply(_, __, args) {
|
|
295
|
-
|
|
296
|
-
},
|
|
297
|
-
construct(_, args) {
|
|
298
|
-
return sendRequest({ type: "construct", path, args });
|
|
299
|
-
}
|
|
199
|
+
async apply(_, __, args) { return sendRequest({ type: "call", path, args }); },
|
|
200
|
+
construct(_, args) { return sendRequest({ type: "construct", path, args }); }
|
|
300
201
|
});
|
|
301
202
|
|
|
302
203
|
export const createUploadStream = async () => {
|
|
303
|
-
const
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
const stream = new WritableStream({
|
|
307
|
-
async write(chunk) {
|
|
308
|
-
const data = chunk instanceof Uint8Array ? Array.from(chunk) : chunk;
|
|
309
|
-
await sendRequest({ type: "writable-write", writableId, chunk: data });
|
|
310
|
-
},
|
|
311
|
-
async close() {
|
|
312
|
-
return sendRequest({ type: "writable-close", writableId });
|
|
313
|
-
},
|
|
314
|
-
async abort(reason) {
|
|
315
|
-
await sendRequest({ type: "writable-abort", writableId });
|
|
316
|
-
}
|
|
317
|
-
});
|
|
318
|
-
|
|
319
|
-
return { stream, writableId };
|
|
204
|
+
const { writableId } = await sendRequest({ type: "writable-create" });
|
|
205
|
+
return { stream: makeWritable(writableId), writableId };
|
|
320
206
|
};
|
|
321
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, { minifiedCore, coreId } 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, minifiedCore, coreId);
|
|
6
|
+
export default createHandler(userExports, generatedTypes, minifiedCore, coreId, minifiedSharedCore);
|
package/handler.js
CHANGED
|
@@ -1,395 +1,133 @@
|
|
|
1
1
|
import { stringify, parse } from "devalue";
|
|
2
|
-
import { CORE_CODE } from "./client.js";
|
|
3
|
-
|
|
4
|
-
const getByPath = (obj, path) => {
|
|
5
|
-
let current = obj;
|
|
6
|
-
for (const key of path) {
|
|
7
|
-
if (current == null) return undefined;
|
|
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";
|
|
18
|
-
|
|
19
|
-
const isClass = (fn) =>
|
|
20
|
-
typeof fn === "function" && /^class\s/.test(Function.prototype.toString.call(fn));
|
|
21
|
-
|
|
22
|
-
// Runtime fallback: generate TypeScript type definitions from exports
|
|
23
|
-
const generateTypeDefinitions = (exports, keys) => {
|
|
24
|
-
const lines = [
|
|
25
|
-
"// Auto-generated type definitions",
|
|
26
|
-
"// All functions are async over the network",
|
|
27
|
-
"",
|
|
28
|
-
];
|
|
29
|
-
|
|
30
|
-
for (const name of keys) {
|
|
31
|
-
const value = exports[name];
|
|
32
|
-
if (isClass(value)) {
|
|
33
|
-
const proto = value.prototype;
|
|
34
|
-
const methodNames = Object.getOwnPropertyNames(proto).filter(
|
|
35
|
-
(n) => n !== "constructor" && typeof proto[n] === "function"
|
|
36
|
-
);
|
|
37
|
-
lines.push(`export declare class ${name} {`);
|
|
38
|
-
lines.push(` constructor(...args: any[]);`);
|
|
39
|
-
for (const method of methodNames) {
|
|
40
|
-
lines.push(` ${method}(...args: any[]): Promise<any>;`);
|
|
41
|
-
}
|
|
42
|
-
lines.push(` [Symbol.dispose](): Promise<void>;`);
|
|
43
|
-
lines.push(` "[release]"(): Promise<void>;`);
|
|
44
|
-
lines.push(`}`);
|
|
45
|
-
} else if (typeof value === "function") {
|
|
46
|
-
const fnStr = Function.prototype.toString.call(value);
|
|
47
|
-
if (fnStr.startsWith("async function*") || fnStr.includes("async *")) {
|
|
48
|
-
lines.push(`export declare function ${name}(...args: any[]): Promise<AsyncIterable<any>>;`);
|
|
49
|
-
} else if (fnStr.includes("ReadableStream")) {
|
|
50
|
-
lines.push(`export declare function ${name}(...args: any[]): Promise<ReadableStream<any>>;`);
|
|
51
|
-
} else {
|
|
52
|
-
lines.push(`export declare function ${name}(...args: any[]): Promise<any>;`);
|
|
53
|
-
}
|
|
54
|
-
} else if (typeof value === "object" && value !== null) {
|
|
55
|
-
const keys = Object.keys(value);
|
|
56
|
-
lines.push(`export declare const ${name}: {`);
|
|
57
|
-
for (const key of keys) {
|
|
58
|
-
const v = value[key];
|
|
59
|
-
if (typeof v === "function") {
|
|
60
|
-
lines.push(` ${key}(...args: any[]): Promise<any>;`);
|
|
61
|
-
} else {
|
|
62
|
-
lines.push(` ${key}: any;`);
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
lines.push(`};`);
|
|
66
|
-
} else {
|
|
67
|
-
lines.push(`export declare const ${name}: any;`);
|
|
68
|
-
}
|
|
69
|
-
lines.push("");
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
lines.push("export declare function createUploadStream(): Promise<{");
|
|
73
|
-
lines.push(" stream: WritableStream<any>;");
|
|
74
|
-
lines.push(" writableId: number;");
|
|
75
|
-
lines.push("}>;");
|
|
76
|
-
|
|
77
|
-
return lines.join("\n");
|
|
78
|
-
};
|
|
2
|
+
import { CORE_CODE, SHARED_CORE_CODE } from "./client.js";
|
|
3
|
+
import { createRpcDispatcher } from "./rpc.js";
|
|
79
4
|
|
|
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";
|
|
80
9
|
|
|
81
|
-
const
|
|
82
|
-
"Content-Type":
|
|
83
|
-
"Access-Control-Allow-Origin": "*",
|
|
84
|
-
...extra,
|
|
85
|
-
});
|
|
10
|
+
const jsResponse = (body, extra = {}) =>
|
|
11
|
+
new Response(body, { headers: { "Content-Type": JS, ...CORS, ...extra } });
|
|
86
12
|
|
|
87
|
-
const
|
|
88
|
-
"Content-Type": "
|
|
89
|
-
"Access-Control-Allow-Origin": "*",
|
|
90
|
-
"Cache-Control": "no-cache",
|
|
91
|
-
});
|
|
13
|
+
const tsResponse = (body, status = 200) =>
|
|
14
|
+
new Response(body, { status, headers: { "Content-Type": TS, ...CORS, "Cache-Control": "no-cache" } });
|
|
92
15
|
|
|
93
|
-
export const createHandler = (exports, generatedTypes, minifiedCore, coreId) => {
|
|
16
|
+
export const createHandler = (exports, generatedTypes, minifiedCore, coreId, minifiedSharedCore) => {
|
|
94
17
|
const exportKeys = Object.keys(exports);
|
|
95
|
-
const iteratorStore = new Map();
|
|
96
|
-
const instanceStore = new Map();
|
|
97
|
-
const streamStore = new Map();
|
|
98
|
-
const writableStreamStore = new Map();
|
|
99
|
-
let nextIteratorId = 1;
|
|
100
|
-
let nextInstanceId = 1;
|
|
101
|
-
let nextStreamId = 1;
|
|
102
|
-
|
|
103
|
-
const send = (ws, data) => {
|
|
104
|
-
ws.send(stringify(data));
|
|
105
|
-
};
|
|
106
18
|
|
|
107
19
|
const coreModuleCode = minifiedCore || CORE_CODE;
|
|
20
|
+
const sharedCoreModuleCode = minifiedSharedCore || SHARED_CORE_CODE;
|
|
108
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);
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
|
|
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);
|
|
72
|
+
};
|
|
109
73
|
|
|
110
74
|
return {
|
|
111
|
-
async fetch(request) {
|
|
75
|
+
async fetch(request, env) {
|
|
112
76
|
const url = new URL(request.url);
|
|
113
|
-
const
|
|
77
|
+
const isShared = url.searchParams.has("shared");
|
|
114
78
|
|
|
115
|
-
// --- WebSocket upgrade
|
|
116
|
-
if (
|
|
79
|
+
// --- WebSocket upgrade ---
|
|
80
|
+
if (request.headers.get("Upgrade") === "websocket") {
|
|
117
81
|
const pair = new WebSocketPair();
|
|
118
82
|
const [client, server] = Object.values(pair);
|
|
119
|
-
|
|
120
83
|
server.accept();
|
|
121
84
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
if (type === "construct") {
|
|
133
|
-
try {
|
|
134
|
-
const Ctor = getByPath(exports, path);
|
|
135
|
-
if (!isClass(Ctor)) {
|
|
136
|
-
send(server, { type: "error", id, error: `${path.join(".")} is not a class` });
|
|
137
|
-
return;
|
|
138
|
-
}
|
|
139
|
-
const instance = new Ctor(...args);
|
|
140
|
-
const instId = nextInstanceId++;
|
|
141
|
-
instanceStore.set(instId, instance);
|
|
142
|
-
send(server, { type: "result", id, instanceId: instId, valueType: "instance" });
|
|
143
|
-
} catch (err) {
|
|
144
|
-
send(server, { type: "error", id, error: String(err) });
|
|
145
|
-
}
|
|
146
|
-
} else if (type === "call") {
|
|
147
|
-
try {
|
|
148
|
-
let target;
|
|
149
|
-
let thisArg;
|
|
150
|
-
|
|
151
|
-
if (instanceId !== undefined) {
|
|
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
|
-
target = getByPath(exports, path);
|
|
161
|
-
thisArg = path.length > 1 ? getByPath(exports, path.slice(0, -1)) : undefined;
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
if (typeof target !== "function") {
|
|
165
|
-
send(server, { type: "error", id, error: `${path.join(".")} is not a function` });
|
|
166
|
-
return;
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
const result = await target.apply(thisArg, args);
|
|
170
|
-
|
|
171
|
-
if (isReadableStream(result)) {
|
|
172
|
-
const streamId = nextStreamId++;
|
|
173
|
-
streamStore.set(streamId, { stream: result, reader: null });
|
|
174
|
-
send(server, { type: "result", id, streamId, valueType: "readablestream" });
|
|
175
|
-
} else if (isAsyncIterable(result)) {
|
|
176
|
-
const iterId = nextIteratorId++;
|
|
177
|
-
iteratorStore.set(iterId, result[Symbol.asyncIterator]());
|
|
178
|
-
send(server, { type: "result", id, iteratorId: iterId, valueType: "asynciterator" });
|
|
179
|
-
} else if (typeof result === "function") {
|
|
180
|
-
send(server, { type: "result", id, path: [...path], valueType: "function" });
|
|
181
|
-
} else {
|
|
182
|
-
send(server, { type: "result", id, value: result });
|
|
183
|
-
}
|
|
184
|
-
} catch (err) {
|
|
185
|
-
send(server, { type: "error", id, error: String(err) });
|
|
186
|
-
}
|
|
187
|
-
} else if (type === "get") {
|
|
188
|
-
try {
|
|
189
|
-
const instance = instanceStore.get(instanceId);
|
|
190
|
-
if (!instance) {
|
|
191
|
-
send(server, { type: "error", id, error: "Instance not found" });
|
|
192
|
-
return;
|
|
193
|
-
}
|
|
194
|
-
const value = getByPath(instance, path);
|
|
195
|
-
if (typeof value === "function") {
|
|
196
|
-
send(server, { type: "result", id, valueType: "function" });
|
|
197
|
-
} else {
|
|
198
|
-
send(server, { type: "result", id, value });
|
|
199
|
-
}
|
|
200
|
-
} catch (err) {
|
|
201
|
-
send(server, { type: "error", id, error: String(err) });
|
|
202
|
-
}
|
|
203
|
-
} else if (type === "set") {
|
|
204
|
-
try {
|
|
205
|
-
const instance = instanceStore.get(instanceId);
|
|
206
|
-
if (!instance) {
|
|
207
|
-
send(server, { type: "error", id, error: "Instance not found" });
|
|
208
|
-
return;
|
|
209
|
-
}
|
|
210
|
-
const parent = path.length > 1 ? getByPath(instance, path.slice(0, -1)) : instance;
|
|
211
|
-
const prop = path[path.length - 1];
|
|
212
|
-
parent[prop] = args[0];
|
|
213
|
-
send(server, { type: "result", id, value: true });
|
|
214
|
-
} catch (err) {
|
|
215
|
-
send(server, { type: "error", id, error: String(err) });
|
|
216
|
-
}
|
|
217
|
-
} else if (type === "release") {
|
|
218
|
-
instanceStore.delete(instanceId);
|
|
219
|
-
send(server, { type: "result", id, value: true });
|
|
220
|
-
} else if (type === "iterate-next") {
|
|
221
|
-
const iter = iteratorStore.get(iteratorId);
|
|
222
|
-
if (!iter) {
|
|
223
|
-
send(server, { type: "error", id, error: "Iterator not found" });
|
|
224
|
-
return;
|
|
225
|
-
}
|
|
226
|
-
try {
|
|
227
|
-
const { value, done } = await iter.next();
|
|
228
|
-
if (done) iteratorStore.delete(iteratorId);
|
|
229
|
-
send(server, { type: "iterate-result", id, value, done: !!done });
|
|
230
|
-
} catch (err) {
|
|
231
|
-
send(server, { type: "error", id, error: String(err) });
|
|
232
|
-
}
|
|
233
|
-
} else if (type === "iterate-return") {
|
|
234
|
-
const iter = iteratorStore.get(iteratorId);
|
|
235
|
-
if (iter?.return) await iter.return(undefined);
|
|
236
|
-
iteratorStore.delete(iteratorId);
|
|
237
|
-
send(server, { type: "iterate-result", id, value: undefined, done: true });
|
|
238
|
-
} else if (type === "stream-read") {
|
|
239
|
-
const { streamId } = msg;
|
|
240
|
-
const entry = streamStore.get(streamId);
|
|
241
|
-
if (!entry) {
|
|
242
|
-
send(server, { type: "error", id, error: "Stream not found" });
|
|
243
|
-
return;
|
|
244
|
-
}
|
|
245
|
-
try {
|
|
246
|
-
let reader = entry.reader;
|
|
247
|
-
if (!reader) {
|
|
248
|
-
reader = entry.stream.getReader();
|
|
249
|
-
entry.reader = reader;
|
|
250
|
-
}
|
|
251
|
-
const { value, done } = await reader.read();
|
|
252
|
-
if (done) {
|
|
253
|
-
streamStore.delete(streamId);
|
|
254
|
-
}
|
|
255
|
-
const serializedValue = value instanceof Uint8Array ? Array.from(value) : value;
|
|
256
|
-
send(server, { type: "stream-result", id, value: serializedValue, done: !!done });
|
|
257
|
-
} catch (err) {
|
|
258
|
-
streamStore.delete(streamId);
|
|
259
|
-
send(server, { type: "error", id, error: String(err) });
|
|
260
|
-
}
|
|
261
|
-
} else if (type === "stream-cancel") {
|
|
262
|
-
const { streamId } = msg;
|
|
263
|
-
const entry = streamStore.get(streamId);
|
|
264
|
-
if (entry) {
|
|
265
|
-
try {
|
|
266
|
-
if (entry.reader) {
|
|
267
|
-
await entry.reader.cancel();
|
|
268
|
-
} else {
|
|
269
|
-
await entry.stream.cancel();
|
|
270
|
-
}
|
|
271
|
-
} catch (e) { /* ignore */ }
|
|
272
|
-
streamStore.delete(streamId);
|
|
273
|
-
}
|
|
274
|
-
send(server, { type: "result", id, value: true });
|
|
275
|
-
} else if (type === "writable-create") {
|
|
276
|
-
const { targetPath, targetInstanceId } = msg;
|
|
277
|
-
let chunks = [];
|
|
278
|
-
const writableId = nextStreamId++;
|
|
279
|
-
|
|
280
|
-
const writable = new WritableStream({
|
|
281
|
-
write(chunk) { chunks.push(chunk); },
|
|
282
|
-
close() {},
|
|
283
|
-
abort(reason) { chunks = []; }
|
|
284
|
-
});
|
|
285
|
-
|
|
286
|
-
writableStreamStore.set(writableId, { writable, chunks, targetPath, targetInstanceId });
|
|
287
|
-
send(server, { type: "result", id, writableId, valueType: "writablestream" });
|
|
288
|
-
} else if (type === "writable-write") {
|
|
289
|
-
const { writableId, chunk } = msg;
|
|
290
|
-
const entry = writableStreamStore.get(writableId);
|
|
291
|
-
if (!entry) {
|
|
292
|
-
send(server, { type: "error", id, error: "WritableStream not found" });
|
|
293
|
-
return;
|
|
294
|
-
}
|
|
295
|
-
try {
|
|
296
|
-
const data = Array.isArray(chunk) ? new Uint8Array(chunk) : chunk;
|
|
297
|
-
entry.chunks.push(data);
|
|
298
|
-
send(server, { type: "result", id, value: true });
|
|
299
|
-
} catch (err) {
|
|
300
|
-
send(server, { type: "error", id, error: String(err) });
|
|
301
|
-
}
|
|
302
|
-
} else if (type === "writable-close") {
|
|
303
|
-
const { writableId } = msg;
|
|
304
|
-
const entry = writableStreamStore.get(writableId);
|
|
305
|
-
if (!entry) {
|
|
306
|
-
send(server, { type: "error", id, error: "WritableStream not found" });
|
|
307
|
-
return;
|
|
308
|
-
}
|
|
309
|
-
writableStreamStore.delete(writableId);
|
|
310
|
-
send(server, { type: "result", id, value: entry.chunks });
|
|
311
|
-
} else if (type === "writable-abort") {
|
|
312
|
-
const { writableId } = msg;
|
|
313
|
-
writableStreamStore.delete(writableId);
|
|
314
|
-
send(server, { type: "result", id, value: true });
|
|
315
|
-
}
|
|
316
|
-
} catch (err) {
|
|
317
|
-
console.error("WebSocket message error:", err);
|
|
318
|
-
}
|
|
319
|
-
});
|
|
320
|
-
|
|
321
|
-
server.addEventListener("close", () => {
|
|
322
|
-
iteratorStore.clear();
|
|
323
|
-
instanceStore.clear();
|
|
324
|
-
streamStore.clear();
|
|
325
|
-
writableStreamStore.clear();
|
|
326
|
-
});
|
|
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
|
+
}
|
|
327
93
|
|
|
328
94
|
return new Response(null, { status: 101, webSocket: client });
|
|
329
95
|
}
|
|
330
96
|
|
|
331
97
|
// --- HTTP routing ---
|
|
332
|
-
|
|
333
|
-
const fullTypes = generatedTypes || generateTypeDefinitions(exports, exportKeys);
|
|
334
98
|
const pathname = url.pathname;
|
|
335
99
|
|
|
336
|
-
//
|
|
337
|
-
if (pathname === corePath) {
|
|
338
|
-
|
|
339
|
-
headers: jsHeaders({ "Cache-Control": "public, max-age=31536000, immutable" }),
|
|
340
|
-
});
|
|
341
|
-
}
|
|
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 });
|
|
342
103
|
|
|
343
104
|
// Type definitions
|
|
344
|
-
if (url.searchParams.has("types")
|
|
345
|
-
if (pathname === "/" ||
|
|
346
|
-
return new Response(fullTypes, { headers: tsHeaders() });
|
|
347
|
-
}
|
|
348
|
-
// Per-export types — re-export from root to avoid duplication
|
|
105
|
+
if (url.searchParams.has("types")) {
|
|
106
|
+
if (pathname === "/") return tsResponse(generatedTypes || "");
|
|
349
107
|
const name = pathname.slice(1);
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
}
|
|
354
|
-
return new Response("// Export not found", { status: 404, headers: tsHeaders() });
|
|
108
|
+
return exportKeys.includes(name)
|
|
109
|
+
? tsResponse(`export { ${name} as default, ${name} } from "./?types";`)
|
|
110
|
+
: tsResponse("// Export not found", 404);
|
|
355
111
|
}
|
|
112
|
+
if (pathname.endsWith(".d.ts")) return tsResponse(generatedTypes || "");
|
|
356
113
|
|
|
357
114
|
const baseUrl = `${url.protocol}//${url.host}`;
|
|
115
|
+
const cpath = isShared ? sharedCorePath : corePath;
|
|
358
116
|
|
|
359
|
-
// Root
|
|
117
|
+
// Root module
|
|
360
118
|
if (pathname === "/") {
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
const code = [
|
|
365
|
-
`import { createProxy, createUploadStream } from ".${corePath}";`,
|
|
366
|
-
namedExports,
|
|
367
|
-
`export { createUploadStream };`,
|
|
368
|
-
].join("\n");
|
|
369
|
-
|
|
370
|
-
return new Response(code, {
|
|
371
|
-
headers: jsHeaders({
|
|
372
|
-
"Cache-Control": "no-cache",
|
|
373
|
-
"X-TypeScript-Types": `${baseUrl}/?types`,
|
|
374
|
-
}),
|
|
119
|
+
return jsResponse(buildIndexModule(cpath), {
|
|
120
|
+
"Cache-Control": "no-cache",
|
|
121
|
+
"X-TypeScript-Types": `${baseUrl}/?types`,
|
|
375
122
|
});
|
|
376
123
|
}
|
|
377
124
|
|
|
378
|
-
// Per-export
|
|
125
|
+
// Per-export module
|
|
379
126
|
const exportName = pathname.slice(1);
|
|
380
127
|
if (exportKeys.includes(exportName)) {
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
`export default _export;`,
|
|
385
|
-
`export { _export as ${exportName} };`,
|
|
386
|
-
].join("\n");
|
|
387
|
-
|
|
388
|
-
return new Response(code, {
|
|
389
|
-
headers: jsHeaders({
|
|
390
|
-
"Cache-Control": "no-cache",
|
|
391
|
-
"X-TypeScript-Types": `${baseUrl}/${exportName}?types`,
|
|
392
|
-
}),
|
|
128
|
+
return jsResponse(buildExportModule(cpath, exportName), {
|
|
129
|
+
"Cache-Control": "no-cache",
|
|
130
|
+
"X-TypeScript-Types": `${baseUrl}/${exportName}?types`,
|
|
393
131
|
});
|
|
394
132
|
}
|
|
395
133
|
|
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,6 +29,8 @@
|
|
|
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": {
|
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
|
+
}
|