@ricsam/isolate-fetch 0.1.12 → 0.1.14

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.
@@ -124,25 +124,32 @@ var headersCode = `
124
124
  }
125
125
 
126
126
  forEach(callback, thisArg) {
127
- for (const [key, [originalName, values]] of this.#headers) {
128
- callback.call(thisArg, values.join(', '), originalName, this);
127
+ const sortedKeys = [...this.#headers.keys()].sort();
128
+ for (const key of sortedKeys) {
129
+ const [, values] = this.#headers.get(key);
130
+ callback.call(thisArg, values.join(', '), key, this);
129
131
  }
130
132
  }
131
133
 
132
134
  *entries() {
133
- for (const [key, [name, values]] of this.#headers) {
134
- yield [name, values.join(', ')];
135
+ const sortedKeys = [...this.#headers.keys()].sort();
136
+ for (const key of sortedKeys) {
137
+ const [, values] = this.#headers.get(key);
138
+ yield [key, values.join(', ')];
135
139
  }
136
140
  }
137
141
 
138
142
  *keys() {
139
- for (const [key, [name]] of this.#headers) {
140
- yield name;
143
+ const sortedKeys = [...this.#headers.keys()].sort();
144
+ for (const key of sortedKeys) {
145
+ yield key;
141
146
  }
142
147
  }
143
148
 
144
149
  *values() {
145
- for (const [key, [name, values]] of this.#headers) {
150
+ const sortedKeys = [...this.#headers.keys()].sort();
151
+ for (const key of sortedKeys) {
152
+ const [, values] = this.#headers.get(key);
146
153
  yield values.join(', ');
147
154
  }
148
155
  }
@@ -410,6 +417,9 @@ function setupStreamCallbacks(context, streamRegistry) {
410
417
  global.setSync("__Stream_isQueueFull", new import_isolated_vm.default.Callback((streamId) => {
411
418
  return streamRegistry.isQueueFull(streamId);
412
419
  }));
420
+ global.setSync("__Stream_cancel", new import_isolated_vm.default.Callback((streamId) => {
421
+ streamRegistry.cancel(streamId);
422
+ }));
413
423
  const pullRef = new import_isolated_vm.default.Reference(async (streamId) => {
414
424
  const result = await streamRegistry.pull(streamId);
415
425
  if (result.done) {
@@ -460,7 +470,7 @@ var hostBackedStreamCode = `
460
470
  async pull(controller) {
461
471
  if (closed) return;
462
472
 
463
- const resultJson = __Stream_pull_ref.applySyncPromise(undefined, [streamId]);
473
+ const resultJson = await __Stream_pull_ref.apply(undefined, [streamId], { result: { promise: true, copy: true } });
464
474
  const result = JSON.parse(resultJson);
465
475
 
466
476
  if (result.done) {
@@ -472,7 +482,7 @@ var hostBackedStreamCode = `
472
482
  },
473
483
  cancel(reason) {
474
484
  closed = true;
475
- __Stream_error(streamId, String(reason || "cancelled"));
485
+ __Stream_cancel(streamId);
476
486
  }
477
487
  });
478
488
 
@@ -499,7 +509,7 @@ var hostBackedStreamCode = `
499
509
  globalThis.HostBackedReadableStream = HostBackedReadableStream;
500
510
  })();
501
511
  `;
502
- function setupResponse(context, stateMap) {
512
+ function setupResponse(context, stateMap, streamRegistry) {
503
513
  const global = context.global;
504
514
  global.setSync("__Response_construct", new import_isolated_vm.default.Callback((bodyBytes, status, statusText, headers) => {
505
515
  const instanceId = nextInstanceId++;
@@ -579,6 +589,10 @@ function setupResponse(context, stateMap) {
579
589
  const state = stateMap.get(instanceId);
580
590
  return state?.type ?? "default";
581
591
  }));
592
+ global.setSync("__Response_get_nullBody", new import_isolated_vm.default.Callback((instanceId) => {
593
+ const state = stateMap.get(instanceId);
594
+ return state?.nullBody ?? false;
595
+ }));
582
596
  global.setSync("__Response_setType", new import_isolated_vm.default.Callback((instanceId, type) => {
583
597
  const state = stateMap.get(instanceId);
584
598
  if (state) {
@@ -612,6 +626,38 @@ function setupResponse(context, stateMap) {
612
626
  if (!state) {
613
627
  throw new Error("[TypeError]Cannot clone invalid Response");
614
628
  }
629
+ if (state.streamId !== null) {
630
+ const streamId1 = streamRegistry.create();
631
+ const streamId2 = streamRegistry.create();
632
+ const origStreamId = state.streamId;
633
+ (async () => {
634
+ try {
635
+ while (true) {
636
+ const result = await streamRegistry.pull(origStreamId);
637
+ if (result.done) {
638
+ streamRegistry.close(streamId1);
639
+ streamRegistry.close(streamId2);
640
+ break;
641
+ }
642
+ streamRegistry.push(streamId1, new Uint8Array(result.value));
643
+ streamRegistry.push(streamId2, new Uint8Array(result.value));
644
+ }
645
+ } catch (err) {
646
+ streamRegistry.error(streamId1, err);
647
+ streamRegistry.error(streamId2, err);
648
+ }
649
+ })();
650
+ state.streamId = streamId1;
651
+ const newId2 = nextInstanceId++;
652
+ const newState2 = {
653
+ ...state,
654
+ streamId: streamId2,
655
+ body: state.body ? new Uint8Array(state.body) : null,
656
+ bodyUsed: false
657
+ };
658
+ stateMap.set(newId2, newState2);
659
+ return newId2;
660
+ }
615
661
  const newId = nextInstanceId++;
616
662
  const newState = {
617
663
  ...state,
@@ -838,6 +884,11 @@ function setupResponse(context, stateMap) {
838
884
  }
839
885
 
840
886
  get body() {
887
+ // Null-body responses (204, 304, HEAD) must return null
888
+ if (__Response_get_nullBody(this.#instanceId)) {
889
+ return null;
890
+ }
891
+
841
892
  // Return cached body if available (WHATWG spec requires same object on repeated access)
842
893
  if (this.#cachedBody !== null) {
843
894
  return this.#cachedBody;
@@ -869,6 +920,26 @@ function setupResponse(context, stateMap) {
869
920
  } catch (err) {
870
921
  throw __decodeError(err);
871
922
  }
923
+ if (__Response_get_nullBody(this.#instanceId)) {
924
+ return "";
925
+ }
926
+ if (this.#streamId !== null) {
927
+ const reader = this.body.getReader();
928
+ const chunks = [];
929
+ while (true) {
930
+ const { done, value } = await reader.read();
931
+ if (done) break;
932
+ if (value) chunks.push(value);
933
+ }
934
+ const totalLength = chunks.reduce((acc, c) => acc + c.length, 0);
935
+ const result = new Uint8Array(totalLength);
936
+ let offset = 0;
937
+ for (const chunk of chunks) {
938
+ result.set(chunk, offset);
939
+ offset += chunk.length;
940
+ }
941
+ return new TextDecoder().decode(result);
942
+ }
872
943
  return __Response_text(this.#instanceId);
873
944
  }
874
945
 
@@ -1119,30 +1190,10 @@ function setupRequest(context, stateMap) {
1119
1190
  return Array.from(new TextEncoder().encode(body.toString()));
1120
1191
  }
1121
1192
  if (body instanceof FormData) {
1122
- // Check if FormData has any File/Blob entries
1123
- let hasFiles = false;
1124
- for (const [, value] of body.entries()) {
1125
- if (value instanceof File || value instanceof Blob) {
1126
- hasFiles = true;
1127
- break;
1128
- }
1129
- }
1130
-
1131
- if (hasFiles) {
1132
- // Serialize as multipart/form-data
1133
- const { body: bytes, contentType } = __serializeFormData(body);
1134
- globalThis.__pendingFormDataContentType = contentType;
1135
- return Array.from(bytes);
1136
- }
1137
-
1138
- // URL-encoded for string-only FormData
1139
- const parts = [];
1140
- body.forEach((value, key) => {
1141
- if (typeof value === 'string') {
1142
- parts.push(encodeURIComponent(key) + '=' + encodeURIComponent(value));
1143
- }
1144
- });
1145
- return Array.from(new TextEncoder().encode(parts.join('&')));
1193
+ // Always serialize as multipart/form-data per spec
1194
+ const { body: bytes, contentType } = __serializeFormData(body);
1195
+ globalThis.__pendingFormDataContentType = contentType;
1196
+ return Array.from(bytes);
1146
1197
  }
1147
1198
  // Try to convert to string
1148
1199
  return Array.from(new TextEncoder().encode(String(body)));
@@ -1240,7 +1291,7 @@ function setupRequest(context, stateMap) {
1240
1291
  if (globalThis.__pendingFormDataContentType) {
1241
1292
  headers.set('content-type', globalThis.__pendingFormDataContentType);
1242
1293
  delete globalThis.__pendingFormDataContentType;
1243
- } else if (body instanceof FormData && !headers.has('content-type')) {
1294
+ } else if (body instanceof URLSearchParams && !headers.has('content-type')) {
1244
1295
  headers.set('content-type', 'application/x-www-form-urlencoded');
1245
1296
  }
1246
1297
 
@@ -1430,32 +1481,62 @@ function setupRequest(context, stateMap) {
1430
1481
  var FETCH_STREAM_THRESHOLD = 64 * 1024;
1431
1482
  function setupFetchFunction(context, stateMap, streamRegistry, options) {
1432
1483
  const global = context.global;
1433
- const fetchRef = new import_isolated_vm.default.Reference(async (url, method, headersJson, bodyJson, signalAborted) => {
1484
+ const fetchAbortControllers = new Map;
1485
+ global.setSync("__fetch_abort", new import_isolated_vm.default.Callback((fetchId) => {
1486
+ const controller = fetchAbortControllers.get(fetchId);
1487
+ if (controller) {
1488
+ setImmediate(() => controller.abort());
1489
+ }
1490
+ }));
1491
+ const fetchRef = new import_isolated_vm.default.Reference(async (url, method, headersJson, bodyJson, signalAborted, fetchId) => {
1434
1492
  if (signalAborted) {
1435
1493
  throw new Error("[AbortError]The operation was aborted.");
1436
1494
  }
1495
+ const hostController = new AbortController;
1496
+ fetchAbortControllers.set(fetchId, hostController);
1437
1497
  const headers = JSON.parse(headersJson);
1438
1498
  const bodyBytes = bodyJson ? JSON.parse(bodyJson) : null;
1439
- const body = bodyBytes ? new Uint8Array(bodyBytes) : null;
1440
- const nativeRequest = new Request(url, {
1499
+ const rawBody = bodyBytes ? new Uint8Array(bodyBytes) : null;
1500
+ const init = {
1441
1501
  method,
1442
1502
  headers,
1443
- body
1444
- });
1445
- const onFetch = options?.onFetch ?? fetch;
1446
- const nativeResponse = await onFetch(nativeRequest);
1447
- const contentLength = nativeResponse.headers.get("content-length");
1448
- const knownSize = contentLength ? parseInt(contentLength, 10) : null;
1449
- const isCallbackStream = nativeResponse.__isCallbackStream;
1450
- const isNetworkResponse = nativeResponse.url && (nativeResponse.url.startsWith("http://") || nativeResponse.url.startsWith("https://"));
1451
- const shouldStream = nativeResponse.body && (isCallbackStream || isNetworkResponse && (knownSize === null || knownSize > FETCH_STREAM_THRESHOLD));
1452
- if (shouldStream && nativeResponse.body) {
1453
- if (isCallbackStream) {
1454
- const streamId2 = streamRegistry.create();
1455
- const passthruMap = getPassthruBodiesForContext(context);
1456
- passthruMap.set(streamId2, nativeResponse.body);
1457
- const instanceId3 = nextInstanceId++;
1458
- const state3 = {
1503
+ rawBody,
1504
+ body: rawBody,
1505
+ signal: hostController.signal
1506
+ };
1507
+ const onFetch = options?.onFetch ?? ((url2, init2) => fetch(url2, {
1508
+ method: init2.method,
1509
+ headers: init2.headers,
1510
+ body: init2.body,
1511
+ signal: init2.signal
1512
+ }));
1513
+ try {
1514
+ let cleanupAbort;
1515
+ const abortPromise = new Promise((_, reject) => {
1516
+ if (hostController.signal.aborted) {
1517
+ reject(Object.assign(new Error("The operation was aborted."), { name: "AbortError" }));
1518
+ return;
1519
+ }
1520
+ const onAbort = () => {
1521
+ reject(Object.assign(new Error("The operation was aborted."), { name: "AbortError" }));
1522
+ };
1523
+ hostController.signal.addEventListener("abort", onAbort, { once: true });
1524
+ cleanupAbort = () => hostController.signal.removeEventListener("abort", onAbort);
1525
+ });
1526
+ abortPromise.catch(() => {});
1527
+ const nativeResponse = await Promise.race([onFetch(url, init), abortPromise]);
1528
+ cleanupAbort?.();
1529
+ const status = nativeResponse.status;
1530
+ const isNullBody = status === 204 || status === 304 || method.toUpperCase() === "HEAD";
1531
+ const isCallbackStream = nativeResponse.__isCallbackStream;
1532
+ const isNetworkResponse = nativeResponse.url && (nativeResponse.url.startsWith("http://") || nativeResponse.url.startsWith("https://"));
1533
+ const shouldStream = !isNullBody && nativeResponse.body && (isCallbackStream || isNetworkResponse);
1534
+ if (shouldStream && nativeResponse.body) {
1535
+ const streamId = streamRegistry.create();
1536
+ const streamCleanupFn = import_stream_state.startNativeStreamReader(nativeResponse.body, streamId, streamRegistry);
1537
+ streamRegistry.setCleanup(streamId, streamCleanupFn);
1538
+ const instanceId2 = nextInstanceId++;
1539
+ const state2 = {
1459
1540
  status: nativeResponse.status,
1460
1541
  statusText: nativeResponse.statusText,
1461
1542
  headers: Array.from(nativeResponse.headers.entries()),
@@ -1464,65 +1545,37 @@ function setupFetchFunction(context, stateMap, streamRegistry, options) {
1464
1545
  type: "default",
1465
1546
  url: nativeResponse.url,
1466
1547
  redirected: nativeResponse.redirected,
1467
- streamId: streamId2
1548
+ streamId,
1549
+ nullBody: isNullBody
1468
1550
  };
1469
- stateMap.set(instanceId3, state3);
1470
- return instanceId3;
1551
+ stateMap.set(instanceId2, state2);
1552
+ return instanceId2;
1471
1553
  }
1472
- const streamId = streamRegistry.create();
1473
- const instanceId2 = nextInstanceId++;
1474
- const state2 = {
1554
+ const responseBody = await nativeResponse.arrayBuffer();
1555
+ const responseBodyArray = Array.from(new Uint8Array(responseBody));
1556
+ const instanceId = nextInstanceId++;
1557
+ const state = {
1475
1558
  status: nativeResponse.status,
1476
1559
  statusText: nativeResponse.statusText,
1477
1560
  headers: Array.from(nativeResponse.headers.entries()),
1478
- body: new Uint8Array(0),
1561
+ body: new Uint8Array(responseBodyArray),
1479
1562
  bodyUsed: false,
1480
1563
  type: "default",
1481
1564
  url: nativeResponse.url,
1482
1565
  redirected: nativeResponse.redirected,
1483
- streamId
1566
+ streamId: null,
1567
+ nullBody: isNullBody
1484
1568
  };
1485
- stateMap.set(instanceId2, state2);
1486
- const reader = nativeResponse.body.getReader();
1487
- (async () => {
1488
- try {
1489
- while (true) {
1490
- const { done, value } = await reader.read();
1491
- if (done) {
1492
- streamRegistry.close(streamId);
1493
- break;
1494
- }
1495
- if (value) {
1496
- while (streamRegistry.isQueueFull(streamId)) {
1497
- await new Promise((r) => setTimeout(r, 1));
1498
- }
1499
- streamRegistry.push(streamId, value);
1500
- }
1501
- }
1502
- } catch (err) {
1503
- streamRegistry.error(streamId, err);
1504
- } finally {
1505
- reader.releaseLock();
1506
- }
1507
- })();
1508
- return instanceId2;
1569
+ stateMap.set(instanceId, state);
1570
+ return instanceId;
1571
+ } catch (err) {
1572
+ if (err instanceof Error && err.name === "AbortError") {
1573
+ throw new Error("[AbortError]The operation was aborted.");
1574
+ }
1575
+ throw err;
1576
+ } finally {
1577
+ fetchAbortControllers.delete(fetchId);
1509
1578
  }
1510
- const responseBody = await nativeResponse.arrayBuffer();
1511
- const responseBodyArray = Array.from(new Uint8Array(responseBody));
1512
- const instanceId = nextInstanceId++;
1513
- const state = {
1514
- status: nativeResponse.status,
1515
- statusText: nativeResponse.statusText,
1516
- headers: Array.from(nativeResponse.headers.entries()),
1517
- body: new Uint8Array(responseBodyArray),
1518
- bodyUsed: false,
1519
- type: "default",
1520
- url: nativeResponse.url,
1521
- redirected: nativeResponse.redirected,
1522
- streamId: null
1523
- };
1524
- stateMap.set(instanceId, state);
1525
- return instanceId;
1526
1579
  });
1527
1580
  global.setSync("__fetch_ref", fetchRef);
1528
1581
  const fetchCode = `
@@ -1540,7 +1593,28 @@ function setupFetchFunction(context, stateMap, streamRegistry, options) {
1540
1593
  return err;
1541
1594
  }
1542
1595
 
1543
- globalThis.fetch = function(input, init = {}) {
1596
+ let __nextFetchId = 1;
1597
+
1598
+ globalThis.fetch = async function(input, init = {}) {
1599
+ // Handle Blob and ReadableStream bodies before creating Request
1600
+ if (init.body instanceof Blob && !(init.body instanceof File)) {
1601
+ const buf = await init.body.arrayBuffer();
1602
+ init = Object.assign({}, init, { body: new Uint8Array(buf) });
1603
+ } else if (init.body instanceof ReadableStream) {
1604
+ const reader = init.body.getReader();
1605
+ const chunks = [];
1606
+ while (true) {
1607
+ const { done, value } = await reader.read();
1608
+ if (done) break;
1609
+ chunks.push(value);
1610
+ }
1611
+ const total = chunks.reduce((s, c) => s + c.length, 0);
1612
+ const buf = new Uint8Array(total);
1613
+ let off = 0;
1614
+ for (const c of chunks) { buf.set(c, off); off += c.length; }
1615
+ init = Object.assign({}, init, { body: buf });
1616
+ }
1617
+
1544
1618
  // Create Request from input
1545
1619
  const request = input instanceof Request ? input : new Request(input, init);
1546
1620
 
@@ -1548,20 +1622,34 @@ function setupFetchFunction(context, stateMap, streamRegistry, options) {
1548
1622
  const signal = init.signal ?? request.signal;
1549
1623
  const signalAborted = signal?.aborted ?? false;
1550
1624
 
1625
+ // Assign a fetch ID for abort tracking
1626
+ const fetchId = __nextFetchId++;
1627
+
1628
+ // Register abort listener if signal exists
1629
+ if (signal && !signalAborted) {
1630
+ signal.addEventListener('abort', () => { __fetch_abort(fetchId); });
1631
+ }
1632
+
1551
1633
  // Serialize headers and body to JSON for transfer
1552
1634
  const headersJson = JSON.stringify(Array.from(request.headers.entries()));
1553
1635
  const bodyBytes = request._getBodyBytes();
1554
1636
  const bodyJson = bodyBytes ? JSON.stringify(bodyBytes) : null;
1555
1637
 
1638
+ // Short-circuit: if signal is already aborted, throw without calling host
1639
+ if (signalAborted) {
1640
+ throw new DOMException('The operation was aborted.', 'AbortError');
1641
+ }
1642
+
1556
1643
  // Call host - returns just the response instance ID
1557
1644
  try {
1558
- const instanceId = __fetch_ref.applySyncPromise(undefined, [
1645
+ const instanceId = await __fetch_ref.apply(undefined, [
1559
1646
  request.url,
1560
1647
  request.method,
1561
1648
  headersJson,
1562
1649
  bodyJson,
1563
- signalAborted
1564
- ]);
1650
+ signalAborted,
1651
+ fetchId
1652
+ ], { result: { promise: true, copy: true } });
1565
1653
 
1566
1654
  // Construct Response from the instance ID
1567
1655
  return Response._fromInstanceId(instanceId);
@@ -1656,6 +1744,373 @@ function setupServerWebSocket(context, wsCommandCallbacks) {
1656
1744
  })();
1657
1745
  `);
1658
1746
  }
1747
+ function setupClientWebSocket(context, clientWsCommandCallbacks) {
1748
+ const global = context.global;
1749
+ global.setSync("__WebSocket_connect", new import_isolated_vm.default.Callback((socketId, url, protocols) => {
1750
+ const cmd = {
1751
+ type: "connect",
1752
+ socketId,
1753
+ url,
1754
+ protocols
1755
+ };
1756
+ for (const cb of clientWsCommandCallbacks)
1757
+ cb(cmd);
1758
+ }));
1759
+ global.setSync("__WebSocket_send", new import_isolated_vm.default.Callback((socketId, data) => {
1760
+ const cmd = { type: "send", socketId, data };
1761
+ for (const cb of clientWsCommandCallbacks)
1762
+ cb(cmd);
1763
+ }));
1764
+ global.setSync("__WebSocket_close", new import_isolated_vm.default.Callback((socketId, code, reason) => {
1765
+ const cmd = { type: "close", socketId, code, reason };
1766
+ for (const cb of clientWsCommandCallbacks)
1767
+ cb(cmd);
1768
+ }));
1769
+ context.evalSync(`
1770
+ (function() {
1771
+ // Socket ID counter
1772
+ let __nextSocketId = 1;
1773
+
1774
+ // Active sockets registry
1775
+ const __clientWebSockets = new Map();
1776
+
1777
+ // Simple Event class (if not defined globally)
1778
+ const _Event = globalThis.Event || class Event {
1779
+ constructor(type, options = {}) {
1780
+ this.type = type;
1781
+ this.bubbles = options.bubbles || false;
1782
+ this.cancelable = options.cancelable || false;
1783
+ this.defaultPrevented = false;
1784
+ this.timeStamp = Date.now();
1785
+ this.target = null;
1786
+ this.currentTarget = null;
1787
+ }
1788
+ preventDefault() {
1789
+ if (this.cancelable) this.defaultPrevented = true;
1790
+ }
1791
+ stopPropagation() {}
1792
+ stopImmediatePropagation() {}
1793
+ };
1794
+
1795
+ // MessageEvent class for WebSocket messages
1796
+ const _MessageEvent = globalThis.MessageEvent || class MessageEvent extends _Event {
1797
+ constructor(type, options = {}) {
1798
+ super(type, options);
1799
+ this.data = options.data !== undefined ? options.data : null;
1800
+ this.origin = options.origin || '';
1801
+ this.lastEventId = options.lastEventId || '';
1802
+ this.source = options.source || null;
1803
+ this.ports = options.ports || [];
1804
+ }
1805
+ };
1806
+
1807
+ // CloseEvent class for WebSocket close
1808
+ const _CloseEvent = globalThis.CloseEvent || class CloseEvent extends _Event {
1809
+ constructor(type, options = {}) {
1810
+ super(type, options);
1811
+ this.code = options.code !== undefined ? options.code : 0;
1812
+ this.reason = options.reason !== undefined ? options.reason : '';
1813
+ this.wasClean = options.wasClean !== undefined ? options.wasClean : false;
1814
+ }
1815
+ };
1816
+
1817
+ // Helper to dispatch events
1818
+ function dispatchEvent(ws, event) {
1819
+ const listeners = ws._listeners.get(event.type) || [];
1820
+ for (const listener of listeners) {
1821
+ try {
1822
+ listener.call(ws, event);
1823
+ } catch (e) {
1824
+ console.error('WebSocket event listener error:', e);
1825
+ }
1826
+ }
1827
+ // Also call on* handler if set
1828
+ const handler = ws['on' + event.type];
1829
+ if (typeof handler === 'function') {
1830
+ try {
1831
+ handler.call(ws, event);
1832
+ } catch (e) {
1833
+ console.error('WebSocket handler error:', e);
1834
+ }
1835
+ }
1836
+ }
1837
+
1838
+ class WebSocket {
1839
+ static CONNECTING = 0;
1840
+ static OPEN = 1;
1841
+ static CLOSING = 2;
1842
+ static CLOSED = 3;
1843
+
1844
+ #socketId;
1845
+ #url;
1846
+ #readyState = WebSocket.CONNECTING;
1847
+ #bufferedAmount = 0;
1848
+ #extensions = '';
1849
+ #protocol = '';
1850
+ #binaryType = 'blob';
1851
+ _listeners = new Map();
1852
+
1853
+ // Event handlers
1854
+ onopen = null;
1855
+ onmessage = null;
1856
+ onerror = null;
1857
+ onclose = null;
1858
+
1859
+ constructor(url, protocols) {
1860
+ // Validate URL
1861
+ let parsedUrl;
1862
+ try {
1863
+ parsedUrl = new URL(url);
1864
+ } catch (e) {
1865
+ throw new DOMException("Failed to construct 'WebSocket': The URL '" + url + "' is invalid.", 'SyntaxError');
1866
+ }
1867
+
1868
+ if (parsedUrl.protocol !== 'ws:' && parsedUrl.protocol !== 'wss:') {
1869
+ throw new DOMException("Failed to construct 'WebSocket': The URL's scheme must be either 'ws' or 'wss'.", 'SyntaxError');
1870
+ }
1871
+
1872
+ // Per WHATWG spec, fragments must be stripped from WebSocket URLs
1873
+ parsedUrl.hash = '';
1874
+ this.#url = parsedUrl.href;
1875
+ this.#socketId = String(__nextSocketId++);
1876
+
1877
+ // Normalize protocols to array
1878
+ let protocolArray = [];
1879
+ if (protocols !== undefined) {
1880
+ if (typeof protocols === 'string') {
1881
+ protocolArray = [protocols];
1882
+ } else if (Array.isArray(protocols)) {
1883
+ protocolArray = protocols;
1884
+ } else {
1885
+ protocolArray = [String(protocols)];
1886
+ }
1887
+ }
1888
+
1889
+ // Check for duplicate protocols
1890
+ const seen = new Set();
1891
+ for (const p of protocolArray) {
1892
+ if (seen.has(p)) {
1893
+ throw new DOMException("Failed to construct 'WebSocket': The subprotocol '" + p + "' is duplicated.", 'SyntaxError');
1894
+ }
1895
+ seen.add(p);
1896
+ }
1897
+
1898
+ // Register socket
1899
+ __clientWebSockets.set(this.#socketId, this);
1900
+
1901
+ // Call host to create connection
1902
+ __WebSocket_connect(this.#socketId, this.#url, protocolArray);
1903
+ }
1904
+
1905
+ get url() {
1906
+ return this.#url;
1907
+ }
1908
+
1909
+ get readyState() {
1910
+ return this.#readyState;
1911
+ }
1912
+
1913
+ get bufferedAmount() {
1914
+ return this.#bufferedAmount;
1915
+ }
1916
+
1917
+ get extensions() {
1918
+ return this.#extensions;
1919
+ }
1920
+
1921
+ get protocol() {
1922
+ return this.#protocol;
1923
+ }
1924
+
1925
+ get binaryType() {
1926
+ return this.#binaryType;
1927
+ }
1928
+
1929
+ set binaryType(value) {
1930
+ if (value !== 'blob' && value !== 'arraybuffer') {
1931
+ throw new DOMException("Failed to set the 'binaryType' property: '" + value + "' is not a valid value.", 'SyntaxError');
1932
+ }
1933
+ this.#binaryType = value;
1934
+ }
1935
+
1936
+ // ReadyState constants
1937
+ get CONNECTING() { return WebSocket.CONNECTING; }
1938
+ get OPEN() { return WebSocket.OPEN; }
1939
+ get CLOSING() { return WebSocket.CLOSING; }
1940
+ get CLOSED() { return WebSocket.CLOSED; }
1941
+
1942
+ send(data) {
1943
+ if (this.#readyState === WebSocket.CONNECTING) {
1944
+ throw new DOMException("Failed to execute 'send' on 'WebSocket': Still in CONNECTING state.", 'InvalidStateError');
1945
+ }
1946
+
1947
+ if (this.#readyState !== WebSocket.OPEN) {
1948
+ // Silently discard if not open (per spec)
1949
+ return;
1950
+ }
1951
+
1952
+ // Convert data to string for transfer
1953
+ let dataStr;
1954
+ if (typeof data === 'string') {
1955
+ dataStr = data;
1956
+ } else if (data instanceof ArrayBuffer) {
1957
+ // Convert ArrayBuffer to base64 for transfer
1958
+ const bytes = new Uint8Array(data);
1959
+ let binary = '';
1960
+ for (let i = 0; i < bytes.byteLength; i++) {
1961
+ binary += String.fromCharCode(bytes[i]);
1962
+ }
1963
+ dataStr = '__BINARY__' + btoa(binary);
1964
+ } else if (ArrayBuffer.isView(data)) {
1965
+ const bytes = new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
1966
+ let binary = '';
1967
+ for (let i = 0; i < bytes.byteLength; i++) {
1968
+ binary += String.fromCharCode(bytes[i]);
1969
+ }
1970
+ dataStr = '__BINARY__' + btoa(binary);
1971
+ } else if (data instanceof Blob) {
1972
+ // Blob.arrayBuffer() is async, but send() is sync
1973
+ // For now, throw - this is a limitation
1974
+ throw new DOMException("Failed to execute 'send' on 'WebSocket': Blob data is not supported in this environment.", 'NotSupportedError');
1975
+ } else {
1976
+ dataStr = String(data);
1977
+ }
1978
+
1979
+ __WebSocket_send(this.#socketId, dataStr);
1980
+ }
1981
+
1982
+ close(code, reason) {
1983
+ if (code !== undefined) {
1984
+ if (typeof code !== 'number' || code !== Math.floor(code)) {
1985
+ throw new DOMException("Failed to execute 'close' on 'WebSocket': The code must be an integer.", 'InvalidAccessError');
1986
+ }
1987
+ if (code !== 1000 && (code < 3000 || code > 4999)) {
1988
+ throw new DOMException("Failed to execute 'close' on 'WebSocket': The code must be either 1000, or between 3000 and 4999.", 'InvalidAccessError');
1989
+ }
1990
+ }
1991
+
1992
+ if (reason !== undefined) {
1993
+ const encoder = new TextEncoder();
1994
+ if (encoder.encode(reason).byteLength > 123) {
1995
+ throw new DOMException("Failed to execute 'close' on 'WebSocket': The message must not be greater than 123 bytes.", 'SyntaxError');
1996
+ }
1997
+ }
1998
+
1999
+ if (this.#readyState === WebSocket.CLOSING || this.#readyState === WebSocket.CLOSED) {
2000
+ return;
2001
+ }
2002
+
2003
+ this.#readyState = WebSocket.CLOSING;
2004
+ __WebSocket_close(this.#socketId, code ?? 1000, reason ?? '');
2005
+ }
2006
+
2007
+ // EventTarget interface
2008
+ addEventListener(type, listener, options) {
2009
+ if (typeof listener !== 'function') return;
2010
+ let listeners = this._listeners.get(type);
2011
+ if (!listeners) {
2012
+ listeners = [];
2013
+ this._listeners.set(type, listeners);
2014
+ }
2015
+ if (!listeners.includes(listener)) {
2016
+ listeners.push(listener);
2017
+ }
2018
+ }
2019
+
2020
+ removeEventListener(type, listener, options) {
2021
+ const listeners = this._listeners.get(type);
2022
+ if (!listeners) return;
2023
+ const index = listeners.indexOf(listener);
2024
+ if (index !== -1) {
2025
+ listeners.splice(index, 1);
2026
+ }
2027
+ }
2028
+
2029
+ dispatchEvent(event) {
2030
+ dispatchEvent(this, event);
2031
+ return !event.defaultPrevented;
2032
+ }
2033
+
2034
+ // Internal methods called from host
2035
+ _setProtocol(protocol) {
2036
+ this.#protocol = protocol;
2037
+ }
2038
+
2039
+ _setExtensions(extensions) {
2040
+ this.#extensions = extensions;
2041
+ }
2042
+
2043
+ _setReadyState(state) {
2044
+ this.#readyState = state;
2045
+ }
2046
+
2047
+ _dispatchOpen() {
2048
+ this.#readyState = WebSocket.OPEN;
2049
+ const event = new _Event('open');
2050
+ dispatchEvent(this, event);
2051
+ }
2052
+
2053
+ _dispatchMessage(data) {
2054
+ // Handle binary data
2055
+ let messageData = data;
2056
+ if (typeof data === 'string' && data.startsWith('__BINARY__')) {
2057
+ const base64 = data.slice(10);
2058
+ const binary = atob(base64);
2059
+ const bytes = new Uint8Array(binary.length);
2060
+ for (let i = 0; i < binary.length; i++) {
2061
+ bytes[i] = binary.charCodeAt(i);
2062
+ }
2063
+ if (this.#binaryType === 'arraybuffer') {
2064
+ messageData = bytes.buffer;
2065
+ } else {
2066
+ messageData = new Blob([bytes]);
2067
+ }
2068
+ }
2069
+
2070
+ const event = new _MessageEvent('message', { data: messageData });
2071
+ dispatchEvent(this, event);
2072
+ }
2073
+
2074
+ _dispatchError() {
2075
+ const event = new _Event('error');
2076
+ dispatchEvent(this, event);
2077
+ }
2078
+
2079
+ _dispatchClose(code, reason, wasClean) {
2080
+ this.#readyState = WebSocket.CLOSED;
2081
+ const event = new _CloseEvent('close', { code, reason, wasClean });
2082
+ dispatchEvent(this, event);
2083
+ __clientWebSockets.delete(this.#socketId);
2084
+ }
2085
+ }
2086
+
2087
+ // Helper to dispatch events from host to a socket by ID
2088
+ globalThis.__dispatchClientWebSocketEvent = function(socketId, eventType, data) {
2089
+ const ws = __clientWebSockets.get(socketId);
2090
+ if (!ws) return;
2091
+
2092
+ switch (eventType) {
2093
+ case 'open':
2094
+ ws._setProtocol(data.protocol || '');
2095
+ ws._setExtensions(data.extensions || '');
2096
+ ws._dispatchOpen();
2097
+ break;
2098
+ case 'message':
2099
+ ws._dispatchMessage(data.data);
2100
+ break;
2101
+ case 'error':
2102
+ ws._dispatchError();
2103
+ break;
2104
+ case 'close':
2105
+ ws._dispatchClose(data.code, data.reason, data.wasClean);
2106
+ break;
2107
+ }
2108
+ };
2109
+
2110
+ globalThis.WebSocket = WebSocket;
2111
+ })();
2112
+ `);
2113
+ }
1659
2114
  function setupServe(context) {
1660
2115
  context.evalSync(`
1661
2116
  (function() {
@@ -1678,7 +2133,7 @@ async function setupFetch(context, options) {
1678
2133
  context.evalSync(multipartCode);
1679
2134
  setupStreamCallbacks(context, streamRegistry);
1680
2135
  context.evalSync(hostBackedStreamCode);
1681
- setupResponse(context, stateMap);
2136
+ setupResponse(context, stateMap, streamRegistry);
1682
2137
  setupRequest(context, stateMap);
1683
2138
  setupFetchFunction(context, stateMap, streamRegistry, options);
1684
2139
  const serveState = {
@@ -1686,9 +2141,11 @@ async function setupFetch(context, options) {
1686
2141
  activeConnections: new Map
1687
2142
  };
1688
2143
  const wsCommandCallbacks = new Set;
2144
+ const clientWsCommandCallbacks = new Set;
1689
2145
  setupServer(context, serveState);
1690
2146
  setupServerWebSocket(context, wsCommandCallbacks);
1691
2147
  setupServe(context);
2148
+ setupClientWebSocket(context, clientWsCommandCallbacks);
1692
2149
  return {
1693
2150
  dispose() {
1694
2151
  stateMap.clear();
@@ -1801,8 +2258,7 @@ async function setupFetch(context, options) {
1801
2258
  },
1802
2259
  cancel() {
1803
2260
  streamDone = true;
1804
- streamRegistry.error(responseStreamId, new Error("Stream cancelled"));
1805
- streamRegistry.delete(responseStreamId);
2261
+ streamRegistry.cancel(responseStreamId);
1806
2262
  }
1807
2263
  });
1808
2264
  const responseHeaders2 = new Headers(responseState.headers);
@@ -1947,8 +2403,55 @@ async function setupFetch(context, options) {
1947
2403
  },
1948
2404
  hasActiveConnections() {
1949
2405
  return serveState.activeConnections.size > 0;
2406
+ },
2407
+ dispatchClientWebSocketOpen(socketId, protocol, extensions) {
2408
+ const safeProtocol = protocol.replace(/\\/g, "\\\\").replace(/"/g, "\\\"");
2409
+ const safeExtensions = extensions.replace(/\\/g, "\\\\").replace(/"/g, "\\\"");
2410
+ context.evalSync(`
2411
+ __dispatchClientWebSocketEvent("${socketId}", "open", {
2412
+ protocol: "${safeProtocol}",
2413
+ extensions: "${safeExtensions}"
2414
+ });
2415
+ `);
2416
+ },
2417
+ dispatchClientWebSocketMessage(socketId, data) {
2418
+ if (typeof data === "string") {
2419
+ const safeData = data.replace(/\\/g, "\\\\").replace(/"/g, "\\\"").replace(/\n/g, "\\n").replace(/\r/g, "\\r");
2420
+ context.evalSync(`
2421
+ __dispatchClientWebSocketEvent("${socketId}", "message", { data: "${safeData}" });
2422
+ `);
2423
+ } else {
2424
+ const bytes = new Uint8Array(data);
2425
+ let binary = "";
2426
+ for (let i = 0;i < bytes.byteLength; i++) {
2427
+ binary += String.fromCharCode(bytes[i]);
2428
+ }
2429
+ const base64 = Buffer.from(binary, "binary").toString("base64");
2430
+ context.evalSync(`
2431
+ __dispatchClientWebSocketEvent("${socketId}", "message", { data: "__BINARY__${base64}" });
2432
+ `);
2433
+ }
2434
+ },
2435
+ dispatchClientWebSocketClose(socketId, code, reason, wasClean) {
2436
+ const safeReason = reason.replace(/\\/g, "\\\\").replace(/"/g, "\\\"").replace(/\n/g, "\\n");
2437
+ context.evalIgnored(`
2438
+ __dispatchClientWebSocketEvent("${socketId}", "close", {
2439
+ code: ${code},
2440
+ reason: "${safeReason}",
2441
+ wasClean: ${wasClean}
2442
+ });
2443
+ `);
2444
+ },
2445
+ dispatchClientWebSocketError(socketId) {
2446
+ context.evalIgnored(`
2447
+ __dispatchClientWebSocketEvent("${socketId}", "error", {});
2448
+ `);
2449
+ },
2450
+ onClientWebSocketCommand(callback) {
2451
+ clientWsCommandCallbacks.add(callback);
2452
+ return () => clientWsCommandCallbacks.delete(callback);
1950
2453
  }
1951
2454
  };
1952
2455
  }
1953
2456
 
1954
- //# debugId=B1E0412945057BF364756E2164756E21
2457
+ //# debugId=7837D23CACB6E1A364756E2164756E21