@ricsam/isolate-fetch 0.1.12 → 0.1.13
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/dist/cjs/index.cjs +173 -99
- 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/index.mjs +173 -99
- 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/stream-state.d.ts +4 -0
- package/package.json +4 -3
package/dist/cjs/index.cjs
CHANGED
|
@@ -410,6 +410,9 @@ function setupStreamCallbacks(context, streamRegistry) {
|
|
|
410
410
|
global.setSync("__Stream_isQueueFull", new import_isolated_vm.default.Callback((streamId) => {
|
|
411
411
|
return streamRegistry.isQueueFull(streamId);
|
|
412
412
|
}));
|
|
413
|
+
global.setSync("__Stream_cancel", new import_isolated_vm.default.Callback((streamId) => {
|
|
414
|
+
streamRegistry.cancel(streamId);
|
|
415
|
+
}));
|
|
413
416
|
const pullRef = new import_isolated_vm.default.Reference(async (streamId) => {
|
|
414
417
|
const result = await streamRegistry.pull(streamId);
|
|
415
418
|
if (result.done) {
|
|
@@ -460,7 +463,7 @@ var hostBackedStreamCode = `
|
|
|
460
463
|
async pull(controller) {
|
|
461
464
|
if (closed) return;
|
|
462
465
|
|
|
463
|
-
const resultJson = __Stream_pull_ref.
|
|
466
|
+
const resultJson = await __Stream_pull_ref.apply(undefined, [streamId], { result: { promise: true, copy: true } });
|
|
464
467
|
const result = JSON.parse(resultJson);
|
|
465
468
|
|
|
466
469
|
if (result.done) {
|
|
@@ -472,7 +475,7 @@ var hostBackedStreamCode = `
|
|
|
472
475
|
},
|
|
473
476
|
cancel(reason) {
|
|
474
477
|
closed = true;
|
|
475
|
-
|
|
478
|
+
__Stream_cancel(streamId);
|
|
476
479
|
}
|
|
477
480
|
});
|
|
478
481
|
|
|
@@ -499,7 +502,7 @@ var hostBackedStreamCode = `
|
|
|
499
502
|
globalThis.HostBackedReadableStream = HostBackedReadableStream;
|
|
500
503
|
})();
|
|
501
504
|
`;
|
|
502
|
-
function setupResponse(context, stateMap) {
|
|
505
|
+
function setupResponse(context, stateMap, streamRegistry) {
|
|
503
506
|
const global = context.global;
|
|
504
507
|
global.setSync("__Response_construct", new import_isolated_vm.default.Callback((bodyBytes, status, statusText, headers) => {
|
|
505
508
|
const instanceId = nextInstanceId++;
|
|
@@ -579,6 +582,10 @@ function setupResponse(context, stateMap) {
|
|
|
579
582
|
const state = stateMap.get(instanceId);
|
|
580
583
|
return state?.type ?? "default";
|
|
581
584
|
}));
|
|
585
|
+
global.setSync("__Response_get_nullBody", new import_isolated_vm.default.Callback((instanceId) => {
|
|
586
|
+
const state = stateMap.get(instanceId);
|
|
587
|
+
return state?.nullBody ?? false;
|
|
588
|
+
}));
|
|
582
589
|
global.setSync("__Response_setType", new import_isolated_vm.default.Callback((instanceId, type) => {
|
|
583
590
|
const state = stateMap.get(instanceId);
|
|
584
591
|
if (state) {
|
|
@@ -612,6 +619,38 @@ function setupResponse(context, stateMap) {
|
|
|
612
619
|
if (!state) {
|
|
613
620
|
throw new Error("[TypeError]Cannot clone invalid Response");
|
|
614
621
|
}
|
|
622
|
+
if (state.streamId !== null) {
|
|
623
|
+
const streamId1 = streamRegistry.create();
|
|
624
|
+
const streamId2 = streamRegistry.create();
|
|
625
|
+
const origStreamId = state.streamId;
|
|
626
|
+
(async () => {
|
|
627
|
+
try {
|
|
628
|
+
while (true) {
|
|
629
|
+
const result = await streamRegistry.pull(origStreamId);
|
|
630
|
+
if (result.done) {
|
|
631
|
+
streamRegistry.close(streamId1);
|
|
632
|
+
streamRegistry.close(streamId2);
|
|
633
|
+
break;
|
|
634
|
+
}
|
|
635
|
+
streamRegistry.push(streamId1, new Uint8Array(result.value));
|
|
636
|
+
streamRegistry.push(streamId2, new Uint8Array(result.value));
|
|
637
|
+
}
|
|
638
|
+
} catch (err) {
|
|
639
|
+
streamRegistry.error(streamId1, err);
|
|
640
|
+
streamRegistry.error(streamId2, err);
|
|
641
|
+
}
|
|
642
|
+
})();
|
|
643
|
+
state.streamId = streamId1;
|
|
644
|
+
const newId2 = nextInstanceId++;
|
|
645
|
+
const newState2 = {
|
|
646
|
+
...state,
|
|
647
|
+
streamId: streamId2,
|
|
648
|
+
body: state.body ? new Uint8Array(state.body) : null,
|
|
649
|
+
bodyUsed: false
|
|
650
|
+
};
|
|
651
|
+
stateMap.set(newId2, newState2);
|
|
652
|
+
return newId2;
|
|
653
|
+
}
|
|
615
654
|
const newId = nextInstanceId++;
|
|
616
655
|
const newState = {
|
|
617
656
|
...state,
|
|
@@ -838,6 +877,11 @@ function setupResponse(context, stateMap) {
|
|
|
838
877
|
}
|
|
839
878
|
|
|
840
879
|
get body() {
|
|
880
|
+
// Null-body responses (204, 304, HEAD) must return null
|
|
881
|
+
if (__Response_get_nullBody(this.#instanceId)) {
|
|
882
|
+
return null;
|
|
883
|
+
}
|
|
884
|
+
|
|
841
885
|
// Return cached body if available (WHATWG spec requires same object on repeated access)
|
|
842
886
|
if (this.#cachedBody !== null) {
|
|
843
887
|
return this.#cachedBody;
|
|
@@ -869,6 +913,26 @@ function setupResponse(context, stateMap) {
|
|
|
869
913
|
} catch (err) {
|
|
870
914
|
throw __decodeError(err);
|
|
871
915
|
}
|
|
916
|
+
if (__Response_get_nullBody(this.#instanceId)) {
|
|
917
|
+
return "";
|
|
918
|
+
}
|
|
919
|
+
if (this.#streamId !== null) {
|
|
920
|
+
const reader = this.body.getReader();
|
|
921
|
+
const chunks = [];
|
|
922
|
+
while (true) {
|
|
923
|
+
const { done, value } = await reader.read();
|
|
924
|
+
if (done) break;
|
|
925
|
+
if (value) chunks.push(value);
|
|
926
|
+
}
|
|
927
|
+
const totalLength = chunks.reduce((acc, c) => acc + c.length, 0);
|
|
928
|
+
const result = new Uint8Array(totalLength);
|
|
929
|
+
let offset = 0;
|
|
930
|
+
for (const chunk of chunks) {
|
|
931
|
+
result.set(chunk, offset);
|
|
932
|
+
offset += chunk.length;
|
|
933
|
+
}
|
|
934
|
+
return new TextDecoder().decode(result);
|
|
935
|
+
}
|
|
872
936
|
return __Response_text(this.#instanceId);
|
|
873
937
|
}
|
|
874
938
|
|
|
@@ -1119,30 +1183,10 @@ function setupRequest(context, stateMap) {
|
|
|
1119
1183
|
return Array.from(new TextEncoder().encode(body.toString()));
|
|
1120
1184
|
}
|
|
1121
1185
|
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('&')));
|
|
1186
|
+
// Always serialize as multipart/form-data per spec
|
|
1187
|
+
const { body: bytes, contentType } = __serializeFormData(body);
|
|
1188
|
+
globalThis.__pendingFormDataContentType = contentType;
|
|
1189
|
+
return Array.from(bytes);
|
|
1146
1190
|
}
|
|
1147
1191
|
// Try to convert to string
|
|
1148
1192
|
return Array.from(new TextEncoder().encode(String(body)));
|
|
@@ -1240,7 +1284,7 @@ function setupRequest(context, stateMap) {
|
|
|
1240
1284
|
if (globalThis.__pendingFormDataContentType) {
|
|
1241
1285
|
headers.set('content-type', globalThis.__pendingFormDataContentType);
|
|
1242
1286
|
delete globalThis.__pendingFormDataContentType;
|
|
1243
|
-
} else if (body instanceof
|
|
1287
|
+
} else if (body instanceof URLSearchParams && !headers.has('content-type')) {
|
|
1244
1288
|
headers.set('content-type', 'application/x-www-form-urlencoded');
|
|
1245
1289
|
}
|
|
1246
1290
|
|
|
@@ -1430,32 +1474,56 @@ function setupRequest(context, stateMap) {
|
|
|
1430
1474
|
var FETCH_STREAM_THRESHOLD = 64 * 1024;
|
|
1431
1475
|
function setupFetchFunction(context, stateMap, streamRegistry, options) {
|
|
1432
1476
|
const global = context.global;
|
|
1433
|
-
const
|
|
1477
|
+
const fetchAbortControllers = new Map;
|
|
1478
|
+
global.setSync("__fetch_abort", new import_isolated_vm.default.Callback((fetchId) => {
|
|
1479
|
+
const controller = fetchAbortControllers.get(fetchId);
|
|
1480
|
+
if (controller) {
|
|
1481
|
+
setImmediate(() => controller.abort());
|
|
1482
|
+
}
|
|
1483
|
+
}));
|
|
1484
|
+
const fetchRef = new import_isolated_vm.default.Reference(async (url, method, headersJson, bodyJson, signalAborted, fetchId) => {
|
|
1434
1485
|
if (signalAborted) {
|
|
1435
1486
|
throw new Error("[AbortError]The operation was aborted.");
|
|
1436
1487
|
}
|
|
1488
|
+
const hostController = new AbortController;
|
|
1489
|
+
fetchAbortControllers.set(fetchId, hostController);
|
|
1437
1490
|
const headers = JSON.parse(headersJson);
|
|
1438
1491
|
const bodyBytes = bodyJson ? JSON.parse(bodyJson) : null;
|
|
1439
1492
|
const body = bodyBytes ? new Uint8Array(bodyBytes) : null;
|
|
1440
1493
|
const nativeRequest = new Request(url, {
|
|
1441
1494
|
method,
|
|
1442
1495
|
headers,
|
|
1443
|
-
body
|
|
1496
|
+
body,
|
|
1497
|
+
signal: hostController.signal
|
|
1444
1498
|
});
|
|
1445
1499
|
const onFetch = options?.onFetch ?? fetch;
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1500
|
+
try {
|
|
1501
|
+
let cleanupAbort;
|
|
1502
|
+
const abortPromise = new Promise((_, reject) => {
|
|
1503
|
+
if (hostController.signal.aborted) {
|
|
1504
|
+
reject(Object.assign(new Error("The operation was aborted."), { name: "AbortError" }));
|
|
1505
|
+
return;
|
|
1506
|
+
}
|
|
1507
|
+
const onAbort = () => {
|
|
1508
|
+
reject(Object.assign(new Error("The operation was aborted."), { name: "AbortError" }));
|
|
1509
|
+
};
|
|
1510
|
+
hostController.signal.addEventListener("abort", onAbort, { once: true });
|
|
1511
|
+
cleanupAbort = () => hostController.signal.removeEventListener("abort", onAbort);
|
|
1512
|
+
});
|
|
1513
|
+
abortPromise.catch(() => {});
|
|
1514
|
+
const nativeResponse = await Promise.race([onFetch(nativeRequest), abortPromise]);
|
|
1515
|
+
cleanupAbort?.();
|
|
1516
|
+
const status = nativeResponse.status;
|
|
1517
|
+
const isNullBody = status === 204 || status === 304 || method.toUpperCase() === "HEAD";
|
|
1518
|
+
const isCallbackStream = nativeResponse.__isCallbackStream;
|
|
1519
|
+
const isNetworkResponse = nativeResponse.url && (nativeResponse.url.startsWith("http://") || nativeResponse.url.startsWith("https://"));
|
|
1520
|
+
const shouldStream = !isNullBody && nativeResponse.body && (isCallbackStream || isNetworkResponse);
|
|
1521
|
+
if (shouldStream && nativeResponse.body) {
|
|
1522
|
+
const streamId = streamRegistry.create();
|
|
1523
|
+
const streamCleanupFn = import_stream_state.startNativeStreamReader(nativeResponse.body, streamId, streamRegistry);
|
|
1524
|
+
streamRegistry.setCleanup(streamId, streamCleanupFn);
|
|
1525
|
+
const instanceId2 = nextInstanceId++;
|
|
1526
|
+
const state2 = {
|
|
1459
1527
|
status: nativeResponse.status,
|
|
1460
1528
|
statusText: nativeResponse.statusText,
|
|
1461
1529
|
headers: Array.from(nativeResponse.headers.entries()),
|
|
@@ -1464,65 +1532,37 @@ function setupFetchFunction(context, stateMap, streamRegistry, options) {
|
|
|
1464
1532
|
type: "default",
|
|
1465
1533
|
url: nativeResponse.url,
|
|
1466
1534
|
redirected: nativeResponse.redirected,
|
|
1467
|
-
streamId
|
|
1535
|
+
streamId,
|
|
1536
|
+
nullBody: isNullBody
|
|
1468
1537
|
};
|
|
1469
|
-
stateMap.set(
|
|
1470
|
-
return
|
|
1538
|
+
stateMap.set(instanceId2, state2);
|
|
1539
|
+
return instanceId2;
|
|
1471
1540
|
}
|
|
1472
|
-
const
|
|
1473
|
-
const
|
|
1474
|
-
const
|
|
1541
|
+
const responseBody = await nativeResponse.arrayBuffer();
|
|
1542
|
+
const responseBodyArray = Array.from(new Uint8Array(responseBody));
|
|
1543
|
+
const instanceId = nextInstanceId++;
|
|
1544
|
+
const state = {
|
|
1475
1545
|
status: nativeResponse.status,
|
|
1476
1546
|
statusText: nativeResponse.statusText,
|
|
1477
1547
|
headers: Array.from(nativeResponse.headers.entries()),
|
|
1478
|
-
body: new Uint8Array(
|
|
1548
|
+
body: new Uint8Array(responseBodyArray),
|
|
1479
1549
|
bodyUsed: false,
|
|
1480
1550
|
type: "default",
|
|
1481
1551
|
url: nativeResponse.url,
|
|
1482
1552
|
redirected: nativeResponse.redirected,
|
|
1483
|
-
streamId
|
|
1553
|
+
streamId: null,
|
|
1554
|
+
nullBody: isNullBody
|
|
1484
1555
|
};
|
|
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;
|
|
1556
|
+
stateMap.set(instanceId, state);
|
|
1557
|
+
return instanceId;
|
|
1558
|
+
} catch (err) {
|
|
1559
|
+
if (err instanceof Error && err.name === "AbortError") {
|
|
1560
|
+
throw new Error("[AbortError]The operation was aborted.");
|
|
1561
|
+
}
|
|
1562
|
+
throw err;
|
|
1563
|
+
} finally {
|
|
1564
|
+
fetchAbortControllers.delete(fetchId);
|
|
1509
1565
|
}
|
|
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
1566
|
});
|
|
1527
1567
|
global.setSync("__fetch_ref", fetchRef);
|
|
1528
1568
|
const fetchCode = `
|
|
@@ -1540,7 +1580,28 @@ function setupFetchFunction(context, stateMap, streamRegistry, options) {
|
|
|
1540
1580
|
return err;
|
|
1541
1581
|
}
|
|
1542
1582
|
|
|
1543
|
-
|
|
1583
|
+
let __nextFetchId = 1;
|
|
1584
|
+
|
|
1585
|
+
globalThis.fetch = async function(input, init = {}) {
|
|
1586
|
+
// Handle Blob and ReadableStream bodies before creating Request
|
|
1587
|
+
if (init.body instanceof Blob && !(init.body instanceof File)) {
|
|
1588
|
+
const buf = await init.body.arrayBuffer();
|
|
1589
|
+
init = Object.assign({}, init, { body: new Uint8Array(buf) });
|
|
1590
|
+
} else if (init.body instanceof ReadableStream) {
|
|
1591
|
+
const reader = init.body.getReader();
|
|
1592
|
+
const chunks = [];
|
|
1593
|
+
while (true) {
|
|
1594
|
+
const { done, value } = await reader.read();
|
|
1595
|
+
if (done) break;
|
|
1596
|
+
chunks.push(value);
|
|
1597
|
+
}
|
|
1598
|
+
const total = chunks.reduce((s, c) => s + c.length, 0);
|
|
1599
|
+
const buf = new Uint8Array(total);
|
|
1600
|
+
let off = 0;
|
|
1601
|
+
for (const c of chunks) { buf.set(c, off); off += c.length; }
|
|
1602
|
+
init = Object.assign({}, init, { body: buf });
|
|
1603
|
+
}
|
|
1604
|
+
|
|
1544
1605
|
// Create Request from input
|
|
1545
1606
|
const request = input instanceof Request ? input : new Request(input, init);
|
|
1546
1607
|
|
|
@@ -1548,20 +1609,34 @@ function setupFetchFunction(context, stateMap, streamRegistry, options) {
|
|
|
1548
1609
|
const signal = init.signal ?? request.signal;
|
|
1549
1610
|
const signalAborted = signal?.aborted ?? false;
|
|
1550
1611
|
|
|
1612
|
+
// Assign a fetch ID for abort tracking
|
|
1613
|
+
const fetchId = __nextFetchId++;
|
|
1614
|
+
|
|
1615
|
+
// Register abort listener if signal exists
|
|
1616
|
+
if (signal && !signalAborted) {
|
|
1617
|
+
signal.addEventListener('abort', () => { __fetch_abort(fetchId); });
|
|
1618
|
+
}
|
|
1619
|
+
|
|
1551
1620
|
// Serialize headers and body to JSON for transfer
|
|
1552
1621
|
const headersJson = JSON.stringify(Array.from(request.headers.entries()));
|
|
1553
1622
|
const bodyBytes = request._getBodyBytes();
|
|
1554
1623
|
const bodyJson = bodyBytes ? JSON.stringify(bodyBytes) : null;
|
|
1555
1624
|
|
|
1625
|
+
// Short-circuit: if signal is already aborted, throw without calling host
|
|
1626
|
+
if (signalAborted) {
|
|
1627
|
+
throw new DOMException('The operation was aborted.', 'AbortError');
|
|
1628
|
+
}
|
|
1629
|
+
|
|
1556
1630
|
// Call host - returns just the response instance ID
|
|
1557
1631
|
try {
|
|
1558
|
-
const instanceId = __fetch_ref.
|
|
1632
|
+
const instanceId = await __fetch_ref.apply(undefined, [
|
|
1559
1633
|
request.url,
|
|
1560
1634
|
request.method,
|
|
1561
1635
|
headersJson,
|
|
1562
1636
|
bodyJson,
|
|
1563
|
-
signalAborted
|
|
1564
|
-
|
|
1637
|
+
signalAborted,
|
|
1638
|
+
fetchId
|
|
1639
|
+
], { result: { promise: true, copy: true } });
|
|
1565
1640
|
|
|
1566
1641
|
// Construct Response from the instance ID
|
|
1567
1642
|
return Response._fromInstanceId(instanceId);
|
|
@@ -1678,7 +1753,7 @@ async function setupFetch(context, options) {
|
|
|
1678
1753
|
context.evalSync(multipartCode);
|
|
1679
1754
|
setupStreamCallbacks(context, streamRegistry);
|
|
1680
1755
|
context.evalSync(hostBackedStreamCode);
|
|
1681
|
-
setupResponse(context, stateMap);
|
|
1756
|
+
setupResponse(context, stateMap, streamRegistry);
|
|
1682
1757
|
setupRequest(context, stateMap);
|
|
1683
1758
|
setupFetchFunction(context, stateMap, streamRegistry, options);
|
|
1684
1759
|
const serveState = {
|
|
@@ -1801,8 +1876,7 @@ async function setupFetch(context, options) {
|
|
|
1801
1876
|
},
|
|
1802
1877
|
cancel() {
|
|
1803
1878
|
streamDone = true;
|
|
1804
|
-
streamRegistry.
|
|
1805
|
-
streamRegistry.delete(responseStreamId);
|
|
1879
|
+
streamRegistry.cancel(responseStreamId);
|
|
1806
1880
|
}
|
|
1807
1881
|
});
|
|
1808
1882
|
const responseHeaders2 = new Headers(responseState.headers);
|
|
@@ -1951,4 +2025,4 @@ async function setupFetch(context, options) {
|
|
|
1951
2025
|
};
|
|
1952
2026
|
}
|
|
1953
2027
|
|
|
1954
|
-
//# debugId=
|
|
2028
|
+
//# debugId=E8993FE5C4E862A664756E2164756E21
|