@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/mjs/index.mjs
CHANGED
|
@@ -80,25 +80,32 @@ var headersCode = `
|
|
|
80
80
|
}
|
|
81
81
|
|
|
82
82
|
forEach(callback, thisArg) {
|
|
83
|
-
|
|
84
|
-
|
|
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
|
-
|
|
90
|
-
|
|
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
|
-
|
|
96
|
-
|
|
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
|
-
|
|
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
|
}
|
|
@@ -366,6 +373,9 @@ function setupStreamCallbacks(context, streamRegistry) {
|
|
|
366
373
|
global.setSync("__Stream_isQueueFull", new ivm.Callback((streamId) => {
|
|
367
374
|
return streamRegistry.isQueueFull(streamId);
|
|
368
375
|
}));
|
|
376
|
+
global.setSync("__Stream_cancel", new ivm.Callback((streamId) => {
|
|
377
|
+
streamRegistry.cancel(streamId);
|
|
378
|
+
}));
|
|
369
379
|
const pullRef = new ivm.Reference(async (streamId) => {
|
|
370
380
|
const result = await streamRegistry.pull(streamId);
|
|
371
381
|
if (result.done) {
|
|
@@ -416,7 +426,7 @@ var hostBackedStreamCode = `
|
|
|
416
426
|
async pull(controller) {
|
|
417
427
|
if (closed) return;
|
|
418
428
|
|
|
419
|
-
const resultJson = __Stream_pull_ref.
|
|
429
|
+
const resultJson = await __Stream_pull_ref.apply(undefined, [streamId], { result: { promise: true, copy: true } });
|
|
420
430
|
const result = JSON.parse(resultJson);
|
|
421
431
|
|
|
422
432
|
if (result.done) {
|
|
@@ -428,7 +438,7 @@ var hostBackedStreamCode = `
|
|
|
428
438
|
},
|
|
429
439
|
cancel(reason) {
|
|
430
440
|
closed = true;
|
|
431
|
-
|
|
441
|
+
__Stream_cancel(streamId);
|
|
432
442
|
}
|
|
433
443
|
});
|
|
434
444
|
|
|
@@ -455,7 +465,7 @@ var hostBackedStreamCode = `
|
|
|
455
465
|
globalThis.HostBackedReadableStream = HostBackedReadableStream;
|
|
456
466
|
})();
|
|
457
467
|
`;
|
|
458
|
-
function setupResponse(context, stateMap) {
|
|
468
|
+
function setupResponse(context, stateMap, streamRegistry) {
|
|
459
469
|
const global = context.global;
|
|
460
470
|
global.setSync("__Response_construct", new ivm.Callback((bodyBytes, status, statusText, headers) => {
|
|
461
471
|
const instanceId = nextInstanceId++;
|
|
@@ -535,6 +545,10 @@ function setupResponse(context, stateMap) {
|
|
|
535
545
|
const state = stateMap.get(instanceId);
|
|
536
546
|
return state?.type ?? "default";
|
|
537
547
|
}));
|
|
548
|
+
global.setSync("__Response_get_nullBody", new ivm.Callback((instanceId) => {
|
|
549
|
+
const state = stateMap.get(instanceId);
|
|
550
|
+
return state?.nullBody ?? false;
|
|
551
|
+
}));
|
|
538
552
|
global.setSync("__Response_setType", new ivm.Callback((instanceId, type) => {
|
|
539
553
|
const state = stateMap.get(instanceId);
|
|
540
554
|
if (state) {
|
|
@@ -568,6 +582,38 @@ function setupResponse(context, stateMap) {
|
|
|
568
582
|
if (!state) {
|
|
569
583
|
throw new Error("[TypeError]Cannot clone invalid Response");
|
|
570
584
|
}
|
|
585
|
+
if (state.streamId !== null) {
|
|
586
|
+
const streamId1 = streamRegistry.create();
|
|
587
|
+
const streamId2 = streamRegistry.create();
|
|
588
|
+
const origStreamId = state.streamId;
|
|
589
|
+
(async () => {
|
|
590
|
+
try {
|
|
591
|
+
while (true) {
|
|
592
|
+
const result = await streamRegistry.pull(origStreamId);
|
|
593
|
+
if (result.done) {
|
|
594
|
+
streamRegistry.close(streamId1);
|
|
595
|
+
streamRegistry.close(streamId2);
|
|
596
|
+
break;
|
|
597
|
+
}
|
|
598
|
+
streamRegistry.push(streamId1, new Uint8Array(result.value));
|
|
599
|
+
streamRegistry.push(streamId2, new Uint8Array(result.value));
|
|
600
|
+
}
|
|
601
|
+
} catch (err) {
|
|
602
|
+
streamRegistry.error(streamId1, err);
|
|
603
|
+
streamRegistry.error(streamId2, err);
|
|
604
|
+
}
|
|
605
|
+
})();
|
|
606
|
+
state.streamId = streamId1;
|
|
607
|
+
const newId2 = nextInstanceId++;
|
|
608
|
+
const newState2 = {
|
|
609
|
+
...state,
|
|
610
|
+
streamId: streamId2,
|
|
611
|
+
body: state.body ? new Uint8Array(state.body) : null,
|
|
612
|
+
bodyUsed: false
|
|
613
|
+
};
|
|
614
|
+
stateMap.set(newId2, newState2);
|
|
615
|
+
return newId2;
|
|
616
|
+
}
|
|
571
617
|
const newId = nextInstanceId++;
|
|
572
618
|
const newState = {
|
|
573
619
|
...state,
|
|
@@ -794,6 +840,11 @@ function setupResponse(context, stateMap) {
|
|
|
794
840
|
}
|
|
795
841
|
|
|
796
842
|
get body() {
|
|
843
|
+
// Null-body responses (204, 304, HEAD) must return null
|
|
844
|
+
if (__Response_get_nullBody(this.#instanceId)) {
|
|
845
|
+
return null;
|
|
846
|
+
}
|
|
847
|
+
|
|
797
848
|
// Return cached body if available (WHATWG spec requires same object on repeated access)
|
|
798
849
|
if (this.#cachedBody !== null) {
|
|
799
850
|
return this.#cachedBody;
|
|
@@ -825,6 +876,26 @@ function setupResponse(context, stateMap) {
|
|
|
825
876
|
} catch (err) {
|
|
826
877
|
throw __decodeError(err);
|
|
827
878
|
}
|
|
879
|
+
if (__Response_get_nullBody(this.#instanceId)) {
|
|
880
|
+
return "";
|
|
881
|
+
}
|
|
882
|
+
if (this.#streamId !== null) {
|
|
883
|
+
const reader = this.body.getReader();
|
|
884
|
+
const chunks = [];
|
|
885
|
+
while (true) {
|
|
886
|
+
const { done, value } = await reader.read();
|
|
887
|
+
if (done) break;
|
|
888
|
+
if (value) chunks.push(value);
|
|
889
|
+
}
|
|
890
|
+
const totalLength = chunks.reduce((acc, c) => acc + c.length, 0);
|
|
891
|
+
const result = new Uint8Array(totalLength);
|
|
892
|
+
let offset = 0;
|
|
893
|
+
for (const chunk of chunks) {
|
|
894
|
+
result.set(chunk, offset);
|
|
895
|
+
offset += chunk.length;
|
|
896
|
+
}
|
|
897
|
+
return new TextDecoder().decode(result);
|
|
898
|
+
}
|
|
828
899
|
return __Response_text(this.#instanceId);
|
|
829
900
|
}
|
|
830
901
|
|
|
@@ -1075,30 +1146,10 @@ function setupRequest(context, stateMap) {
|
|
|
1075
1146
|
return Array.from(new TextEncoder().encode(body.toString()));
|
|
1076
1147
|
}
|
|
1077
1148
|
if (body instanceof FormData) {
|
|
1078
|
-
//
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
hasFiles = true;
|
|
1083
|
-
break;
|
|
1084
|
-
}
|
|
1085
|
-
}
|
|
1086
|
-
|
|
1087
|
-
if (hasFiles) {
|
|
1088
|
-
// Serialize as multipart/form-data
|
|
1089
|
-
const { body: bytes, contentType } = __serializeFormData(body);
|
|
1090
|
-
globalThis.__pendingFormDataContentType = contentType;
|
|
1091
|
-
return Array.from(bytes);
|
|
1092
|
-
}
|
|
1093
|
-
|
|
1094
|
-
// URL-encoded for string-only FormData
|
|
1095
|
-
const parts = [];
|
|
1096
|
-
body.forEach((value, key) => {
|
|
1097
|
-
if (typeof value === 'string') {
|
|
1098
|
-
parts.push(encodeURIComponent(key) + '=' + encodeURIComponent(value));
|
|
1099
|
-
}
|
|
1100
|
-
});
|
|
1101
|
-
return Array.from(new TextEncoder().encode(parts.join('&')));
|
|
1149
|
+
// Always serialize as multipart/form-data per spec
|
|
1150
|
+
const { body: bytes, contentType } = __serializeFormData(body);
|
|
1151
|
+
globalThis.__pendingFormDataContentType = contentType;
|
|
1152
|
+
return Array.from(bytes);
|
|
1102
1153
|
}
|
|
1103
1154
|
// Try to convert to string
|
|
1104
1155
|
return Array.from(new TextEncoder().encode(String(body)));
|
|
@@ -1196,7 +1247,7 @@ function setupRequest(context, stateMap) {
|
|
|
1196
1247
|
if (globalThis.__pendingFormDataContentType) {
|
|
1197
1248
|
headers.set('content-type', globalThis.__pendingFormDataContentType);
|
|
1198
1249
|
delete globalThis.__pendingFormDataContentType;
|
|
1199
|
-
} else if (body instanceof
|
|
1250
|
+
} else if (body instanceof URLSearchParams && !headers.has('content-type')) {
|
|
1200
1251
|
headers.set('content-type', 'application/x-www-form-urlencoded');
|
|
1201
1252
|
}
|
|
1202
1253
|
|
|
@@ -1386,32 +1437,62 @@ function setupRequest(context, stateMap) {
|
|
|
1386
1437
|
var FETCH_STREAM_THRESHOLD = 64 * 1024;
|
|
1387
1438
|
function setupFetchFunction(context, stateMap, streamRegistry, options) {
|
|
1388
1439
|
const global = context.global;
|
|
1389
|
-
const
|
|
1440
|
+
const fetchAbortControllers = new Map;
|
|
1441
|
+
global.setSync("__fetch_abort", new ivm.Callback((fetchId) => {
|
|
1442
|
+
const controller = fetchAbortControllers.get(fetchId);
|
|
1443
|
+
if (controller) {
|
|
1444
|
+
setImmediate(() => controller.abort());
|
|
1445
|
+
}
|
|
1446
|
+
}));
|
|
1447
|
+
const fetchRef = new ivm.Reference(async (url, method, headersJson, bodyJson, signalAborted, fetchId) => {
|
|
1390
1448
|
if (signalAborted) {
|
|
1391
1449
|
throw new Error("[AbortError]The operation was aborted.");
|
|
1392
1450
|
}
|
|
1451
|
+
const hostController = new AbortController;
|
|
1452
|
+
fetchAbortControllers.set(fetchId, hostController);
|
|
1393
1453
|
const headers = JSON.parse(headersJson);
|
|
1394
1454
|
const bodyBytes = bodyJson ? JSON.parse(bodyJson) : null;
|
|
1395
|
-
const
|
|
1396
|
-
const
|
|
1455
|
+
const rawBody = bodyBytes ? new Uint8Array(bodyBytes) : null;
|
|
1456
|
+
const init = {
|
|
1397
1457
|
method,
|
|
1398
1458
|
headers,
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
const
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1459
|
+
rawBody,
|
|
1460
|
+
body: rawBody,
|
|
1461
|
+
signal: hostController.signal
|
|
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
|
+
}));
|
|
1469
|
+
try {
|
|
1470
|
+
let cleanupAbort;
|
|
1471
|
+
const abortPromise = new Promise((_, reject) => {
|
|
1472
|
+
if (hostController.signal.aborted) {
|
|
1473
|
+
reject(Object.assign(new Error("The operation was aborted."), { name: "AbortError" }));
|
|
1474
|
+
return;
|
|
1475
|
+
}
|
|
1476
|
+
const onAbort = () => {
|
|
1477
|
+
reject(Object.assign(new Error("The operation was aborted."), { name: "AbortError" }));
|
|
1478
|
+
};
|
|
1479
|
+
hostController.signal.addEventListener("abort", onAbort, { once: true });
|
|
1480
|
+
cleanupAbort = () => hostController.signal.removeEventListener("abort", onAbort);
|
|
1481
|
+
});
|
|
1482
|
+
abortPromise.catch(() => {});
|
|
1483
|
+
const nativeResponse = await Promise.race([onFetch(url, init), abortPromise]);
|
|
1484
|
+
cleanupAbort?.();
|
|
1485
|
+
const status = nativeResponse.status;
|
|
1486
|
+
const isNullBody = status === 204 || status === 304 || method.toUpperCase() === "HEAD";
|
|
1487
|
+
const isCallbackStream = nativeResponse.__isCallbackStream;
|
|
1488
|
+
const isNetworkResponse = nativeResponse.url && (nativeResponse.url.startsWith("http://") || nativeResponse.url.startsWith("https://"));
|
|
1489
|
+
const shouldStream = !isNullBody && nativeResponse.body && (isCallbackStream || isNetworkResponse);
|
|
1490
|
+
if (shouldStream && nativeResponse.body) {
|
|
1491
|
+
const streamId = streamRegistry.create();
|
|
1492
|
+
const streamCleanupFn = startNativeStreamReader(nativeResponse.body, streamId, streamRegistry);
|
|
1493
|
+
streamRegistry.setCleanup(streamId, streamCleanupFn);
|
|
1494
|
+
const instanceId2 = nextInstanceId++;
|
|
1495
|
+
const state2 = {
|
|
1415
1496
|
status: nativeResponse.status,
|
|
1416
1497
|
statusText: nativeResponse.statusText,
|
|
1417
1498
|
headers: Array.from(nativeResponse.headers.entries()),
|
|
@@ -1420,65 +1501,37 @@ function setupFetchFunction(context, stateMap, streamRegistry, options) {
|
|
|
1420
1501
|
type: "default",
|
|
1421
1502
|
url: nativeResponse.url,
|
|
1422
1503
|
redirected: nativeResponse.redirected,
|
|
1423
|
-
streamId
|
|
1504
|
+
streamId,
|
|
1505
|
+
nullBody: isNullBody
|
|
1424
1506
|
};
|
|
1425
|
-
stateMap.set(
|
|
1426
|
-
return
|
|
1507
|
+
stateMap.set(instanceId2, state2);
|
|
1508
|
+
return instanceId2;
|
|
1427
1509
|
}
|
|
1428
|
-
const
|
|
1429
|
-
const
|
|
1430
|
-
const
|
|
1510
|
+
const responseBody = await nativeResponse.arrayBuffer();
|
|
1511
|
+
const responseBodyArray = Array.from(new Uint8Array(responseBody));
|
|
1512
|
+
const instanceId = nextInstanceId++;
|
|
1513
|
+
const state = {
|
|
1431
1514
|
status: nativeResponse.status,
|
|
1432
1515
|
statusText: nativeResponse.statusText,
|
|
1433
1516
|
headers: Array.from(nativeResponse.headers.entries()),
|
|
1434
|
-
body: new Uint8Array(
|
|
1517
|
+
body: new Uint8Array(responseBodyArray),
|
|
1435
1518
|
bodyUsed: false,
|
|
1436
1519
|
type: "default",
|
|
1437
1520
|
url: nativeResponse.url,
|
|
1438
1521
|
redirected: nativeResponse.redirected,
|
|
1439
|
-
streamId
|
|
1522
|
+
streamId: null,
|
|
1523
|
+
nullBody: isNullBody
|
|
1440
1524
|
};
|
|
1441
|
-
stateMap.set(
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
}
|
|
1451
|
-
if (value) {
|
|
1452
|
-
while (streamRegistry.isQueueFull(streamId)) {
|
|
1453
|
-
await new Promise((r) => setTimeout(r, 1));
|
|
1454
|
-
}
|
|
1455
|
-
streamRegistry.push(streamId, value);
|
|
1456
|
-
}
|
|
1457
|
-
}
|
|
1458
|
-
} catch (err) {
|
|
1459
|
-
streamRegistry.error(streamId, err);
|
|
1460
|
-
} finally {
|
|
1461
|
-
reader.releaseLock();
|
|
1462
|
-
}
|
|
1463
|
-
})();
|
|
1464
|
-
return instanceId2;
|
|
1525
|
+
stateMap.set(instanceId, state);
|
|
1526
|
+
return instanceId;
|
|
1527
|
+
} catch (err) {
|
|
1528
|
+
if (err instanceof Error && err.name === "AbortError") {
|
|
1529
|
+
throw new Error("[AbortError]The operation was aborted.");
|
|
1530
|
+
}
|
|
1531
|
+
throw err;
|
|
1532
|
+
} finally {
|
|
1533
|
+
fetchAbortControllers.delete(fetchId);
|
|
1465
1534
|
}
|
|
1466
|
-
const responseBody = await nativeResponse.arrayBuffer();
|
|
1467
|
-
const responseBodyArray = Array.from(new Uint8Array(responseBody));
|
|
1468
|
-
const instanceId = nextInstanceId++;
|
|
1469
|
-
const state = {
|
|
1470
|
-
status: nativeResponse.status,
|
|
1471
|
-
statusText: nativeResponse.statusText,
|
|
1472
|
-
headers: Array.from(nativeResponse.headers.entries()),
|
|
1473
|
-
body: new Uint8Array(responseBodyArray),
|
|
1474
|
-
bodyUsed: false,
|
|
1475
|
-
type: "default",
|
|
1476
|
-
url: nativeResponse.url,
|
|
1477
|
-
redirected: nativeResponse.redirected,
|
|
1478
|
-
streamId: null
|
|
1479
|
-
};
|
|
1480
|
-
stateMap.set(instanceId, state);
|
|
1481
|
-
return instanceId;
|
|
1482
1535
|
});
|
|
1483
1536
|
global.setSync("__fetch_ref", fetchRef);
|
|
1484
1537
|
const fetchCode = `
|
|
@@ -1496,7 +1549,28 @@ function setupFetchFunction(context, stateMap, streamRegistry, options) {
|
|
|
1496
1549
|
return err;
|
|
1497
1550
|
}
|
|
1498
1551
|
|
|
1499
|
-
|
|
1552
|
+
let __nextFetchId = 1;
|
|
1553
|
+
|
|
1554
|
+
globalThis.fetch = async function(input, init = {}) {
|
|
1555
|
+
// Handle Blob and ReadableStream bodies before creating Request
|
|
1556
|
+
if (init.body instanceof Blob && !(init.body instanceof File)) {
|
|
1557
|
+
const buf = await init.body.arrayBuffer();
|
|
1558
|
+
init = Object.assign({}, init, { body: new Uint8Array(buf) });
|
|
1559
|
+
} else if (init.body instanceof ReadableStream) {
|
|
1560
|
+
const reader = init.body.getReader();
|
|
1561
|
+
const chunks = [];
|
|
1562
|
+
while (true) {
|
|
1563
|
+
const { done, value } = await reader.read();
|
|
1564
|
+
if (done) break;
|
|
1565
|
+
chunks.push(value);
|
|
1566
|
+
}
|
|
1567
|
+
const total = chunks.reduce((s, c) => s + c.length, 0);
|
|
1568
|
+
const buf = new Uint8Array(total);
|
|
1569
|
+
let off = 0;
|
|
1570
|
+
for (const c of chunks) { buf.set(c, off); off += c.length; }
|
|
1571
|
+
init = Object.assign({}, init, { body: buf });
|
|
1572
|
+
}
|
|
1573
|
+
|
|
1500
1574
|
// Create Request from input
|
|
1501
1575
|
const request = input instanceof Request ? input : new Request(input, init);
|
|
1502
1576
|
|
|
@@ -1504,20 +1578,34 @@ function setupFetchFunction(context, stateMap, streamRegistry, options) {
|
|
|
1504
1578
|
const signal = init.signal ?? request.signal;
|
|
1505
1579
|
const signalAborted = signal?.aborted ?? false;
|
|
1506
1580
|
|
|
1581
|
+
// Assign a fetch ID for abort tracking
|
|
1582
|
+
const fetchId = __nextFetchId++;
|
|
1583
|
+
|
|
1584
|
+
// Register abort listener if signal exists
|
|
1585
|
+
if (signal && !signalAborted) {
|
|
1586
|
+
signal.addEventListener('abort', () => { __fetch_abort(fetchId); });
|
|
1587
|
+
}
|
|
1588
|
+
|
|
1507
1589
|
// Serialize headers and body to JSON for transfer
|
|
1508
1590
|
const headersJson = JSON.stringify(Array.from(request.headers.entries()));
|
|
1509
1591
|
const bodyBytes = request._getBodyBytes();
|
|
1510
1592
|
const bodyJson = bodyBytes ? JSON.stringify(bodyBytes) : null;
|
|
1511
1593
|
|
|
1594
|
+
// Short-circuit: if signal is already aborted, throw without calling host
|
|
1595
|
+
if (signalAborted) {
|
|
1596
|
+
throw new DOMException('The operation was aborted.', 'AbortError');
|
|
1597
|
+
}
|
|
1598
|
+
|
|
1512
1599
|
// Call host - returns just the response instance ID
|
|
1513
1600
|
try {
|
|
1514
|
-
const instanceId = __fetch_ref.
|
|
1601
|
+
const instanceId = await __fetch_ref.apply(undefined, [
|
|
1515
1602
|
request.url,
|
|
1516
1603
|
request.method,
|
|
1517
1604
|
headersJson,
|
|
1518
1605
|
bodyJson,
|
|
1519
|
-
signalAborted
|
|
1520
|
-
|
|
1606
|
+
signalAborted,
|
|
1607
|
+
fetchId
|
|
1608
|
+
], { result: { promise: true, copy: true } });
|
|
1521
1609
|
|
|
1522
1610
|
// Construct Response from the instance ID
|
|
1523
1611
|
return Response._fromInstanceId(instanceId);
|
|
@@ -1612,6 +1700,373 @@ function setupServerWebSocket(context, wsCommandCallbacks) {
|
|
|
1612
1700
|
})();
|
|
1613
1701
|
`);
|
|
1614
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
|
+
}
|
|
1615
2070
|
function setupServe(context) {
|
|
1616
2071
|
context.evalSync(`
|
|
1617
2072
|
(function() {
|
|
@@ -1634,7 +2089,7 @@ async function setupFetch(context, options) {
|
|
|
1634
2089
|
context.evalSync(multipartCode);
|
|
1635
2090
|
setupStreamCallbacks(context, streamRegistry);
|
|
1636
2091
|
context.evalSync(hostBackedStreamCode);
|
|
1637
|
-
setupResponse(context, stateMap);
|
|
2092
|
+
setupResponse(context, stateMap, streamRegistry);
|
|
1638
2093
|
setupRequest(context, stateMap);
|
|
1639
2094
|
setupFetchFunction(context, stateMap, streamRegistry, options);
|
|
1640
2095
|
const serveState = {
|
|
@@ -1642,9 +2097,11 @@ async function setupFetch(context, options) {
|
|
|
1642
2097
|
activeConnections: new Map
|
|
1643
2098
|
};
|
|
1644
2099
|
const wsCommandCallbacks = new Set;
|
|
2100
|
+
const clientWsCommandCallbacks = new Set;
|
|
1645
2101
|
setupServer(context, serveState);
|
|
1646
2102
|
setupServerWebSocket(context, wsCommandCallbacks);
|
|
1647
2103
|
setupServe(context);
|
|
2104
|
+
setupClientWebSocket(context, clientWsCommandCallbacks);
|
|
1648
2105
|
return {
|
|
1649
2106
|
dispose() {
|
|
1650
2107
|
stateMap.clear();
|
|
@@ -1757,8 +2214,7 @@ async function setupFetch(context, options) {
|
|
|
1757
2214
|
},
|
|
1758
2215
|
cancel() {
|
|
1759
2216
|
streamDone = true;
|
|
1760
|
-
streamRegistry.
|
|
1761
|
-
streamRegistry.delete(responseStreamId);
|
|
2217
|
+
streamRegistry.cancel(responseStreamId);
|
|
1762
2218
|
}
|
|
1763
2219
|
});
|
|
1764
2220
|
const responseHeaders2 = new Headers(responseState.headers);
|
|
@@ -1903,6 +2359,53 @@ async function setupFetch(context, options) {
|
|
|
1903
2359
|
},
|
|
1904
2360
|
hasActiveConnections() {
|
|
1905
2361
|
return serveState.activeConnections.size > 0;
|
|
2362
|
+
},
|
|
2363
|
+
dispatchClientWebSocketOpen(socketId, protocol, extensions) {
|
|
2364
|
+
const safeProtocol = protocol.replace(/\\/g, "\\\\").replace(/"/g, "\\\"");
|
|
2365
|
+
const safeExtensions = extensions.replace(/\\/g, "\\\\").replace(/"/g, "\\\"");
|
|
2366
|
+
context.evalSync(`
|
|
2367
|
+
__dispatchClientWebSocketEvent("${socketId}", "open", {
|
|
2368
|
+
protocol: "${safeProtocol}",
|
|
2369
|
+
extensions: "${safeExtensions}"
|
|
2370
|
+
});
|
|
2371
|
+
`);
|
|
2372
|
+
},
|
|
2373
|
+
dispatchClientWebSocketMessage(socketId, data) {
|
|
2374
|
+
if (typeof data === "string") {
|
|
2375
|
+
const safeData = data.replace(/\\/g, "\\\\").replace(/"/g, "\\\"").replace(/\n/g, "\\n").replace(/\r/g, "\\r");
|
|
2376
|
+
context.evalSync(`
|
|
2377
|
+
__dispatchClientWebSocketEvent("${socketId}", "message", { data: "${safeData}" });
|
|
2378
|
+
`);
|
|
2379
|
+
} else {
|
|
2380
|
+
const bytes = new Uint8Array(data);
|
|
2381
|
+
let binary = "";
|
|
2382
|
+
for (let i = 0;i < bytes.byteLength; i++) {
|
|
2383
|
+
binary += String.fromCharCode(bytes[i]);
|
|
2384
|
+
}
|
|
2385
|
+
const base64 = Buffer.from(binary, "binary").toString("base64");
|
|
2386
|
+
context.evalSync(`
|
|
2387
|
+
__dispatchClientWebSocketEvent("${socketId}", "message", { data: "__BINARY__${base64}" });
|
|
2388
|
+
`);
|
|
2389
|
+
}
|
|
2390
|
+
},
|
|
2391
|
+
dispatchClientWebSocketClose(socketId, code, reason, wasClean) {
|
|
2392
|
+
const safeReason = reason.replace(/\\/g, "\\\\").replace(/"/g, "\\\"").replace(/\n/g, "\\n");
|
|
2393
|
+
context.evalIgnored(`
|
|
2394
|
+
__dispatchClientWebSocketEvent("${socketId}", "close", {
|
|
2395
|
+
code: ${code},
|
|
2396
|
+
reason: "${safeReason}",
|
|
2397
|
+
wasClean: ${wasClean}
|
|
2398
|
+
});
|
|
2399
|
+
`);
|
|
2400
|
+
},
|
|
2401
|
+
dispatchClientWebSocketError(socketId) {
|
|
2402
|
+
context.evalIgnored(`
|
|
2403
|
+
__dispatchClientWebSocketEvent("${socketId}", "error", {});
|
|
2404
|
+
`);
|
|
2405
|
+
},
|
|
2406
|
+
onClientWebSocketCommand(callback) {
|
|
2407
|
+
clientWsCommandCallbacks.add(callback);
|
|
2408
|
+
return () => clientWsCommandCallbacks.delete(callback);
|
|
1906
2409
|
}
|
|
1907
2410
|
};
|
|
1908
2411
|
}
|
|
@@ -1911,4 +2414,4 @@ export {
|
|
|
1911
2414
|
clearAllInstanceState
|
|
1912
2415
|
};
|
|
1913
2416
|
|
|
1914
|
-
//# debugId=
|
|
2417
|
+
//# debugId=9CCD39D3AEDB908864756E2164756E21
|