@ricsam/isolate-fetch 0.1.9 → 0.1.11

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.
@@ -50,6 +50,7 @@ var import_isolated_vm = __toESM(require("isolated-vm"));
50
50
  var import_isolate_core = require("@ricsam/isolate-core");
51
51
  var import_stream_state = require("./stream-state.cjs");
52
52
  var instanceStateMap = new WeakMap;
53
+ var passthruBodies = new WeakMap;
53
54
  var nextInstanceId = 1;
54
55
  function getInstanceStateMapForContext(context) {
55
56
  let map = instanceStateMap.get(context);
@@ -59,6 +60,14 @@ function getInstanceStateMapForContext(context) {
59
60
  }
60
61
  return map;
61
62
  }
63
+ function getPassthruBodiesForContext(context) {
64
+ let map = passthruBodies.get(context);
65
+ if (!map) {
66
+ map = new Map;
67
+ passthruBodies.set(context, map);
68
+ }
69
+ return map;
70
+ }
62
71
  var headersCode = `
63
72
  (function() {
64
73
  class Headers {
@@ -414,77 +423,79 @@ var hostBackedStreamCode = `
414
423
  (function() {
415
424
  const _streamIds = new WeakMap();
416
425
 
417
- class HostBackedReadableStream {
426
+ // Polyfill values() on ReadableStream if not available (older V8 versions)
427
+ if (typeof ReadableStream.prototype.values !== 'function') {
428
+ ReadableStream.prototype.values = function(options) {
429
+ const reader = this.getReader();
430
+ return {
431
+ async next() {
432
+ const { value, done } = await reader.read();
433
+ if (done) {
434
+ reader.releaseLock();
435
+ return { value: undefined, done: true };
436
+ }
437
+ return { value, done: false };
438
+ },
439
+ async return(value) {
440
+ reader.releaseLock();
441
+ return { value, done: true };
442
+ },
443
+ [Symbol.asyncIterator]() {
444
+ return this;
445
+ }
446
+ };
447
+ };
448
+ }
449
+
450
+ // Create a proper ReadableStream subclass that reports as "ReadableStream"
451
+ class HostBackedReadableStream extends ReadableStream {
418
452
  constructor(streamId) {
419
453
  if (streamId === undefined) {
420
454
  streamId = __Stream_create();
421
455
  }
422
- _streamIds.set(this, streamId);
423
- }
424
456
 
425
- _getStreamId() {
426
- return _streamIds.get(this);
427
- }
457
+ let closed = false;
428
458
 
429
- getReader() {
430
- const streamId = this._getStreamId();
431
- let released = false;
459
+ super({
460
+ async pull(controller) {
461
+ if (closed) return;
432
462
 
433
- return {
434
- read: async () => {
435
- if (released) {
436
- throw new TypeError("Reader has been released");
437
- }
438
463
  const resultJson = __Stream_pull_ref.applySyncPromise(undefined, [streamId]);
439
464
  const result = JSON.parse(resultJson);
440
465
 
441
466
  if (result.done) {
442
- return { done: true, value: undefined };
467
+ closed = true;
468
+ controller.close();
469
+ return;
443
470
  }
444
- return { done: false, value: new Uint8Array(result.value) };
445
- },
446
-
447
- releaseLock: () => {
448
- released = true;
449
- },
450
-
451
- get closed() {
452
- return new Promise(() => {});
471
+ controller.enqueue(new Uint8Array(result.value));
453
472
  },
454
-
455
- cancel: async (reason) => {
473
+ cancel(reason) {
474
+ closed = true;
456
475
  __Stream_error(streamId, String(reason || "cancelled"));
457
476
  }
458
- };
459
- }
477
+ });
460
478
 
461
- async cancel(reason) {
462
- __Stream_error(this._getStreamId(), String(reason || "cancelled"));
479
+ _streamIds.set(this, streamId);
463
480
  }
464
481
 
465
- get locked() {
466
- return false;
482
+ // Override to report as ReadableStream for spec compliance
483
+ get [Symbol.toStringTag]() {
484
+ return 'ReadableStream';
467
485
  }
468
486
 
469
- async *[Symbol.asyncIterator]() {
470
- const reader = this.getReader();
471
- try {
472
- while (true) {
473
- const { value, done } = await reader.read();
474
- if (done) return;
475
- yield value;
476
- }
477
- } finally {
478
- reader.releaseLock();
479
- }
487
+ _getStreamId() {
488
+ return _streamIds.get(this);
480
489
  }
481
490
 
482
- // Static method to create from existing stream ID
483
491
  static _fromStreamId(streamId) {
484
492
  return new HostBackedReadableStream(streamId);
485
493
  }
486
494
  }
487
495
 
496
+ // Make constructor.name return 'ReadableStream' for spec compliance
497
+ Object.defineProperty(HostBackedReadableStream, 'name', { value: 'ReadableStream' });
498
+
488
499
  globalThis.HostBackedReadableStream = HostBackedReadableStream;
489
500
  })();
490
501
  `;
@@ -647,8 +658,12 @@ function setupResponse(context, stateMap) {
647
658
  // Mark as needing async Blob handling - will be read in constructor
648
659
  return { __isBlob: true, blob: body };
649
660
  }
650
- // Handle ReadableStream (both native and host-backed)
651
- if (body instanceof ReadableStream || body instanceof HostBackedReadableStream) {
661
+ // Handle HostBackedReadableStream specially - preserve streamId
662
+ if (body instanceof HostBackedReadableStream) {
663
+ return { __isHostStream: true, stream: body, streamId: body._getStreamId() };
664
+ }
665
+ // Handle native ReadableStream
666
+ if (body instanceof ReadableStream) {
652
667
  return { __isStream: true, stream: body };
653
668
  }
654
669
  // Try to convert to string
@@ -660,6 +675,7 @@ function setupResponse(context, stateMap) {
660
675
  #headers;
661
676
  #streamId = null;
662
677
  #blobInitPromise = null; // For async Blob body initialization
678
+ #cachedBody = null; // Memoized body stream for spec compliance
663
679
 
664
680
  constructor(body, init = {}) {
665
681
  // Handle internal construction from instance ID
@@ -703,7 +719,27 @@ function setupResponse(context, stateMap) {
703
719
  return;
704
720
  }
705
721
 
706
- // Handle streaming body
722
+ // Handle HostBackedReadableStream - reuse existing streamId for pass-through
723
+ if (preparedBody && preparedBody.__isHostStream) {
724
+ // Reuse the existing streamId to preserve the pass-through body mapping
725
+ this.#streamId = preparedBody.streamId;
726
+ const status = init.status ?? 200;
727
+ const statusText = init.statusText ?? '';
728
+ const headers = new Headers(init.headers);
729
+ const headersArray = Array.from(headers.entries());
730
+
731
+ this.#instanceId = __Response_constructStreaming(
732
+ this.#streamId,
733
+ status,
734
+ statusText,
735
+ headersArray
736
+ );
737
+ this.#headers = headers;
738
+ // Don't pump - the body is already backed by this streamId
739
+ return;
740
+ }
741
+
742
+ // Handle native ReadableStream body
707
743
  if (preparedBody && preparedBody.__isStream) {
708
744
  this.#streamId = __Stream_create();
709
745
  const status = init.status ?? 200;
@@ -802,9 +838,15 @@ function setupResponse(context, stateMap) {
802
838
  }
803
839
 
804
840
  get body() {
841
+ // Return cached body if available (WHATWG spec requires same object on repeated access)
842
+ if (this.#cachedBody !== null) {
843
+ return this.#cachedBody;
844
+ }
845
+
805
846
  const streamId = __Response_getStreamId(this.#instanceId);
806
847
  if (streamId !== null) {
807
- return HostBackedReadableStream._fromStreamId(streamId);
848
+ this.#cachedBody = HostBackedReadableStream._fromStreamId(streamId);
849
+ return this.#cachedBody;
808
850
  }
809
851
 
810
852
  // Fallback: create host-backed stream from buffered body
@@ -817,7 +859,8 @@ function setupResponse(context, stateMap) {
817
859
  }
818
860
  __Stream_close(newStreamId);
819
861
 
820
- return HostBackedReadableStream._fromStreamId(newStreamId);
862
+ this.#cachedBody = HostBackedReadableStream._fromStreamId(newStreamId);
863
+ return this.#cachedBody;
821
864
  }
822
865
 
823
866
  async text() {
@@ -1384,7 +1427,8 @@ function setupRequest(context, stateMap) {
1384
1427
  `;
1385
1428
  context.evalSync(requestCode);
1386
1429
  }
1387
- function setupFetchFunction(context, stateMap, options) {
1430
+ var FETCH_STREAM_THRESHOLD = 64 * 1024;
1431
+ function setupFetchFunction(context, stateMap, streamRegistry, options) {
1388
1432
  const global = context.global;
1389
1433
  const fetchRef = new import_isolated_vm.default.Reference(async (url, method, headersJson, bodyJson, signalAborted) => {
1390
1434
  if (signalAborted) {
@@ -1400,6 +1444,69 @@ function setupFetchFunction(context, stateMap, options) {
1400
1444
  });
1401
1445
  const onFetch = options?.onFetch ?? fetch;
1402
1446
  const nativeResponse = await onFetch(nativeRequest);
1447
+ const contentLength = nativeResponse.headers.get("content-length");
1448
+ const knownSize = contentLength ? parseInt(contentLength, 10) : null;
1449
+ const isCallbackStream = nativeResponse.__isCallbackStream;
1450
+ const isNetworkResponse = nativeResponse.url && (nativeResponse.url.startsWith("http://") || nativeResponse.url.startsWith("https://"));
1451
+ const shouldStream = nativeResponse.body && (isCallbackStream || isNetworkResponse && (knownSize === null || knownSize > FETCH_STREAM_THRESHOLD));
1452
+ if (shouldStream && nativeResponse.body) {
1453
+ if (isCallbackStream) {
1454
+ const streamId2 = streamRegistry.create();
1455
+ const passthruMap = getPassthruBodiesForContext(context);
1456
+ passthruMap.set(streamId2, nativeResponse.body);
1457
+ const instanceId3 = nextInstanceId++;
1458
+ const state3 = {
1459
+ status: nativeResponse.status,
1460
+ statusText: nativeResponse.statusText,
1461
+ headers: Array.from(nativeResponse.headers.entries()),
1462
+ body: new Uint8Array(0),
1463
+ bodyUsed: false,
1464
+ type: "default",
1465
+ url: nativeResponse.url,
1466
+ redirected: nativeResponse.redirected,
1467
+ streamId: streamId2
1468
+ };
1469
+ stateMap.set(instanceId3, state3);
1470
+ return instanceId3;
1471
+ }
1472
+ const streamId = streamRegistry.create();
1473
+ const instanceId2 = nextInstanceId++;
1474
+ const state2 = {
1475
+ status: nativeResponse.status,
1476
+ statusText: nativeResponse.statusText,
1477
+ headers: Array.from(nativeResponse.headers.entries()),
1478
+ body: new Uint8Array(0),
1479
+ bodyUsed: false,
1480
+ type: "default",
1481
+ url: nativeResponse.url,
1482
+ redirected: nativeResponse.redirected,
1483
+ streamId
1484
+ };
1485
+ stateMap.set(instanceId2, state2);
1486
+ const reader = nativeResponse.body.getReader();
1487
+ (async () => {
1488
+ try {
1489
+ while (true) {
1490
+ const { done, value } = await reader.read();
1491
+ if (done) {
1492
+ streamRegistry.close(streamId);
1493
+ break;
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;
1509
+ }
1403
1510
  const responseBody = await nativeResponse.arrayBuffer();
1404
1511
  const responseBodyArray = Array.from(new Uint8Array(responseBody));
1405
1512
  const instanceId = nextInstanceId++;
@@ -1573,7 +1680,7 @@ async function setupFetch(context, options) {
1573
1680
  context.evalSync(hostBackedStreamCode);
1574
1681
  setupResponse(context, stateMap);
1575
1682
  setupRequest(context, stateMap);
1576
- setupFetchFunction(context, stateMap, options);
1683
+ setupFetchFunction(context, stateMap, streamRegistry, options);
1577
1684
  const serveState = {
1578
1685
  pendingUpgrade: null,
1579
1686
  activeConnections: new Map
@@ -1629,6 +1736,12 @@ async function setupFetch(context, options) {
1629
1736
  const request = Request._fromInstanceId(${requestInstanceId});
1630
1737
  const server = new __Server__();
1631
1738
  const response = await Promise.resolve(__serveOptions__.fetch(request, server));
1739
+ if (response == null) {
1740
+ throw new TypeError("fetch handler did not return a Response object (got " + (response === null ? "null" : "undefined") + ")");
1741
+ }
1742
+ if (typeof response._getInstanceId !== 'function') {
1743
+ throw new TypeError("fetch handler must return a Response object (got " + (typeof response) + ")");
1744
+ }
1632
1745
  return response._getInstanceId();
1633
1746
  })()
1634
1747
  `, { promise: true });
@@ -1636,6 +1749,22 @@ async function setupFetch(context, options) {
1636
1749
  if (!responseState) {
1637
1750
  throw new Error("Response state not found");
1638
1751
  }
1752
+ if (responseState.streamId !== null) {
1753
+ const passthruMap = getPassthruBodiesForContext(context);
1754
+ const passthruBody = passthruMap.get(responseState.streamId);
1755
+ if (passthruBody) {
1756
+ passthruMap.delete(responseState.streamId);
1757
+ const responseHeaders2 = new Headers(responseState.headers);
1758
+ const status2 = responseState.status === 101 ? 200 : responseState.status;
1759
+ const response2 = new Response(passthruBody, {
1760
+ status: status2,
1761
+ statusText: responseState.statusText,
1762
+ headers: responseHeaders2
1763
+ });
1764
+ response2._originalStatus = responseState.status;
1765
+ return response2;
1766
+ }
1767
+ }
1639
1768
  if (responseState.streamId !== null) {
1640
1769
  const responseStreamId = responseState.streamId;
1641
1770
  let streamDone = false;
@@ -1697,12 +1826,17 @@ async function setupFetch(context, options) {
1697
1826
  response._originalStatus = responseState.status;
1698
1827
  return response;
1699
1828
  } finally {
1829
+ if (requestStreamId !== null) {
1830
+ const startTime = Date.now();
1831
+ let streamState = streamRegistry.get(requestStreamId);
1832
+ while (streamState && !streamState.closed && !streamState.errored && Date.now() - startTime < 100) {
1833
+ await new Promise((resolve) => setTimeout(resolve, 5));
1834
+ streamState = streamRegistry.get(requestStreamId);
1835
+ }
1836
+ }
1700
1837
  if (streamCleanup) {
1701
1838
  await streamCleanup();
1702
1839
  }
1703
- if (requestStreamId !== null) {
1704
- streamRegistry.delete(requestStreamId);
1705
- }
1706
1840
  }
1707
1841
  },
1708
1842
  getUpgradeRequest() {
@@ -1817,4 +1951,4 @@ async function setupFetch(context, options) {
1817
1951
  };
1818
1952
  }
1819
1953
 
1820
- //# debugId=653D8B1934FCEAA064756E2164756E21
1954
+ //# debugId=B1E0412945057BF364756E2164756E21