@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.cjs CHANGED
@@ -579,6 +579,207 @@ async function* parseSSEStream(stream$1, signal) {
579
579
  }
580
580
  }
581
581
 
582
+ //#endregion
583
+ //#region src/stream-response-state.ts
584
+ /**
585
+ * Abstract base class for stream response state.
586
+ * All state transitions return new immutable state objects.
587
+ */
588
+ var StreamResponseState = class {
589
+ shouldContinueLive(stopAfterUpToDate, liveMode) {
590
+ if (stopAfterUpToDate && this.upToDate) return false;
591
+ if (liveMode === false) return false;
592
+ if (this.streamClosed) return false;
593
+ return true;
594
+ }
595
+ };
596
+ /**
597
+ * State for long-poll mode. shouldUseSse() returns false.
598
+ */
599
+ var LongPollState = class LongPollState extends StreamResponseState {
600
+ offset;
601
+ cursor;
602
+ upToDate;
603
+ streamClosed;
604
+ constructor(fields) {
605
+ super();
606
+ this.offset = fields.offset;
607
+ this.cursor = fields.cursor;
608
+ this.upToDate = fields.upToDate;
609
+ this.streamClosed = fields.streamClosed;
610
+ }
611
+ shouldUseSse() {
612
+ return false;
613
+ }
614
+ withResponseMetadata(update) {
615
+ return new LongPollState({
616
+ offset: update.offset ?? this.offset,
617
+ cursor: update.cursor ?? this.cursor,
618
+ upToDate: update.upToDate,
619
+ streamClosed: this.streamClosed || update.streamClosed
620
+ });
621
+ }
622
+ withSSEControl(event) {
623
+ const streamClosed = this.streamClosed || (event.streamClosed ?? false);
624
+ return new LongPollState({
625
+ offset: event.streamNextOffset,
626
+ cursor: event.streamCursor || this.cursor,
627
+ upToDate: event.streamClosed ?? false ? true : event.upToDate ?? this.upToDate,
628
+ streamClosed
629
+ });
630
+ }
631
+ pause() {
632
+ return new PausedState(this);
633
+ }
634
+ };
635
+ /**
636
+ * State for SSE mode. shouldUseSse() returns true.
637
+ * Tracks SSE connection resilience (short connection detection).
638
+ */
639
+ var SSEState = class SSEState extends StreamResponseState {
640
+ offset;
641
+ cursor;
642
+ upToDate;
643
+ streamClosed;
644
+ consecutiveShortConnections;
645
+ connectionStartTime;
646
+ constructor(fields) {
647
+ super();
648
+ this.offset = fields.offset;
649
+ this.cursor = fields.cursor;
650
+ this.upToDate = fields.upToDate;
651
+ this.streamClosed = fields.streamClosed;
652
+ this.consecutiveShortConnections = fields.consecutiveShortConnections ?? 0;
653
+ this.connectionStartTime = fields.connectionStartTime;
654
+ }
655
+ shouldUseSse() {
656
+ return true;
657
+ }
658
+ withResponseMetadata(update) {
659
+ return new SSEState({
660
+ offset: update.offset ?? this.offset,
661
+ cursor: update.cursor ?? this.cursor,
662
+ upToDate: update.upToDate,
663
+ streamClosed: this.streamClosed || update.streamClosed,
664
+ consecutiveShortConnections: this.consecutiveShortConnections,
665
+ connectionStartTime: this.connectionStartTime
666
+ });
667
+ }
668
+ withSSEControl(event) {
669
+ const streamClosed = this.streamClosed || (event.streamClosed ?? false);
670
+ return new SSEState({
671
+ offset: event.streamNextOffset,
672
+ cursor: event.streamCursor || this.cursor,
673
+ upToDate: event.streamClosed ?? false ? true : event.upToDate ?? this.upToDate,
674
+ streamClosed,
675
+ consecutiveShortConnections: this.consecutiveShortConnections,
676
+ connectionStartTime: this.connectionStartTime
677
+ });
678
+ }
679
+ startConnection(now) {
680
+ return new SSEState({
681
+ offset: this.offset,
682
+ cursor: this.cursor,
683
+ upToDate: this.upToDate,
684
+ streamClosed: this.streamClosed,
685
+ consecutiveShortConnections: this.consecutiveShortConnections,
686
+ connectionStartTime: now
687
+ });
688
+ }
689
+ handleConnectionEnd(now, wasAborted, config) {
690
+ if (this.connectionStartTime === void 0) return {
691
+ action: `healthy`,
692
+ state: this
693
+ };
694
+ const duration = now - this.connectionStartTime;
695
+ if (duration < config.minConnectionDuration && !wasAborted) {
696
+ const newCount = this.consecutiveShortConnections + 1;
697
+ if (newCount >= config.maxShortConnections) return {
698
+ action: `fallback`,
699
+ state: new LongPollState({
700
+ offset: this.offset,
701
+ cursor: this.cursor,
702
+ upToDate: this.upToDate,
703
+ streamClosed: this.streamClosed
704
+ })
705
+ };
706
+ return {
707
+ action: `reconnect`,
708
+ state: new SSEState({
709
+ offset: this.offset,
710
+ cursor: this.cursor,
711
+ upToDate: this.upToDate,
712
+ streamClosed: this.streamClosed,
713
+ consecutiveShortConnections: newCount,
714
+ connectionStartTime: this.connectionStartTime
715
+ }),
716
+ backoffAttempt: newCount
717
+ };
718
+ }
719
+ if (duration >= config.minConnectionDuration) return {
720
+ action: `healthy`,
721
+ state: new SSEState({
722
+ offset: this.offset,
723
+ cursor: this.cursor,
724
+ upToDate: this.upToDate,
725
+ streamClosed: this.streamClosed,
726
+ consecutiveShortConnections: 0,
727
+ connectionStartTime: this.connectionStartTime
728
+ })
729
+ };
730
+ return {
731
+ action: `healthy`,
732
+ state: this
733
+ };
734
+ }
735
+ pause() {
736
+ return new PausedState(this);
737
+ }
738
+ };
739
+ /**
740
+ * Paused state wrapper. Delegates all sync field access to the inner state.
741
+ * resume() returns the wrapped state unchanged (identity preserved).
742
+ */
743
+ var PausedState = class PausedState extends StreamResponseState {
744
+ #inner;
745
+ constructor(inner) {
746
+ super();
747
+ this.#inner = inner;
748
+ }
749
+ get offset() {
750
+ return this.#inner.offset;
751
+ }
752
+ get cursor() {
753
+ return this.#inner.cursor;
754
+ }
755
+ get upToDate() {
756
+ return this.#inner.upToDate;
757
+ }
758
+ get streamClosed() {
759
+ return this.#inner.streamClosed;
760
+ }
761
+ shouldUseSse() {
762
+ return this.#inner.shouldUseSse();
763
+ }
764
+ withResponseMetadata(update) {
765
+ const newInner = this.#inner.withResponseMetadata(update);
766
+ return new PausedState(newInner);
767
+ }
768
+ withSSEControl(event) {
769
+ const newInner = this.#inner.withSSEControl(event);
770
+ return new PausedState(newInner);
771
+ }
772
+ pause() {
773
+ return this;
774
+ }
775
+ resume() {
776
+ return {
777
+ state: this.#inner,
778
+ justResumed: true
779
+ };
780
+ }
781
+ };
782
+
582
783
  //#endregion
583
784
  //#region src/response.ts
584
785
  /**
@@ -598,10 +799,7 @@ var StreamResponseImpl = class {
598
799
  #statusText;
599
800
  #ok;
600
801
  #isLoading;
601
- #offset;
602
- #cursor;
603
- #upToDate;
604
- #streamClosed;
802
+ #syncState;
605
803
  #isJsonMode;
606
804
  #abortController;
607
805
  #fetchNext;
@@ -616,11 +814,7 @@ var StreamResponseImpl = class {
616
814
  #unsubscribeFromVisibilityChanges;
617
815
  #pausePromise;
618
816
  #pauseResolve;
619
- #justResumedFromPause = false;
620
817
  #sseResilience;
621
- #lastSSEConnectionStartTime;
622
- #consecutiveShortSSEConnections = 0;
623
- #sseFallbackToLongPoll = false;
624
818
  #encoding;
625
819
  #responseStream;
626
820
  constructor(config) {
@@ -628,10 +822,13 @@ var StreamResponseImpl = class {
628
822
  this.contentType = config.contentType;
629
823
  this.live = config.live;
630
824
  this.startOffset = config.startOffset;
631
- this.#offset = config.initialOffset;
632
- this.#cursor = config.initialCursor;
633
- this.#upToDate = config.initialUpToDate;
634
- this.#streamClosed = config.initialStreamClosed;
825
+ const syncFields = {
826
+ offset: config.initialOffset,
827
+ cursor: config.initialCursor,
828
+ upToDate: config.initialUpToDate,
829
+ streamClosed: config.initialStreamClosed
830
+ };
831
+ this.#syncState = config.startSSE ? new SSEState(syncFields) : new LongPollState(syncFields);
635
832
  this.#headers = config.firstResponse.headers;
636
833
  this.#status = config.firstResponse.status;
637
834
  this.#statusText = config.firstResponse.statusText;
@@ -688,6 +885,7 @@ var StreamResponseImpl = class {
688
885
  #pause() {
689
886
  if (this.#state === `active`) {
690
887
  this.#state = `pause-requested`;
888
+ this.#syncState = this.#syncState.pause();
691
889
  this.#pausePromise = new Promise((resolve) => {
692
890
  this.#pauseResolve = resolve;
693
891
  });
@@ -701,8 +899,8 @@ var StreamResponseImpl = class {
701
899
  #resume() {
702
900
  if (this.#state === `paused` || this.#state === `pause-requested`) {
703
901
  if (this.#abortController.signal.aborted) return;
902
+ if (this.#syncState instanceof PausedState) this.#syncState = this.#syncState.resume().state;
704
903
  this.#state = `active`;
705
- this.#justResumedFromPause = true;
706
904
  this.#pauseResolve?.();
707
905
  this.#pausePromise = void 0;
708
906
  this.#pauseResolve = void 0;
@@ -724,16 +922,16 @@ var StreamResponseImpl = class {
724
922
  return this.#isLoading;
725
923
  }
726
924
  get offset() {
727
- return this.#offset;
925
+ return this.#syncState.offset;
728
926
  }
729
927
  get cursor() {
730
- return this.#cursor;
928
+ return this.#syncState.cursor;
731
929
  }
732
930
  get upToDate() {
733
- return this.#upToDate;
931
+ return this.#syncState.upToDate;
734
932
  }
735
933
  get streamClosed() {
736
- return this.#streamClosed;
934
+ return this.#syncState.streamClosed;
737
935
  }
738
936
  #ensureJsonMode() {
739
937
  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`);
@@ -759,159 +957,61 @@ var StreamResponseImpl = class {
759
957
  * and whether we've received upToDate or streamClosed.
760
958
  */
761
959
  #shouldContinueLive() {
762
- if (this.#stopAfterUpToDate && this.upToDate) return false;
763
- if (this.live === false) return false;
764
- if (this.#streamClosed) return false;
765
- return true;
960
+ return this.#syncState.shouldContinueLive(this.#stopAfterUpToDate, this.live);
766
961
  }
767
962
  /**
768
963
  * Update state from response headers.
769
964
  */
770
965
  #updateStateFromResponse(response) {
771
- const offset = response.headers.get(STREAM_OFFSET_HEADER);
772
- if (offset) this.#offset = offset;
773
- const cursor = response.headers.get(STREAM_CURSOR_HEADER);
774
- if (cursor) this.#cursor = cursor;
775
- this.#upToDate = response.headers.has(STREAM_UP_TO_DATE_HEADER);
776
- const streamClosedHeader = response.headers.get(STREAM_CLOSED_HEADER);
777
- if (streamClosedHeader?.toLowerCase() === `true`) this.#streamClosed = true;
966
+ this.#syncState = this.#syncState.withResponseMetadata({
967
+ offset: response.headers.get(STREAM_OFFSET_HEADER) || void 0,
968
+ cursor: response.headers.get(STREAM_CURSOR_HEADER) || void 0,
969
+ upToDate: response.headers.has(STREAM_UP_TO_DATE_HEADER),
970
+ streamClosed: response.headers.get(STREAM_CLOSED_HEADER)?.toLowerCase() === `true`
971
+ });
778
972
  this.#headers = response.headers;
779
973
  this.#status = response.status;
780
974
  this.#statusText = response.statusText;
781
975
  this.#ok = response.ok;
782
976
  }
783
977
  /**
784
- * Extract stream metadata from Response headers.
785
- * Used by subscriber APIs to get the correct offset/cursor/upToDate/streamClosed for each
786
- * specific Response, rather than reading from `this` which may be stale due to
787
- * ReadableStream prefetching or timing issues.
788
- */
789
- #getMetadataFromResponse(response) {
790
- const offset = response.headers.get(STREAM_OFFSET_HEADER);
791
- const cursor = response.headers.get(STREAM_CURSOR_HEADER);
792
- const upToDate = response.headers.has(STREAM_UP_TO_DATE_HEADER);
793
- const streamClosed = response.headers.get(STREAM_CLOSED_HEADER)?.toLowerCase() === `true`;
794
- return {
795
- offset: offset ?? this.offset,
796
- cursor: cursor ?? this.cursor,
797
- upToDate,
798
- streamClosed: streamClosed || this.streamClosed
799
- };
800
- }
801
- /**
802
- * Decode base64 string to Uint8Array.
803
- * Per protocol: concatenate data lines, remove \n and \r, then decode.
804
- */
805
- #decodeBase64(base64Str) {
806
- const cleaned = base64Str.replace(/[\n\r]/g, ``);
807
- if (cleaned.length === 0) return new Uint8Array(0);
808
- if (cleaned.length % 4 !== 0) throw new DurableStreamError(`Invalid base64 data: length ${cleaned.length} is not a multiple of 4`, `PARSE_ERROR`);
809
- try {
810
- if (typeof Buffer !== `undefined`) return new Uint8Array(Buffer.from(cleaned, `base64`));
811
- else {
812
- const binaryStr = atob(cleaned);
813
- const bytes = new Uint8Array(binaryStr.length);
814
- for (let i = 0; i < binaryStr.length; i++) bytes[i] = binaryStr.charCodeAt(i);
815
- return bytes;
816
- }
817
- } catch (err) {
818
- throw new DurableStreamError(`Failed to decode base64 data: ${err instanceof Error ? err.message : String(err)}`, `PARSE_ERROR`);
819
- }
820
- }
821
- /**
822
- * Create a synthetic Response from SSE data with proper headers.
823
- * Includes offset/cursor/upToDate/streamClosed in headers so subscribers can read them.
824
- */
825
- #createSSESyntheticResponse(data, offset, cursor, upToDate, streamClosed) {
826
- return this.#createSSESyntheticResponseFromParts([data], offset, cursor, upToDate, streamClosed);
827
- }
828
- /**
829
- * Create a synthetic Response from multiple SSE data parts.
830
- * For base64 mode, each part is independently encoded, so we decode each
831
- * separately and concatenate the binary results.
832
- * For text mode, parts are simply concatenated as strings.
833
- */
834
- #createSSESyntheticResponseFromParts(dataParts, offset, cursor, upToDate, streamClosed) {
835
- const headers = {
836
- "content-type": this.contentType ?? `application/json`,
837
- [STREAM_OFFSET_HEADER]: String(offset)
838
- };
839
- if (cursor) headers[STREAM_CURSOR_HEADER] = cursor;
840
- if (upToDate) headers[STREAM_UP_TO_DATE_HEADER] = `true`;
841
- if (streamClosed) headers[STREAM_CLOSED_HEADER] = `true`;
842
- let body;
843
- if (this.#encoding === `base64`) {
844
- const decodedParts = dataParts.filter((part) => part.length > 0).map((part) => this.#decodeBase64(part));
845
- if (decodedParts.length === 0) body = new ArrayBuffer(0);
846
- else if (decodedParts.length === 1) {
847
- const decoded = decodedParts[0];
848
- body = decoded.buffer.slice(decoded.byteOffset, decoded.byteOffset + decoded.byteLength);
849
- } else {
850
- const totalLength = decodedParts.reduce((sum, part) => sum + part.length, 0);
851
- const combined = new Uint8Array(totalLength);
852
- let offset$1 = 0;
853
- for (const part of decodedParts) {
854
- combined.set(part, offset$1);
855
- offset$1 += part.length;
856
- }
857
- body = combined.buffer;
858
- }
859
- } else body = dataParts.join(``);
860
- return new Response(body, {
861
- status: 200,
862
- headers
863
- });
864
- }
865
- /**
866
978
  * Update instance state from an SSE control event.
867
979
  */
868
980
  #updateStateFromSSEControl(controlEvent) {
869
- this.#offset = controlEvent.streamNextOffset;
870
- if (controlEvent.streamCursor) this.#cursor = controlEvent.streamCursor;
871
- if (controlEvent.upToDate !== void 0) this.#upToDate = controlEvent.upToDate;
872
- if (controlEvent.streamClosed) {
873
- this.#streamClosed = true;
874
- this.#upToDate = true;
875
- }
981
+ this.#syncState = this.#syncState.withSSEControl(controlEvent);
876
982
  }
877
983
  /**
878
984
  * Mark the start of an SSE connection for duration tracking.
985
+ * If the state is not SSEState (e.g., auto-detected SSE from content-type),
986
+ * transitions to SSEState first.
879
987
  */
880
988
  #markSSEConnectionStart() {
881
- this.#lastSSEConnectionStartTime = Date.now();
882
- }
883
- /**
884
- * Handle SSE connection end - check duration and manage fallback state.
885
- * Returns a delay to wait before reconnecting, or null if should not reconnect.
886
- */
887
- async #handleSSEConnectionEnd() {
888
- if (this.#lastSSEConnectionStartTime === void 0) return 0;
889
- const connectionDuration = Date.now() - this.#lastSSEConnectionStartTime;
890
- const wasAborted = this.#abortController.signal.aborted;
891
- if (connectionDuration < this.#sseResilience.minConnectionDuration && !wasAborted) {
892
- this.#consecutiveShortSSEConnections++;
893
- if (this.#consecutiveShortSSEConnections >= this.#sseResilience.maxShortConnections) {
894
- this.#sseFallbackToLongPoll = true;
895
- 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.");
896
- return null;
897
- } else {
898
- const maxDelay = Math.min(this.#sseResilience.backoffMaxDelay, this.#sseResilience.backoffBaseDelay * Math.pow(2, this.#consecutiveShortSSEConnections));
899
- const delayMs = Math.floor(Math.random() * maxDelay);
900
- await new Promise((resolve) => setTimeout(resolve, delayMs));
901
- return delayMs;
902
- }
903
- } else if (connectionDuration >= this.#sseResilience.minConnectionDuration) this.#consecutiveShortSSEConnections = 0;
904
- return 0;
989
+ if (!(this.#syncState instanceof SSEState)) this.#syncState = new SSEState({
990
+ offset: this.#syncState.offset,
991
+ cursor: this.#syncState.cursor,
992
+ upToDate: this.#syncState.upToDate,
993
+ streamClosed: this.#syncState.streamClosed
994
+ });
995
+ this.#syncState = this.#syncState.startConnection(Date.now());
905
996
  }
906
997
  /**
907
998
  * Try to reconnect SSE and return the new iterator, or null if reconnection
908
999
  * is not possible or fails.
909
1000
  */
910
1001
  async #trySSEReconnect() {
911
- if (this.#sseFallbackToLongPoll) return null;
1002
+ if (!this.#syncState.shouldUseSse()) return null;
912
1003
  if (!this.#shouldContinueLive() || !this.#startSSE) return null;
913
- const delayOrNull = await this.#handleSSEConnectionEnd();
914
- if (delayOrNull === null) return null;
1004
+ const result = this.#syncState.handleConnectionEnd(Date.now(), this.#abortController.signal.aborted, this.#sseResilience);
1005
+ this.#syncState = result.state;
1006
+ if (result.action === `fallback`) {
1007
+ 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.");
1008
+ return null;
1009
+ }
1010
+ if (result.action === `reconnect`) {
1011
+ const maxDelay = Math.min(this.#sseResilience.backoffMaxDelay, this.#sseResilience.backoffBaseDelay * Math.pow(2, result.backoffAttempt));
1012
+ const delayMs = Math.floor(Math.random() * maxDelay);
1013
+ await new Promise((resolve) => setTimeout(resolve, delayMs));
1014
+ }
915
1015
  this.#markSSEConnectionStart();
916
1016
  this.#requestAbortController = new AbortController();
917
1017
  const newSSEResponse = await this.#startSSE(this.offset, this.cursor, this.#requestAbortController.signal);
@@ -946,7 +1046,7 @@ var StreamResponseImpl = class {
946
1046
  if (event.type === `data`) return this.#processSSEDataEvent(event.data, sseEventIterator);
947
1047
  this.#updateStateFromSSEControl(event);
948
1048
  if (event.upToDate) {
949
- const response = this.#createSSESyntheticResponse(``, event.streamNextOffset, event.streamCursor, true, event.streamClosed ?? false);
1049
+ const response = createSSESyntheticResponse(``, event.streamNextOffset, event.streamCursor, true, event.streamClosed ?? false, this.contentType, this.#encoding);
950
1050
  return {
951
1051
  type: `response`,
952
1052
  response
@@ -967,7 +1067,7 @@ var StreamResponseImpl = class {
967
1067
  while (true) {
968
1068
  const { done: controlDone, value: controlEvent } = await sseEventIterator.next();
969
1069
  if (controlDone) {
970
- const response = this.#createSSESyntheticResponseFromParts(bufferedDataParts, this.offset, this.cursor, this.upToDate, this.streamClosed);
1070
+ const response = createSSESyntheticResponseFromParts(bufferedDataParts, this.offset, this.cursor, this.upToDate, this.streamClosed, this.contentType, this.#encoding, this.#isJsonMode);
971
1071
  try {
972
1072
  const newIterator = await this.#trySSEReconnect();
973
1073
  return {
@@ -984,7 +1084,7 @@ var StreamResponseImpl = class {
984
1084
  }
985
1085
  if (controlEvent.type === `control`) {
986
1086
  this.#updateStateFromSSEControl(controlEvent);
987
- const response = this.#createSSESyntheticResponseFromParts(bufferedDataParts, controlEvent.streamNextOffset, controlEvent.streamCursor, controlEvent.upToDate ?? false, controlEvent.streamClosed ?? false);
1087
+ const response = createSSESyntheticResponseFromParts(bufferedDataParts, controlEvent.streamNextOffset, controlEvent.streamCursor, controlEvent.upToDate ?? false, controlEvent.streamClosed ?? false, this.contentType, this.#encoding, this.#isJsonMode);
988
1088
  return {
989
1089
  type: `response`,
990
1090
  response
@@ -1062,6 +1162,7 @@ var StreamResponseImpl = class {
1062
1162
  }
1063
1163
  }
1064
1164
  if (this.#shouldContinueLive()) {
1165
+ let resumingFromPause = false;
1065
1166
  if (this.#state === `pause-requested` || this.#state === `paused`) {
1066
1167
  this.#state = `paused`;
1067
1168
  if (this.#pausePromise) await this.#pausePromise;
@@ -1070,14 +1171,13 @@ var StreamResponseImpl = class {
1070
1171
  controller.close();
1071
1172
  return;
1072
1173
  }
1174
+ resumingFromPause = true;
1073
1175
  }
1074
1176
  if (this.#abortController.signal.aborted) {
1075
1177
  this.#markClosed();
1076
1178
  controller.close();
1077
1179
  return;
1078
1180
  }
1079
- const resumingFromPause = this.#justResumedFromPause;
1080
- this.#justResumedFromPause = false;
1081
1181
  this.#requestAbortController = new AbortController();
1082
1182
  const response = await this.#fetchNext(this.offset, this.cursor, this.#requestAbortController.signal, resumingFromPause);
1083
1183
  this.#updateStateFromResponse(response);
@@ -1243,23 +1343,28 @@ var StreamResponseImpl = class {
1243
1343
  controller.enqueue(pendingItems.shift());
1244
1344
  return;
1245
1345
  }
1246
- const { done, value: response } = await reader.read();
1247
- if (done) {
1248
- this.#markClosed();
1249
- controller.close();
1250
- return;
1251
- }
1252
- const text = await response.text();
1253
- const content = text.trim() || `[]`;
1254
- let parsed;
1255
- try {
1256
- parsed = JSON.parse(content);
1257
- } catch (err) {
1258
- const preview = content.length > 100 ? content.slice(0, 100) + `...` : content;
1259
- throw new DurableStreamError(`Failed to parse JSON response: ${err instanceof Error ? err.message : String(err)}. Data: ${preview}`, `PARSE_ERROR`);
1346
+ let result = await reader.read();
1347
+ while (!result.done) {
1348
+ const response = result.value;
1349
+ const text = await response.text();
1350
+ const content = text.trim() || `[]`;
1351
+ let parsed;
1352
+ try {
1353
+ parsed = JSON.parse(content);
1354
+ } catch (err) {
1355
+ const preview = content.length > 100 ? content.slice(0, 100) + `...` : content;
1356
+ throw new DurableStreamError(`Failed to parse JSON response: ${err instanceof Error ? err.message : String(err)}. Data: ${preview}`, `PARSE_ERROR`);
1357
+ }
1358
+ pendingItems = Array.isArray(parsed) ? parsed : [parsed];
1359
+ if (pendingItems.length > 0) {
1360
+ controller.enqueue(pendingItems.shift());
1361
+ return;
1362
+ }
1363
+ result = await reader.read();
1260
1364
  }
1261
- pendingItems = Array.isArray(parsed) ? parsed : [parsed];
1262
- if (pendingItems.length > 0) controller.enqueue(pendingItems.shift());
1365
+ this.#markClosed();
1366
+ controller.close();
1367
+ return;
1263
1368
  },
1264
1369
  cancel: () => {
1265
1370
  reader.releaseLock();
@@ -1293,7 +1398,7 @@ var StreamResponseImpl = class {
1293
1398
  while (!result.done) {
1294
1399
  if (abortController.signal.aborted) break;
1295
1400
  const response = result.value;
1296
- const { offset, cursor, upToDate, streamClosed } = this.#getMetadataFromResponse(response);
1401
+ const { offset, cursor, upToDate, streamClosed } = getMetadataFromResponse(response, this.offset, this.cursor, this.streamClosed);
1297
1402
  const text = await response.text();
1298
1403
  const content = text.trim() || `[]`;
1299
1404
  let parsed;
@@ -1339,7 +1444,7 @@ var StreamResponseImpl = class {
1339
1444
  while (!result.done) {
1340
1445
  if (abortController.signal.aborted) break;
1341
1446
  const response = result.value;
1342
- const { offset, cursor, upToDate, streamClosed } = this.#getMetadataFromResponse(response);
1447
+ const { offset, cursor, upToDate, streamClosed } = getMetadataFromResponse(response, this.offset, this.cursor, this.streamClosed);
1343
1448
  const buffer = await response.arrayBuffer();
1344
1449
  await subscriber({
1345
1450
  data: new Uint8Array(buffer),
@@ -1376,7 +1481,7 @@ var StreamResponseImpl = class {
1376
1481
  while (!result.done) {
1377
1482
  if (abortController.signal.aborted) break;
1378
1483
  const response = result.value;
1379
- const { offset, cursor, upToDate, streamClosed } = this.#getMetadataFromResponse(response);
1484
+ const { offset, cursor, upToDate, streamClosed } = getMetadataFromResponse(response, this.offset, this.cursor, this.streamClosed);
1380
1485
  const text = await response.text();
1381
1486
  await subscriber({
1382
1487
  text,
@@ -1412,6 +1517,97 @@ var StreamResponseImpl = class {
1412
1517
  return this.#closed;
1413
1518
  }
1414
1519
  };
1520
+ /**
1521
+ * Extract stream metadata from Response headers.
1522
+ * Falls back to the provided defaults when headers are absent.
1523
+ */
1524
+ function getMetadataFromResponse(response, fallbackOffset, fallbackCursor, fallbackStreamClosed) {
1525
+ const offset = response.headers.get(STREAM_OFFSET_HEADER);
1526
+ const cursor = response.headers.get(STREAM_CURSOR_HEADER);
1527
+ const upToDate = response.headers.has(STREAM_UP_TO_DATE_HEADER);
1528
+ const streamClosed = response.headers.get(STREAM_CLOSED_HEADER)?.toLowerCase() === `true`;
1529
+ return {
1530
+ offset: offset ?? fallbackOffset,
1531
+ cursor: cursor ?? fallbackCursor,
1532
+ upToDate,
1533
+ streamClosed: streamClosed || fallbackStreamClosed
1534
+ };
1535
+ }
1536
+ /**
1537
+ * Decode base64 string to Uint8Array.
1538
+ * Per protocol: concatenate data lines, remove \n and \r, then decode.
1539
+ */
1540
+ function decodeBase64(base64Str) {
1541
+ const cleaned = base64Str.replace(/[\n\r]/g, ``);
1542
+ if (cleaned.length === 0) return new Uint8Array(0);
1543
+ if (cleaned.length % 4 !== 0) throw new DurableStreamError(`Invalid base64 data: length ${cleaned.length} is not a multiple of 4`, `PARSE_ERROR`);
1544
+ try {
1545
+ if (typeof Buffer !== `undefined`) return new Uint8Array(Buffer.from(cleaned, `base64`));
1546
+ else {
1547
+ const binaryStr = atob(cleaned);
1548
+ const bytes = new Uint8Array(binaryStr.length);
1549
+ for (let i = 0; i < binaryStr.length; i++) bytes[i] = binaryStr.charCodeAt(i);
1550
+ return bytes;
1551
+ }
1552
+ } catch (err) {
1553
+ throw new DurableStreamError(`Failed to decode base64 data: ${err instanceof Error ? err.message : String(err)}`, `PARSE_ERROR`);
1554
+ }
1555
+ }
1556
+ /**
1557
+ * Create a synthetic Response from SSE data with proper headers.
1558
+ * Includes offset/cursor/upToDate/streamClosed in headers so subscribers can read them.
1559
+ */
1560
+ function createSSESyntheticResponse(data, offset, cursor, upToDate, streamClosed, contentType, encoding) {
1561
+ return createSSESyntheticResponseFromParts([data], offset, cursor, upToDate, streamClosed, contentType, encoding);
1562
+ }
1563
+ /**
1564
+ * Create a synthetic Response from multiple SSE data parts.
1565
+ * For base64 mode, each part is independently encoded, so we decode each
1566
+ * separately and concatenate the binary results.
1567
+ * For text mode, parts are simply concatenated as strings.
1568
+ */
1569
+ function createSSESyntheticResponseFromParts(dataParts, offset, cursor, upToDate, streamClosed, contentType, encoding, isJsonMode) {
1570
+ const headers = {
1571
+ "content-type": contentType ?? `application/json`,
1572
+ [STREAM_OFFSET_HEADER]: String(offset)
1573
+ };
1574
+ if (cursor) headers[STREAM_CURSOR_HEADER] = cursor;
1575
+ if (upToDate) headers[STREAM_UP_TO_DATE_HEADER] = `true`;
1576
+ if (streamClosed) headers[STREAM_CLOSED_HEADER] = `true`;
1577
+ let body;
1578
+ if (encoding === `base64`) {
1579
+ const decodedParts = dataParts.filter((part) => part.length > 0).map((part) => decodeBase64(part));
1580
+ if (decodedParts.length === 0) body = new ArrayBuffer(0);
1581
+ else if (decodedParts.length === 1) {
1582
+ const decoded = decodedParts[0];
1583
+ body = decoded.buffer.slice(decoded.byteOffset, decoded.byteOffset + decoded.byteLength);
1584
+ } else {
1585
+ const totalLength = decodedParts.reduce((sum, part) => sum + part.length, 0);
1586
+ const combined = new Uint8Array(totalLength);
1587
+ let offset$1 = 0;
1588
+ for (const part of decodedParts) {
1589
+ combined.set(part, offset$1);
1590
+ offset$1 += part.length;
1591
+ }
1592
+ body = combined.buffer;
1593
+ }
1594
+ } else if (isJsonMode) {
1595
+ const mergedParts = [];
1596
+ for (const part of dataParts) {
1597
+ const trimmed = part.trim();
1598
+ if (trimmed.length === 0) continue;
1599
+ if (trimmed.startsWith(`[`) && trimmed.endsWith(`]`)) {
1600
+ const inner = trimmed.slice(1, -1).trim();
1601
+ if (inner.length > 0) mergedParts.push(inner);
1602
+ } else mergedParts.push(trimmed);
1603
+ }
1604
+ body = `[${mergedParts.join(`,`)}]`;
1605
+ } else body = dataParts.join(``);
1606
+ return new Response(body, {
1607
+ status: 200,
1608
+ headers
1609
+ });
1610
+ }
1415
1611
 
1416
1612
  //#endregion
1417
1613
  //#region src/utils.ts