export-runtime 0.0.6 → 0.0.8
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 +69 -10
- package/client.js +59 -184
- package/entry.js +3 -2
- package/handler.js +89 -355
- package/package.json +3 -1
- package/rpc.js +133 -0
- package/shared-do.js +21 -0
package/bin/generate-types.mjs
CHANGED
|
@@ -212,33 +212,92 @@ for (const node of program.body) {
|
|
|
212
212
|
}
|
|
213
213
|
}
|
|
214
214
|
|
|
215
|
-
|
|
216
|
-
lines.push("export declare function createUploadStream(): Promise<{");
|
|
217
|
-
lines.push(" stream: WritableStream<any>;");
|
|
218
|
-
lines.push(" writableId: number;");
|
|
219
|
-
lines.push("}>;");
|
|
215
|
+
|
|
220
216
|
|
|
221
217
|
const typeDefinitions = lines.join("\n");
|
|
222
218
|
|
|
223
|
-
// --- Minify core
|
|
219
|
+
// --- Minify core modules ---
|
|
224
220
|
|
|
225
221
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
226
|
-
const { CORE_CODE } = await import(path.join(__dirname, "..", "client.js"));
|
|
222
|
+
const { CORE_CODE, SHARED_CORE_CODE } = await import(path.join(__dirname, "..", "client.js"));
|
|
227
223
|
|
|
228
|
-
// CORE_CODE uses import.meta.url for WS URL — no placeholders needed.
|
|
229
224
|
const minified = minifySync("_core.js", CORE_CODE);
|
|
230
225
|
if (minified.errors?.length) {
|
|
231
|
-
console.error("Minification errors:", minified.errors);
|
|
226
|
+
console.error("Minification errors (core):", minified.errors);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const minifiedShared = minifySync("_core-shared.js", SHARED_CORE_CODE);
|
|
230
|
+
if (minifiedShared.errors?.length) {
|
|
231
|
+
console.error("Minification errors (shared core):", minifiedShared.errors);
|
|
232
232
|
}
|
|
233
233
|
|
|
234
234
|
// Generate a unique ID per build for cache-busting the core module path
|
|
235
235
|
const coreId = crypto.randomUUID();
|
|
236
236
|
|
|
237
|
-
// Write as a JS module
|
|
237
|
+
// Write as a JS module
|
|
238
238
|
const outPath = path.join(cwd, ".export-types.js");
|
|
239
239
|
fs.writeFileSync(outPath, [
|
|
240
240
|
`export default ${JSON.stringify(typeDefinitions)};`,
|
|
241
241
|
`export const minifiedCore = ${JSON.stringify(minified.code)};`,
|
|
242
|
+
`export const minifiedSharedCore = ${JSON.stringify(minifiedShared.code)};`,
|
|
242
243
|
`export const coreId = ${JSON.stringify(coreId)};`,
|
|
243
244
|
].join("\n") + "\n");
|
|
245
|
+
|
|
246
|
+
// Generate Worker-side shared import module (.export-shared.js)
|
|
247
|
+
const exportNames = [];
|
|
248
|
+
for (const node of program.body) {
|
|
249
|
+
if (node.type !== "ExportNamedDeclaration" || !node.declaration) continue;
|
|
250
|
+
const decl = node.declaration;
|
|
251
|
+
if (decl.id?.name) exportNames.push(decl.id.name);
|
|
252
|
+
else if (decl.declarations) {
|
|
253
|
+
for (const d of decl.declarations) {
|
|
254
|
+
if (d.id?.name) exportNames.push(d.id.name);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const sharedModulePath = path.join(cwd, ".export-shared.js");
|
|
260
|
+
const sharedModuleLines = [
|
|
261
|
+
`import { env } from "cloudflare:workers";`,
|
|
262
|
+
``,
|
|
263
|
+
`const getStub = (room = "default") =>`,
|
|
264
|
+
` env.SHARED_EXPORT.get(env.SHARED_EXPORT.idFromName(room));`,
|
|
265
|
+
``,
|
|
266
|
+
`const createSharedInstanceProxy = (stub, instanceId, path = []) =>`,
|
|
267
|
+
` new Proxy(function(){}, {`,
|
|
268
|
+
` get(_, prop) {`,
|
|
269
|
+
` if (prop === "then" || prop === Symbol.toStringTag) return undefined;`,
|
|
270
|
+
` if (prop === Symbol.dispose || prop === Symbol.asyncDispose || prop === "[release]")`,
|
|
271
|
+
` return () => stub.rpcRelease(instanceId);`,
|
|
272
|
+
` return createSharedInstanceProxy(stub, instanceId, [...path, prop]);`,
|
|
273
|
+
` },`,
|
|
274
|
+
` async apply(_, __, args) {`,
|
|
275
|
+
` const r = await stub.rpcInstanceCall(instanceId, path, args);`,
|
|
276
|
+
` return r.value;`,
|
|
277
|
+
` },`,
|
|
278
|
+
` });`,
|
|
279
|
+
``,
|
|
280
|
+
`const createSharedProxy = (stub, path = []) =>`,
|
|
281
|
+
` new Proxy(function(){}, {`,
|
|
282
|
+
` get(_, prop) {`,
|
|
283
|
+
` if (prop === "then" || prop === Symbol.toStringTag) return undefined;`,
|
|
284
|
+
` return createSharedProxy(stub, [...path, prop]);`,
|
|
285
|
+
` },`,
|
|
286
|
+
` async apply(_, __, args) {`,
|
|
287
|
+
` const r = await stub.rpcCall(path, args);`,
|
|
288
|
+
` return r.value;`,
|
|
289
|
+
` },`,
|
|
290
|
+
` async construct(_, args) {`,
|
|
291
|
+
` const r = await stub.rpcConstruct(path, args);`,
|
|
292
|
+
` return createSharedInstanceProxy(stub, r.instanceId);`,
|
|
293
|
+
` },`,
|
|
294
|
+
` });`,
|
|
295
|
+
``,
|
|
296
|
+
`const _stub = getStub();`,
|
|
297
|
+
...exportNames.map(n => `export const ${n} = createSharedProxy(_stub, [${JSON.stringify(n)}]);`),
|
|
298
|
+
`export { getStub };`,
|
|
299
|
+
];
|
|
300
|
+
fs.writeFileSync(sharedModulePath, sharedModuleLines.join("\n") + "\n");
|
|
301
|
+
|
|
244
302
|
console.log("Generated type definitions + minified core →", outPath);
|
|
303
|
+
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;
|
|
@@ -189,133 +137,60 @@ const sendRequest = async (msg) => {
|
|
|
189
137
|
|
|
190
138
|
ws.onmessage = (event) => {
|
|
191
139
|
const msg = parse(event.data);
|
|
192
|
-
|
|
193
|
-
if (msg.type === "pong") return;
|
|
194
|
-
|
|
195
140
|
const resolver = pending.get(msg.id);
|
|
196
141
|
if (!resolver) return;
|
|
142
|
+
pending.delete(msg.id);
|
|
197
143
|
|
|
198
144
|
if (msg.type === "error") {
|
|
199
145
|
resolver.reject(new Error(msg.error));
|
|
200
|
-
pending.delete(msg.id);
|
|
201
146
|
} 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
|
-
} else if (msg.valueType === "readablestream") {
|
|
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);
|
|
147
|
+
if (msg.valueType === "function") resolver.resolve(createProxy(msg.path));
|
|
148
|
+
else if (msg.valueType === "instance") resolver.resolve(createInstanceProxy(msg.instanceId));
|
|
149
|
+
else if (msg.valueType === "asynciterator") resolver.resolve({
|
|
150
|
+
[Symbol.asyncIterator]() { return this; },
|
|
151
|
+
next: () => sendRequest({ type: "iterate-next", iteratorId: msg.iteratorId }),
|
|
152
|
+
return: () => sendRequest({ type: "iterate-return", iteratorId: msg.iteratorId })
|
|
153
|
+
});
|
|
154
|
+
else if (msg.valueType === "readablestream") resolver.resolve(new ReadableStream({
|
|
155
|
+
async pull(c) {
|
|
156
|
+
try { const r = await sendRequest({ type: "stream-read", streamId: msg.streamId }); r.done ? c.close() : c.enqueue(r.value); }
|
|
157
|
+
catch (e) { c.error(e); }
|
|
158
|
+
},
|
|
159
|
+
cancel: () => sendRequest({ type: "stream-cancel", streamId: msg.streamId })
|
|
160
|
+
}));
|
|
161
|
+
else resolver.resolve(msg.value);
|
|
256
162
|
} else if (msg.type === "iterate-result") {
|
|
257
163
|
resolver.resolve({ value: msg.value, done: msg.done });
|
|
258
|
-
pending.delete(msg.id);
|
|
259
164
|
} else if (msg.type === "stream-result") {
|
|
260
|
-
|
|
261
|
-
resolver.resolve({ value, done: msg.done });
|
|
262
|
-
pending.delete(msg.id);
|
|
165
|
+
resolver.resolve({ value: Array.isArray(msg.value) ? new Uint8Array(msg.value) : msg.value, done: msg.done });
|
|
263
166
|
}
|
|
264
167
|
};
|
|
265
168
|
|
|
266
|
-
const createInstanceProxy = (instanceId, path = []) => {
|
|
267
|
-
const proxy = new Proxy(function(){}, {
|
|
268
|
-
get(_, prop) {
|
|
269
|
-
if (prop === "then" || prop === Symbol.toStringTag) return undefined;
|
|
270
|
-
if (prop === Symbol.dispose || prop === Symbol.asyncDispose) {
|
|
271
|
-
return () => sendRequest({ type: "release", instanceId });
|
|
272
|
-
}
|
|
273
|
-
if (prop === "[release]") {
|
|
274
|
-
return () => sendRequest({ type: "release", instanceId });
|
|
275
|
-
}
|
|
276
|
-
return createInstanceProxy(instanceId, [...path, prop]);
|
|
277
|
-
},
|
|
278
|
-
set(_, prop, value) {
|
|
279
|
-
sendRequest({ type: "set", instanceId, path: [...path, prop], args: [value] });
|
|
280
|
-
return true;
|
|
281
|
-
},
|
|
282
|
-
async apply(_, __, args) {
|
|
283
|
-
return sendRequest({ type: "call", instanceId, path, args });
|
|
284
|
-
}
|
|
285
|
-
});
|
|
286
|
-
return proxy;
|
|
287
|
-
};
|
|
288
|
-
|
|
289
|
-
export const createProxy = (path = []) => new Proxy(function(){}, {
|
|
169
|
+
const createInstanceProxy = (instanceId, path = []) => new Proxy(function(){}, {
|
|
290
170
|
get(_, prop) {
|
|
291
171
|
if (prop === "then" || prop === Symbol.toStringTag) return undefined;
|
|
292
|
-
|
|
172
|
+
if (prop === Symbol.dispose || prop === Symbol.asyncDispose || prop === "[release]")
|
|
173
|
+
return () => sendRequest({ type: "release", instanceId });
|
|
174
|
+
return createInstanceProxy(instanceId, [...path, prop]);
|
|
293
175
|
},
|
|
294
|
-
|
|
295
|
-
|
|
176
|
+
set(_, prop, value) {
|
|
177
|
+
sendRequest({ type: "set", instanceId, path: [...path, prop], args: [value] });
|
|
178
|
+
return true;
|
|
296
179
|
},
|
|
297
|
-
|
|
298
|
-
return sendRequest({ type: "
|
|
180
|
+
async apply(_, __, args) {
|
|
181
|
+
return sendRequest({ type: "call", instanceId, path, args });
|
|
299
182
|
}
|
|
300
183
|
});
|
|
301
184
|
|
|
302
|
-
export const
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
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 };
|
|
320
|
-
};
|
|
185
|
+
export const createProxy = (path = []) => new Proxy(function(){}, {
|
|
186
|
+
get(_, prop) {
|
|
187
|
+
if (prop === "then" || prop === Symbol.toStringTag) return undefined;
|
|
188
|
+
return createProxy([...path, prop]);
|
|
189
|
+
},
|
|
190
|
+
async apply(_, __, args) { return sendRequest({ type: "call", path, args }); },
|
|
191
|
+
construct(_, args) { return sendRequest({ type: "construct", path, args }); }
|
|
192
|
+
});
|
|
321
193
|
`;
|
|
194
|
+
|
|
195
|
+
export const CORE_CODE = CORE_TEMPLATE.replace("__WS_SUFFIX__", "./");
|
|
196
|
+
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,129 @@
|
|
|
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 } from ".${cpath}";\n${namedExportsCode}`;
|
|
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 } = 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
|
+
}
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const wireWebSocket = (server, dispatcher, onClose) => {
|
|
56
|
+
server.addEventListener("message", async (event) => {
|
|
57
|
+
let id;
|
|
58
|
+
try {
|
|
59
|
+
const msg = parse(event.data);
|
|
60
|
+
id = msg.id;
|
|
61
|
+
const result = await dispatchMessage(dispatcher, msg);
|
|
62
|
+
if (result) server.send(stringify({ ...result, id }));
|
|
63
|
+
} catch (err) {
|
|
64
|
+
if (id !== undefined) server.send(stringify({ type: "error", id, error: String(err) }));
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
if (onClose) server.addEventListener("close", onClose);
|
|
68
|
+
};
|
|
109
69
|
|
|
110
70
|
return {
|
|
111
|
-
async fetch(request) {
|
|
71
|
+
async fetch(request, env) {
|
|
112
72
|
const url = new URL(request.url);
|
|
113
|
-
const
|
|
73
|
+
const isShared = url.searchParams.has("shared");
|
|
114
74
|
|
|
115
|
-
// --- WebSocket upgrade
|
|
116
|
-
if (
|
|
75
|
+
// --- WebSocket upgrade ---
|
|
76
|
+
if (request.headers.get("Upgrade") === "websocket") {
|
|
117
77
|
const pair = new WebSocketPair();
|
|
118
78
|
const [client, server] = Object.values(pair);
|
|
119
|
-
|
|
120
79
|
server.accept();
|
|
121
80
|
|
|
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
|
-
});
|
|
81
|
+
if (isShared && env?.SHARED_EXPORT) {
|
|
82
|
+
const room = url.searchParams.get("room") || "default";
|
|
83
|
+
const stub = env.SHARED_EXPORT.get(env.SHARED_EXPORT.idFromName(room));
|
|
84
|
+
wireWebSocket(server, stub);
|
|
85
|
+
} else {
|
|
86
|
+
const dispatcher = createRpcDispatcher(exports);
|
|
87
|
+
wireWebSocket(server, dispatcher, () => dispatcher.clearAll());
|
|
88
|
+
}
|
|
327
89
|
|
|
328
90
|
return new Response(null, { status: 101, webSocket: client });
|
|
329
91
|
}
|
|
330
92
|
|
|
331
93
|
// --- HTTP routing ---
|
|
332
|
-
|
|
333
|
-
const fullTypes = generatedTypes || generateTypeDefinitions(exports, exportKeys);
|
|
334
94
|
const pathname = url.pathname;
|
|
335
95
|
|
|
336
|
-
//
|
|
337
|
-
if (pathname === corePath) {
|
|
338
|
-
|
|
339
|
-
headers: jsHeaders({ "Cache-Control": "public, max-age=31536000, immutable" }),
|
|
340
|
-
});
|
|
341
|
-
}
|
|
96
|
+
// Core modules (cached immutably)
|
|
97
|
+
if (pathname === corePath) return jsResponse(coreModuleCode, { "Cache-Control": IMMUTABLE });
|
|
98
|
+
if (pathname === sharedCorePath) return jsResponse(sharedCoreModuleCode, { "Cache-Control": IMMUTABLE });
|
|
342
99
|
|
|
343
100
|
// 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
|
|
101
|
+
if (url.searchParams.has("types")) {
|
|
102
|
+
if (pathname === "/") return tsResponse(generatedTypes || "");
|
|
349
103
|
const name = pathname.slice(1);
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
}
|
|
354
|
-
return new Response("// Export not found", { status: 404, headers: tsHeaders() });
|
|
104
|
+
return exportKeys.includes(name)
|
|
105
|
+
? tsResponse(`export { ${name} as default, ${name} } from "./?types";`)
|
|
106
|
+
: tsResponse("// Export not found", 404);
|
|
355
107
|
}
|
|
108
|
+
if (pathname.endsWith(".d.ts")) return tsResponse(generatedTypes || "");
|
|
356
109
|
|
|
357
110
|
const baseUrl = `${url.protocol}//${url.host}`;
|
|
111
|
+
const cpath = isShared ? sharedCorePath : corePath;
|
|
358
112
|
|
|
359
|
-
// Root
|
|
113
|
+
// Root module
|
|
360
114
|
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
|
-
}),
|
|
115
|
+
return jsResponse(buildIndexModule(cpath), {
|
|
116
|
+
"Cache-Control": "no-cache",
|
|
117
|
+
"X-TypeScript-Types": `${baseUrl}/?types`,
|
|
375
118
|
});
|
|
376
119
|
}
|
|
377
120
|
|
|
378
|
-
// Per-export
|
|
121
|
+
// Per-export module
|
|
379
122
|
const exportName = pathname.slice(1);
|
|
380
123
|
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
|
-
}),
|
|
124
|
+
return jsResponse(buildExportModule(cpath, exportName), {
|
|
125
|
+
"Cache-Control": "no-cache",
|
|
126
|
+
"X-TypeScript-Types": `${baseUrl}/${exportName}?types`,
|
|
393
127
|
});
|
|
394
128
|
}
|
|
395
129
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "export-runtime",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.8",
|
|
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,133 @@
|
|
|
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
|
+
];
|
|
23
|
+
|
|
24
|
+
export function createRpcDispatcher(exports) {
|
|
25
|
+
const instances = new Map();
|
|
26
|
+
const iterators = new Map();
|
|
27
|
+
const streams = new Map();
|
|
28
|
+
let nextId = 1;
|
|
29
|
+
|
|
30
|
+
const requireInstance = (id) => {
|
|
31
|
+
const inst = instances.get(id);
|
|
32
|
+
if (!inst) throw new Error("Instance not found");
|
|
33
|
+
return inst;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const wrapResult = (result, path) => {
|
|
37
|
+
if (isReadableStream(result)) {
|
|
38
|
+
const id = nextId++;
|
|
39
|
+
streams.set(id, { stream: result, reader: null });
|
|
40
|
+
return { type: "result", streamId: id, valueType: "readablestream" };
|
|
41
|
+
}
|
|
42
|
+
if (isAsyncIterable(result)) {
|
|
43
|
+
const id = nextId++;
|
|
44
|
+
iterators.set(id, result[Symbol.asyncIterator]());
|
|
45
|
+
return { type: "result", iteratorId: id, valueType: "asynciterator" };
|
|
46
|
+
}
|
|
47
|
+
if (typeof result === "function") {
|
|
48
|
+
return { type: "result", path: [...path], valueType: "function" };
|
|
49
|
+
}
|
|
50
|
+
return { type: "result", value: result };
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const callTarget = async (obj, path, args) => {
|
|
54
|
+
const target = getByPath(obj, path);
|
|
55
|
+
const thisArg = path.length > 1 ? getByPath(obj, path.slice(0, -1)) : (obj === exports ? undefined : obj);
|
|
56
|
+
if (typeof target !== "function") throw new Error(`${path.join(".")} is not a function`);
|
|
57
|
+
return wrapResult(await target.apply(thisArg, args), path);
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
return {
|
|
61
|
+
rpcCall: (path, args = []) => callTarget(exports, path, args),
|
|
62
|
+
|
|
63
|
+
async rpcConstruct(path, args = []) {
|
|
64
|
+
const Ctor = getByPath(exports, path);
|
|
65
|
+
if (!isClass(Ctor)) throw new Error(`${path.join(".")} is not a class`);
|
|
66
|
+
const id = nextId++;
|
|
67
|
+
instances.set(id, new Ctor(...args));
|
|
68
|
+
return { type: "result", instanceId: id, valueType: "instance" };
|
|
69
|
+
},
|
|
70
|
+
|
|
71
|
+
rpcInstanceCall: (instanceId, path, args = []) =>
|
|
72
|
+
callTarget(requireInstance(instanceId), path, args),
|
|
73
|
+
|
|
74
|
+
async rpcGet(instanceId, path) {
|
|
75
|
+
const value = getByPath(requireInstance(instanceId), path);
|
|
76
|
+
return typeof value === "function"
|
|
77
|
+
? { type: "result", valueType: "function" }
|
|
78
|
+
: { type: "result", value };
|
|
79
|
+
},
|
|
80
|
+
|
|
81
|
+
async rpcSet(instanceId, path, value) {
|
|
82
|
+
const inst = requireInstance(instanceId);
|
|
83
|
+
const parent = path.length > 1 ? getByPath(inst, path.slice(0, -1)) : inst;
|
|
84
|
+
parent[path.at(-1)] = value;
|
|
85
|
+
return { type: "result", value: true };
|
|
86
|
+
},
|
|
87
|
+
|
|
88
|
+
async rpcRelease(instanceId) {
|
|
89
|
+
instances.delete(instanceId);
|
|
90
|
+
return { type: "result", value: true };
|
|
91
|
+
},
|
|
92
|
+
|
|
93
|
+
async rpcIterateNext(iteratorId) {
|
|
94
|
+
const iter = iterators.get(iteratorId);
|
|
95
|
+
if (!iter) throw new Error("Iterator not found");
|
|
96
|
+
const { value, done } = await iter.next();
|
|
97
|
+
if (done) iterators.delete(iteratorId);
|
|
98
|
+
return { type: "iterate-result", value, done: !!done };
|
|
99
|
+
},
|
|
100
|
+
|
|
101
|
+
async rpcIterateReturn(iteratorId) {
|
|
102
|
+
const iter = iterators.get(iteratorId);
|
|
103
|
+
if (iter?.return) await iter.return(undefined);
|
|
104
|
+
iterators.delete(iteratorId);
|
|
105
|
+
return { type: "iterate-result", value: undefined, done: true };
|
|
106
|
+
},
|
|
107
|
+
|
|
108
|
+
async rpcStreamRead(streamId) {
|
|
109
|
+
const entry = streams.get(streamId);
|
|
110
|
+
if (!entry) throw new Error("Stream not found");
|
|
111
|
+
if (!entry.reader) entry.reader = entry.stream.getReader();
|
|
112
|
+
const { value, done } = await entry.reader.read();
|
|
113
|
+
if (done) streams.delete(streamId);
|
|
114
|
+
const v = value instanceof Uint8Array ? Array.from(value) : value;
|
|
115
|
+
return { type: "stream-result", value: v, done: !!done };
|
|
116
|
+
},
|
|
117
|
+
|
|
118
|
+
async rpcStreamCancel(streamId) {
|
|
119
|
+
const entry = streams.get(streamId);
|
|
120
|
+
if (entry) {
|
|
121
|
+
try { await (entry.reader || entry.stream).cancel(); } catch {}
|
|
122
|
+
streams.delete(streamId);
|
|
123
|
+
}
|
|
124
|
+
return { type: "result", value: true };
|
|
125
|
+
},
|
|
126
|
+
|
|
127
|
+
clearAll() {
|
|
128
|
+
instances.clear();
|
|
129
|
+
iterators.clear();
|
|
130
|
+
streams.clear();
|
|
131
|
+
},
|
|
132
|
+
};
|
|
133
|
+
}
|
package/shared-do.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
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
|
+
}
|