@durable-streams/client 0.2.1 → 0.2.2

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/index.js CHANGED
@@ -555,6 +555,207 @@ async function* parseSSEStream(stream$1, signal) {
555
555
  }
556
556
  }
557
557
 
558
+ //#endregion
559
+ //#region src/stream-response-state.ts
560
+ /**
561
+ * Abstract base class for stream response state.
562
+ * All state transitions return new immutable state objects.
563
+ */
564
+ var StreamResponseState = class {
565
+ shouldContinueLive(stopAfterUpToDate, liveMode) {
566
+ if (stopAfterUpToDate && this.upToDate) return false;
567
+ if (liveMode === false) return false;
568
+ if (this.streamClosed) return false;
569
+ return true;
570
+ }
571
+ };
572
+ /**
573
+ * State for long-poll mode. shouldUseSse() returns false.
574
+ */
575
+ var LongPollState = class LongPollState extends StreamResponseState {
576
+ offset;
577
+ cursor;
578
+ upToDate;
579
+ streamClosed;
580
+ constructor(fields) {
581
+ super();
582
+ this.offset = fields.offset;
583
+ this.cursor = fields.cursor;
584
+ this.upToDate = fields.upToDate;
585
+ this.streamClosed = fields.streamClosed;
586
+ }
587
+ shouldUseSse() {
588
+ return false;
589
+ }
590
+ withResponseMetadata(update) {
591
+ return new LongPollState({
592
+ offset: update.offset ?? this.offset,
593
+ cursor: update.cursor ?? this.cursor,
594
+ upToDate: update.upToDate,
595
+ streamClosed: this.streamClosed || update.streamClosed
596
+ });
597
+ }
598
+ withSSEControl(event) {
599
+ const streamClosed = this.streamClosed || (event.streamClosed ?? false);
600
+ return new LongPollState({
601
+ offset: event.streamNextOffset,
602
+ cursor: event.streamCursor || this.cursor,
603
+ upToDate: event.streamClosed ?? false ? true : event.upToDate ?? this.upToDate,
604
+ streamClosed
605
+ });
606
+ }
607
+ pause() {
608
+ return new PausedState(this);
609
+ }
610
+ };
611
+ /**
612
+ * State for SSE mode. shouldUseSse() returns true.
613
+ * Tracks SSE connection resilience (short connection detection).
614
+ */
615
+ var SSEState = class SSEState extends StreamResponseState {
616
+ offset;
617
+ cursor;
618
+ upToDate;
619
+ streamClosed;
620
+ consecutiveShortConnections;
621
+ connectionStartTime;
622
+ constructor(fields) {
623
+ super();
624
+ this.offset = fields.offset;
625
+ this.cursor = fields.cursor;
626
+ this.upToDate = fields.upToDate;
627
+ this.streamClosed = fields.streamClosed;
628
+ this.consecutiveShortConnections = fields.consecutiveShortConnections ?? 0;
629
+ this.connectionStartTime = fields.connectionStartTime;
630
+ }
631
+ shouldUseSse() {
632
+ return true;
633
+ }
634
+ withResponseMetadata(update) {
635
+ return new SSEState({
636
+ offset: update.offset ?? this.offset,
637
+ cursor: update.cursor ?? this.cursor,
638
+ upToDate: update.upToDate,
639
+ streamClosed: this.streamClosed || update.streamClosed,
640
+ consecutiveShortConnections: this.consecutiveShortConnections,
641
+ connectionStartTime: this.connectionStartTime
642
+ });
643
+ }
644
+ withSSEControl(event) {
645
+ const streamClosed = this.streamClosed || (event.streamClosed ?? false);
646
+ return new SSEState({
647
+ offset: event.streamNextOffset,
648
+ cursor: event.streamCursor || this.cursor,
649
+ upToDate: event.streamClosed ?? false ? true : event.upToDate ?? this.upToDate,
650
+ streamClosed,
651
+ consecutiveShortConnections: this.consecutiveShortConnections,
652
+ connectionStartTime: this.connectionStartTime
653
+ });
654
+ }
655
+ startConnection(now) {
656
+ return new SSEState({
657
+ offset: this.offset,
658
+ cursor: this.cursor,
659
+ upToDate: this.upToDate,
660
+ streamClosed: this.streamClosed,
661
+ consecutiveShortConnections: this.consecutiveShortConnections,
662
+ connectionStartTime: now
663
+ });
664
+ }
665
+ handleConnectionEnd(now, wasAborted, config) {
666
+ if (this.connectionStartTime === void 0) return {
667
+ action: `healthy`,
668
+ state: this
669
+ };
670
+ const duration = now - this.connectionStartTime;
671
+ if (duration < config.minConnectionDuration && !wasAborted) {
672
+ const newCount = this.consecutiveShortConnections + 1;
673
+ if (newCount >= config.maxShortConnections) return {
674
+ action: `fallback`,
675
+ state: new LongPollState({
676
+ offset: this.offset,
677
+ cursor: this.cursor,
678
+ upToDate: this.upToDate,
679
+ streamClosed: this.streamClosed
680
+ })
681
+ };
682
+ return {
683
+ action: `reconnect`,
684
+ state: new SSEState({
685
+ offset: this.offset,
686
+ cursor: this.cursor,
687
+ upToDate: this.upToDate,
688
+ streamClosed: this.streamClosed,
689
+ consecutiveShortConnections: newCount,
690
+ connectionStartTime: this.connectionStartTime
691
+ }),
692
+ backoffAttempt: newCount
693
+ };
694
+ }
695
+ if (duration >= config.minConnectionDuration) return {
696
+ action: `healthy`,
697
+ state: new SSEState({
698
+ offset: this.offset,
699
+ cursor: this.cursor,
700
+ upToDate: this.upToDate,
701
+ streamClosed: this.streamClosed,
702
+ consecutiveShortConnections: 0,
703
+ connectionStartTime: this.connectionStartTime
704
+ })
705
+ };
706
+ return {
707
+ action: `healthy`,
708
+ state: this
709
+ };
710
+ }
711
+ pause() {
712
+ return new PausedState(this);
713
+ }
714
+ };
715
+ /**
716
+ * Paused state wrapper. Delegates all sync field access to the inner state.
717
+ * resume() returns the wrapped state unchanged (identity preserved).
718
+ */
719
+ var PausedState = class PausedState extends StreamResponseState {
720
+ #inner;
721
+ constructor(inner) {
722
+ super();
723
+ this.#inner = inner;
724
+ }
725
+ get offset() {
726
+ return this.#inner.offset;
727
+ }
728
+ get cursor() {
729
+ return this.#inner.cursor;
730
+ }
731
+ get upToDate() {
732
+ return this.#inner.upToDate;
733
+ }
734
+ get streamClosed() {
735
+ return this.#inner.streamClosed;
736
+ }
737
+ shouldUseSse() {
738
+ return this.#inner.shouldUseSse();
739
+ }
740
+ withResponseMetadata(update) {
741
+ const newInner = this.#inner.withResponseMetadata(update);
742
+ return new PausedState(newInner);
743
+ }
744
+ withSSEControl(event) {
745
+ const newInner = this.#inner.withSSEControl(event);
746
+ return new PausedState(newInner);
747
+ }
748
+ pause() {
749
+ return this;
750
+ }
751
+ resume() {
752
+ return {
753
+ state: this.#inner,
754
+ justResumed: true
755
+ };
756
+ }
757
+ };
758
+
558
759
  //#endregion
559
760
  //#region src/response.ts
560
761
  /**
@@ -574,10 +775,7 @@ var StreamResponseImpl = class {
574
775
  #statusText;
575
776
  #ok;
576
777
  #isLoading;
577
- #offset;
578
- #cursor;
579
- #upToDate;
580
- #streamClosed;
778
+ #syncState;
581
779
  #isJsonMode;
582
780
  #abortController;
583
781
  #fetchNext;
@@ -592,11 +790,7 @@ var StreamResponseImpl = class {
592
790
  #unsubscribeFromVisibilityChanges;
593
791
  #pausePromise;
594
792
  #pauseResolve;
595
- #justResumedFromPause = false;
596
793
  #sseResilience;
597
- #lastSSEConnectionStartTime;
598
- #consecutiveShortSSEConnections = 0;
599
- #sseFallbackToLongPoll = false;
600
794
  #encoding;
601
795
  #responseStream;
602
796
  constructor(config) {
@@ -604,10 +798,13 @@ var StreamResponseImpl = class {
604
798
  this.contentType = config.contentType;
605
799
  this.live = config.live;
606
800
  this.startOffset = config.startOffset;
607
- this.#offset = config.initialOffset;
608
- this.#cursor = config.initialCursor;
609
- this.#upToDate = config.initialUpToDate;
610
- this.#streamClosed = config.initialStreamClosed;
801
+ const syncFields = {
802
+ offset: config.initialOffset,
803
+ cursor: config.initialCursor,
804
+ upToDate: config.initialUpToDate,
805
+ streamClosed: config.initialStreamClosed
806
+ };
807
+ this.#syncState = config.startSSE ? new SSEState(syncFields) : new LongPollState(syncFields);
611
808
  this.#headers = config.firstResponse.headers;
612
809
  this.#status = config.firstResponse.status;
613
810
  this.#statusText = config.firstResponse.statusText;
@@ -664,6 +861,7 @@ var StreamResponseImpl = class {
664
861
  #pause() {
665
862
  if (this.#state === `active`) {
666
863
  this.#state = `pause-requested`;
864
+ this.#syncState = this.#syncState.pause();
667
865
  this.#pausePromise = new Promise((resolve) => {
668
866
  this.#pauseResolve = resolve;
669
867
  });
@@ -677,8 +875,8 @@ var StreamResponseImpl = class {
677
875
  #resume() {
678
876
  if (this.#state === `paused` || this.#state === `pause-requested`) {
679
877
  if (this.#abortController.signal.aborted) return;
878
+ if (this.#syncState instanceof PausedState) this.#syncState = this.#syncState.resume().state;
680
879
  this.#state = `active`;
681
- this.#justResumedFromPause = true;
682
880
  this.#pauseResolve?.();
683
881
  this.#pausePromise = void 0;
684
882
  this.#pauseResolve = void 0;
@@ -700,16 +898,16 @@ var StreamResponseImpl = class {
700
898
  return this.#isLoading;
701
899
  }
702
900
  get offset() {
703
- return this.#offset;
901
+ return this.#syncState.offset;
704
902
  }
705
903
  get cursor() {
706
- return this.#cursor;
904
+ return this.#syncState.cursor;
707
905
  }
708
906
  get upToDate() {
709
- return this.#upToDate;
907
+ return this.#syncState.upToDate;
710
908
  }
711
909
  get streamClosed() {
712
- return this.#streamClosed;
910
+ return this.#syncState.streamClosed;
713
911
  }
714
912
  #ensureJsonMode() {
715
913
  if (!this.#isJsonMode) throw new DurableStreamError(`JSON methods are only valid for JSON-mode streams. Content-Type is "${this.contentType}" and json hint was not set.`, `BAD_REQUEST`);
@@ -735,159 +933,61 @@ var StreamResponseImpl = class {
735
933
  * and whether we've received upToDate or streamClosed.
736
934
  */
737
935
  #shouldContinueLive() {
738
- if (this.#stopAfterUpToDate && this.upToDate) return false;
739
- if (this.live === false) return false;
740
- if (this.#streamClosed) return false;
741
- return true;
936
+ return this.#syncState.shouldContinueLive(this.#stopAfterUpToDate, this.live);
742
937
  }
743
938
  /**
744
939
  * Update state from response headers.
745
940
  */
746
941
  #updateStateFromResponse(response) {
747
- const offset = response.headers.get(STREAM_OFFSET_HEADER);
748
- if (offset) this.#offset = offset;
749
- const cursor = response.headers.get(STREAM_CURSOR_HEADER);
750
- if (cursor) this.#cursor = cursor;
751
- this.#upToDate = response.headers.has(STREAM_UP_TO_DATE_HEADER);
752
- const streamClosedHeader = response.headers.get(STREAM_CLOSED_HEADER);
753
- if (streamClosedHeader?.toLowerCase() === `true`) this.#streamClosed = true;
942
+ this.#syncState = this.#syncState.withResponseMetadata({
943
+ offset: response.headers.get(STREAM_OFFSET_HEADER) || void 0,
944
+ cursor: response.headers.get(STREAM_CURSOR_HEADER) || void 0,
945
+ upToDate: response.headers.has(STREAM_UP_TO_DATE_HEADER),
946
+ streamClosed: response.headers.get(STREAM_CLOSED_HEADER)?.toLowerCase() === `true`
947
+ });
754
948
  this.#headers = response.headers;
755
949
  this.#status = response.status;
756
950
  this.#statusText = response.statusText;
757
951
  this.#ok = response.ok;
758
952
  }
759
953
  /**
760
- * Extract stream metadata from Response headers.
761
- * Used by subscriber APIs to get the correct offset/cursor/upToDate/streamClosed for each
762
- * specific Response, rather than reading from `this` which may be stale due to
763
- * ReadableStream prefetching or timing issues.
764
- */
765
- #getMetadataFromResponse(response) {
766
- const offset = response.headers.get(STREAM_OFFSET_HEADER);
767
- const cursor = response.headers.get(STREAM_CURSOR_HEADER);
768
- const upToDate = response.headers.has(STREAM_UP_TO_DATE_HEADER);
769
- const streamClosed = response.headers.get(STREAM_CLOSED_HEADER)?.toLowerCase() === `true`;
770
- return {
771
- offset: offset ?? this.offset,
772
- cursor: cursor ?? this.cursor,
773
- upToDate,
774
- streamClosed: streamClosed || this.streamClosed
775
- };
776
- }
777
- /**
778
- * Decode base64 string to Uint8Array.
779
- * Per protocol: concatenate data lines, remove \n and \r, then decode.
780
- */
781
- #decodeBase64(base64Str) {
782
- const cleaned = base64Str.replace(/[\n\r]/g, ``);
783
- if (cleaned.length === 0) return new Uint8Array(0);
784
- if (cleaned.length % 4 !== 0) throw new DurableStreamError(`Invalid base64 data: length ${cleaned.length} is not a multiple of 4`, `PARSE_ERROR`);
785
- try {
786
- if (typeof Buffer !== `undefined`) return new Uint8Array(Buffer.from(cleaned, `base64`));
787
- else {
788
- const binaryStr = atob(cleaned);
789
- const bytes = new Uint8Array(binaryStr.length);
790
- for (let i = 0; i < binaryStr.length; i++) bytes[i] = binaryStr.charCodeAt(i);
791
- return bytes;
792
- }
793
- } catch (err) {
794
- throw new DurableStreamError(`Failed to decode base64 data: ${err instanceof Error ? err.message : String(err)}`, `PARSE_ERROR`);
795
- }
796
- }
797
- /**
798
- * Create a synthetic Response from SSE data with proper headers.
799
- * Includes offset/cursor/upToDate/streamClosed in headers so subscribers can read them.
800
- */
801
- #createSSESyntheticResponse(data, offset, cursor, upToDate, streamClosed) {
802
- return this.#createSSESyntheticResponseFromParts([data], offset, cursor, upToDate, streamClosed);
803
- }
804
- /**
805
- * Create a synthetic Response from multiple SSE data parts.
806
- * For base64 mode, each part is independently encoded, so we decode each
807
- * separately and concatenate the binary results.
808
- * For text mode, parts are simply concatenated as strings.
809
- */
810
- #createSSESyntheticResponseFromParts(dataParts, offset, cursor, upToDate, streamClosed) {
811
- const headers = {
812
- "content-type": this.contentType ?? `application/json`,
813
- [STREAM_OFFSET_HEADER]: String(offset)
814
- };
815
- if (cursor) headers[STREAM_CURSOR_HEADER] = cursor;
816
- if (upToDate) headers[STREAM_UP_TO_DATE_HEADER] = `true`;
817
- if (streamClosed) headers[STREAM_CLOSED_HEADER] = `true`;
818
- let body;
819
- if (this.#encoding === `base64`) {
820
- const decodedParts = dataParts.filter((part) => part.length > 0).map((part) => this.#decodeBase64(part));
821
- if (decodedParts.length === 0) body = new ArrayBuffer(0);
822
- else if (decodedParts.length === 1) {
823
- const decoded = decodedParts[0];
824
- body = decoded.buffer.slice(decoded.byteOffset, decoded.byteOffset + decoded.byteLength);
825
- } else {
826
- const totalLength = decodedParts.reduce((sum, part) => sum + part.length, 0);
827
- const combined = new Uint8Array(totalLength);
828
- let offset$1 = 0;
829
- for (const part of decodedParts) {
830
- combined.set(part, offset$1);
831
- offset$1 += part.length;
832
- }
833
- body = combined.buffer;
834
- }
835
- } else body = dataParts.join(``);
836
- return new Response(body, {
837
- status: 200,
838
- headers
839
- });
840
- }
841
- /**
842
954
  * Update instance state from an SSE control event.
843
955
  */
844
956
  #updateStateFromSSEControl(controlEvent) {
845
- this.#offset = controlEvent.streamNextOffset;
846
- if (controlEvent.streamCursor) this.#cursor = controlEvent.streamCursor;
847
- if (controlEvent.upToDate !== void 0) this.#upToDate = controlEvent.upToDate;
848
- if (controlEvent.streamClosed) {
849
- this.#streamClosed = true;
850
- this.#upToDate = true;
851
- }
957
+ this.#syncState = this.#syncState.withSSEControl(controlEvent);
852
958
  }
853
959
  /**
854
960
  * Mark the start of an SSE connection for duration tracking.
961
+ * If the state is not SSEState (e.g., auto-detected SSE from content-type),
962
+ * transitions to SSEState first.
855
963
  */
856
964
  #markSSEConnectionStart() {
857
- this.#lastSSEConnectionStartTime = Date.now();
858
- }
859
- /**
860
- * Handle SSE connection end - check duration and manage fallback state.
861
- * Returns a delay to wait before reconnecting, or null if should not reconnect.
862
- */
863
- async #handleSSEConnectionEnd() {
864
- if (this.#lastSSEConnectionStartTime === void 0) return 0;
865
- const connectionDuration = Date.now() - this.#lastSSEConnectionStartTime;
866
- const wasAborted = this.#abortController.signal.aborted;
867
- if (connectionDuration < this.#sseResilience.minConnectionDuration && !wasAborted) {
868
- this.#consecutiveShortSSEConnections++;
869
- if (this.#consecutiveShortSSEConnections >= this.#sseResilience.maxShortConnections) {
870
- this.#sseFallbackToLongPoll = true;
871
- if (this.#sseResilience.logWarnings) console.warn("[Durable Streams] SSE connections are closing immediately (possibly due to proxy buffering or misconfiguration). Falling back to long polling. Your proxy must support streaming SSE responses (not buffer the complete response). Configuration: Nginx add 'X-Accel-Buffering: no', Caddy add 'flush_interval -1' to reverse_proxy.");
872
- return null;
873
- } else {
874
- const maxDelay = Math.min(this.#sseResilience.backoffMaxDelay, this.#sseResilience.backoffBaseDelay * Math.pow(2, this.#consecutiveShortSSEConnections));
875
- const delayMs = Math.floor(Math.random() * maxDelay);
876
- await new Promise((resolve) => setTimeout(resolve, delayMs));
877
- return delayMs;
878
- }
879
- } else if (connectionDuration >= this.#sseResilience.minConnectionDuration) this.#consecutiveShortSSEConnections = 0;
880
- return 0;
965
+ if (!(this.#syncState instanceof SSEState)) this.#syncState = new SSEState({
966
+ offset: this.#syncState.offset,
967
+ cursor: this.#syncState.cursor,
968
+ upToDate: this.#syncState.upToDate,
969
+ streamClosed: this.#syncState.streamClosed
970
+ });
971
+ this.#syncState = this.#syncState.startConnection(Date.now());
881
972
  }
882
973
  /**
883
974
  * Try to reconnect SSE and return the new iterator, or null if reconnection
884
975
  * is not possible or fails.
885
976
  */
886
977
  async #trySSEReconnect() {
887
- if (this.#sseFallbackToLongPoll) return null;
978
+ if (!this.#syncState.shouldUseSse()) return null;
888
979
  if (!this.#shouldContinueLive() || !this.#startSSE) return null;
889
- const delayOrNull = await this.#handleSSEConnectionEnd();
890
- if (delayOrNull === null) return null;
980
+ const result = this.#syncState.handleConnectionEnd(Date.now(), this.#abortController.signal.aborted, this.#sseResilience);
981
+ this.#syncState = result.state;
982
+ if (result.action === `fallback`) {
983
+ if (this.#sseResilience.logWarnings) console.warn("[Durable Streams] SSE connections are closing immediately (possibly due to proxy buffering or misconfiguration). Falling back to long polling. Your proxy must support streaming SSE responses (not buffer the complete response). Configuration: Nginx add 'X-Accel-Buffering: no', Caddy add 'flush_interval -1' to reverse_proxy.");
984
+ return null;
985
+ }
986
+ if (result.action === `reconnect`) {
987
+ const maxDelay = Math.min(this.#sseResilience.backoffMaxDelay, this.#sseResilience.backoffBaseDelay * Math.pow(2, result.backoffAttempt));
988
+ const delayMs = Math.floor(Math.random() * maxDelay);
989
+ await new Promise((resolve) => setTimeout(resolve, delayMs));
990
+ }
891
991
  this.#markSSEConnectionStart();
892
992
  this.#requestAbortController = new AbortController();
893
993
  const newSSEResponse = await this.#startSSE(this.offset, this.cursor, this.#requestAbortController.signal);
@@ -922,7 +1022,7 @@ var StreamResponseImpl = class {
922
1022
  if (event.type === `data`) return this.#processSSEDataEvent(event.data, sseEventIterator);
923
1023
  this.#updateStateFromSSEControl(event);
924
1024
  if (event.upToDate) {
925
- const response = this.#createSSESyntheticResponse(``, event.streamNextOffset, event.streamCursor, true, event.streamClosed ?? false);
1025
+ const response = createSSESyntheticResponse(``, event.streamNextOffset, event.streamCursor, true, event.streamClosed ?? false, this.contentType, this.#encoding);
926
1026
  return {
927
1027
  type: `response`,
928
1028
  response
@@ -943,7 +1043,7 @@ var StreamResponseImpl = class {
943
1043
  while (true) {
944
1044
  const { done: controlDone, value: controlEvent } = await sseEventIterator.next();
945
1045
  if (controlDone) {
946
- const response = this.#createSSESyntheticResponseFromParts(bufferedDataParts, this.offset, this.cursor, this.upToDate, this.streamClosed);
1046
+ const response = createSSESyntheticResponseFromParts(bufferedDataParts, this.offset, this.cursor, this.upToDate, this.streamClosed, this.contentType, this.#encoding, this.#isJsonMode);
947
1047
  try {
948
1048
  const newIterator = await this.#trySSEReconnect();
949
1049
  return {
@@ -960,7 +1060,7 @@ var StreamResponseImpl = class {
960
1060
  }
961
1061
  if (controlEvent.type === `control`) {
962
1062
  this.#updateStateFromSSEControl(controlEvent);
963
- const response = this.#createSSESyntheticResponseFromParts(bufferedDataParts, controlEvent.streamNextOffset, controlEvent.streamCursor, controlEvent.upToDate ?? false, controlEvent.streamClosed ?? false);
1063
+ const response = createSSESyntheticResponseFromParts(bufferedDataParts, controlEvent.streamNextOffset, controlEvent.streamCursor, controlEvent.upToDate ?? false, controlEvent.streamClosed ?? false, this.contentType, this.#encoding, this.#isJsonMode);
964
1064
  return {
965
1065
  type: `response`,
966
1066
  response
@@ -1038,6 +1138,7 @@ var StreamResponseImpl = class {
1038
1138
  }
1039
1139
  }
1040
1140
  if (this.#shouldContinueLive()) {
1141
+ let resumingFromPause = false;
1041
1142
  if (this.#state === `pause-requested` || this.#state === `paused`) {
1042
1143
  this.#state = `paused`;
1043
1144
  if (this.#pausePromise) await this.#pausePromise;
@@ -1046,14 +1147,13 @@ var StreamResponseImpl = class {
1046
1147
  controller.close();
1047
1148
  return;
1048
1149
  }
1150
+ resumingFromPause = true;
1049
1151
  }
1050
1152
  if (this.#abortController.signal.aborted) {
1051
1153
  this.#markClosed();
1052
1154
  controller.close();
1053
1155
  return;
1054
1156
  }
1055
- const resumingFromPause = this.#justResumedFromPause;
1056
- this.#justResumedFromPause = false;
1057
1157
  this.#requestAbortController = new AbortController();
1058
1158
  const response = await this.#fetchNext(this.offset, this.cursor, this.#requestAbortController.signal, resumingFromPause);
1059
1159
  this.#updateStateFromResponse(response);
@@ -1219,23 +1319,28 @@ var StreamResponseImpl = class {
1219
1319
  controller.enqueue(pendingItems.shift());
1220
1320
  return;
1221
1321
  }
1222
- const { done, value: response } = await reader.read();
1223
- if (done) {
1224
- this.#markClosed();
1225
- controller.close();
1226
- return;
1227
- }
1228
- const text = await response.text();
1229
- const content = text.trim() || `[]`;
1230
- let parsed;
1231
- try {
1232
- parsed = JSON.parse(content);
1233
- } catch (err) {
1234
- const preview = content.length > 100 ? content.slice(0, 100) + `...` : content;
1235
- throw new DurableStreamError(`Failed to parse JSON response: ${err instanceof Error ? err.message : String(err)}. Data: ${preview}`, `PARSE_ERROR`);
1322
+ let result = await reader.read();
1323
+ while (!result.done) {
1324
+ const response = result.value;
1325
+ const text = await response.text();
1326
+ const content = text.trim() || `[]`;
1327
+ let parsed;
1328
+ try {
1329
+ parsed = JSON.parse(content);
1330
+ } catch (err) {
1331
+ const preview = content.length > 100 ? content.slice(0, 100) + `...` : content;
1332
+ throw new DurableStreamError(`Failed to parse JSON response: ${err instanceof Error ? err.message : String(err)}. Data: ${preview}`, `PARSE_ERROR`);
1333
+ }
1334
+ pendingItems = Array.isArray(parsed) ? parsed : [parsed];
1335
+ if (pendingItems.length > 0) {
1336
+ controller.enqueue(pendingItems.shift());
1337
+ return;
1338
+ }
1339
+ result = await reader.read();
1236
1340
  }
1237
- pendingItems = Array.isArray(parsed) ? parsed : [parsed];
1238
- if (pendingItems.length > 0) controller.enqueue(pendingItems.shift());
1341
+ this.#markClosed();
1342
+ controller.close();
1343
+ return;
1239
1344
  },
1240
1345
  cancel: () => {
1241
1346
  reader.releaseLock();
@@ -1269,7 +1374,7 @@ var StreamResponseImpl = class {
1269
1374
  while (!result.done) {
1270
1375
  if (abortController.signal.aborted) break;
1271
1376
  const response = result.value;
1272
- const { offset, cursor, upToDate, streamClosed } = this.#getMetadataFromResponse(response);
1377
+ const { offset, cursor, upToDate, streamClosed } = getMetadataFromResponse(response, this.offset, this.cursor, this.streamClosed);
1273
1378
  const text = await response.text();
1274
1379
  const content = text.trim() || `[]`;
1275
1380
  let parsed;
@@ -1315,7 +1420,7 @@ var StreamResponseImpl = class {
1315
1420
  while (!result.done) {
1316
1421
  if (abortController.signal.aborted) break;
1317
1422
  const response = result.value;
1318
- const { offset, cursor, upToDate, streamClosed } = this.#getMetadataFromResponse(response);
1423
+ const { offset, cursor, upToDate, streamClosed } = getMetadataFromResponse(response, this.offset, this.cursor, this.streamClosed);
1319
1424
  const buffer = await response.arrayBuffer();
1320
1425
  await subscriber({
1321
1426
  data: new Uint8Array(buffer),
@@ -1352,7 +1457,7 @@ var StreamResponseImpl = class {
1352
1457
  while (!result.done) {
1353
1458
  if (abortController.signal.aborted) break;
1354
1459
  const response = result.value;
1355
- const { offset, cursor, upToDate, streamClosed } = this.#getMetadataFromResponse(response);
1460
+ const { offset, cursor, upToDate, streamClosed } = getMetadataFromResponse(response, this.offset, this.cursor, this.streamClosed);
1356
1461
  const text = await response.text();
1357
1462
  await subscriber({
1358
1463
  text,
@@ -1388,6 +1493,97 @@ var StreamResponseImpl = class {
1388
1493
  return this.#closed;
1389
1494
  }
1390
1495
  };
1496
+ /**
1497
+ * Extract stream metadata from Response headers.
1498
+ * Falls back to the provided defaults when headers are absent.
1499
+ */
1500
+ function getMetadataFromResponse(response, fallbackOffset, fallbackCursor, fallbackStreamClosed) {
1501
+ const offset = response.headers.get(STREAM_OFFSET_HEADER);
1502
+ const cursor = response.headers.get(STREAM_CURSOR_HEADER);
1503
+ const upToDate = response.headers.has(STREAM_UP_TO_DATE_HEADER);
1504
+ const streamClosed = response.headers.get(STREAM_CLOSED_HEADER)?.toLowerCase() === `true`;
1505
+ return {
1506
+ offset: offset ?? fallbackOffset,
1507
+ cursor: cursor ?? fallbackCursor,
1508
+ upToDate,
1509
+ streamClosed: streamClosed || fallbackStreamClosed
1510
+ };
1511
+ }
1512
+ /**
1513
+ * Decode base64 string to Uint8Array.
1514
+ * Per protocol: concatenate data lines, remove \n and \r, then decode.
1515
+ */
1516
+ function decodeBase64(base64Str) {
1517
+ const cleaned = base64Str.replace(/[\n\r]/g, ``);
1518
+ if (cleaned.length === 0) return new Uint8Array(0);
1519
+ if (cleaned.length % 4 !== 0) throw new DurableStreamError(`Invalid base64 data: length ${cleaned.length} is not a multiple of 4`, `PARSE_ERROR`);
1520
+ try {
1521
+ if (typeof Buffer !== `undefined`) return new Uint8Array(Buffer.from(cleaned, `base64`));
1522
+ else {
1523
+ const binaryStr = atob(cleaned);
1524
+ const bytes = new Uint8Array(binaryStr.length);
1525
+ for (let i = 0; i < binaryStr.length; i++) bytes[i] = binaryStr.charCodeAt(i);
1526
+ return bytes;
1527
+ }
1528
+ } catch (err) {
1529
+ throw new DurableStreamError(`Failed to decode base64 data: ${err instanceof Error ? err.message : String(err)}`, `PARSE_ERROR`);
1530
+ }
1531
+ }
1532
+ /**
1533
+ * Create a synthetic Response from SSE data with proper headers.
1534
+ * Includes offset/cursor/upToDate/streamClosed in headers so subscribers can read them.
1535
+ */
1536
+ function createSSESyntheticResponse(data, offset, cursor, upToDate, streamClosed, contentType, encoding) {
1537
+ return createSSESyntheticResponseFromParts([data], offset, cursor, upToDate, streamClosed, contentType, encoding);
1538
+ }
1539
+ /**
1540
+ * Create a synthetic Response from multiple SSE data parts.
1541
+ * For base64 mode, each part is independently encoded, so we decode each
1542
+ * separately and concatenate the binary results.
1543
+ * For text mode, parts are simply concatenated as strings.
1544
+ */
1545
+ function createSSESyntheticResponseFromParts(dataParts, offset, cursor, upToDate, streamClosed, contentType, encoding, isJsonMode) {
1546
+ const headers = {
1547
+ "content-type": contentType ?? `application/json`,
1548
+ [STREAM_OFFSET_HEADER]: String(offset)
1549
+ };
1550
+ if (cursor) headers[STREAM_CURSOR_HEADER] = cursor;
1551
+ if (upToDate) headers[STREAM_UP_TO_DATE_HEADER] = `true`;
1552
+ if (streamClosed) headers[STREAM_CLOSED_HEADER] = `true`;
1553
+ let body;
1554
+ if (encoding === `base64`) {
1555
+ const decodedParts = dataParts.filter((part) => part.length > 0).map((part) => decodeBase64(part));
1556
+ if (decodedParts.length === 0) body = new ArrayBuffer(0);
1557
+ else if (decodedParts.length === 1) {
1558
+ const decoded = decodedParts[0];
1559
+ body = decoded.buffer.slice(decoded.byteOffset, decoded.byteOffset + decoded.byteLength);
1560
+ } else {
1561
+ const totalLength = decodedParts.reduce((sum, part) => sum + part.length, 0);
1562
+ const combined = new Uint8Array(totalLength);
1563
+ let offset$1 = 0;
1564
+ for (const part of decodedParts) {
1565
+ combined.set(part, offset$1);
1566
+ offset$1 += part.length;
1567
+ }
1568
+ body = combined.buffer;
1569
+ }
1570
+ } else if (isJsonMode) {
1571
+ const mergedParts = [];
1572
+ for (const part of dataParts) {
1573
+ const trimmed = part.trim();
1574
+ if (trimmed.length === 0) continue;
1575
+ if (trimmed.startsWith(`[`) && trimmed.endsWith(`]`)) {
1576
+ const inner = trimmed.slice(1, -1).trim();
1577
+ if (inner.length > 0) mergedParts.push(inner);
1578
+ } else mergedParts.push(trimmed);
1579
+ }
1580
+ body = `[${mergedParts.join(`,`)}]`;
1581
+ } else body = dataParts.join(``);
1582
+ return new Response(body, {
1583
+ status: 200,
1584
+ headers
1585
+ });
1586
+ }
1391
1587
 
1392
1588
  //#endregion
1393
1589
  //#region src/utils.ts