@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.
- package/README.md +131 -4
- package/dist/cjs/consistency/origins.cjs +2 -2
- package/dist/cjs/consistency/origins.cjs.map +3 -3
- package/dist/cjs/index.cjs +613 -110
- package/dist/cjs/index.cjs.map +3 -3
- package/dist/cjs/package.json +1 -1
- package/dist/cjs/stream-state.cjs +14 -1
- package/dist/cjs/stream-state.cjs.map +3 -3
- package/dist/mjs/consistency/origins.mjs +2 -2
- package/dist/mjs/consistency/origins.mjs.map +3 -3
- package/dist/mjs/index.mjs +613 -110
- package/dist/mjs/index.mjs.map +3 -3
- package/dist/mjs/package.json +1 -1
- package/dist/mjs/stream-state.mjs +14 -1
- package/dist/mjs/stream-state.mjs.map +3 -3
- package/dist/types/index.d.ts +29 -3
- package/dist/types/isolate.d.ts +233 -0
- package/dist/types/stream-state.d.ts +4 -0
- package/package.json +4 -3
package/dist/cjs/index.cjs
CHANGED
|
@@ -124,25 +124,32 @@ var headersCode = `
|
|
|
124
124
|
}
|
|
125
125
|
|
|
126
126
|
forEach(callback, thisArg) {
|
|
127
|
-
|
|
128
|
-
|
|
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
|
-
|
|
134
|
-
|
|
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
|
-
|
|
140
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
//
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
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
|
|
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
|
|
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
|
|
1440
|
-
const
|
|
1499
|
+
const rawBody = bodyBytes ? new Uint8Array(bodyBytes) : null;
|
|
1500
|
+
const init = {
|
|
1441
1501
|
method,
|
|
1442
1502
|
headers,
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
const
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
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
|
|
1548
|
+
streamId,
|
|
1549
|
+
nullBody: isNullBody
|
|
1468
1550
|
};
|
|
1469
|
-
stateMap.set(
|
|
1470
|
-
return
|
|
1551
|
+
stateMap.set(instanceId2, state2);
|
|
1552
|
+
return instanceId2;
|
|
1471
1553
|
}
|
|
1472
|
-
const
|
|
1473
|
-
const
|
|
1474
|
-
const
|
|
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(
|
|
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(
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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=
|
|
2457
|
+
//# debugId=7837D23CACB6E1A364756E2164756E21
|