@loro-dev/streams-crdt 0.11.1 → 0.12.1

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.
@@ -135,10 +135,20 @@ const DEFAULT_RECONNECT_CONFIG = {
135
135
  jitterFraction: .2,
136
136
  maxAttempts: Infinity,
137
137
  connectTimeoutMs: 1e4,
138
+ initialSyncSlowAfterMs: 3e4,
139
+ initialSyncHardTimeoutMs: 12e4,
138
140
  pollTimeoutMs: 3e4,
139
141
  liveInactivityTimeoutMs: 45e3
140
142
  };
141
143
  /**
144
+ * Minimum time a live SSE must stay open before it counts as a "successful
145
+ * session". A read that stayed open at least this long before erroring is
146
+ * treated as a fresh disconnect (e.g. a routine server-side stream recycle)
147
+ * rather than a failing-to-connect retry, so its reconnect does not consume the
148
+ * backoff budget. Shared by the durable and ephemeral read loops.
149
+ */
150
+ const MIN_SUCCESSFUL_SSE_DURATION_MS = 5e3;
151
+ /**
142
152
  * Apply jitter of +/- fraction to a base delay.
143
153
  * E.g. applyJitter(1000, 0.2) returns a value in [800, 1200].
144
154
  */
@@ -503,6 +513,17 @@ var TimeoutError = class extends Error {
503
513
  this.timeoutMs = timeoutMs;
504
514
  }
505
515
  };
516
+ var InitialSyncTimeoutError = class extends Error {
517
+ operation;
518
+ timeoutMs;
519
+ elapsedMs;
520
+ constructor(operation, timeoutMs, elapsedMs) {
521
+ super(`initial ${operation} did not complete within ${timeoutMs}ms; the in-flight bootstrap/catch-up request was aborted at the hard timeout`);
522
+ this.operation = operation;
523
+ this.timeoutMs = timeoutMs;
524
+ this.elapsedMs = elapsedMs;
525
+ }
526
+ };
506
527
  var ProtocolError = class extends Error {
507
528
  status;
508
529
  constructor(message, status) {
@@ -673,6 +694,12 @@ function classifyFetchError(err) {
673
694
  retriable: true,
674
695
  atMs: Date.now()
675
696
  };
697
+ if (err instanceof InitialSyncTimeoutError) return {
698
+ code: "initial_sync_timeout",
699
+ message,
700
+ retriable: true,
701
+ atMs: Date.now()
702
+ };
676
703
  if (err instanceof TypeError) return {
677
704
  code: "network",
678
705
  message,
@@ -748,6 +775,14 @@ function toTransportError(error) {
748
775
  phase: error.timeoutType,
749
776
  message: error.message
750
777
  };
778
+ if (error instanceof InitialSyncTimeoutError) return {
779
+ code: "initial_sync_timeout",
780
+ retryable: true,
781
+ operation: error.operation,
782
+ timeoutMs: error.timeoutMs,
783
+ elapsedMs: error.elapsedMs,
784
+ message: error.message
785
+ };
751
786
  if (error instanceof HttpError && error.status === 410) return {
752
787
  code: "gone",
753
788
  retryable: true,
@@ -903,9 +938,10 @@ function normalizeShardUrlsOptions(options) {
903
938
  bootstrap: normalizeShardOrigins(options.bootstrap, "shardUrls.bootstrap"),
904
939
  catchup: normalizeShardOrigins(options.catchup, "shardUrls.catchup"),
905
940
  largePost: normalizeShardOrigins(options.largePost, "shardUrls.largePost"),
941
+ other: normalizeShardOrigins(options.other, "shardUrls.other"),
906
942
  largePostMinBytes
907
943
  };
908
- if (normalized.bootstrap == null && normalized.catchup == null && normalized.largePost == null) return;
944
+ if (normalized.bootstrap == null && normalized.catchup == null && normalized.largePost == null && normalized.other == null) return;
909
945
  return normalized;
910
946
  }
911
947
  function requestBodyByteLength(body) {
@@ -940,7 +976,9 @@ function normalizedStreamPath(streamUrl) {
940
976
  var StreamsTransportClient = class {
941
977
  streamUrl;
942
978
  adapter;
979
+ streamTtlSeconds;
943
980
  client;
981
+ initialSyncClient;
944
982
  fetchImpl;
945
983
  authProvider;
946
984
  reconnectConfig;
@@ -950,6 +988,7 @@ var StreamsTransportClient = class {
950
988
  constructor(options) {
951
989
  this.streamUrl = options.streamUrl;
952
990
  this.adapter = options.adapter;
991
+ this.streamTtlSeconds = normalizeStreamTtlSeconds(options.streamTtlSeconds);
953
992
  this.fetchImpl = options.fetchImpl;
954
993
  this.shardUrls = normalizeShardUrlsOptions(options.shardUrls);
955
994
  this.client = new StreamsClient({
@@ -962,6 +1001,16 @@ var StreamsTransportClient = class {
962
1001
  pollTimeoutMs: options.reconnectConfig.pollTimeoutMs
963
1002
  }
964
1003
  });
1004
+ this.initialSyncClient = new StreamsClient({
1005
+ url: options.streamUrl,
1006
+ auth: options.authProvider,
1007
+ fetch: this.fetchRouted.bind(this),
1008
+ retry: { maxAttempts: 0 },
1009
+ timeout: {
1010
+ connectTimeoutMs: 0,
1011
+ pollTimeoutMs: options.reconnectConfig.pollTimeoutMs
1012
+ }
1013
+ });
965
1014
  this.authProvider = options.authProvider;
966
1015
  this.reconnectConfig = options.reconnectConfig;
967
1016
  }
@@ -972,17 +1021,20 @@ var StreamsTransportClient = class {
972
1021
  const shardUrls = this.shardUrls;
973
1022
  if (shardUrls == null) return input;
974
1023
  const operation = this.classifyShardOperation(input, init, shardUrls);
975
- if (operation == null) return input;
976
- const origins = operation === "bootstrap" ? shardUrls.bootstrap : operation === "catchup" ? shardUrls.catchup : shardUrls.largePost;
1024
+ const origins = this.originsForShardOperation(operation, shardUrls);
977
1025
  if (origins == null || origins.length === 0) return input;
978
1026
  const target = new URL(input);
979
- const shardOrigin = new URL(this.nextShardOrigin(operation, origins));
1027
+ const shardOrigin = new URL(this.nextShardOrigin(operation ?? "other", origins));
980
1028
  target.protocol = shardOrigin.protocol;
981
1029
  target.host = shardOrigin.host;
982
1030
  target.username = "";
983
1031
  target.password = "";
984
1032
  return target.toString();
985
1033
  }
1034
+ originsForShardOperation(operation, shardUrls) {
1035
+ if (operation == null) return shardUrls.other;
1036
+ return (operation === "bootstrap" ? shardUrls.bootstrap : operation === "catchup" ? shardUrls.catchup : shardUrls.largePost) ?? shardUrls.other;
1037
+ }
986
1038
  nextShardOrigin(operation, origins) {
987
1039
  const currentIndex = this.shardCursorIndexes[operation] ?? createRandomShardStart(origins.length);
988
1040
  this.shardCursorIndexes[operation] = (currentIndex + 1) % origins.length;
@@ -1002,7 +1054,10 @@ var StreamsTransportClient = class {
1002
1054
  }
1003
1055
  async createStreamRequest() {
1004
1056
  const adapterHeaders = this.adapter.getCreateStreamHeaders ? await this.adapter.getCreateStreamHeaders() : void 0;
1005
- const result = await this.client.create({ headers: adapterHeaders });
1057
+ const result = await this.client.create({
1058
+ headers: adapterHeaders,
1059
+ ttlSeconds: this.streamTtlSeconds
1060
+ });
1006
1061
  if (result.ok) return result.result.created;
1007
1062
  throw this.toTransportFailure(result.result);
1008
1063
  }
@@ -1014,8 +1069,8 @@ var StreamsTransportClient = class {
1014
1069
  async readFromOffsetSseItems(offset, cursor, onControl, onOpen, signal, onActivity) {
1015
1070
  return this.readItemsSse(offset, cursor, onControl, onOpen, signal, onActivity);
1016
1071
  }
1017
- async readFromOffset(offset, cursor, live, signal) {
1018
- const result = await this.client.readOnce({
1072
+ async readFromOffset(offset, cursor, live, signal, timeoutMode = "default") {
1073
+ const result = await (live == null && timeoutMode === "initial" ? this.initialSyncClient : this.client).readOnce({
1019
1074
  offset,
1020
1075
  cursor,
1021
1076
  live,
@@ -1036,8 +1091,8 @@ var StreamsTransportClient = class {
1036
1091
  }
1037
1092
  };
1038
1093
  }
1039
- async bootstrap() {
1040
- const result = await this.client.bootstrap();
1094
+ async bootstrap(signal, timeoutMode = "default") {
1095
+ const result = await (timeoutMode === "initial" ? this.initialSyncClient : this.client).bootstrap({ signal });
1041
1096
  if (!result.ok) throw this.toTransportFailure(result.result);
1042
1097
  const snapshot = result.result.snapshotOffset === "-1" ? new Uint8Array() : result.result.snapshot?.body ?? new Uint8Array();
1043
1098
  return {
@@ -1178,15 +1233,16 @@ var StreamsTransportClient = class {
1178
1233
  }
1179
1234
  }
1180
1235
  async fetchAuthorized(input, init, timeoutMs, phase) {
1236
+ const requestUrl = this.routeRequestUrl(input, init);
1181
1237
  const firstToken = await this.resolveToken({ reason: "request" });
1182
- const firstResponse = await this.fetchWithTimeout(input, {
1238
+ const firstResponse = await this.fetchWithTimeout(requestUrl, {
1183
1239
  ...init,
1184
1240
  headers: this.attachAuthorization(init.headers, firstToken)
1185
1241
  }, timeoutMs, phase);
1186
1242
  if (this.authProvider == null || firstResponse.status !== 401 && firstResponse.status !== 403) return firstResponse;
1187
1243
  await firstResponse.body?.cancel().catch(() => {});
1188
1244
  const refreshedToken = await this.refreshTokenAfterFailure(firstResponse.status, firstToken);
1189
- return this.fetchWithTimeout(input, {
1245
+ return this.fetchWithTimeout(requestUrl, {
1190
1246
  ...init,
1191
1247
  headers: this.attachAuthorization(init.headers, refreshedToken)
1192
1248
  }, timeoutMs, phase);
@@ -1375,6 +1431,11 @@ var StreamsTransportClient = class {
1375
1431
  };
1376
1432
  }
1377
1433
  };
1434
+ function normalizeStreamTtlSeconds(value) {
1435
+ if (value == null) return;
1436
+ if (!Number.isSafeInteger(value) || value < 0) throw new Error("streamTtlSeconds must be a non-negative safe integer");
1437
+ return value;
1438
+ }
1378
1439
  const REQUEST_ID_HEADER_NAMES = [
1379
1440
  "x-request-id",
1380
1441
  "x-amzn-trace-id",
@@ -1466,7 +1527,8 @@ var EphemeralStreamCrdt = class {
1466
1527
  adapter: createNoopAdapter(),
1467
1528
  authProvider: normalizeAuthProvider(options.auth),
1468
1529
  fetchImpl: options.fetch ?? globalThis.fetch.bind(globalThis),
1469
- reconnectConfig: this.reconnectConfig
1530
+ reconnectConfig: this.reconnectConfig,
1531
+ shardUrls: options.shardUrls
1470
1532
  });
1471
1533
  }
1472
1534
  async join(params) {
@@ -1626,9 +1688,11 @@ var EphemeralStreamCrdt = class {
1626
1688
  while (!state.closed) {
1627
1689
  state.requestController ??= new AbortController();
1628
1690
  const capturedController = state.requestController;
1691
+ let sseOpenedAtMs = 0;
1629
1692
  try {
1630
1693
  const result = await this.client.readEphemeralSse(async (bootstrap) => {
1631
1694
  if (bootstrap.byteLength > 0) await this.adaptor.applyRemoteUpdates([bootstrap]);
1695
+ sseOpenedAtMs = Date.now();
1632
1696
  this.setReadStatus(state, "ok");
1633
1697
  this.resolveFirstOpen(state);
1634
1698
  }, async (update) => {
@@ -1661,6 +1725,7 @@ var EphemeralStreamCrdt = class {
1661
1725
  return;
1662
1726
  }
1663
1727
  this.setReadStatus(state, "reconnecting");
1728
+ if (sseOpenedAtMs > 0 && Date.now() - sseOpenedAtMs >= 5e3) state.retryAttempt = 0;
1664
1729
  if (state.retryAttempt >= this.reconnectConfig.maxAttempts) {
1665
1730
  this.logError("ephemeral live reconnect exhausted", error, {
1666
1731
  streamUrl: this.streamUrl,
@@ -2218,13 +2283,12 @@ async function disposeJoinState(state, options) {
2218
2283
  //#endregion
2219
2284
  //#region src/transport-runtime.ts
2220
2285
  const MIN_RECONNECT_DELAY_MS = 1e3;
2221
- const MIN_SUCCESSFUL_SSE_DURATION_MS = 5e3;
2222
2286
  function reconnectDelayWithFloor(attempt, config) {
2223
2287
  return Math.max(computeRetryDelay(attempt, config), MIN_RECONNECT_DELAY_MS);
2224
2288
  }
2225
2289
  function fastSseCloseDelay(startedAtMs) {
2226
2290
  const elapsedMs = Math.max(0, Date.now() - startedAtMs);
2227
- if (elapsedMs >= MIN_SUCCESSFUL_SSE_DURATION_MS) return 0;
2291
+ if (elapsedMs >= 5e3) return 0;
2228
2292
  return Math.max(MIN_RECONNECT_DELAY_MS, MIN_SUCCESSFUL_SSE_DURATION_MS - elapsedMs);
2229
2293
  }
2230
2294
  /**
@@ -2295,6 +2359,7 @@ var StreamsCrdt = class StreamsCrdt {
2295
2359
  this.client = new StreamsTransportClient({
2296
2360
  streamUrl: this.streamUrl,
2297
2361
  adapter: this.adapter,
2362
+ streamTtlSeconds: options.streamTtlSeconds,
2298
2363
  authProvider,
2299
2364
  fetchImpl,
2300
2365
  reconnectConfig: this.reconnectConfig,
@@ -2417,7 +2482,7 @@ var StreamsCrdt = class StreamsCrdt {
2417
2482
  async uploadSnapshotForTesting() {
2418
2483
  try {
2419
2484
  await this.adapter.flushLocalExports?.();
2420
- const cursor = await this.syncOnce();
2485
+ const cursor = await this.syncOnce("sync");
2421
2486
  const rawSnapshot = this.adapter.exportSnapshot();
2422
2487
  const snapshot = this.encodeSnapshot == null ? rawSnapshot : await this.encodeSnapshot(rawSnapshot);
2423
2488
  await this.client.putSnapshotAt(cursor.nextOffset, snapshot);
@@ -2485,7 +2550,7 @@ var StreamsCrdt = class StreamsCrdt {
2485
2550
  this.disableWriteOnlyMode("appendWriteOnly cannot be used after sync() on the same transport");
2486
2551
  return {
2487
2552
  ok: true,
2488
- value: { cursor: await this.syncOnce() }
2553
+ value: { cursor: await this.syncOnce("sync") }
2489
2554
  };
2490
2555
  } catch (error) {
2491
2556
  return {
@@ -2509,11 +2574,11 @@ var StreamsCrdt = class StreamsCrdt {
2509
2574
  const state = this.joinState;
2510
2575
  if (state != null && !state.closed) return {
2511
2576
  ok: true,
2512
- value: { cursor: await this.catchupJoinState(state, params) }
2577
+ value: { cursor: await this.catchupJoinState(state, params, state.requestController?.signal) }
2513
2578
  };
2514
2579
  return {
2515
2580
  ok: true,
2516
- value: { cursor: (await this.resolvePullOnlyRemoteState()).cursor }
2581
+ value: { cursor: (await this.resolvePullOnlyRemoteState("catchup")).cursor }
2517
2582
  };
2518
2583
  } catch (error) {
2519
2584
  return {
@@ -2692,10 +2757,46 @@ var StreamsCrdt = class StreamsCrdt {
2692
2757
  }
2693
2758
  });
2694
2759
  }
2760
+ async runInitialSyncWithDeadline(operation, task, parentSignal) {
2761
+ const startedAtMs = Date.now();
2762
+ const hardController = new AbortController();
2763
+ const signal = mergeAbortSignals(parentSignal, hardController.signal);
2764
+ const slowAfterMs = this.reconnectConfig.initialSyncSlowAfterMs;
2765
+ const hardTimeoutMs = this.reconnectConfig.initialSyncHardTimeoutMs;
2766
+ let slowTimer;
2767
+ let hardTimer;
2768
+ let hardTimeoutError;
2769
+ let hardTimeoutPromise;
2770
+ if (typeof slowAfterMs === "number" && Number.isFinite(slowAfterMs) && slowAfterMs > 0) slowTimer = setTimeout(() => {
2771
+ this.logDebug("initial sync still pending", {
2772
+ streamUrl: this.streamUrl,
2773
+ operation,
2774
+ elapsedMs: Date.now() - startedAtMs,
2775
+ slowAfterMs
2776
+ });
2777
+ }, slowAfterMs);
2778
+ if (typeof hardTimeoutMs === "number" && Number.isFinite(hardTimeoutMs) && hardTimeoutMs > 0) hardTimeoutPromise = new Promise((_, reject) => {
2779
+ hardTimer = setTimeout(() => {
2780
+ hardTimeoutError = new InitialSyncTimeoutError(operation, hardTimeoutMs, Date.now() - startedAtMs);
2781
+ hardController.abort(hardTimeoutError);
2782
+ reject(hardTimeoutError);
2783
+ }, hardTimeoutMs);
2784
+ });
2785
+ try {
2786
+ const taskPromise = task(signal);
2787
+ return hardTimeoutPromise == null ? await taskPromise : await Promise.race([taskPromise, hardTimeoutPromise]);
2788
+ } catch (error) {
2789
+ if (hardTimeoutError != null) throw hardTimeoutError;
2790
+ throw error;
2791
+ } finally {
2792
+ if (slowTimer != null) clearTimeout(slowTimer);
2793
+ if (hardTimer != null) clearTimeout(hardTimer);
2794
+ }
2795
+ }
2695
2796
  async startJoin(state) {
2696
2797
  try {
2697
2798
  state.requestController ??= new AbortController();
2698
- const joined = await this.performInitialJoinSync(state.liveMode);
2799
+ const joined = await this.performInitialJoinSync(state.liveMode, state.requestController?.signal);
2699
2800
  state.cursor = joined.cursor;
2700
2801
  state.streamCursor = joined.streamCursor;
2701
2802
  state.liveMode = joined.liveMode;
@@ -2727,14 +2828,14 @@ var StreamsCrdt = class StreamsCrdt {
2727
2828
  throw error;
2728
2829
  }
2729
2830
  }
2730
- async syncOnce() {
2731
- let cursor = (await this.finalizeDirectLocalAppendIfNeeded(await this.resolveInitialRemoteState())).cursor;
2831
+ async syncOnce(operation, parentSignal) {
2832
+ let cursor = (await this.finalizeDirectLocalAppendIfNeeded(await this.resolveInitialRemoteState(operation, parentSignal))).cursor;
2732
2833
  const localBatch = this.exportUpdates(cursor.serverLowerBoundVersion);
2733
2834
  if (localBatch.batch != null) cursor = (await this.appendLocalBatchDurably(cursor, localBatch.batch)).cursor;
2734
2835
  return cursor;
2735
2836
  }
2736
- async performInitialJoinSync(preferredLiveMode) {
2737
- const initial = await this.finalizeDirectLocalAppendIfNeeded(await this.resolveInitialRemoteState());
2837
+ async performInitialJoinSync(preferredLiveMode, parentSignal) {
2838
+ const initial = await this.finalizeDirectLocalAppendIfNeeded(await this.resolveInitialRemoteState("join", parentSignal));
2738
2839
  let cursor = initial.cursor;
2739
2840
  let localExportRefVersion = cursor.serverLowerBoundVersion;
2740
2841
  let streamCursor = initial.streamCursor;
@@ -2761,33 +2862,51 @@ var StreamsCrdt = class StreamsCrdt {
2761
2862
  serverLowerBoundVersion: this.adapter.emptyVersion()
2762
2863
  });
2763
2864
  }
2764
- async resolveInitialRemoteState() {
2865
+ async resolveInitialRemoteState(operation, parentSignal) {
2765
2866
  const loaded = await this.remoteCursorStore.load(this.streamUrl);
2766
2867
  const cursor = loaded ?? this.createInitialCursor();
2868
+ const runInitial = (task) => this.runInitialSyncWithDeadline(operation, task, parentSignal);
2767
2869
  try {
2768
- if (loaded == null) return await this.bootstrapState(cursor);
2769
- const caughtUp = await this.catchupOrGone(cursor, { goneStrategy: "return-gone" });
2770
- if ("kind" in caughtUp) return await this.recoverFromGoneWithLocalBootstrap(cursor);
2870
+ if (loaded == null) return await runInitial((signal) => this.bootstrapState(cursor, {
2871
+ signal,
2872
+ timeoutMode: "initial"
2873
+ }));
2874
+ const caughtUp = await runInitial((signal) => this.catchupOrGone(cursor, {
2875
+ goneStrategy: "return-gone",
2876
+ signal,
2877
+ timeoutMode: "initial"
2878
+ }));
2879
+ if ("kind" in caughtUp) return await this.recoverFromGoneWithLocalBootstrap(cursor, operation, parentSignal);
2771
2880
  return caughtUp;
2772
2881
  } catch (error) {
2773
2882
  if (!isStreamNotFoundError(error) || !this.createStreamIfMissing) throw error;
2774
2883
  await this.ensureStreamExists();
2775
- return await this.bootstrapState(this.createInitialCursor());
2884
+ return await runInitial((signal) => this.bootstrapState(this.createInitialCursor(), {
2885
+ signal,
2886
+ timeoutMode: "initial"
2887
+ }));
2776
2888
  }
2777
2889
  }
2778
- async resolvePullOnlyRemoteState() {
2890
+ async resolvePullOnlyRemoteState(operation, parentSignal) {
2779
2891
  const loaded = await this.remoteCursorStore.load(this.streamUrl);
2780
2892
  const cursor = loaded ?? this.createInitialCursor();
2893
+ const runInitial = (task) => this.runInitialSyncWithDeadline(operation, task, parentSignal);
2781
2894
  try {
2782
- if (loaded == null) return await this.bootstrapState(cursor);
2783
- return await this.catchupFromCursor(cursor);
2895
+ if (loaded == null) return await runInitial((signal) => this.bootstrapState(cursor, {
2896
+ signal,
2897
+ timeoutMode: "initial"
2898
+ }));
2899
+ return await runInitial((signal) => this.catchupFromCursor(cursor, void 0, void 0, void 0, signal, "initial"));
2784
2900
  } catch (error) {
2785
2901
  if (!isStreamNotFoundError(error) || !this.createStreamIfMissing) throw error;
2786
2902
  await this.ensureStreamExists();
2787
- return await this.bootstrapState(this.createInitialCursor());
2903
+ return await runInitial((signal) => this.bootstrapState(this.createInitialCursor(), {
2904
+ signal,
2905
+ timeoutMode: "initial"
2906
+ }));
2788
2907
  }
2789
2908
  }
2790
- async catchupJoinState(state, params) {
2909
+ async catchupJoinState(state, params, parentSignal) {
2791
2910
  if (state.readSubStatus === "connecting") {
2792
2911
  await state.started;
2793
2912
  return state.cursor;
@@ -2795,9 +2914,9 @@ var StreamsCrdt = class StreamsCrdt {
2795
2914
  if (state.readSubStatus === "ok" && params.force !== true) return state.cursor;
2796
2915
  const cursor = await this.enqueueExclusive(async () => {
2797
2916
  if (state.closed) throw new Error("subscription closed");
2798
- const caughtUp = await this.catchupFromCursor(state.cursor, void 0, state.streamCursor, (previousVersion, nextVersion) => {
2917
+ const caughtUp = await this.runInitialSyncWithDeadline("catchup", (signal) => this.catchupFromCursor(state.cursor, void 0, state.streamCursor, (previousVersion, nextVersion) => {
2799
2918
  this.snapshotManager.observeRemoteVersion(state, previousVersion, nextVersion);
2800
- });
2919
+ }, signal, "initial"), parentSignal);
2801
2920
  state.cursor = caughtUp.cursor;
2802
2921
  state.streamCursor = caughtUp.streamCursor;
2803
2922
  return state.cursor;
@@ -2811,13 +2930,15 @@ var StreamsCrdt = class StreamsCrdt {
2811
2930
  }
2812
2931
  async bootstrapState(cursor, options = {}) {
2813
2932
  const cursorManager = options.cursorManager ?? this.cursorManager;
2814
- const result = await this.client.bootstrap();
2933
+ const result = await this.client.bootstrap(options.signal, options.timeoutMode);
2815
2934
  let saved = await cursorManager.applyBootstrapPayload(cursor, result.snapshot, result.updates, result.nextOffset, result.upToDate);
2816
2935
  let bootstrapCursor = result.streamCursor;
2817
2936
  if (!result.upToDate) {
2818
2937
  const caughtUp = await this.catchupOrGone(saved, {
2819
2938
  streamCursor: bootstrapCursor,
2820
- cursorManager
2939
+ cursorManager,
2940
+ signal: options.signal,
2941
+ timeoutMode: options.timeoutMode
2821
2942
  });
2822
2943
  if ("kind" in caughtUp) throw new Error("bootstrap catch-up unexpectedly returned gone");
2823
2944
  saved = caughtUp.cursor;
@@ -2828,11 +2949,13 @@ var StreamsCrdt = class StreamsCrdt {
2828
2949
  streamCursor: bootstrapCursor
2829
2950
  };
2830
2951
  }
2831
- async catchupFromCursor(cursor, live, streamCursor, onRemoteVersionApplied) {
2952
+ async catchupFromCursor(cursor, live, streamCursor, onRemoteVersionApplied, signal, timeoutMode) {
2832
2953
  const result = await this.catchupOrGone(cursor, {
2833
2954
  live,
2834
2955
  streamCursor,
2835
- onRemoteVersionApplied
2956
+ onRemoteVersionApplied,
2957
+ signal,
2958
+ timeoutMode
2836
2959
  });
2837
2960
  if ("kind" in result) throw new Error("catchup unexpectedly returned gone");
2838
2961
  return result;
@@ -2844,14 +2967,18 @@ var StreamsCrdt = class StreamsCrdt {
2844
2967
  const cursorManager = options.cursorManager ?? this.cursorManager;
2845
2968
  while (true) {
2846
2969
  const requestOffset = current.nextOffset;
2847
- const response = await this.client.readFromOffset(requestOffset, currentStreamCursor, options.live);
2970
+ const response = await this.client.readFromOffset(requestOffset, currentStreamCursor, options.live, options.signal, options.timeoutMode);
2848
2971
  if (response.kind === "gone") {
2849
2972
  if (goneStrategy === "return-gone") return StreamsCrdt.GONE_CATCHUP;
2850
2973
  return await this.bootstrapState({
2851
2974
  ...current,
2852
2975
  nextOffset: "-1",
2853
2976
  serverLowerBoundVersion: this.adapter.emptyVersion()
2854
- }, { cursorManager });
2977
+ }, {
2978
+ cursorManager,
2979
+ signal: options.signal,
2980
+ timeoutMode: options.timeoutMode
2981
+ });
2855
2982
  }
2856
2983
  const result = response.value;
2857
2984
  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`);
@@ -2862,7 +2989,11 @@ var StreamsCrdt = class StreamsCrdt {
2862
2989
  ...current,
2863
2990
  nextOffset: "-1",
2864
2991
  serverLowerBoundVersion: this.adapter.emptyVersion()
2865
- }, { cursorManager });
2992
+ }, {
2993
+ cursorManager,
2994
+ signal: options.signal,
2995
+ timeoutMode: options.timeoutMode
2996
+ });
2866
2997
  throw error;
2867
2998
  }
2868
2999
  currentStreamCursor = result.streamCursor;
@@ -2878,7 +3009,7 @@ var StreamsCrdt = class StreamsCrdt {
2878
3009
  ...state.cursor,
2879
3010
  nextOffset: "-1",
2880
3011
  serverLowerBoundVersion: this.adapter.emptyVersion()
2881
- });
3012
+ }, { signal: state.requestController?.signal });
2882
3013
  state.cursor = bootstrapped.cursor;
2883
3014
  state.streamCursor = bootstrapped.streamCursor;
2884
3015
  state.retryAttempt = 0;
@@ -3080,8 +3211,12 @@ var StreamsCrdt = class StreamsCrdt {
3080
3211
  * exports and appends any local delta, then re-bootstraps to converge
3081
3212
  * persisted cursor and local CRDT state on server truth.
3082
3213
  */
3083
- async inferRemoteAndAppendLocal(bootstrapCursor, cursorManager) {
3084
- const inferredRemote = await this.bootstrapState(bootstrapCursor, { cursorManager });
3214
+ async inferRemoteAndAppendLocal(bootstrapCursor, cursorManager, operation, parentSignal) {
3215
+ const inferredRemote = await this.runInitialSyncWithDeadline(operation, (signal) => this.bootstrapState(bootstrapCursor, {
3216
+ cursorManager,
3217
+ signal,
3218
+ timeoutMode: "initial"
3219
+ }), parentSignal);
3085
3220
  let pendingAppend;
3086
3221
  if (this.frozenLocalAppend != null) {
3087
3222
  if (this.frozenLocalAppend.pendingCount != null) throw new Error("pending local append must be finalized by pending flush");
@@ -3090,22 +3225,29 @@ var StreamsCrdt = class StreamsCrdt {
3090
3225
  const localBatch = this.exportUpdates(inferredRemote.cursor.serverLowerBoundVersion);
3091
3226
  if (localBatch.batch != null) pendingAppend = await this.appendLocalBatchRemote(inferredRemote.cursor, localBatch.batch);
3092
3227
  }
3093
- const result = await this.bootstrapState(bootstrapCursor);
3228
+ const result = await this.runInitialSyncWithDeadline(operation, (signal) => this.bootstrapState(bootstrapCursor, {
3229
+ signal,
3230
+ timeoutMode: "initial"
3231
+ }), parentSignal);
3094
3232
  if (pendingAppend != null) this.commitProducerAck(pendingAppend.producerAck);
3095
3233
  return result;
3096
3234
  }
3097
- async recoverFromGoneWithLocalBootstrap(cursor) {
3235
+ async recoverFromGoneWithLocalBootstrap(cursor, operation, parentSignal) {
3098
3236
  const isolatedCursorManager = await this.createIsolatedCursorManager();
3099
3237
  const bootstrapCursor = {
3100
3238
  ...cursor,
3101
3239
  nextOffset: "-1",
3102
3240
  serverLowerBoundVersion: this.adapter.emptyVersion()
3103
3241
  };
3104
- if (isolatedCursorManager != null) return await this.inferRemoteAndAppendLocal(bootstrapCursor, isolatedCursorManager);
3242
+ if (isolatedCursorManager != null) return await this.inferRemoteAndAppendLocal(bootstrapCursor, isolatedCursorManager, operation, parentSignal);
3105
3243
  const preservedLocalSnapshot = this.adapter.exportSnapshot();
3106
3244
  let restoredLocalSnapshot = false;
3107
3245
  try {
3108
- const inferredRemote = await this.bootstrapState(bootstrapCursor, { cursorManager: this.volatileCursorManager });
3246
+ const inferredRemote = await this.runInitialSyncWithDeadline(operation, (signal) => this.bootstrapState(bootstrapCursor, {
3247
+ cursorManager: this.volatileCursorManager,
3248
+ signal,
3249
+ timeoutMode: "initial"
3250
+ }), parentSignal);
3109
3251
  await this.adapter.applySnapshot(preservedLocalSnapshot);
3110
3252
  restoredLocalSnapshot = true;
3111
3253
  let pendingAppend;
@@ -3116,7 +3258,10 @@ var StreamsCrdt = class StreamsCrdt {
3116
3258
  const localBatch = this.exportUpdates(inferredRemote.cursor.serverLowerBoundVersion);
3117
3259
  if (localBatch.batch != null) pendingAppend = await this.appendLocalBatchRemote(inferredRemote.cursor, localBatch.batch);
3118
3260
  }
3119
- const result = await this.bootstrapState(bootstrapCursor);
3261
+ const result = await this.runInitialSyncWithDeadline(operation, (signal) => this.bootstrapState(bootstrapCursor, {
3262
+ signal,
3263
+ timeoutMode: "initial"
3264
+ }), parentSignal);
3120
3265
  if (pendingAppend != null) this.commitProducerAck(pendingAppend.producerAck);
3121
3266
  return result;
3122
3267
  } catch (error) {
@@ -3130,6 +3275,7 @@ var StreamsCrdt = class StreamsCrdt {
3130
3275
  const capturedController = state.requestController;
3131
3276
  let inactivityTimer = null;
3132
3277
  let inactivityTriggered = false;
3278
+ let sseOpenedAtMs = 0;
3133
3279
  const clearInactivity = () => {
3134
3280
  if (inactivityTimer != null) {
3135
3281
  clearTimeout(inactivityTimer);
@@ -3153,7 +3299,7 @@ var StreamsCrdt = class StreamsCrdt {
3153
3299
  ...state.cursor,
3154
3300
  nextOffset: "-1",
3155
3301
  serverLowerBoundVersion: this.adapter.emptyVersion()
3156
- });
3302
+ }, { signal: capturedController?.signal });
3157
3303
  state.cursor = bootstrapped.cursor;
3158
3304
  state.streamCursor = bootstrapped.streamCursor;
3159
3305
  state.retryAttempt = 0;
@@ -3216,6 +3362,7 @@ var StreamsCrdt = class StreamsCrdt {
3216
3362
  });
3217
3363
  }, () => {
3218
3364
  resetInactivity();
3365
+ sseOpenedAtMs = Date.now();
3219
3366
  this.setReadSubStatus(state, "ok");
3220
3367
  }, capturedController?.signal, resetInactivity);
3221
3368
  clearInactivity();
@@ -3246,7 +3393,7 @@ var StreamsCrdt = class StreamsCrdt {
3246
3393
  ...state.cursor,
3247
3394
  nextOffset: "-1",
3248
3395
  serverLowerBoundVersion: this.adapter.emptyVersion()
3249
- });
3396
+ }, { signal: capturedController?.signal });
3250
3397
  state.cursor = bootstrapped.cursor;
3251
3398
  state.streamCursor = bootstrapped.streamCursor;
3252
3399
  state.retryAttempt = 0;
@@ -3316,6 +3463,7 @@ var StreamsCrdt = class StreamsCrdt {
3316
3463
  return;
3317
3464
  }
3318
3465
  this.setReadSubStatus(state, "reconnecting");
3466
+ if (sseOpenedAtMs > 0 && Date.now() - sseOpenedAtMs >= 5e3) state.retryAttempt = 0;
3319
3467
  const maxAttempts = this.reconnectConfig.maxAttempts;
3320
3468
  if (state.retryAttempt >= maxAttempts) {
3321
3469
  this.logError("live read reconnect exhausted", error, {
@@ -3544,4 +3692,4 @@ function createStreamUrl(input) {
3544
3692
  //#endregion
3545
3693
  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 };
3546
3694
 
3547
- //# sourceMappingURL=stream-id-fOF9ZqCH.js.map
3695
+ //# sourceMappingURL=stream-id-D-UirMkk.js.map