export-runtime 0.0.5 → 0.0.7

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