@ricsam/isolate-fetch 0.1.13 → 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
  }
@@ -1489,14 +1496,20 @@ function setupFetchFunction(context, stateMap, streamRegistry, options) {
1489
1496
  fetchAbortControllers.set(fetchId, hostController);
1490
1497
  const headers = JSON.parse(headersJson);
1491
1498
  const bodyBytes = bodyJson ? JSON.parse(bodyJson) : null;
1492
- const body = bodyBytes ? new Uint8Array(bodyBytes) : null;
1493
- const nativeRequest = new Request(url, {
1499
+ const rawBody = bodyBytes ? new Uint8Array(bodyBytes) : null;
1500
+ const init = {
1494
1501
  method,
1495
1502
  headers,
1496
- body,
1503
+ rawBody,
1504
+ body: rawBody,
1497
1505
  signal: hostController.signal
1498
- });
1499
- const onFetch = options?.onFetch ?? fetch;
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
+ }));
1500
1513
  try {
1501
1514
  let cleanupAbort;
1502
1515
  const abortPromise = new Promise((_, reject) => {
@@ -1511,7 +1524,7 @@ function setupFetchFunction(context, stateMap, streamRegistry, options) {
1511
1524
  cleanupAbort = () => hostController.signal.removeEventListener("abort", onAbort);
1512
1525
  });
1513
1526
  abortPromise.catch(() => {});
1514
- const nativeResponse = await Promise.race([onFetch(nativeRequest), abortPromise]);
1527
+ const nativeResponse = await Promise.race([onFetch(url, init), abortPromise]);
1515
1528
  cleanupAbort?.();
1516
1529
  const status = nativeResponse.status;
1517
1530
  const isNullBody = status === 204 || status === 304 || method.toUpperCase() === "HEAD";
@@ -1731,6 +1744,373 @@ function setupServerWebSocket(context, wsCommandCallbacks) {
1731
1744
  })();
1732
1745
  `);
1733
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
+ }
1734
2114
  function setupServe(context) {
1735
2115
  context.evalSync(`
1736
2116
  (function() {
@@ -1761,9 +2141,11 @@ async function setupFetch(context, options) {
1761
2141
  activeConnections: new Map
1762
2142
  };
1763
2143
  const wsCommandCallbacks = new Set;
2144
+ const clientWsCommandCallbacks = new Set;
1764
2145
  setupServer(context, serveState);
1765
2146
  setupServerWebSocket(context, wsCommandCallbacks);
1766
2147
  setupServe(context);
2148
+ setupClientWebSocket(context, clientWsCommandCallbacks);
1767
2149
  return {
1768
2150
  dispose() {
1769
2151
  stateMap.clear();
@@ -2021,8 +2403,55 @@ async function setupFetch(context, options) {
2021
2403
  },
2022
2404
  hasActiveConnections() {
2023
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);
2024
2453
  }
2025
2454
  };
2026
2455
  }
2027
2456
 
2028
- //# debugId=E8993FE5C4E862A664756E2164756E21
2457
+ //# debugId=7837D23CACB6E1A364756E2164756E21