export-runtime 0.0.2 → 0.0.3

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 +112 -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,6 +13,9 @@ 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
 
@@ -20,8 +23,11 @@ export const createHandler = (exports) => {
20
23
  const exportKeys = Object.keys(exports);
21
24
  const iteratorStore = new Map();
22
25
  const instanceStore = new Map();
26
+ const streamStore = new Map();
27
+ const writableStreamStore = new Map();
23
28
  let nextIteratorId = 1;
24
29
  let nextInstanceId = 1;
30
+ let nextStreamId = 1;
25
31
 
26
32
  const send = (ws, data) => {
27
33
  ws.send(stringify(data));
@@ -43,6 +49,12 @@ export const createHandler = (exports) => {
43
49
  const msg = parse(event.data);
44
50
  const { type, id, path = [], args = [], iteratorId, instanceId } = msg;
45
51
 
52
+ // Keepalive ping/pong
53
+ if (type === "ping") {
54
+ send(server, { type: "pong", id });
55
+ return;
56
+ }
57
+
46
58
  if (type === "construct") {
47
59
  // Class instantiation
48
60
  try {
@@ -86,7 +98,11 @@ export const createHandler = (exports) => {
86
98
  // Await result to support both sync and async functions
87
99
  const result = await target.apply(thisArg, args);
88
100
 
89
- if (isAsyncIterable(result)) {
101
+ if (isReadableStream(result)) {
102
+ const streamId = nextStreamId++;
103
+ streamStore.set(streamId, { stream: result, reader: null });
104
+ send(server, { type: "result", id, streamId, valueType: "readablestream" });
105
+ } else if (isAsyncIterable(result)) {
90
106
  const iterId = nextIteratorId++;
91
107
  iteratorStore.set(iterId, result[Symbol.asyncIterator]());
92
108
  send(server, { type: "result", id, iteratorId: iterId, valueType: "asynciterator" });
@@ -152,6 +168,99 @@ export const createHandler = (exports) => {
152
168
  if (iter?.return) await iter.return(undefined);
153
169
  iteratorStore.delete(iteratorId);
154
170
  send(server, { type: "iterate-result", id, value: undefined, done: true });
171
+ } else if (type === "stream-read") {
172
+ // ReadableStream chunk read
173
+ const { streamId } = msg;
174
+ const entry = streamStore.get(streamId);
175
+ if (!entry) {
176
+ send(server, { type: "error", id, error: "Stream not found" });
177
+ return;
178
+ }
179
+ try {
180
+ // Get or create reader for this stream
181
+ let reader = entry.reader;
182
+ if (!reader) {
183
+ reader = entry.stream.getReader();
184
+ entry.reader = reader;
185
+ }
186
+ const { value, done } = await reader.read();
187
+ if (done) {
188
+ streamStore.delete(streamId);
189
+ }
190
+ // Convert Uint8Array to array for devalue serialization
191
+ const serializedValue = value instanceof Uint8Array ? Array.from(value) : value;
192
+ send(server, { type: "stream-result", id, value: serializedValue, done: !!done });
193
+ } catch (err) {
194
+ streamStore.delete(streamId);
195
+ send(server, { type: "error", id, error: String(err) });
196
+ }
197
+ } else if (type === "stream-cancel") {
198
+ // Cancel ReadableStream
199
+ const { streamId } = msg;
200
+ const entry = streamStore.get(streamId);
201
+ if (entry) {
202
+ try {
203
+ if (entry.reader) {
204
+ await entry.reader.cancel();
205
+ } else {
206
+ await entry.stream.cancel();
207
+ }
208
+ } catch (e) { /* ignore */ }
209
+ streamStore.delete(streamId);
210
+ }
211
+ send(server, { type: "result", id, value: true });
212
+ } else if (type === "writable-create") {
213
+ // Create a WritableStream on server side
214
+ const { targetPath, targetInstanceId } = msg;
215
+ let chunks = [];
216
+ const writableId = nextStreamId++;
217
+
218
+ const writable = new WritableStream({
219
+ write(chunk) {
220
+ chunks.push(chunk);
221
+ },
222
+ close() {
223
+ // Resolve with all chunks when stream closes
224
+ },
225
+ abort(reason) {
226
+ chunks = [];
227
+ }
228
+ });
229
+
230
+ writableStreamStore.set(writableId, { writable, chunks, targetPath, targetInstanceId });
231
+ send(server, { type: "result", id, writableId, valueType: "writablestream" });
232
+ } else if (type === "writable-write") {
233
+ // Write chunk to WritableStream
234
+ const { writableId, chunk } = msg;
235
+ const entry = writableStreamStore.get(writableId);
236
+ if (!entry) {
237
+ send(server, { type: "error", id, error: "WritableStream not found" });
238
+ return;
239
+ }
240
+ try {
241
+ // Convert array back to Uint8Array if needed
242
+ const data = Array.isArray(chunk) ? new Uint8Array(chunk) : chunk;
243
+ entry.chunks.push(data);
244
+ send(server, { type: "result", id, value: true });
245
+ } catch (err) {
246
+ send(server, { type: "error", id, error: String(err) });
247
+ }
248
+ } else if (type === "writable-close") {
249
+ // Close WritableStream and return collected chunks
250
+ const { writableId } = msg;
251
+ const entry = writableStreamStore.get(writableId);
252
+ if (!entry) {
253
+ send(server, { type: "error", id, error: "WritableStream not found" });
254
+ return;
255
+ }
256
+ writableStreamStore.delete(writableId);
257
+ // Return the collected data
258
+ send(server, { type: "result", id, value: entry.chunks });
259
+ } else if (type === "writable-abort") {
260
+ // Abort WritableStream
261
+ const { writableId } = msg;
262
+ writableStreamStore.delete(writableId);
263
+ send(server, { type: "result", id, value: true });
155
264
  }
156
265
  } catch (err) {
157
266
  console.error("WebSocket message error:", err);
@@ -161,6 +270,8 @@ export const createHandler = (exports) => {
161
270
  server.addEventListener("close", () => {
162
271
  iteratorStore.clear();
163
272
  instanceStore.clear();
273
+ streamStore.clear();
274
+ writableStreamStore.clear();
164
275
  });
165
276
 
166
277
  return new Response(null, { status: 101, webSocket: client });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "export-runtime",
3
- "version": "0.0.2",
3
+ "version": "0.0.3",
4
4
  "description": "Cloudflare Workers ESM Export Framework Runtime",
5
5
  "keywords": [
6
6
  "cloudflare",