@loro-dev/streams-crdt 0.8.1 → 0.8.3

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.
@@ -597,6 +597,12 @@ function mergeAbortSignals(...signals) {
597
597
  function classifyFetchError(err) {
598
598
  const message = err instanceof Error ? err.message : String(err);
599
599
  const status = err instanceof HttpError ? err.status : void 0;
600
+ if (isCrdtApplyIncompleteError(err)) return {
601
+ code: "apply_incomplete",
602
+ message,
603
+ retriable: true,
604
+ atMs: Date.now()
605
+ };
600
606
  if (status === 401 || status === 403) return {
601
607
  code: "auth_failed",
602
608
  message,
@@ -650,6 +656,12 @@ function isStreamNotFoundError(error) {
650
656
  return error instanceof HttpError && error.status === 404;
651
657
  }
652
658
  function toTransportError(error) {
659
+ if (isCrdtApplyIncompleteError(error)) return {
660
+ code: "apply_incomplete",
661
+ retryable: true,
662
+ reason: "loro_pending_updates",
663
+ message: error instanceof Error ? error.message : String(error)
664
+ };
653
665
  if (isStreamNotFoundError(error)) return {
654
666
  code: "stream_not_found",
655
667
  retryable: false
@@ -710,6 +722,9 @@ function toTransportError(error) {
710
722
  message: String(error)
711
723
  };
712
724
  }
725
+ function isCrdtApplyIncompleteError(error) {
726
+ return error instanceof Error && Array.isArray(error.unresolved) && error.message.includes("CRDT apply incomplete");
727
+ }
713
728
  function isReadNoProgress(requestOffset, result) {
714
729
  return !result.upToDate && result.updates.length === 0 && result.nextOffset === requestOffset;
715
730
  }
@@ -1714,67 +1729,115 @@ function createInitialRemoteCursor(input) {
1714
1729
  }
1715
1730
  //#endregion
1716
1731
  //#region src/transport-cursor.ts
1732
+ var CrdtApplyIncompleteError = class extends Error {
1733
+ unresolved;
1734
+ constructor(unresolved) {
1735
+ super("CRDT apply incomplete: unresolved remote updates remain");
1736
+ this.unresolved = unresolved;
1737
+ }
1738
+ };
1739
+ function isApplyOutcome(value) {
1740
+ return typeof value === "object" && value != null && "version" in value && "complete" in value && typeof value.complete === "boolean";
1741
+ }
1742
+ function normalizeApplyReturn(value) {
1743
+ if (isApplyOutcome(value)) return value;
1744
+ return {
1745
+ version: value,
1746
+ complete: true
1747
+ };
1748
+ }
1717
1749
  var TransportCursorManager = class {
1718
1750
  adapter;
1719
1751
  decodeSnapshot;
1720
1752
  decodeUpdateItems;
1721
1753
  saveRemoteCursor;
1754
+ incompleteCursors = /* @__PURE__ */ new WeakMap();
1722
1755
  constructor(options) {
1723
1756
  this.adapter = options.adapter;
1724
1757
  this.decodeSnapshot = options.decodeSnapshot;
1725
1758
  this.decodeUpdateItems = options.decodeUpdateItems;
1726
1759
  this.saveRemoteCursor = options.saveRemoteCursor;
1727
1760
  }
1728
- async applyBootstrapPayload(cursor, snapshot, updates, nextOffset) {
1761
+ isCursorComplete(cursor) {
1762
+ return !this.incompleteCursors.has(cursor);
1763
+ }
1764
+ async applyBootstrapPayload(cursor, snapshot, updates, nextOffset, upToDate = false) {
1729
1765
  let nextVersion = cursor.serverLowerBoundVersion;
1730
- if (snapshot.byteLength > 0) nextVersion = await this.adapter.applySnapshot(this.decodeSnapshot == null ? snapshot : await this.decodeSnapshot(snapshot));
1766
+ let complete = true;
1767
+ let unresolved = [];
1768
+ if (snapshot.byteLength > 0) {
1769
+ const outcome = normalizeApplyReturn(await this.adapter.applySnapshot(this.decodeSnapshot == null ? snapshot : await this.decodeSnapshot(snapshot)));
1770
+ nextVersion = outcome.version;
1771
+ complete = outcome.complete;
1772
+ unresolved = outcome.unresolved ?? [];
1773
+ }
1731
1774
  const clearUpdates = await this.decodeRemoteUpdateItems(updates);
1732
- if (clearUpdates.length > 0) nextVersion = await this.adapter.applyRemoteUpdates(clearUpdates, nextVersion);
1775
+ if (clearUpdates.length > 0) {
1776
+ const outcome = normalizeApplyReturn(await this.adapter.applyRemoteUpdates(clearUpdates, nextVersion));
1777
+ nextVersion = outcome.version;
1778
+ complete = outcome.complete;
1779
+ unresolved = outcome.unresolved ?? [];
1780
+ }
1733
1781
  return await this.finalizeCursor({
1734
1782
  ...cursor,
1735
1783
  nextOffset,
1736
1784
  serverLowerBoundVersion: nextVersion
1737
- }, "bootstrap");
1785
+ }, "bootstrap", complete, unresolved, upToDate);
1738
1786
  }
1739
- async applyBootstrapItems(cursor, items, nextOffset) {
1787
+ async applyBootstrapItems(cursor, items, nextOffset, upToDate = false) {
1740
1788
  const snapshot = items[0] ?? new Uint8Array();
1741
- return await this.applyBootstrapPayload(cursor, snapshot, items.slice(1), nextOffset);
1789
+ return await this.applyBootstrapPayload(cursor, snapshot, items.slice(1), nextOffset, upToDate);
1742
1790
  }
1743
1791
  async applyRemoteItems(cursor, items, nextOffset) {
1744
1792
  return await this.applyRemotePayload(cursor, [...items], nextOffset, "remote", void 0);
1745
1793
  }
1746
1794
  async applyCatchupResult(cursor, result, onRemoteVersionApplied) {
1747
- return await this.applyRemotePayload(cursor, result.updates, result.nextOffset, "remote", onRemoteVersionApplied);
1795
+ return await this.applyRemotePayload(cursor, result.updates, result.nextOffset, "remote", onRemoteVersionApplied, result.upToDate);
1748
1796
  }
1749
1797
  async applyLiveResult(cursor, requestOffset, result, onRemoteVersionApplied) {
1750
1798
  const previousVersion = cursor.serverLowerBoundVersion;
1751
1799
  let nextVersion = previousVersion;
1800
+ let complete = this.isCursorComplete(cursor);
1801
+ let unresolved = this.incompleteCursors.get(cursor) ?? [];
1752
1802
  const clearUpdates = await this.decodeRemoteUpdateItems(result.updates);
1753
1803
  if (clearUpdates.length > 0) {
1754
- nextVersion = await this.adapter.applyRemoteUpdates(clearUpdates, nextVersion);
1804
+ const outcome = normalizeApplyReturn(await this.adapter.applyRemoteUpdates(clearUpdates, nextVersion));
1805
+ nextVersion = outcome.version;
1806
+ complete = outcome.complete;
1807
+ unresolved = outcome.unresolved ?? [];
1755
1808
  await onRemoteVersionApplied?.(previousVersion, nextVersion);
1756
1809
  }
1757
1810
  return await this.finalizeCursor({
1758
1811
  ...cursor,
1759
1812
  nextOffset: resolveNextOffsetAfterConcurrentAdvance(requestOffset, cursor.nextOffset, result.nextOffset),
1760
1813
  serverLowerBoundVersion: nextVersion
1761
- }, "remote");
1814
+ }, "remote", complete, unresolved, result.upToDate === true);
1762
1815
  }
1763
- async applyRemotePayload(cursor, updates, nextOffset, source, onRemoteVersionApplied) {
1816
+ async applyRemotePayload(cursor, updates, nextOffset, source, onRemoteVersionApplied, upToDate = false) {
1764
1817
  const previousVersion = cursor.serverLowerBoundVersion;
1765
1818
  let nextVersion = previousVersion;
1819
+ let complete = this.isCursorComplete(cursor);
1820
+ let unresolved = this.incompleteCursors.get(cursor) ?? [];
1766
1821
  const clearUpdates = await this.decodeRemoteUpdateItems(updates);
1767
1822
  if (clearUpdates.length > 0) {
1768
- nextVersion = await this.adapter.applyRemoteUpdates(clearUpdates, nextVersion);
1823
+ const outcome = normalizeApplyReturn(await this.adapter.applyRemoteUpdates(clearUpdates, nextVersion));
1824
+ nextVersion = outcome.version;
1825
+ complete = outcome.complete;
1826
+ unresolved = outcome.unresolved ?? [];
1769
1827
  await onRemoteVersionApplied?.(previousVersion, nextVersion);
1770
1828
  }
1771
1829
  return await this.finalizeCursor({
1772
1830
  ...cursor,
1773
1831
  nextOffset,
1774
1832
  serverLowerBoundVersion: nextVersion
1775
- }, source);
1833
+ }, source, complete, unresolved, upToDate);
1776
1834
  }
1777
- async finalizeCursor(cursor, source) {
1835
+ async finalizeCursor(cursor, source, complete, unresolved, upToDate) {
1836
+ if (!complete) {
1837
+ if (upToDate) throw new CrdtApplyIncompleteError(unresolved);
1838
+ this.incompleteCursors.set(cursor, unresolved);
1839
+ return cursor;
1840
+ }
1778
1841
  return await this.saveRemoteCursor(cursor, source);
1779
1842
  }
1780
1843
  async decodeRemoteUpdateItems(items) {
@@ -1807,6 +1870,7 @@ var TransportSnapshotManager = class {
1807
1870
  logDebug;
1808
1871
  logError;
1809
1872
  isCurrentState;
1873
+ isCursorComplete;
1810
1874
  constructor(options) {
1811
1875
  this.adapter = options.adapter;
1812
1876
  this.config = options.config;
@@ -1817,6 +1881,7 @@ var TransportSnapshotManager = class {
1817
1881
  this.logDebug = options.logDebug;
1818
1882
  this.logError = options.logError;
1819
1883
  this.isCurrentState = options.isCurrentState;
1884
+ this.isCursorComplete = options.isCursorComplete;
1820
1885
  }
1821
1886
  dispose(state) {
1822
1887
  this.clearDebounceTimer(state.snapshot);
@@ -1866,6 +1931,10 @@ var TransportSnapshotManager = class {
1866
1931
  async maybeUpload(state) {
1867
1932
  const config = await this.resolveUploadConfig(state);
1868
1933
  if (config == null) return;
1934
+ if (!this.isCursorComplete(state.cursor)) {
1935
+ this.logDebug("skip snapshot upload: current cursor is incomplete", { offset: state.cursor.nextOffset });
1936
+ return;
1937
+ }
1869
1938
  const tailBytes = parseSnapshotByteOffset(state.cursor.nextOffset);
1870
1939
  if (tailBytes == null) {
1871
1940
  this.logDebug("skip snapshot upload: non-numeric offset token", { offset: state.cursor.nextOffset });
@@ -1980,6 +2049,7 @@ var StreamsCrdt = class StreamsCrdt {
1980
2049
  nextProducerSeq = 0;
1981
2050
  frozenLocalAppend;
1982
2051
  committedPendingCount;
2052
+ committedLocalStreamCursor;
1983
2053
  pendingWriteOnly = new Deque();
1984
2054
  writeOnlyCursor;
1985
2055
  unsubscribeWriteOnlyLocal;
@@ -2037,7 +2107,8 @@ var StreamsCrdt = class StreamsCrdt {
2037
2107
  enqueueExclusive: (task) => this.enqueueExclusive(task),
2038
2108
  logDebug: (message, detail) => this.logDebug(message, detail),
2039
2109
  logError: (message, error, detail) => this.logError(message, error, detail),
2040
- isCurrentState: (state) => this.joinState === state
2110
+ isCurrentState: (state) => this.joinState === state,
2111
+ isCursorComplete: (cursor) => this.cursorManager.isCursorComplete(cursor)
2041
2112
  });
2042
2113
  this.writeOnlyCursor = this.createInitialCursor();
2043
2114
  this.unsubscribeWriteOnlyLocal = this.adapter.subscribeLocalUpdates((batch) => {
@@ -2191,6 +2262,7 @@ var StreamsCrdt = class StreamsCrdt {
2191
2262
  this.nextProducerSeq = 0;
2192
2263
  this.frozenLocalAppend = void 0;
2193
2264
  this.committedPendingCount = void 0;
2265
+ this.committedLocalStreamCursor = void 0;
2194
2266
  return deleted;
2195
2267
  }) }
2196
2268
  };
@@ -2426,10 +2498,7 @@ var StreamsCrdt = class StreamsCrdt {
2426
2498
  async syncOnce() {
2427
2499
  let cursor = (await this.finalizeDirectLocalAppendIfNeeded(await this.resolveInitialRemoteState())).cursor;
2428
2500
  const localBatch = this.exportUpdates(cursor.serverLowerBoundVersion);
2429
- if (localBatch.batch != null) {
2430
- cursor = await this.appendLocalBatch(cursor, localBatch.batch);
2431
- cursor = (await this.catchup(cursor)).cursor;
2432
- }
2501
+ if (localBatch.batch != null) cursor = (await this.appendLocalBatchDurably(cursor, localBatch.batch)).cursor;
2433
2502
  return cursor;
2434
2503
  }
2435
2504
  async performInitialJoinSync(preferredLiveMode) {
@@ -2441,10 +2510,9 @@ var StreamsCrdt = class StreamsCrdt {
2441
2510
  const exportedLocal = this.exportUpdates(localExportRefVersion);
2442
2511
  localExportRefVersion = exportedLocal.nextRefVersion;
2443
2512
  if (exportedLocal.batch != null) {
2444
- cursor = await this.appendLocalBatch(cursor, exportedLocal.batch);
2445
- const caughtUp = await this.catchup(cursor);
2446
- cursor = caughtUp.cursor;
2447
- streamCursor = caughtUp.streamCursor;
2513
+ const committed = await this.appendLocalBatchDurably(cursor, exportedLocal.batch, void 0, "direct", streamCursor);
2514
+ cursor = committed.cursor;
2515
+ streamCursor = committed.streamCursor;
2448
2516
  }
2449
2517
  localExportRefVersion = cursor.serverLowerBoundVersion;
2450
2518
  return {
@@ -2478,7 +2546,7 @@ var StreamsCrdt = class StreamsCrdt {
2478
2546
  async bootstrapState(cursor, options = {}) {
2479
2547
  const cursorManager = options.cursorManager ?? this.cursorManager;
2480
2548
  const result = await this.client.bootstrap();
2481
- let saved = await cursorManager.applyBootstrapPayload(cursor, result.snapshot, result.updates, result.nextOffset);
2549
+ let saved = await cursorManager.applyBootstrapPayload(cursor, result.snapshot, result.updates, result.nextOffset, result.upToDate);
2482
2550
  let bootstrapCursor = result.streamCursor;
2483
2551
  if (!result.upToDate) {
2484
2552
  const caughtUp = await this.catchupOrGone(saved, {
@@ -2521,7 +2589,16 @@ var StreamsCrdt = class StreamsCrdt {
2521
2589
  }
2522
2590
  const result = response.value;
2523
2591
  if (isReadNoProgress(requestOffset, result)) throw new Error(`catch-up made no progress at offset ${requestOffset}; server returned empty updates and unchanged next offset with up_to_date=false`);
2524
- current = await cursorManager.applyCatchupResult(current, result, options.onRemoteVersionApplied);
2592
+ try {
2593
+ current = await cursorManager.applyCatchupResult(current, result, options.onRemoteVersionApplied);
2594
+ } catch (error) {
2595
+ if (error instanceof CrdtApplyIncompleteError && result.upToDate) return await this.bootstrapState({
2596
+ ...current,
2597
+ nextOffset: "-1",
2598
+ serverLowerBoundVersion: this.adapter.emptyVersion()
2599
+ }, { cursorManager });
2600
+ throw error;
2601
+ }
2525
2602
  currentStreamCursor = result.streamCursor;
2526
2603
  if (result.upToDate) return {
2527
2604
  cursor: current,
@@ -2529,20 +2606,64 @@ var StreamsCrdt = class StreamsCrdt {
2529
2606
  };
2530
2607
  }
2531
2608
  }
2532
- async appendLocalBatch(cursor, batch, pendingCount) {
2609
+ async recoverLiveIncompleteRemoteApply(state) {
2610
+ try {
2611
+ const bootstrapped = await this.bootstrapState({
2612
+ ...state.cursor,
2613
+ nextOffset: "-1",
2614
+ serverLowerBoundVersion: this.adapter.emptyVersion()
2615
+ });
2616
+ state.cursor = bootstrapped.cursor;
2617
+ state.streamCursor = bootstrapped.streamCursor;
2618
+ state.retryAttempt = 0;
2619
+ state.lastError = void 0;
2620
+ this.setReadSubStatus(state, "ok");
2621
+ if (state.pendingLocal.length > 0) this.flushPendingLocal(state);
2622
+ return true;
2623
+ } catch (error) {
2624
+ const classified = classifyFetchError(error);
2625
+ this.logError("live incomplete apply recovery failed", error, {
2626
+ streamUrl: this.streamUrl,
2627
+ code: classified.code
2628
+ });
2629
+ state.lastError = toTransportError(error);
2630
+ this.setReadSubStatus(state, classified.retriable ? "disconnected" : "error");
2631
+ return false;
2632
+ }
2633
+ }
2634
+ async appendLocalBatch(cursor, batch, pendingCount, streamCursor) {
2533
2635
  const source = pendingCount == null ? "direct" : "pending";
2534
- const committed = await this.appendLocalBatchDurably(cursor, batch, pendingCount, source);
2636
+ const committed = await this.appendLocalBatchDurably(cursor, batch, pendingCount, source, streamCursor);
2535
2637
  this.committedPendingCount = committed.pendingCount;
2638
+ this.committedLocalStreamCursor = committed.streamCursor;
2536
2639
  return committed.cursor;
2537
2640
  }
2538
- async appendLocalBatchDurably(cursor, batch, pendingCount, source = pendingCount == null ? "direct" : "pending") {
2641
+ async appendLocalBatchDurably(cursor, batch, pendingCount, source = pendingCount == null ? "direct" : "pending", streamCursor) {
2539
2642
  const frozen = await this.getOrCreateFrozenLocalAppend(cursor, batch, pendingCount, source);
2540
2643
  const appended = await this.appendFrozenLocalBatchRemote(frozen);
2541
- const saved = await this.saveRemoteCursor(appended.cursor, "local");
2644
+ const caughtUp = await this.catchupLocalAppend(frozen, streamCursor);
2542
2645
  this.commitProducerAck(appended.producerAck);
2543
2646
  return {
2544
- cursor: saved,
2545
- pendingCount: frozen.pendingCount
2647
+ cursor: caughtUp.cursor,
2648
+ pendingCount: frozen.pendingCount,
2649
+ streamCursor: caughtUp.streamCursor
2650
+ };
2651
+ }
2652
+ localAppendReadCursor(frozen) {
2653
+ return {
2654
+ ...frozen.baseCursor,
2655
+ serverLowerBoundVersion: this.adapter.mergeVersions(frozen.baseCursor.serverLowerBoundVersion, frozen.batch.version)
2656
+ };
2657
+ }
2658
+ async catchupLocalAppend(frozen, streamCursor) {
2659
+ const caughtUp = await this.catchupOrGone(this.localAppendReadCursor(frozen), {
2660
+ streamCursor,
2661
+ cursorManager: this.volatileCursorManager
2662
+ });
2663
+ if ("kind" in caughtUp) throw new Error("local append catch-up unexpectedly returned gone");
2664
+ return {
2665
+ cursor: await this.saveRemoteCursor(caughtUp.cursor, "local"),
2666
+ streamCursor: caughtUp.streamCursor
2546
2667
  };
2547
2668
  }
2548
2669
  async appendLocalBatchRemote(cursor, batch, pendingCount, source = pendingCount == null ? "direct" : "pending") {
@@ -2616,9 +2737,9 @@ var StreamsCrdt = class StreamsCrdt {
2616
2737
  const frozen = this.frozenLocalAppend;
2617
2738
  if (frozen == null || frozen.pendingCount != null) return state;
2618
2739
  const appended = await this.appendFrozenLocalBatchRemote(frozen);
2619
- const saved = await this.saveRemoteCursor(appended.cursor, "local");
2740
+ const caughtUp = await this.catchupLocalAppend(frozen, state.streamCursor);
2620
2741
  this.commitProducerAck(appended.producerAck);
2621
- return await this.catchup(saved, void 0, state.streamCursor);
2742
+ return caughtUp;
2622
2743
  }
2623
2744
  createCurrentProducerSession() {
2624
2745
  return {
@@ -2643,6 +2764,11 @@ var StreamsCrdt = class StreamsCrdt {
2643
2764
  this.committedPendingCount = void 0;
2644
2765
  return count;
2645
2766
  }
2767
+ consumeCommittedLocalStreamCursor() {
2768
+ const streamCursor = this.committedLocalStreamCursor;
2769
+ this.committedLocalStreamCursor = void 0;
2770
+ return streamCursor;
2771
+ }
2646
2772
  /**
2647
2773
  * Ensures the target stream exists, creating it if necessary.
2648
2774
  * Tolerates 409 Conflict (stream already created concurrently).
@@ -2779,6 +2905,7 @@ var StreamsCrdt = class StreamsCrdt {
2779
2905
  if (state.closed) return;
2780
2906
  state.retryAttempt = 0;
2781
2907
  this.setReadSubStatus(state, "ok");
2908
+ if (state.pendingLocal.length > 0) this.flushPendingLocal(state);
2782
2909
  await sleep(0);
2783
2910
  continue;
2784
2911
  }
@@ -2787,12 +2914,13 @@ var StreamsCrdt = class StreamsCrdt {
2787
2914
  let flushChain = Promise.resolve();
2788
2915
  resetInactivity();
2789
2916
  const sseStartedAtMs = Date.now();
2790
- const sseResult = await this.client.readFromOffsetSseItems(sseRequestOffset, state.streamCursor, (updates, nextOffset, _upToDate, streamCursor) => {
2917
+ const sseResult = await this.client.readFromOffsetSseItems(sseRequestOffset, state.streamCursor, (updates, nextOffset, upToDate, streamCursor) => {
2791
2918
  if (capturedController?.signal.aborted) return "stop";
2792
2919
  resetInactivity();
2793
2920
  pendingRemote.push({
2794
2921
  updates,
2795
2922
  nextOffset,
2923
+ upToDate,
2796
2924
  streamCursor
2797
2925
  });
2798
2926
  flushChain = flushChain.then(async () => {
@@ -2804,7 +2932,8 @@ var StreamsCrdt = class StreamsCrdt {
2804
2932
  if (state.closed) return;
2805
2933
  state.cursor = await this.cursorManager.applyLiveResult(state.cursor, sseRequestOffset, {
2806
2934
  nextOffset: lastEvent.nextOffset,
2807
- updates: allUpdates
2935
+ updates: allUpdates,
2936
+ upToDate: lastEvent.upToDate
2808
2937
  }, (previousVersion, nextVersion) => {
2809
2938
  this.snapshotManager.observeRemoteVersion(state, previousVersion, nextVersion);
2810
2939
  });
@@ -2814,6 +2943,7 @@ var StreamsCrdt = class StreamsCrdt {
2814
2943
  }
2815
2944
  });
2816
2945
  this.setReadSubStatus(state, "ok");
2946
+ if (state.pendingLocal.length > 0) this.flushPendingLocal(state);
2817
2947
  }).catch((flushError) => {
2818
2948
  this.logError("sse flush chain error", flushError, { streamUrl: this.streamUrl });
2819
2949
  capturedController?.abort(flushError);
@@ -2857,6 +2987,11 @@ var StreamsCrdt = class StreamsCrdt {
2857
2987
  } catch (error) {
2858
2988
  clearInactivity();
2859
2989
  if (state.closed) return;
2990
+ if (error instanceof CrdtApplyIncompleteError) {
2991
+ if (capturedController?.signal.aborted && state.requestController === capturedController) state.requestController = new AbortController();
2992
+ if (await this.recoverLiveIncompleteRemoteApply(state)) continue;
2993
+ return;
2994
+ }
2860
2995
  if (capturedController?.signal.aborted) {
2861
2996
  if (inactivityTriggered) {
2862
2997
  if (state.requestController === capturedController) state.requestController = new AbortController();
@@ -2883,6 +3018,11 @@ var StreamsCrdt = class StreamsCrdt {
2883
3018
  }
2884
3019
  const abortReason = capturedController.signal.reason;
2885
3020
  const flushError = abortReason instanceof Error ? abortReason : error;
3021
+ if (flushError instanceof CrdtApplyIncompleteError) {
3022
+ if (state.requestController === capturedController) state.requestController = new AbortController();
3023
+ if (await this.recoverLiveIncompleteRemoteApply(state)) continue;
3024
+ return;
3025
+ }
2886
3026
  if (flushError instanceof Error && !(flushError instanceof DOMException && flushError.name === "AbortError")) {
2887
3027
  const flushClassified = classifyFetchError(flushError);
2888
3028
  if (!flushClassified.retriable || isAuthOrProtocolError(flushError)) {
@@ -2969,6 +3109,7 @@ var StreamsCrdt = class StreamsCrdt {
2969
3109
  }
2970
3110
  async flushPendingLocal(state) {
2971
3111
  if (state.flushingLocal || state.closed) return;
3112
+ if (!this.cursorManager.isCursorComplete(state.cursor)) return;
2972
3113
  state.flushingLocal = true;
2973
3114
  try {
2974
3115
  while (!state.closed) {
@@ -2976,7 +3117,9 @@ var StreamsCrdt = class StreamsCrdt {
2976
3117
  if (drained == null) return;
2977
3118
  let attempts = 0;
2978
3119
  while (!state.closed) try {
2979
- state.cursor = await this.enqueueExclusive(async () => this.appendLocalBatch(state.cursor, drained.batch, drained.count));
3120
+ state.cursor = await this.enqueueExclusive(async () => this.appendLocalBatch(state.cursor, drained.batch, drained.count, state.streamCursor));
3121
+ const committedStreamCursor = this.consumeCommittedLocalStreamCursor();
3122
+ if (committedStreamCursor != null) state.streamCursor = committedStreamCursor;
2980
3123
  const committedCount = this.consumeCommittedPendingCount() ?? drained.count;
2981
3124
  for (let i = 0; i < committedCount; i += 1) state.pendingLocal.popFront();
2982
3125
  this.notifySyncWaiters(state, committedCount);
@@ -3135,4 +3278,4 @@ function createStreamUrl(input) {
3135
3278
  //#endregion
3136
3279
  export { InMemoryRemoteCursorStore as a, EphemeralStreamCrdt as c, StreamsCrdt as i, PayloadProtectionError as l, isValidBucketId as n, IndexedDbRemoteCursorStore as o, isValidRillId as r, createInitialRemoteCursor as s, createStreamUrl as t };
3137
3280
 
3138
- //# sourceMappingURL=stream-id-CCPEBb6G.js.map
3281
+ //# sourceMappingURL=stream-id-BjmGvObf.js.map