@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.
@@ -6,6 +6,7 @@ import {
6
6
  startNativeStreamReader
7
7
  } from "./stream-state.mjs";
8
8
  var instanceStateMap = new WeakMap;
9
+ var passthruBodies = new WeakMap;
9
10
  var nextInstanceId = 1;
10
11
  function getInstanceStateMapForContext(context) {
11
12
  let map = instanceStateMap.get(context);
@@ -15,6 +16,14 @@ function getInstanceStateMapForContext(context) {
15
16
  }
16
17
  return map;
17
18
  }
19
+ function getPassthruBodiesForContext(context) {
20
+ let map = passthruBodies.get(context);
21
+ if (!map) {
22
+ map = new Map;
23
+ passthruBodies.set(context, map);
24
+ }
25
+ return map;
26
+ }
18
27
  var headersCode = `
19
28
  (function() {
20
29
  class Headers {
@@ -370,77 +379,79 @@ var hostBackedStreamCode = `
370
379
  (function() {
371
380
  const _streamIds = new WeakMap();
372
381
 
373
- class HostBackedReadableStream {
382
+ // Polyfill values() on ReadableStream if not available (older V8 versions)
383
+ if (typeof ReadableStream.prototype.values !== 'function') {
384
+ ReadableStream.prototype.values = function(options) {
385
+ const reader = this.getReader();
386
+ return {
387
+ async next() {
388
+ const { value, done } = await reader.read();
389
+ if (done) {
390
+ reader.releaseLock();
391
+ return { value: undefined, done: true };
392
+ }
393
+ return { value, done: false };
394
+ },
395
+ async return(value) {
396
+ reader.releaseLock();
397
+ return { value, done: true };
398
+ },
399
+ [Symbol.asyncIterator]() {
400
+ return this;
401
+ }
402
+ };
403
+ };
404
+ }
405
+
406
+ // Create a proper ReadableStream subclass that reports as "ReadableStream"
407
+ class HostBackedReadableStream extends ReadableStream {
374
408
  constructor(streamId) {
375
409
  if (streamId === undefined) {
376
410
  streamId = __Stream_create();
377
411
  }
378
- _streamIds.set(this, streamId);
379
- }
380
412
 
381
- _getStreamId() {
382
- return _streamIds.get(this);
383
- }
413
+ let closed = false;
384
414
 
385
- getReader() {
386
- const streamId = this._getStreamId();
387
- let released = false;
415
+ super({
416
+ async pull(controller) {
417
+ if (closed) return;
388
418
 
389
- return {
390
- read: async () => {
391
- if (released) {
392
- throw new TypeError("Reader has been released");
393
- }
394
419
  const resultJson = __Stream_pull_ref.applySyncPromise(undefined, [streamId]);
395
420
  const result = JSON.parse(resultJson);
396
421
 
397
422
  if (result.done) {
398
- return { done: true, value: undefined };
423
+ closed = true;
424
+ controller.close();
425
+ return;
399
426
  }
400
- return { done: false, value: new Uint8Array(result.value) };
401
- },
402
-
403
- releaseLock: () => {
404
- released = true;
405
- },
406
-
407
- get closed() {
408
- return new Promise(() => {});
427
+ controller.enqueue(new Uint8Array(result.value));
409
428
  },
410
-
411
- cancel: async (reason) => {
429
+ cancel(reason) {
430
+ closed = true;
412
431
  __Stream_error(streamId, String(reason || "cancelled"));
413
432
  }
414
- };
415
- }
433
+ });
416
434
 
417
- async cancel(reason) {
418
- __Stream_error(this._getStreamId(), String(reason || "cancelled"));
435
+ _streamIds.set(this, streamId);
419
436
  }
420
437
 
421
- get locked() {
422
- return false;
438
+ // Override to report as ReadableStream for spec compliance
439
+ get [Symbol.toStringTag]() {
440
+ return 'ReadableStream';
423
441
  }
424
442
 
425
- async *[Symbol.asyncIterator]() {
426
- const reader = this.getReader();
427
- try {
428
- while (true) {
429
- const { value, done } = await reader.read();
430
- if (done) return;
431
- yield value;
432
- }
433
- } finally {
434
- reader.releaseLock();
435
- }
443
+ _getStreamId() {
444
+ return _streamIds.get(this);
436
445
  }
437
446
 
438
- // Static method to create from existing stream ID
439
447
  static _fromStreamId(streamId) {
440
448
  return new HostBackedReadableStream(streamId);
441
449
  }
442
450
  }
443
451
 
452
+ // Make constructor.name return 'ReadableStream' for spec compliance
453
+ Object.defineProperty(HostBackedReadableStream, 'name', { value: 'ReadableStream' });
454
+
444
455
  globalThis.HostBackedReadableStream = HostBackedReadableStream;
445
456
  })();
446
457
  `;
@@ -603,8 +614,12 @@ function setupResponse(context, stateMap) {
603
614
  // Mark as needing async Blob handling - will be read in constructor
604
615
  return { __isBlob: true, blob: body };
605
616
  }
606
- // Handle ReadableStream (both native and host-backed)
607
- if (body instanceof ReadableStream || body instanceof HostBackedReadableStream) {
617
+ // Handle HostBackedReadableStream specially - preserve streamId
618
+ if (body instanceof HostBackedReadableStream) {
619
+ return { __isHostStream: true, stream: body, streamId: body._getStreamId() };
620
+ }
621
+ // Handle native ReadableStream
622
+ if (body instanceof ReadableStream) {
608
623
  return { __isStream: true, stream: body };
609
624
  }
610
625
  // Try to convert to string
@@ -616,6 +631,7 @@ function setupResponse(context, stateMap) {
616
631
  #headers;
617
632
  #streamId = null;
618
633
  #blobInitPromise = null; // For async Blob body initialization
634
+ #cachedBody = null; // Memoized body stream for spec compliance
619
635
 
620
636
  constructor(body, init = {}) {
621
637
  // Handle internal construction from instance ID
@@ -659,7 +675,27 @@ function setupResponse(context, stateMap) {
659
675
  return;
660
676
  }
661
677
 
662
- // Handle streaming body
678
+ // Handle HostBackedReadableStream - reuse existing streamId for pass-through
679
+ if (preparedBody && preparedBody.__isHostStream) {
680
+ // Reuse the existing streamId to preserve the pass-through body mapping
681
+ this.#streamId = preparedBody.streamId;
682
+ const status = init.status ?? 200;
683
+ const statusText = init.statusText ?? '';
684
+ const headers = new Headers(init.headers);
685
+ const headersArray = Array.from(headers.entries());
686
+
687
+ this.#instanceId = __Response_constructStreaming(
688
+ this.#streamId,
689
+ status,
690
+ statusText,
691
+ headersArray
692
+ );
693
+ this.#headers = headers;
694
+ // Don't pump - the body is already backed by this streamId
695
+ return;
696
+ }
697
+
698
+ // Handle native ReadableStream body
663
699
  if (preparedBody && preparedBody.__isStream) {
664
700
  this.#streamId = __Stream_create();
665
701
  const status = init.status ?? 200;
@@ -758,9 +794,15 @@ function setupResponse(context, stateMap) {
758
794
  }
759
795
 
760
796
  get body() {
797
+ // Return cached body if available (WHATWG spec requires same object on repeated access)
798
+ if (this.#cachedBody !== null) {
799
+ return this.#cachedBody;
800
+ }
801
+
761
802
  const streamId = __Response_getStreamId(this.#instanceId);
762
803
  if (streamId !== null) {
763
- return HostBackedReadableStream._fromStreamId(streamId);
804
+ this.#cachedBody = HostBackedReadableStream._fromStreamId(streamId);
805
+ return this.#cachedBody;
764
806
  }
765
807
 
766
808
  // Fallback: create host-backed stream from buffered body
@@ -773,7 +815,8 @@ function setupResponse(context, stateMap) {
773
815
  }
774
816
  __Stream_close(newStreamId);
775
817
 
776
- return HostBackedReadableStream._fromStreamId(newStreamId);
818
+ this.#cachedBody = HostBackedReadableStream._fromStreamId(newStreamId);
819
+ return this.#cachedBody;
777
820
  }
778
821
 
779
822
  async text() {
@@ -1340,7 +1383,8 @@ function setupRequest(context, stateMap) {
1340
1383
  `;
1341
1384
  context.evalSync(requestCode);
1342
1385
  }
1343
- function setupFetchFunction(context, stateMap, options) {
1386
+ var FETCH_STREAM_THRESHOLD = 64 * 1024;
1387
+ function setupFetchFunction(context, stateMap, streamRegistry, options) {
1344
1388
  const global = context.global;
1345
1389
  const fetchRef = new ivm.Reference(async (url, method, headersJson, bodyJson, signalAborted) => {
1346
1390
  if (signalAborted) {
@@ -1356,6 +1400,69 @@ function setupFetchFunction(context, stateMap, options) {
1356
1400
  });
1357
1401
  const onFetch = options?.onFetch ?? fetch;
1358
1402
  const nativeResponse = await onFetch(nativeRequest);
1403
+ const contentLength = nativeResponse.headers.get("content-length");
1404
+ const knownSize = contentLength ? parseInt(contentLength, 10) : null;
1405
+ const isCallbackStream = nativeResponse.__isCallbackStream;
1406
+ const isNetworkResponse = nativeResponse.url && (nativeResponse.url.startsWith("http://") || nativeResponse.url.startsWith("https://"));
1407
+ const shouldStream = nativeResponse.body && (isCallbackStream || isNetworkResponse && (knownSize === null || knownSize > FETCH_STREAM_THRESHOLD));
1408
+ if (shouldStream && nativeResponse.body) {
1409
+ if (isCallbackStream) {
1410
+ const streamId2 = streamRegistry.create();
1411
+ const passthruMap = getPassthruBodiesForContext(context);
1412
+ passthruMap.set(streamId2, nativeResponse.body);
1413
+ const instanceId3 = nextInstanceId++;
1414
+ const state3 = {
1415
+ status: nativeResponse.status,
1416
+ statusText: nativeResponse.statusText,
1417
+ headers: Array.from(nativeResponse.headers.entries()),
1418
+ body: new Uint8Array(0),
1419
+ bodyUsed: false,
1420
+ type: "default",
1421
+ url: nativeResponse.url,
1422
+ redirected: nativeResponse.redirected,
1423
+ streamId: streamId2
1424
+ };
1425
+ stateMap.set(instanceId3, state3);
1426
+ return instanceId3;
1427
+ }
1428
+ const streamId = streamRegistry.create();
1429
+ const instanceId2 = nextInstanceId++;
1430
+ const state2 = {
1431
+ status: nativeResponse.status,
1432
+ statusText: nativeResponse.statusText,
1433
+ headers: Array.from(nativeResponse.headers.entries()),
1434
+ body: new Uint8Array(0),
1435
+ bodyUsed: false,
1436
+ type: "default",
1437
+ url: nativeResponse.url,
1438
+ redirected: nativeResponse.redirected,
1439
+ streamId
1440
+ };
1441
+ stateMap.set(instanceId2, state2);
1442
+ const reader = nativeResponse.body.getReader();
1443
+ (async () => {
1444
+ try {
1445
+ while (true) {
1446
+ const { done, value } = await reader.read();
1447
+ if (done) {
1448
+ streamRegistry.close(streamId);
1449
+ break;
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;
1465
+ }
1359
1466
  const responseBody = await nativeResponse.arrayBuffer();
1360
1467
  const responseBodyArray = Array.from(new Uint8Array(responseBody));
1361
1468
  const instanceId = nextInstanceId++;
@@ -1529,7 +1636,7 @@ async function setupFetch(context, options) {
1529
1636
  context.evalSync(hostBackedStreamCode);
1530
1637
  setupResponse(context, stateMap);
1531
1638
  setupRequest(context, stateMap);
1532
- setupFetchFunction(context, stateMap, options);
1639
+ setupFetchFunction(context, stateMap, streamRegistry, options);
1533
1640
  const serveState = {
1534
1641
  pendingUpgrade: null,
1535
1642
  activeConnections: new Map
@@ -1585,6 +1692,12 @@ async function setupFetch(context, options) {
1585
1692
  const request = Request._fromInstanceId(${requestInstanceId});
1586
1693
  const server = new __Server__();
1587
1694
  const response = await Promise.resolve(__serveOptions__.fetch(request, server));
1695
+ if (response == null) {
1696
+ throw new TypeError("fetch handler did not return a Response object (got " + (response === null ? "null" : "undefined") + ")");
1697
+ }
1698
+ if (typeof response._getInstanceId !== 'function') {
1699
+ throw new TypeError("fetch handler must return a Response object (got " + (typeof response) + ")");
1700
+ }
1588
1701
  return response._getInstanceId();
1589
1702
  })()
1590
1703
  `, { promise: true });
@@ -1592,6 +1705,22 @@ async function setupFetch(context, options) {
1592
1705
  if (!responseState) {
1593
1706
  throw new Error("Response state not found");
1594
1707
  }
1708
+ if (responseState.streamId !== null) {
1709
+ const passthruMap = getPassthruBodiesForContext(context);
1710
+ const passthruBody = passthruMap.get(responseState.streamId);
1711
+ if (passthruBody) {
1712
+ passthruMap.delete(responseState.streamId);
1713
+ const responseHeaders2 = new Headers(responseState.headers);
1714
+ const status2 = responseState.status === 101 ? 200 : responseState.status;
1715
+ const response2 = new Response(passthruBody, {
1716
+ status: status2,
1717
+ statusText: responseState.statusText,
1718
+ headers: responseHeaders2
1719
+ });
1720
+ response2._originalStatus = responseState.status;
1721
+ return response2;
1722
+ }
1723
+ }
1595
1724
  if (responseState.streamId !== null) {
1596
1725
  const responseStreamId = responseState.streamId;
1597
1726
  let streamDone = false;
@@ -1653,12 +1782,17 @@ async function setupFetch(context, options) {
1653
1782
  response._originalStatus = responseState.status;
1654
1783
  return response;
1655
1784
  } finally {
1785
+ if (requestStreamId !== null) {
1786
+ const startTime = Date.now();
1787
+ let streamState = streamRegistry.get(requestStreamId);
1788
+ while (streamState && !streamState.closed && !streamState.errored && Date.now() - startTime < 100) {
1789
+ await new Promise((resolve) => setTimeout(resolve, 5));
1790
+ streamState = streamRegistry.get(requestStreamId);
1791
+ }
1792
+ }
1656
1793
  if (streamCleanup) {
1657
1794
  await streamCleanup();
1658
1795
  }
1659
- if (requestStreamId !== null) {
1660
- streamRegistry.delete(requestStreamId);
1661
- }
1662
1796
  }
1663
1797
  },
1664
1798
  getUpgradeRequest() {
@@ -1777,4 +1911,4 @@ export {
1777
1911
  clearAllInstanceState
1778
1912
  };
1779
1913
 
1780
- //# debugId=CBC79B470DCCB48D64756E2164756E21
1914
+ //# debugId=5C3C175E215A892A64756E2164756E21