@ricsam/isolate-fetch 0.1.13 → 0.1.15

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.
@@ -80,25 +80,32 @@ var headersCode = `
80
80
  }
81
81
 
82
82
  forEach(callback, thisArg) {
83
- for (const [key, [originalName, values]] of this.#headers) {
84
- callback.call(thisArg, values.join(', '), originalName, this);
83
+ const sortedKeys = [...this.#headers.keys()].sort();
84
+ for (const key of sortedKeys) {
85
+ const [, values] = this.#headers.get(key);
86
+ callback.call(thisArg, values.join(', '), key, this);
85
87
  }
86
88
  }
87
89
 
88
90
  *entries() {
89
- for (const [key, [name, values]] of this.#headers) {
90
- yield [name, values.join(', ')];
91
+ const sortedKeys = [...this.#headers.keys()].sort();
92
+ for (const key of sortedKeys) {
93
+ const [, values] = this.#headers.get(key);
94
+ yield [key, values.join(', ')];
91
95
  }
92
96
  }
93
97
 
94
98
  *keys() {
95
- for (const [key, [name]] of this.#headers) {
96
- yield name;
99
+ const sortedKeys = [...this.#headers.keys()].sort();
100
+ for (const key of sortedKeys) {
101
+ yield key;
97
102
  }
98
103
  }
99
104
 
100
105
  *values() {
101
- for (const [key, [name, values]] of this.#headers) {
106
+ const sortedKeys = [...this.#headers.keys()].sort();
107
+ for (const key of sortedKeys) {
108
+ const [, values] = this.#headers.get(key);
102
109
  yield values.join(', ');
103
110
  }
104
111
  }
@@ -1445,14 +1452,20 @@ function setupFetchFunction(context, stateMap, streamRegistry, options) {
1445
1452
  fetchAbortControllers.set(fetchId, hostController);
1446
1453
  const headers = JSON.parse(headersJson);
1447
1454
  const bodyBytes = bodyJson ? JSON.parse(bodyJson) : null;
1448
- const body = bodyBytes ? new Uint8Array(bodyBytes) : null;
1449
- const nativeRequest = new Request(url, {
1455
+ const rawBody = bodyBytes ? new Uint8Array(bodyBytes) : null;
1456
+ const init = {
1450
1457
  method,
1451
1458
  headers,
1452
- body,
1459
+ rawBody,
1460
+ body: rawBody,
1453
1461
  signal: hostController.signal
1454
- });
1455
- const onFetch = options?.onFetch ?? fetch;
1462
+ };
1463
+ const onFetch = options?.onFetch ?? ((url2, init2) => fetch(url2, {
1464
+ method: init2.method,
1465
+ headers: init2.headers,
1466
+ body: init2.body,
1467
+ signal: init2.signal
1468
+ }));
1456
1469
  try {
1457
1470
  let cleanupAbort;
1458
1471
  const abortPromise = new Promise((_, reject) => {
@@ -1467,7 +1480,7 @@ function setupFetchFunction(context, stateMap, streamRegistry, options) {
1467
1480
  cleanupAbort = () => hostController.signal.removeEventListener("abort", onAbort);
1468
1481
  });
1469
1482
  abortPromise.catch(() => {});
1470
- const nativeResponse = await Promise.race([onFetch(nativeRequest), abortPromise]);
1483
+ const nativeResponse = await Promise.race([onFetch(url, init), abortPromise]);
1471
1484
  cleanupAbort?.();
1472
1485
  const status = nativeResponse.status;
1473
1486
  const isNullBody = status === 204 || status === 304 || method.toUpperCase() === "HEAD";
@@ -1687,6 +1700,373 @@ function setupServerWebSocket(context, wsCommandCallbacks) {
1687
1700
  })();
1688
1701
  `);
1689
1702
  }
1703
+ function setupClientWebSocket(context, clientWsCommandCallbacks) {
1704
+ const global = context.global;
1705
+ global.setSync("__WebSocket_connect", new ivm.Callback((socketId, url, protocols) => {
1706
+ const cmd = {
1707
+ type: "connect",
1708
+ socketId,
1709
+ url,
1710
+ protocols
1711
+ };
1712
+ for (const cb of clientWsCommandCallbacks)
1713
+ cb(cmd);
1714
+ }));
1715
+ global.setSync("__WebSocket_send", new ivm.Callback((socketId, data) => {
1716
+ const cmd = { type: "send", socketId, data };
1717
+ for (const cb of clientWsCommandCallbacks)
1718
+ cb(cmd);
1719
+ }));
1720
+ global.setSync("__WebSocket_close", new ivm.Callback((socketId, code, reason) => {
1721
+ const cmd = { type: "close", socketId, code, reason };
1722
+ for (const cb of clientWsCommandCallbacks)
1723
+ cb(cmd);
1724
+ }));
1725
+ context.evalSync(`
1726
+ (function() {
1727
+ // Socket ID counter
1728
+ let __nextSocketId = 1;
1729
+
1730
+ // Active sockets registry
1731
+ const __clientWebSockets = new Map();
1732
+
1733
+ // Simple Event class (if not defined globally)
1734
+ const _Event = globalThis.Event || class Event {
1735
+ constructor(type, options = {}) {
1736
+ this.type = type;
1737
+ this.bubbles = options.bubbles || false;
1738
+ this.cancelable = options.cancelable || false;
1739
+ this.defaultPrevented = false;
1740
+ this.timeStamp = Date.now();
1741
+ this.target = null;
1742
+ this.currentTarget = null;
1743
+ }
1744
+ preventDefault() {
1745
+ if (this.cancelable) this.defaultPrevented = true;
1746
+ }
1747
+ stopPropagation() {}
1748
+ stopImmediatePropagation() {}
1749
+ };
1750
+
1751
+ // MessageEvent class for WebSocket messages
1752
+ const _MessageEvent = globalThis.MessageEvent || class MessageEvent extends _Event {
1753
+ constructor(type, options = {}) {
1754
+ super(type, options);
1755
+ this.data = options.data !== undefined ? options.data : null;
1756
+ this.origin = options.origin || '';
1757
+ this.lastEventId = options.lastEventId || '';
1758
+ this.source = options.source || null;
1759
+ this.ports = options.ports || [];
1760
+ }
1761
+ };
1762
+
1763
+ // CloseEvent class for WebSocket close
1764
+ const _CloseEvent = globalThis.CloseEvent || class CloseEvent extends _Event {
1765
+ constructor(type, options = {}) {
1766
+ super(type, options);
1767
+ this.code = options.code !== undefined ? options.code : 0;
1768
+ this.reason = options.reason !== undefined ? options.reason : '';
1769
+ this.wasClean = options.wasClean !== undefined ? options.wasClean : false;
1770
+ }
1771
+ };
1772
+
1773
+ // Helper to dispatch events
1774
+ function dispatchEvent(ws, event) {
1775
+ const listeners = ws._listeners.get(event.type) || [];
1776
+ for (const listener of listeners) {
1777
+ try {
1778
+ listener.call(ws, event);
1779
+ } catch (e) {
1780
+ console.error('WebSocket event listener error:', e);
1781
+ }
1782
+ }
1783
+ // Also call on* handler if set
1784
+ const handler = ws['on' + event.type];
1785
+ if (typeof handler === 'function') {
1786
+ try {
1787
+ handler.call(ws, event);
1788
+ } catch (e) {
1789
+ console.error('WebSocket handler error:', e);
1790
+ }
1791
+ }
1792
+ }
1793
+
1794
+ class WebSocket {
1795
+ static CONNECTING = 0;
1796
+ static OPEN = 1;
1797
+ static CLOSING = 2;
1798
+ static CLOSED = 3;
1799
+
1800
+ #socketId;
1801
+ #url;
1802
+ #readyState = WebSocket.CONNECTING;
1803
+ #bufferedAmount = 0;
1804
+ #extensions = '';
1805
+ #protocol = '';
1806
+ #binaryType = 'blob';
1807
+ _listeners = new Map();
1808
+
1809
+ // Event handlers
1810
+ onopen = null;
1811
+ onmessage = null;
1812
+ onerror = null;
1813
+ onclose = null;
1814
+
1815
+ constructor(url, protocols) {
1816
+ // Validate URL
1817
+ let parsedUrl;
1818
+ try {
1819
+ parsedUrl = new URL(url);
1820
+ } catch (e) {
1821
+ throw new DOMException("Failed to construct 'WebSocket': The URL '" + url + "' is invalid.", 'SyntaxError');
1822
+ }
1823
+
1824
+ if (parsedUrl.protocol !== 'ws:' && parsedUrl.protocol !== 'wss:') {
1825
+ throw new DOMException("Failed to construct 'WebSocket': The URL's scheme must be either 'ws' or 'wss'.", 'SyntaxError');
1826
+ }
1827
+
1828
+ // Per WHATWG spec, fragments must be stripped from WebSocket URLs
1829
+ parsedUrl.hash = '';
1830
+ this.#url = parsedUrl.href;
1831
+ this.#socketId = String(__nextSocketId++);
1832
+
1833
+ // Normalize protocols to array
1834
+ let protocolArray = [];
1835
+ if (protocols !== undefined) {
1836
+ if (typeof protocols === 'string') {
1837
+ protocolArray = [protocols];
1838
+ } else if (Array.isArray(protocols)) {
1839
+ protocolArray = protocols;
1840
+ } else {
1841
+ protocolArray = [String(protocols)];
1842
+ }
1843
+ }
1844
+
1845
+ // Check for duplicate protocols
1846
+ const seen = new Set();
1847
+ for (const p of protocolArray) {
1848
+ if (seen.has(p)) {
1849
+ throw new DOMException("Failed to construct 'WebSocket': The subprotocol '" + p + "' is duplicated.", 'SyntaxError');
1850
+ }
1851
+ seen.add(p);
1852
+ }
1853
+
1854
+ // Register socket
1855
+ __clientWebSockets.set(this.#socketId, this);
1856
+
1857
+ // Call host to create connection
1858
+ __WebSocket_connect(this.#socketId, this.#url, protocolArray);
1859
+ }
1860
+
1861
+ get url() {
1862
+ return this.#url;
1863
+ }
1864
+
1865
+ get readyState() {
1866
+ return this.#readyState;
1867
+ }
1868
+
1869
+ get bufferedAmount() {
1870
+ return this.#bufferedAmount;
1871
+ }
1872
+
1873
+ get extensions() {
1874
+ return this.#extensions;
1875
+ }
1876
+
1877
+ get protocol() {
1878
+ return this.#protocol;
1879
+ }
1880
+
1881
+ get binaryType() {
1882
+ return this.#binaryType;
1883
+ }
1884
+
1885
+ set binaryType(value) {
1886
+ if (value !== 'blob' && value !== 'arraybuffer') {
1887
+ throw new DOMException("Failed to set the 'binaryType' property: '" + value + "' is not a valid value.", 'SyntaxError');
1888
+ }
1889
+ this.#binaryType = value;
1890
+ }
1891
+
1892
+ // ReadyState constants
1893
+ get CONNECTING() { return WebSocket.CONNECTING; }
1894
+ get OPEN() { return WebSocket.OPEN; }
1895
+ get CLOSING() { return WebSocket.CLOSING; }
1896
+ get CLOSED() { return WebSocket.CLOSED; }
1897
+
1898
+ send(data) {
1899
+ if (this.#readyState === WebSocket.CONNECTING) {
1900
+ throw new DOMException("Failed to execute 'send' on 'WebSocket': Still in CONNECTING state.", 'InvalidStateError');
1901
+ }
1902
+
1903
+ if (this.#readyState !== WebSocket.OPEN) {
1904
+ // Silently discard if not open (per spec)
1905
+ return;
1906
+ }
1907
+
1908
+ // Convert data to string for transfer
1909
+ let dataStr;
1910
+ if (typeof data === 'string') {
1911
+ dataStr = data;
1912
+ } else if (data instanceof ArrayBuffer) {
1913
+ // Convert ArrayBuffer to base64 for transfer
1914
+ const bytes = new Uint8Array(data);
1915
+ let binary = '';
1916
+ for (let i = 0; i < bytes.byteLength; i++) {
1917
+ binary += String.fromCharCode(bytes[i]);
1918
+ }
1919
+ dataStr = '__BINARY__' + btoa(binary);
1920
+ } else if (ArrayBuffer.isView(data)) {
1921
+ const bytes = new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
1922
+ let binary = '';
1923
+ for (let i = 0; i < bytes.byteLength; i++) {
1924
+ binary += String.fromCharCode(bytes[i]);
1925
+ }
1926
+ dataStr = '__BINARY__' + btoa(binary);
1927
+ } else if (data instanceof Blob) {
1928
+ // Blob.arrayBuffer() is async, but send() is sync
1929
+ // For now, throw - this is a limitation
1930
+ throw new DOMException("Failed to execute 'send' on 'WebSocket': Blob data is not supported in this environment.", 'NotSupportedError');
1931
+ } else {
1932
+ dataStr = String(data);
1933
+ }
1934
+
1935
+ __WebSocket_send(this.#socketId, dataStr);
1936
+ }
1937
+
1938
+ close(code, reason) {
1939
+ if (code !== undefined) {
1940
+ if (typeof code !== 'number' || code !== Math.floor(code)) {
1941
+ throw new DOMException("Failed to execute 'close' on 'WebSocket': The code must be an integer.", 'InvalidAccessError');
1942
+ }
1943
+ if (code !== 1000 && (code < 3000 || code > 4999)) {
1944
+ throw new DOMException("Failed to execute 'close' on 'WebSocket': The code must be either 1000, or between 3000 and 4999.", 'InvalidAccessError');
1945
+ }
1946
+ }
1947
+
1948
+ if (reason !== undefined) {
1949
+ const encoder = new TextEncoder();
1950
+ if (encoder.encode(reason).byteLength > 123) {
1951
+ throw new DOMException("Failed to execute 'close' on 'WebSocket': The message must not be greater than 123 bytes.", 'SyntaxError');
1952
+ }
1953
+ }
1954
+
1955
+ if (this.#readyState === WebSocket.CLOSING || this.#readyState === WebSocket.CLOSED) {
1956
+ return;
1957
+ }
1958
+
1959
+ this.#readyState = WebSocket.CLOSING;
1960
+ __WebSocket_close(this.#socketId, code ?? 1000, reason ?? '');
1961
+ }
1962
+
1963
+ // EventTarget interface
1964
+ addEventListener(type, listener, options) {
1965
+ if (typeof listener !== 'function') return;
1966
+ let listeners = this._listeners.get(type);
1967
+ if (!listeners) {
1968
+ listeners = [];
1969
+ this._listeners.set(type, listeners);
1970
+ }
1971
+ if (!listeners.includes(listener)) {
1972
+ listeners.push(listener);
1973
+ }
1974
+ }
1975
+
1976
+ removeEventListener(type, listener, options) {
1977
+ const listeners = this._listeners.get(type);
1978
+ if (!listeners) return;
1979
+ const index = listeners.indexOf(listener);
1980
+ if (index !== -1) {
1981
+ listeners.splice(index, 1);
1982
+ }
1983
+ }
1984
+
1985
+ dispatchEvent(event) {
1986
+ dispatchEvent(this, event);
1987
+ return !event.defaultPrevented;
1988
+ }
1989
+
1990
+ // Internal methods called from host
1991
+ _setProtocol(protocol) {
1992
+ this.#protocol = protocol;
1993
+ }
1994
+
1995
+ _setExtensions(extensions) {
1996
+ this.#extensions = extensions;
1997
+ }
1998
+
1999
+ _setReadyState(state) {
2000
+ this.#readyState = state;
2001
+ }
2002
+
2003
+ _dispatchOpen() {
2004
+ this.#readyState = WebSocket.OPEN;
2005
+ const event = new _Event('open');
2006
+ dispatchEvent(this, event);
2007
+ }
2008
+
2009
+ _dispatchMessage(data) {
2010
+ // Handle binary data
2011
+ let messageData = data;
2012
+ if (typeof data === 'string' && data.startsWith('__BINARY__')) {
2013
+ const base64 = data.slice(10);
2014
+ const binary = atob(base64);
2015
+ const bytes = new Uint8Array(binary.length);
2016
+ for (let i = 0; i < binary.length; i++) {
2017
+ bytes[i] = binary.charCodeAt(i);
2018
+ }
2019
+ if (this.#binaryType === 'arraybuffer') {
2020
+ messageData = bytes.buffer;
2021
+ } else {
2022
+ messageData = new Blob([bytes]);
2023
+ }
2024
+ }
2025
+
2026
+ const event = new _MessageEvent('message', { data: messageData });
2027
+ dispatchEvent(this, event);
2028
+ }
2029
+
2030
+ _dispatchError() {
2031
+ const event = new _Event('error');
2032
+ dispatchEvent(this, event);
2033
+ }
2034
+
2035
+ _dispatchClose(code, reason, wasClean) {
2036
+ this.#readyState = WebSocket.CLOSED;
2037
+ const event = new _CloseEvent('close', { code, reason, wasClean });
2038
+ dispatchEvent(this, event);
2039
+ __clientWebSockets.delete(this.#socketId);
2040
+ }
2041
+ }
2042
+
2043
+ // Helper to dispatch events from host to a socket by ID
2044
+ globalThis.__dispatchClientWebSocketEvent = function(socketId, eventType, data) {
2045
+ const ws = __clientWebSockets.get(socketId);
2046
+ if (!ws) return;
2047
+
2048
+ switch (eventType) {
2049
+ case 'open':
2050
+ ws._setProtocol(data.protocol || '');
2051
+ ws._setExtensions(data.extensions || '');
2052
+ ws._dispatchOpen();
2053
+ break;
2054
+ case 'message':
2055
+ ws._dispatchMessage(data.data);
2056
+ break;
2057
+ case 'error':
2058
+ ws._dispatchError();
2059
+ break;
2060
+ case 'close':
2061
+ ws._dispatchClose(data.code, data.reason, data.wasClean);
2062
+ break;
2063
+ }
2064
+ };
2065
+
2066
+ globalThis.WebSocket = WebSocket;
2067
+ })();
2068
+ `);
2069
+ }
1690
2070
  function setupServe(context) {
1691
2071
  context.evalSync(`
1692
2072
  (function() {
@@ -1717,9 +2097,46 @@ async function setupFetch(context, options) {
1717
2097
  activeConnections: new Map
1718
2098
  };
1719
2099
  const wsCommandCallbacks = new Set;
2100
+ const clientWsCommandCallbacks = new Set;
1720
2101
  setupServer(context, serveState);
1721
2102
  setupServerWebSocket(context, wsCommandCallbacks);
1722
2103
  setupServe(context);
2104
+ setupClientWebSocket(context, clientWsCommandCallbacks);
2105
+ const eventCallbacks = new Set;
2106
+ context.global.setSync("__emit", new ivm.Callback((eventName, payloadJson) => {
2107
+ const payload = JSON.parse(payloadJson);
2108
+ for (const cb of eventCallbacks)
2109
+ cb(eventName, payload);
2110
+ }));
2111
+ context.evalSync(`
2112
+ (function() {
2113
+ const __eventListeners = new Map();
2114
+
2115
+ globalThis.__on = function(event, callback) {
2116
+ let listeners = __eventListeners.get(event);
2117
+ if (!listeners) {
2118
+ listeners = new Set();
2119
+ __eventListeners.set(event, listeners);
2120
+ }
2121
+ listeners.add(callback);
2122
+ return function() {
2123
+ listeners.delete(callback);
2124
+ if (listeners.size === 0) {
2125
+ __eventListeners.delete(event);
2126
+ }
2127
+ };
2128
+ };
2129
+
2130
+ globalThis.__dispatchExternalEvent = function(event, payloadJson) {
2131
+ const listeners = __eventListeners.get(event);
2132
+ if (!listeners) return;
2133
+ const payload = JSON.parse(payloadJson);
2134
+ for (const cb of listeners) {
2135
+ try { cb(payload); } catch (e) { console.error('Event listener error:', e); }
2136
+ }
2137
+ };
2138
+ })();
2139
+ `);
1723
2140
  return {
1724
2141
  dispose() {
1725
2142
  stateMap.clear();
@@ -1977,6 +2394,63 @@ async function setupFetch(context, options) {
1977
2394
  },
1978
2395
  hasActiveConnections() {
1979
2396
  return serveState.activeConnections.size > 0;
2397
+ },
2398
+ dispatchClientWebSocketOpen(socketId, protocol, extensions) {
2399
+ const safeProtocol = protocol.replace(/\\/g, "\\\\").replace(/"/g, "\\\"");
2400
+ const safeExtensions = extensions.replace(/\\/g, "\\\\").replace(/"/g, "\\\"");
2401
+ context.evalSync(`
2402
+ __dispatchClientWebSocketEvent("${socketId}", "open", {
2403
+ protocol: "${safeProtocol}",
2404
+ extensions: "${safeExtensions}"
2405
+ });
2406
+ `);
2407
+ },
2408
+ dispatchClientWebSocketMessage(socketId, data) {
2409
+ if (typeof data === "string") {
2410
+ const safeData = data.replace(/\\/g, "\\\\").replace(/"/g, "\\\"").replace(/\n/g, "\\n").replace(/\r/g, "\\r");
2411
+ context.evalSync(`
2412
+ __dispatchClientWebSocketEvent("${socketId}", "message", { data: "${safeData}" });
2413
+ `);
2414
+ } else {
2415
+ const bytes = new Uint8Array(data);
2416
+ let binary = "";
2417
+ for (let i = 0;i < bytes.byteLength; i++) {
2418
+ binary += String.fromCharCode(bytes[i]);
2419
+ }
2420
+ const base64 = Buffer.from(binary, "binary").toString("base64");
2421
+ context.evalSync(`
2422
+ __dispatchClientWebSocketEvent("${socketId}", "message", { data: "__BINARY__${base64}" });
2423
+ `);
2424
+ }
2425
+ },
2426
+ dispatchClientWebSocketClose(socketId, code, reason, wasClean) {
2427
+ const safeReason = reason.replace(/\\/g, "\\\\").replace(/"/g, "\\\"").replace(/\n/g, "\\n");
2428
+ context.evalIgnored(`
2429
+ __dispatchClientWebSocketEvent("${socketId}", "close", {
2430
+ code: ${code},
2431
+ reason: "${safeReason}",
2432
+ wasClean: ${wasClean}
2433
+ });
2434
+ `);
2435
+ },
2436
+ dispatchClientWebSocketError(socketId) {
2437
+ context.evalIgnored(`
2438
+ __dispatchClientWebSocketEvent("${socketId}", "error", {});
2439
+ `);
2440
+ },
2441
+ onClientWebSocketCommand(callback) {
2442
+ clientWsCommandCallbacks.add(callback);
2443
+ return () => clientWsCommandCallbacks.delete(callback);
2444
+ },
2445
+ onEvent(callback) {
2446
+ eventCallbacks.add(callback);
2447
+ return () => eventCallbacks.delete(callback);
2448
+ },
2449
+ dispatchEvent(event, payload) {
2450
+ const json = JSON.stringify(payload);
2451
+ const safeEvent = JSON.stringify(event);
2452
+ const safeJson = JSON.stringify(json);
2453
+ context.evalSync(`__dispatchExternalEvent(${safeEvent}, ${safeJson});`);
1980
2454
  }
1981
2455
  };
1982
2456
  }
@@ -1985,4 +2459,4 @@ export {
1985
2459
  clearAllInstanceState
1986
2460
  };
1987
2461
 
1988
- //# debugId=D6C7C0BA2C0C13D164756E2164756E21
2462
+ //# debugId=98B5825CD3CA576464756E2164756E21