export-runtime 0.0.2 → 0.0.4

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.
Files changed (3) hide show
  1. package/client.js +84 -1
  2. package/handler.js +200 -1
  3. package/package.json +1 -1
package/client.js CHANGED
@@ -162,12 +162,28 @@ __DEVALUE_PARSE__
162
162
  const ws = new WebSocket(__WS_URL__);
163
163
  const pending = new Map();
164
164
  let nextId = 1;
165
+ let keepaliveInterval = null;
165
166
 
166
167
  const ready = new Promise((resolve, reject) => {
167
- ws.onopen = () => resolve(undefined);
168
+ ws.onopen = () => {
169
+ // Start keepalive ping every 30 seconds
170
+ keepaliveInterval = setInterval(() => {
171
+ if (ws.readyState === WebSocket.OPEN) {
172
+ ws.send(stringify({ type: "ping", id: 0 }));
173
+ }
174
+ }, 30000);
175
+ resolve(undefined);
176
+ };
168
177
  ws.onerror = (e) => reject(e);
169
178
  });
170
179
 
180
+ ws.onclose = () => {
181
+ if (keepaliveInterval) {
182
+ clearInterval(keepaliveInterval);
183
+ keepaliveInterval = null;
184
+ }
185
+ };
186
+
171
187
  const sendRequest = async (msg) => {
172
188
  await ready;
173
189
  const id = nextId++;
@@ -179,6 +195,10 @@ const sendRequest = async (msg) => {
179
195
 
180
196
  ws.onmessage = (event) => {
181
197
  const msg = parse(event.data);
198
+
199
+ // Ignore pong responses (keepalive)
200
+ if (msg.type === "pong") return;
201
+
182
202
  const resolver = pending.get(msg.id);
183
203
  if (!resolver) return;
184
204
 
@@ -201,6 +221,43 @@ ws.onmessage = (event) => {
201
221
  }
202
222
  };
203
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);
204
261
  } else {
205
262
  resolver.resolve(msg.value);
206
263
  }
@@ -208,6 +265,11 @@ ws.onmessage = (event) => {
208
265
  } else if (msg.type === "iterate-result") {
209
266
  resolver.resolve({ value: msg.value, done: msg.done });
210
267
  pending.delete(msg.id);
268
+ } 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);
211
273
  }
212
274
  };
213
275
 
@@ -249,5 +311,26 @@ const createProxy = (path = []) => new Proxy(function(){}, {
249
311
  }
250
312
  });
251
313
 
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
+ });
331
+
332
+ return { stream, writableId };
333
+ };
334
+
252
335
  __NAMED_EXPORTS__
253
336
  `;
package/handler.js CHANGED
@@ -13,15 +13,93 @@ const getByPath = (obj, path) => {
13
13
  const isAsyncIterable = (value) =>
14
14
  value != null && typeof value[Symbol.asyncIterator] === "function";
15
15
 
16
+ const isReadableStream = (value) =>
17
+ value != null && typeof value.getReader === "function" && typeof value.pipeTo === "function";
18
+
16
19
  const isClass = (fn) =>
17
20
  typeof fn === "function" && /^class\s/.test(Function.prototype.toString.call(fn));
18
21
 
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
+ ];
29
+
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
+ );
37
+
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;`);
77
+ }
78
+ };
79
+
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
+
19
94
  export const createHandler = (exports) => {
20
95
  const exportKeys = Object.keys(exports);
21
96
  const iteratorStore = new Map();
22
97
  const instanceStore = new Map();
98
+ const streamStore = new Map();
99
+ const writableStreamStore = new Map();
23
100
  let nextIteratorId = 1;
24
101
  let nextInstanceId = 1;
102
+ let nextStreamId = 1;
25
103
 
26
104
  const send = (ws, data) => {
27
105
  ws.send(stringify(data));
@@ -43,6 +121,12 @@ export const createHandler = (exports) => {
43
121
  const msg = parse(event.data);
44
122
  const { type, id, path = [], args = [], iteratorId, instanceId } = msg;
45
123
 
124
+ // Keepalive ping/pong
125
+ if (type === "ping") {
126
+ send(server, { type: "pong", id });
127
+ return;
128
+ }
129
+
46
130
  if (type === "construct") {
47
131
  // Class instantiation
48
132
  try {
@@ -86,7 +170,11 @@ export const createHandler = (exports) => {
86
170
  // Await result to support both sync and async functions
87
171
  const result = await target.apply(thisArg, args);
88
172
 
89
- if (isAsyncIterable(result)) {
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)) {
90
178
  const iterId = nextIteratorId++;
91
179
  iteratorStore.set(iterId, result[Symbol.asyncIterator]());
92
180
  send(server, { type: "result", id, iteratorId: iterId, valueType: "asynciterator" });
@@ -152,6 +240,99 @@ export const createHandler = (exports) => {
152
240
  if (iter?.return) await iter.return(undefined);
153
241
  iteratorStore.delete(iteratorId);
154
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++;
289
+
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
+ });
301
+
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 });
155
336
  }
156
337
  } catch (err) {
157
338
  console.error("WebSocket message error:", err);
@@ -161,6 +342,8 @@ export const createHandler = (exports) => {
161
342
  server.addEventListener("close", () => {
162
343
  iteratorStore.clear();
163
344
  instanceStore.clear();
345
+ streamStore.clear();
346
+ writableStreamStore.clear();
164
347
  });
165
348
 
166
349
  return new Response(null, { status: 101, webSocket: client });
@@ -169,6 +352,18 @@ export const createHandler = (exports) => {
169
352
  const wsProtocol = url.protocol === "https:" ? "wss:" : "ws:";
170
353
  const wsUrl = `${wsProtocol}//${url.host}${url.pathname}`;
171
354
 
355
+ // Serve TypeScript type definitions
356
+ if (url.searchParams.has("types") || url.pathname.endsWith(".d.ts")) {
357
+ const typeDefinitions = 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
+ },
364
+ });
365
+ }
366
+
172
367
  // Generate named exports
173
368
  const namedExports = exportKeys
174
369
  .map((key) => `export const ${key} = createProxy([${JSON.stringify(key)}]);`)
@@ -180,11 +375,15 @@ export const createHandler = (exports) => {
180
375
  .replace("__DEVALUE_PARSE__", DEVALUE_PARSE)
181
376
  .replace("__NAMED_EXPORTS__", namedExports);
182
377
 
378
+ // Build types URL for X-TypeScript-Types header
379
+ const typesUrl = `${url.protocol}//${url.host}${url.pathname}?types`;
380
+
183
381
  return new Response(clientCode, {
184
382
  headers: {
185
383
  "Content-Type": "application/javascript; charset=utf-8",
186
384
  "Access-Control-Allow-Origin": "*",
187
385
  "Cache-Control": "no-cache",
386
+ "X-TypeScript-Types": typesUrl,
188
387
  },
189
388
  });
190
389
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "export-runtime",
3
- "version": "0.0.2",
3
+ "version": "0.0.4",
4
4
  "description": "Cloudflare Workers ESM Export Framework Runtime",
5
5
  "keywords": [
6
6
  "cloudflare",