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.
@@ -212,33 +212,92 @@ for (const node of program.body) {
212
212
  }
213
213
  }
214
214
 
215
- // Add createUploadStream helper type
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 module ---
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 that exports type definitions, minified core, and core ID
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 code - self-contained ES module that derives WS URL from import.meta.url.
2
- // Served at /_core.js with long cache. Exports createProxy and createUploadStream.
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 instanceof Int8Array) {
42
- stringified[index] = ['Int8Array', ...[...thing].map(flatten)];
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 === UNDEFINED) return undefined;
98
- if (index === HOLE) return undefined;
99
- if (index === NAN) return NaN;
100
- if (index === POSITIVE_INFINITY) return Infinity;
101
- if (index === NEGATIVE_INFINITY) return -Infinity;
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 buffer = new ArrayBuffer(bytes.length);
129
- new Uint8Array(buffer).set(bytes);
130
- hydrated[index] = buffer;
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("./", import.meta.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 = null;
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(undefined);
122
+ resolve();
170
123
  };
171
- ws.onerror = (e) => reject(e);
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
- resolver.resolve(createProxy(msg.path));
204
- } else if (msg.valueType === "instance") {
205
- resolver.resolve(createInstanceProxy(msg.instanceId));
206
- } else if (msg.valueType === "asynciterator") {
207
- const iteratorProxy = {
208
- [Symbol.asyncIterator]() { return this; },
209
- async next() {
210
- return sendRequest({ type: "iterate-next", iteratorId: msg.iteratorId });
211
- },
212
- async return(value) {
213
- return sendRequest({ type: "iterate-return", iteratorId: msg.iteratorId, value });
214
- }
215
- };
216
- resolver.resolve(iteratorProxy);
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
- const value = Array.isArray(msg.value) ? new Uint8Array(msg.value) : msg.value;
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
- return createProxy([...path, prop]);
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
- async apply(_, __, args) {
295
- return sendRequest({ type: "call", path, args });
176
+ set(_, prop, value) {
177
+ sendRequest({ type: "set", instanceId, path: [...path, prop], args: [value] });
178
+ return true;
296
179
  },
297
- construct(_, args) {
298
- return sendRequest({ type: "construct", path, args });
180
+ async apply(_, __, args) {
181
+ return sendRequest({ type: "call", instanceId, path, args });
299
182
  }
300
183
  });
301
184
 
302
- export const createUploadStream = async () => {
303
- const result = await sendRequest({ type: "writable-create" });
304
- const writableId = result.writableId;
305
-
306
- const stream = new WritableStream({
307
- async write(chunk) {
308
- const data = chunk instanceof Uint8Array ? Array.from(chunk) : chunk;
309
- await sendRequest({ type: "writable-write", writableId, chunk: data });
310
- },
311
- async close() {
312
- return sendRequest({ type: "writable-close", writableId });
313
- },
314
- async abort(reason) {
315
- await sendRequest({ type: "writable-abort", writableId });
316
- }
317
- });
318
-
319
- return { stream, writableId };
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 jsHeaders = (extra = {}) => ({
82
- "Content-Type": "application/javascript; charset=utf-8",
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 tsHeaders = () => ({
88
- "Content-Type": "application/typescript; charset=utf-8",
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 upgradeHeader = request.headers.get("Upgrade");
73
+ const isShared = url.searchParams.has("shared");
114
74
 
115
- // --- WebSocket upgrade (path-agnostic) ---
116
- if (upgradeHeader === "websocket") {
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
- server.addEventListener("message", async (event) => {
123
- try {
124
- const msg = parse(event.data);
125
- const { type, id, path = [], args = [], iteratorId, instanceId } = msg;
126
-
127
- if (type === "ping") {
128
- send(server, { type: "pong", id });
129
- return;
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
- // Serve core module — long-cached, content-independent of deployment URL
337
- if (pathname === corePath) {
338
- return new Response(coreModuleCode, {
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") || pathname.endsWith(".d.ts")) {
345
- if (pathname === "/" || pathname.endsWith(".d.ts")) {
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
- if (exportKeys.includes(name)) {
351
- const code = `export { ${name} as default, ${name} } from "./?types";`;
352
- return new Response(code, { headers: tsHeaders() });
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 — re-exports all from ${corePath}
113
+ // Root module
360
114
  if (pathname === "/") {
361
- const namedExports = exportKeys
362
- .map((key) => `export const ${key} = createProxy([${JSON.stringify(key)}]);`)
363
- .join("\n");
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 path — e.g. /greet, /Counter
121
+ // Per-export module
379
122
  const exportName = pathname.slice(1);
380
123
  if (exportKeys.includes(exportName)) {
381
- const code = [
382
- `import { createProxy } from ".${corePath}";`,
383
- `const _export = createProxy([${JSON.stringify(exportName)}]);`,
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.6",
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
+ }