export-runtime 0.0.1 → 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 +126 -19
  2. package/handler.js +193 -6
  3. package/package.json +1 -1
package/client.js CHANGED
@@ -162,14 +162,43 @@ __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
+
187
+ const sendRequest = async (msg) => {
188
+ await ready;
189
+ const id = nextId++;
190
+ return new Promise((resolve, reject) => {
191
+ pending.set(id, { resolve, reject });
192
+ ws.send(stringify({ ...msg, id }));
193
+ });
194
+ };
195
+
171
196
  ws.onmessage = (event) => {
172
197
  const msg = parse(event.data);
198
+
199
+ // Ignore pong responses (keepalive)
200
+ if (msg.type === "pong") return;
201
+
173
202
  const resolver = pending.get(msg.id);
174
203
  if (!resolver) return;
175
204
 
@@ -179,27 +208,56 @@ ws.onmessage = (event) => {
179
208
  } else if (msg.type === "result") {
180
209
  if (msg.valueType === "function") {
181
210
  resolver.resolve(createProxy(msg.path));
211
+ } else if (msg.valueType === "instance") {
212
+ resolver.resolve(createInstanceProxy(msg.instanceId));
182
213
  } else if (msg.valueType === "asynciterator") {
183
214
  const iteratorProxy = {
184
215
  [Symbol.asyncIterator]() { return this; },
185
216
  async next() {
186
- await ready;
187
- const id = nextId++;
188
- return new Promise((resolve, reject) => {
189
- pending.set(id, { resolve, reject });
190
- ws.send(stringify({ type: "iterate-next", id, iteratorId: msg.iteratorId }));
191
- });
217
+ return sendRequest({ type: "iterate-next", iteratorId: msg.iteratorId });
192
218
  },
193
219
  async return(value) {
194
- await ready;
195
- const id = nextId++;
196
- return new Promise((resolve, reject) => {
197
- pending.set(id, { resolve, reject });
198
- ws.send(stringify({ type: "iterate-return", id, iteratorId: msg.iteratorId, value }));
199
- });
220
+ return sendRequest({ type: "iterate-return", iteratorId: msg.iteratorId, value });
200
221
  }
201
222
  };
202
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);
203
261
  } else {
204
262
  resolver.resolve(msg.value);
205
263
  }
@@ -207,23 +265,72 @@ ws.onmessage = (event) => {
207
265
  } else if (msg.type === "iterate-result") {
208
266
  resolver.resolve({ value: msg.value, done: msg.done });
209
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);
210
273
  }
211
274
  };
212
275
 
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)
213
301
  const createProxy = (path = []) => new Proxy(function(){}, {
214
302
  get(_, prop) {
215
303
  if (prop === "then" || prop === Symbol.toStringTag) return undefined;
216
304
  return createProxy([...path, prop]);
217
305
  },
218
306
  async apply(_, __, args) {
219
- await ready;
220
- const id = nextId++;
221
- return new Promise((resolve, reject) => {
222
- pending.set(id, { resolve, reject });
223
- ws.send(stringify({ type: "call", id, path, args }));
224
- });
307
+ return sendRequest({ type: "call", path, args });
308
+ },
309
+ construct(_, args) {
310
+ return sendRequest({ type: "construct", path, args });
225
311
  }
226
312
  });
227
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
+
228
335
  __NAMED_EXPORTS__
229
336
  `;
package/handler.js CHANGED
@@ -13,10 +13,21 @@ 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
+
19
+ const isClass = (fn) =>
20
+ typeof fn === "function" && /^class\s/.test(Function.prototype.toString.call(fn));
21
+
16
22
  export const createHandler = (exports) => {
17
23
  const exportKeys = Object.keys(exports);
18
24
  const iteratorStore = new Map();
25
+ const instanceStore = new Map();
26
+ const streamStore = new Map();
27
+ const writableStreamStore = new Map();
19
28
  let nextIteratorId = 1;
29
+ let nextInstanceId = 1;
30
+ let nextStreamId = 1;
20
31
 
21
32
  const send = (ws, data) => {
22
33
  ws.send(stringify(data));
@@ -36,18 +47,62 @@ export const createHandler = (exports) => {
36
47
  server.addEventListener("message", async (event) => {
37
48
  try {
38
49
  const msg = parse(event.data);
39
- const { type, id, path = [], args = [], iteratorId } = msg;
50
+ const { type, id, path = [], args = [], iteratorId, instanceId } = msg;
51
+
52
+ // Keepalive ping/pong
53
+ if (type === "ping") {
54
+ send(server, { type: "pong", id });
55
+ return;
56
+ }
40
57
 
41
- if (type === "call") {
58
+ if (type === "construct") {
59
+ // Class instantiation
42
60
  try {
43
- const fn = getByPath(exports, path);
44
- if (typeof fn !== "function") {
61
+ const Ctor = getByPath(exports, path);
62
+ if (!isClass(Ctor)) {
63
+ send(server, { type: "error", id, error: `${path.join(".")} is not a class` });
64
+ return;
65
+ }
66
+ const instance = new Ctor(...args);
67
+ const instId = nextInstanceId++;
68
+ instanceStore.set(instId, instance);
69
+ send(server, { type: "result", id, instanceId: instId, valueType: "instance" });
70
+ } catch (err) {
71
+ send(server, { type: "error", id, error: String(err) });
72
+ }
73
+ } else if (type === "call") {
74
+ try {
75
+ let target;
76
+ let thisArg;
77
+
78
+ if (instanceId !== undefined) {
79
+ // Method call on instance
80
+ const instance = instanceStore.get(instanceId);
81
+ if (!instance) {
82
+ send(server, { type: "error", id, error: "Instance not found" });
83
+ return;
84
+ }
85
+ target = getByPath(instance, path);
86
+ thisArg = path.length > 1 ? getByPath(instance, path.slice(0, -1)) : instance;
87
+ } else {
88
+ // Regular function call
89
+ target = getByPath(exports, path);
90
+ thisArg = path.length > 1 ? getByPath(exports, path.slice(0, -1)) : undefined;
91
+ }
92
+
93
+ if (typeof target !== "function") {
45
94
  send(server, { type: "error", id, error: `${path.join(".")} is not a function` });
46
95
  return;
47
96
  }
48
- const result = await fn.apply(undefined, args);
49
97
 
50
- if (isAsyncIterable(result)) {
98
+ // Await result to support both sync and async functions
99
+ const result = await target.apply(thisArg, args);
100
+
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)) {
51
106
  const iterId = nextIteratorId++;
52
107
  iteratorStore.set(iterId, result[Symbol.asyncIterator]());
53
108
  send(server, { type: "result", id, iteratorId: iterId, valueType: "asynciterator" });
@@ -59,6 +114,42 @@ export const createHandler = (exports) => {
59
114
  } catch (err) {
60
115
  send(server, { type: "error", id, error: String(err) });
61
116
  }
117
+ } else if (type === "get") {
118
+ // Property access on instance
119
+ try {
120
+ const instance = instanceStore.get(instanceId);
121
+ if (!instance) {
122
+ send(server, { type: "error", id, error: "Instance not found" });
123
+ return;
124
+ }
125
+ const value = getByPath(instance, path);
126
+ if (typeof value === "function") {
127
+ send(server, { type: "result", id, valueType: "function" });
128
+ } else {
129
+ send(server, { type: "result", id, value });
130
+ }
131
+ } catch (err) {
132
+ send(server, { type: "error", id, error: String(err) });
133
+ }
134
+ } else if (type === "set") {
135
+ // Property assignment on instance
136
+ try {
137
+ const instance = instanceStore.get(instanceId);
138
+ if (!instance) {
139
+ send(server, { type: "error", id, error: "Instance not found" });
140
+ return;
141
+ }
142
+ const parent = path.length > 1 ? getByPath(instance, path.slice(0, -1)) : instance;
143
+ const prop = path[path.length - 1];
144
+ parent[prop] = args[0];
145
+ send(server, { type: "result", id, value: true });
146
+ } catch (err) {
147
+ send(server, { type: "error", id, error: String(err) });
148
+ }
149
+ } else if (type === "release") {
150
+ // Release instance
151
+ instanceStore.delete(instanceId);
152
+ send(server, { type: "result", id, value: true });
62
153
  } else if (type === "iterate-next") {
63
154
  const iter = iteratorStore.get(iteratorId);
64
155
  if (!iter) {
@@ -77,6 +168,99 @@ export const createHandler = (exports) => {
77
168
  if (iter?.return) await iter.return(undefined);
78
169
  iteratorStore.delete(iteratorId);
79
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 });
80
264
  }
81
265
  } catch (err) {
82
266
  console.error("WebSocket message error:", err);
@@ -85,6 +269,9 @@ export const createHandler = (exports) => {
85
269
 
86
270
  server.addEventListener("close", () => {
87
271
  iteratorStore.clear();
272
+ instanceStore.clear();
273
+ streamStore.clear();
274
+ writableStreamStore.clear();
88
275
  });
89
276
 
90
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.1",
3
+ "version": "0.0.3",
4
4
  "description": "Cloudflare Workers ESM Export Framework Runtime",
5
5
  "keywords": [
6
6
  "cloudflare",