export-runtime 0.0.19 → 0.0.21

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/handler.js +43 -22
  2. package/package.json +1 -1
  3. package/rpc.js +22 -0
package/handler.js CHANGED
@@ -323,6 +323,7 @@ export const createHandler = (moduleMap, generatedTypes, minifiedCore, coreId, m
323
323
  // Track session state and connection state for this WebSocket connection
324
324
  const wsSession = { token: null };
325
325
  let isClosed = false;
326
+ const pendingOps = new Set();
326
327
 
327
328
  // Safe send that ignores errors when connection is closed
328
329
  const safeSend = (data) => {
@@ -334,32 +335,51 @@ export const createHandler = (moduleMap, generatedTypes, minifiedCore, coreId, m
334
335
  }
335
336
  };
336
337
 
337
- server.addEventListener("message", async (event) => {
338
- if (isClosed) return;
339
- let id;
338
+ // Wrap async operations to track them
339
+ const trackOp = async (op) => {
340
+ const promise = op();
341
+ pendingOps.add(promise);
340
342
  try {
341
- const msg = parse(event.data);
342
- id = msg.id;
343
-
344
- // Handle auth token updates (on reconnect or explicit setToken)
345
- if (msg.type === "auth" && msg.token && !msg.method) {
346
- // Direct token send on reconnect - just update session
347
- wsSession.token = msg.token;
348
- safeSend(stringify({ type: "auth-result", id, success: true }));
349
- return;
350
- }
343
+ return await promise;
344
+ } finally {
345
+ pendingOps.delete(promise);
346
+ }
347
+ };
351
348
 
352
- const result = await dispatchMessage(dispatcher, msg, env, wsSession);
349
+ server.addEventListener("message", (event) => {
350
+ if (isClosed) return;
353
351
 
354
- // Extract token from auth responses
355
- if (result?.value?.token && msg.type === "auth") {
356
- wsSession.token = result.value.token;
357
- }
352
+ // Handle message asynchronously but don't await it
353
+ trackOp(async () => {
354
+ let id;
355
+ try {
356
+ if (isClosed) return;
357
+ const msg = parse(event.data);
358
+ id = msg.id;
359
+
360
+ // Handle auth token updates (on reconnect or explicit setToken)
361
+ if (msg.type === "auth" && msg.token && !msg.method) {
362
+ wsSession.token = msg.token;
363
+ safeSend(stringify({ type: "auth-result", id, success: true }));
364
+ return;
365
+ }
358
366
 
359
- if (result) safeSend(stringify({ ...result, id }));
360
- } catch (err) {
361
- if (id !== undefined) safeSend(stringify({ type: "error", id, error: String(err) }));
362
- }
367
+ if (isClosed) return;
368
+ const result = await dispatchMessage(dispatcher, msg, env, wsSession);
369
+
370
+ if (isClosed) return;
371
+
372
+ // Extract token from auth responses
373
+ if (result?.value?.token && msg.type === "auth") {
374
+ wsSession.token = result.value.token;
375
+ }
376
+
377
+ if (result) safeSend(stringify({ ...result, id }));
378
+ } catch (err) {
379
+ if (isClosed || String(err).includes("Connection closed")) return;
380
+ if (id !== undefined) safeSend(stringify({ type: "error", id, error: String(err) }));
381
+ }
382
+ });
363
383
  });
364
384
 
365
385
  server.addEventListener("close", () => {
@@ -369,6 +389,7 @@ export const createHandler = (moduleMap, generatedTypes, minifiedCore, coreId, m
369
389
 
370
390
  server.addEventListener("error", () => {
371
391
  isClosed = true;
392
+ if (onClose) onClose();
372
393
  });
373
394
  };
374
395
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "export-runtime",
3
- "version": "0.0.19",
3
+ "version": "0.0.21",
4
4
  "description": "Cloudflare Workers ESM Export Framework Runtime",
5
5
  "keywords": [
6
6
  "cloudflare",
package/rpc.js CHANGED
@@ -38,6 +38,7 @@ export function createRpcDispatcher(moduleMap) {
38
38
  const iterators = new Map();
39
39
  const streams = new Map();
40
40
  let nextId = 1;
41
+ let closed = false;
41
42
 
42
43
  const requireInstance = (id) => {
43
44
  const inst = instances.get(id);
@@ -115,9 +116,11 @@ export function createRpcDispatcher(moduleMap) {
115
116
  },
116
117
 
117
118
  async rpcIterateNext(iteratorId) {
119
+ if (closed) throw new Error("Connection closed");
118
120
  const iter = iterators.get(iteratorId);
119
121
  if (!iter) throw new Error("Iterator not found");
120
122
  const { value, done } = await iter.next();
123
+ if (closed) throw new Error("Connection closed");
121
124
  if (done) iterators.delete(iteratorId);
122
125
  return { type: "iterate-result", value, done: !!done };
123
126
  },
@@ -130,10 +133,12 @@ export function createRpcDispatcher(moduleMap) {
130
133
  },
131
134
 
132
135
  async rpcStreamRead(streamId) {
136
+ if (closed) throw new Error("Connection closed");
133
137
  const entry = streams.get(streamId);
134
138
  if (!entry) throw new Error("Stream not found");
135
139
  if (!entry.reader) entry.reader = entry.stream.getReader();
136
140
  const { value, done } = await entry.reader.read();
141
+ if (closed) throw new Error("Connection closed");
137
142
  if (done) streams.delete(streamId);
138
143
  const v = value instanceof Uint8Array ? Array.from(value) : value;
139
144
  return { type: "stream-result", value: v, done: !!done };
@@ -149,6 +154,23 @@ export function createRpcDispatcher(moduleMap) {
149
154
  },
150
155
 
151
156
  clearAll() {
157
+ closed = true;
158
+ // Cancel all active iterators
159
+ for (const iter of iterators.values()) {
160
+ try {
161
+ if (iter?.return) iter.return(undefined);
162
+ } catch {}
163
+ }
164
+ // Cancel all active streams
165
+ for (const entry of streams.values()) {
166
+ try {
167
+ if (entry.reader) {
168
+ entry.reader.cancel();
169
+ } else if (entry.stream) {
170
+ entry.stream.cancel();
171
+ }
172
+ } catch {}
173
+ }
152
174
  instances.clear();
153
175
  iterators.clear();
154
176
  streams.clear();