export-runtime 0.0.20 → 0.0.22

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 +57 -28
  2. package/package.json +1 -1
  3. package/rpc.js +11 -16
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,37 +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
- // Don't send if connection closed during dispatch
355
- if (isClosed) return;
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
+ }
356
366
 
357
- // Extract token from auth responses
358
- if (result?.value?.token && msg.type === "auth") {
359
- wsSession.token = result.value.token;
360
- }
367
+ if (isClosed) return;
368
+ const result = await dispatchMessage(dispatcher, msg, env, wsSession);
361
369
 
362
- if (result) safeSend(stringify({ ...result, id }));
363
- } catch (err) {
364
- // Don't send errors if connection is closed or error is about closed connection
365
- if (isClosed || String(err).includes("Connection closed")) return;
366
- if (id !== undefined) safeSend(stringify({ type: "error", id, error: String(err) }));
367
- }
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
+ });
368
383
  });
369
384
 
370
385
  server.addEventListener("close", () => {
@@ -372,13 +387,15 @@ export const createHandler = (moduleMap, generatedTypes, minifiedCore, coreId, m
372
387
  if (onClose) onClose();
373
388
  });
374
389
 
375
- server.addEventListener("error", () => {
390
+ server.addEventListener("error", (err) => {
376
391
  isClosed = true;
392
+ if (onClose) onClose();
393
+ try { server.close(); } catch {}
377
394
  });
378
395
  };
379
396
 
380
397
  return {
381
- async fetch(request, env) {
398
+ async fetch(request, env, ctx) {
382
399
  const url = new URL(request.url);
383
400
  const isShared = url.searchParams.has("shared");
384
401
  const origin = request.headers.get("Origin");
@@ -401,6 +418,13 @@ export const createHandler = (moduleMap, generatedTypes, minifiedCore, coreId, m
401
418
 
402
419
  const pair = new WebSocketPair();
403
420
  const [client, server] = Object.values(pair);
421
+
422
+ // Create a promise that resolves when the WebSocket closes
423
+ const wsLifetime = new Promise((resolve) => {
424
+ server.addEventListener("close", resolve, { once: true });
425
+ server.addEventListener("error", resolve, { once: true });
426
+ });
427
+
404
428
  server.accept();
405
429
 
406
430
  if (isShared && env?.SHARED_EXPORT) {
@@ -413,6 +437,11 @@ export const createHandler = (moduleMap, generatedTypes, minifiedCore, coreId, m
413
437
  wireWebSocket(server, dispatcher, env, () => dispatcher.clearAll());
414
438
  }
415
439
 
440
+ // Keep worker alive for the WebSocket lifetime
441
+ if (ctx?.waitUntil) {
442
+ ctx.waitUntil(wsLifetime);
443
+ }
444
+
416
445
  return new Response(null, { status: 101, webSocket: client });
417
446
  }
418
447
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "export-runtime",
3
- "version": "0.0.20",
3
+ "version": "0.0.22",
4
4
  "description": "Cloudflare Workers ESM Export Framework Runtime",
5
5
  "keywords": [
6
6
  "cloudflare",
package/rpc.js CHANGED
@@ -155,25 +155,20 @@ export function createRpcDispatcher(moduleMap) {
155
155
 
156
156
  clearAll() {
157
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
- }
158
+ // Synchronously clear all maps to prevent further access
159
+ const iters = [...iterators.values()];
160
+ const strms = [...streams.values()];
174
161
  instances.clear();
175
162
  iterators.clear();
176
163
  streams.clear();
164
+
165
+ // Cancel iterators and streams without awaiting (fire and forget)
166
+ for (const iter of iters) {
167
+ try { iter?.return?.(); } catch {}
168
+ }
169
+ for (const entry of strms) {
170
+ try { (entry.reader || entry.stream)?.cancel?.(); } catch {}
171
+ }
177
172
  },
178
173
  };
179
174
  }