@loro-dev/streams-crdt 0.6.0 → 0.6.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.
package/dist/flock.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { A as RemoteCursorSaveSource, C as TransportSyncSuccess, D as IndexedDbRemoteCursorStore, E as InMemoryRemoteCursorStore, M as createInitialRemoteCursor, O as IndexedDbRemoteCursorStoreOptions, S as TransportSubscription, T as BeforeRemoteCursorSaveHook, _ as TransportCreateStreamSuccess, a as CrdtAdapter, b as TransportJoinParams, c as JsonObject, d as SnapshotCodec, f as SnapshotTransformHook, g as StreamsCrdtOptions, h as StreamsAuthProvider, i as StreamsCrdt, j as RemoteCursorStore, k as RemoteCursor, l as JsonValue, m as StreamsAuthContext, n as isValidBucketId, o as CrdtUpdateBatch, p as SnapshotUploadOptions, r as isValidRillId, s as IsolatedCrdtAdapter, t as createStreamUrl, u as Result, v as TransportDeleteStreamSuccess, w as BeforeRemoteCursorSaveContext, x as TransportRoomStatus, y as TransportError } from "./stream-id-Do4h2lOD.js";
1
+ import { A as RemoteCursorSaveSource, C as TransportSyncSuccess, D as IndexedDbRemoteCursorStore, E as InMemoryRemoteCursorStore, M as createInitialRemoteCursor, O as IndexedDbRemoteCursorStoreOptions, S as TransportSubscription, T as BeforeRemoteCursorSaveHook, _ as TransportCreateStreamSuccess, a as CrdtAdapter, b as TransportJoinParams, c as JsonObject, d as SnapshotCodec, f as SnapshotTransformHook, g as StreamsCrdtOptions, h as StreamsAuthProvider, i as StreamsCrdt, j as RemoteCursorStore, k as RemoteCursor, l as JsonValue, m as StreamsAuthContext, n as isValidBucketId, o as CrdtUpdateBatch, p as SnapshotUploadOptions, r as isValidRillId, s as IsolatedCrdtAdapter, t as createStreamUrl, u as Result, v as TransportDeleteStreamSuccess, w as BeforeRemoteCursorSaveContext, x as TransportRoomStatus, y as TransportError } from "./stream-id-Dr8ZBpUQ.js";
2
2
  import { ExportBundle, Flock, VersionVector, VersionVector as VersionVector$1 } from "@loro-dev/flock-wasm";
3
3
 
4
4
  //#region src/flock.d.ts
package/dist/flock.js CHANGED
@@ -1,4 +1,4 @@
1
- import { a as InMemoryRemoteCursorStore, i as StreamsCrdt, n as isValidBucketId, o as IndexedDbRemoteCursorStore, r as isValidRillId, s as createInitialRemoteCursor, t as createStreamUrl } from "./stream-id-DBym_2Mj.js";
1
+ import { a as InMemoryRemoteCursorStore, i as StreamsCrdt, n as isValidBucketId, o as IndexedDbRemoteCursorStore, r as isValidRillId, s as createInitialRemoteCursor, t as createStreamUrl } from "./stream-id-0dTFsiaO.js";
2
2
  import { Flock } from "@loro-dev/flock-wasm";
3
3
  //#region src/flock.ts
4
4
  function isAfter(entry, current) {
package/dist/index.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- import { A as RemoteCursorSaveSource, C as TransportSyncSuccess, D as IndexedDbRemoteCursorStore, E as InMemoryRemoteCursorStore, M as createInitialRemoteCursor, O as IndexedDbRemoteCursorStoreOptions, S as TransportSubscription, T as BeforeRemoteCursorSaveHook, _ as TransportCreateStreamSuccess, a as CrdtAdapter, b as TransportJoinParams, c as JsonObject, d as SnapshotCodec, f as SnapshotTransformHook, g as StreamsCrdtOptions, h as StreamsAuthProvider, i as StreamsCrdt, j as RemoteCursorStore, k as RemoteCursor, l as JsonValue, m as StreamsAuthContext, n as isValidBucketId, o as CrdtUpdateBatch, p as SnapshotUploadOptions, r as isValidRillId, s as IsolatedCrdtAdapter, t as createStreamUrl, u as Result, v as TransportDeleteStreamSuccess, w as BeforeRemoteCursorSaveContext, x as TransportRoomStatus, y as TransportError } from "./stream-id-Do4h2lOD.js";
1
+ import { A as RemoteCursorSaveSource, C as TransportSyncSuccess, D as IndexedDbRemoteCursorStore, E as InMemoryRemoteCursorStore, M as createInitialRemoteCursor, O as IndexedDbRemoteCursorStoreOptions, S as TransportSubscription, T as BeforeRemoteCursorSaveHook, _ as TransportCreateStreamSuccess, a as CrdtAdapter, b as TransportJoinParams, c as JsonObject, d as SnapshotCodec, f as SnapshotTransformHook, g as StreamsCrdtOptions, h as StreamsAuthProvider, i as StreamsCrdt, j as RemoteCursorStore, k as RemoteCursor, l as JsonValue, m as StreamsAuthContext, n as isValidBucketId, o as CrdtUpdateBatch, p as SnapshotUploadOptions, r as isValidRillId, s as IsolatedCrdtAdapter, t as createStreamUrl, u as Result, v as TransportDeleteStreamSuccess, w as BeforeRemoteCursorSaveContext, x as TransportRoomStatus, y as TransportError } from "./stream-id-Dr8ZBpUQ.js";
2
2
  export { BeforeRemoteCursorSaveContext, BeforeRemoteCursorSaveHook, CrdtAdapter, CrdtUpdateBatch, InMemoryRemoteCursorStore, IndexedDbRemoteCursorStore, IndexedDbRemoteCursorStoreOptions, IsolatedCrdtAdapter, JsonObject, JsonValue, RemoteCursor, RemoteCursorSaveSource, RemoteCursorStore, Result, SnapshotCodec, SnapshotTransformHook, SnapshotUploadOptions, StreamsAuthContext, StreamsAuthProvider, StreamsCrdt, StreamsCrdtOptions, TransportCreateStreamSuccess, TransportDeleteStreamSuccess, TransportError, TransportJoinParams, TransportRoomStatus, TransportSubscription, TransportSyncSuccess, createInitialRemoteCursor, createStreamUrl, isValidBucketId, isValidRillId };
package/dist/index.js CHANGED
@@ -1,2 +1,2 @@
1
- import { a as InMemoryRemoteCursorStore, i as StreamsCrdt, n as isValidBucketId, o as IndexedDbRemoteCursorStore, r as isValidRillId, s as createInitialRemoteCursor, t as createStreamUrl } from "./stream-id-DBym_2Mj.js";
1
+ import { a as InMemoryRemoteCursorStore, i as StreamsCrdt, n as isValidBucketId, o as IndexedDbRemoteCursorStore, r as isValidRillId, s as createInitialRemoteCursor, t as createStreamUrl } from "./stream-id-0dTFsiaO.js";
2
2
  export { InMemoryRemoteCursorStore, IndexedDbRemoteCursorStore, StreamsCrdt, createInitialRemoteCursor, createStreamUrl, isValidBucketId, isValidRillId };
package/dist/loro.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { A as RemoteCursorSaveSource, C as TransportSyncSuccess, D as IndexedDbRemoteCursorStore, E as InMemoryRemoteCursorStore, M as createInitialRemoteCursor, O as IndexedDbRemoteCursorStoreOptions, S as TransportSubscription, T as BeforeRemoteCursorSaveHook, _ as TransportCreateStreamSuccess, a as CrdtAdapter, b as TransportJoinParams, c as JsonObject, d as SnapshotCodec, f as SnapshotTransformHook, g as StreamsCrdtOptions, h as StreamsAuthProvider, i as StreamsCrdt, j as RemoteCursorStore, k as RemoteCursor, l as JsonValue, m as StreamsAuthContext, n as isValidBucketId, o as CrdtUpdateBatch, p as SnapshotUploadOptions, r as isValidRillId, s as IsolatedCrdtAdapter, t as createStreamUrl, u as Result, v as TransportDeleteStreamSuccess, w as BeforeRemoteCursorSaveContext, x as TransportRoomStatus, y as TransportError } from "./stream-id-Do4h2lOD.js";
1
+ import { A as RemoteCursorSaveSource, C as TransportSyncSuccess, D as IndexedDbRemoteCursorStore, E as InMemoryRemoteCursorStore, M as createInitialRemoteCursor, O as IndexedDbRemoteCursorStoreOptions, S as TransportSubscription, T as BeforeRemoteCursorSaveHook, _ as TransportCreateStreamSuccess, a as CrdtAdapter, b as TransportJoinParams, c as JsonObject, d as SnapshotCodec, f as SnapshotTransformHook, g as StreamsCrdtOptions, h as StreamsAuthProvider, i as StreamsCrdt, j as RemoteCursorStore, k as RemoteCursor, l as JsonValue, m as StreamsAuthContext, n as isValidBucketId, o as CrdtUpdateBatch, p as SnapshotUploadOptions, r as isValidRillId, s as IsolatedCrdtAdapter, t as createStreamUrl, u as Result, v as TransportDeleteStreamSuccess, w as BeforeRemoteCursorSaveContext, x as TransportRoomStatus, y as TransportError } from "./stream-id-Dr8ZBpUQ.js";
2
2
  import { LoroDoc } from "loro-crdt";
3
3
 
4
4
  //#region src/loro.d.ts
package/dist/loro.js CHANGED
@@ -1,4 +1,4 @@
1
- import { a as InMemoryRemoteCursorStore, i as StreamsCrdt, n as isValidBucketId, o as IndexedDbRemoteCursorStore, r as isValidRillId, s as createInitialRemoteCursor, t as createStreamUrl } from "./stream-id-DBym_2Mj.js";
1
+ import { a as InMemoryRemoteCursorStore, i as StreamsCrdt, n as isValidBucketId, o as IndexedDbRemoteCursorStore, r as isValidRillId, s as createInitialRemoteCursor, t as createStreamUrl } from "./stream-id-0dTFsiaO.js";
2
2
  import { LoroDoc, VersionVector, decodeImportBlobMeta } from "loro-crdt";
3
3
  //#region src/loro.ts
4
4
  function batchImportUpdates(doc, updates) {
@@ -184,7 +184,8 @@ const DEFAULT_RECONNECT_CONFIG = {
184
184
  jitterFraction: .2,
185
185
  maxAttempts: Infinity,
186
186
  connectTimeoutMs: 1e4,
187
- pollTimeoutMs: 3e4
187
+ pollTimeoutMs: 3e4,
188
+ liveInactivityTimeoutMs: 45e3
188
189
  };
189
190
  /**
190
191
  * Apply jitter of +/- fraction to a base delay.
@@ -677,13 +678,29 @@ var StreamsTransportClient = class {
677
678
  async readItemsSse(initialOffset, cursor, onControl, onOpen, signal) {
678
679
  const opened = await this.openItemsSseSession(initialOffset, cursor, onOpen, signal);
679
680
  if (opened.kind !== "ok") return opened;
680
- const result = await opened.session.continue(onControl);
681
- if (result.paused) await opened.session.cancel();
682
- return {
683
- kind: "ok",
684
- latestOffset: result.latestOffset,
685
- latestCursor: result.latestCursor
686
- };
681
+ let abortHandler;
682
+ const abortRace = signal != null && !signal.aborted ? new Promise((_, reject) => {
683
+ abortHandler = () => {
684
+ opened.session.cancel();
685
+ reject(signal.reason ?? new DOMException("The operation was aborted.", "AbortError"));
686
+ };
687
+ signal.addEventListener("abort", abortHandler, { once: true });
688
+ }) : null;
689
+ try {
690
+ if (signal?.aborted) {
691
+ await opened.session.cancel();
692
+ throw signal.reason ?? new DOMException("The operation was aborted.", "AbortError");
693
+ }
694
+ const result = abortRace != null ? await Promise.race([opened.session.continue(onControl), abortRace]) : await opened.session.continue(onControl);
695
+ if (result.paused) await opened.session.cancel();
696
+ return {
697
+ kind: "ok",
698
+ latestOffset: result.latestOffset,
699
+ latestCursor: result.latestCursor
700
+ };
701
+ } finally {
702
+ if (abortHandler != null && signal != null) signal.removeEventListener("abort", abortHandler);
703
+ }
687
704
  }
688
705
  async openItemsSseSession(initialOffset, cursor, onOpen, signal) {
689
706
  const result = await this.client.openSseSession({
@@ -1074,6 +1091,8 @@ function createJoinState(cursor, liveMode = "sse") {
1074
1091
  refCount: 0,
1075
1092
  closed: false,
1076
1093
  status: "connecting",
1094
+ readSubStatus: "connecting",
1095
+ writeSubStatus: "ok",
1077
1096
  statusListeners: /* @__PURE__ */ new Set(),
1078
1097
  pendingLocal: new Deque(),
1079
1098
  flushingLocal: false,
@@ -1178,22 +1197,31 @@ var StreamsCrdt = class StreamsCrdt {
1178
1197
  * Resets the retry counter and immediately attempts to reconnect.
1179
1198
  *
1180
1199
  * Use this after device wake or network recovery to skip any pending
1181
- * backoff delay. No-op when the room is already joined or no join is active.
1200
+ * backoff delay. No-op when the room is already fully healthy (both
1201
+ * read and write sub-statuses are "ok") or no join is active.
1182
1202
  *
1183
- * If the room reached `"disconnected"` status (only possible with a finite
1184
- * `maxAttempts` config), this restarts the read loop.
1203
+ * If the room reached `"disconnected"` or `"error"` status, this
1204
+ * restarts the affected loop(s).
1185
1205
  */
1186
1206
  rejoin() {
1187
1207
  const state = this.joinState;
1188
- if (state == null || state.closed || state.status === "joined") return;
1189
- const wasDisconnected = state.status === "disconnected";
1208
+ if (state == null || state.closed) return;
1209
+ if (state.readSubStatus === "ok" && state.writeSubStatus === "ok") return;
1190
1210
  state.retryAttempt = 0;
1191
1211
  state.retrySleepController.abort();
1192
1212
  state.retrySleepController = new AbortController();
1193
- if (wasDisconnected) {
1194
- this.setJoinStatus(state, "reconnecting");
1213
+ const previousController = state.requestController;
1214
+ state.requestController = new AbortController();
1215
+ previousController?.abort();
1216
+ if (state.readSubStatus === "disconnected" || state.readSubStatus === "error") {
1217
+ this.setReadSubStatus(state, "reconnecting");
1195
1218
  this.runReadLoop(state);
1196
1219
  }
1220
+ if (state.writeSubStatus === "error") {
1221
+ state.writeSubStatus = state.pendingLocal.length > 0 ? "reconnecting" : "ok";
1222
+ this.recomputeJoinStatus(state);
1223
+ if (state.pendingLocal.length > 0) this.flushPendingLocal(state);
1224
+ }
1197
1225
  }
1198
1226
  /**
1199
1227
  * Reads the latest remote snapshot offset, if the server exposes one.
@@ -1377,13 +1405,16 @@ var StreamsCrdt = class StreamsCrdt {
1377
1405
  if (state.status === "error" || state.status === "disconnected") throw new Error("join local sync failed before subscription became ready");
1378
1406
  }
1379
1407
  if (state.closed) return;
1380
- this.setJoinStatus(state, "joined");
1408
+ this.setReadSubStatus(state, "ok");
1409
+ this.setWriteSubStatus(state, "ok");
1381
1410
  this.runReadLoop(state);
1382
1411
  } catch (error) {
1383
1412
  if (state.closed) return;
1384
1413
  this.snapshotManager.dispose(state);
1385
1414
  this.logError("join first sync failed", error, { streamUrl: this.streamUrl });
1386
- this.setJoinStatus(state, "error");
1415
+ state.readSubStatus = "error";
1416
+ state.writeSubStatus = "error";
1417
+ this.recomputeJoinStatus(state);
1387
1418
  throw error;
1388
1419
  }
1389
1420
  }
@@ -1435,7 +1466,7 @@ var StreamsCrdt = class StreamsCrdt {
1435
1466
  return caughtUp;
1436
1467
  } catch (error) {
1437
1468
  if (!isStreamNotFoundError(error) || !this.createStreamIfMissing) throw error;
1438
- await this.client.createStreamRequest();
1469
+ await this.ensureStreamExists();
1439
1470
  return await this.bootstrapState(this.createInitialCursor());
1440
1471
  }
1441
1472
  }
@@ -1500,7 +1531,16 @@ var StreamsCrdt = class StreamsCrdt {
1500
1531
  nextProducerSeq: this.nextProducerSeq
1501
1532
  };
1502
1533
  for (let attempt = 0; attempt < 3; attempt += 1) {
1503
- const result = await this.client.appendUpdates(batch.updates, producerSession);
1534
+ let result;
1535
+ try {
1536
+ result = await this.client.appendUpdates(batch.updates, producerSession);
1537
+ } catch (error) {
1538
+ if (isStreamNotFoundError(error) && this.createStreamIfMissing) {
1539
+ await this.ensureStreamExists();
1540
+ continue;
1541
+ }
1542
+ throw error;
1543
+ }
1504
1544
  if (result.kind === "ok") {
1505
1545
  this.producerEpoch = result.ackEpoch;
1506
1546
  this.nextProducerSeq = result.ackSeq + 1;
@@ -1526,6 +1566,17 @@ var StreamsCrdt = class StreamsCrdt {
1526
1566
  }
1527
1567
  throw new Error("append retry budget exhausted");
1528
1568
  }
1569
+ /**
1570
+ * Ensures the target stream exists, creating it if necessary.
1571
+ * Tolerates 409 Conflict (stream already created concurrently).
1572
+ */
1573
+ async ensureStreamExists() {
1574
+ try {
1575
+ await this.client.createStreamRequest();
1576
+ } catch (error) {
1577
+ if (!(error instanceof HttpError && error.status === 409)) throw error;
1578
+ }
1579
+ }
1529
1580
  exportUpdates(fromVersion) {
1530
1581
  const exported = this.adapter.exportUpdates(fromVersion);
1531
1582
  if (exported == null) return { nextRefVersion: fromVersion };
@@ -1589,121 +1640,166 @@ var StreamsCrdt = class StreamsCrdt {
1589
1640
  }
1590
1641
  async runReadLoop(state) {
1591
1642
  state.requestController ??= new AbortController();
1592
- while (!state.closed) try {
1593
- if (state.liveMode === "long-poll") {
1594
- const requestOffset = state.cursor.nextOffset;
1595
- const response = await this.client.readFromOffset(requestOffset, state.streamCursor, "long-poll", state.requestController?.signal);
1596
- if (response.kind === "gone") {
1597
- const bootstrapped = await this.bootstrapState({
1598
- ...state.cursor,
1599
- nextOffset: "-1",
1600
- serverLowerBoundVersion: this.adapter.emptyVersion()
1643
+ while (!state.closed) {
1644
+ const capturedController = state.requestController;
1645
+ let inactivityTimer = null;
1646
+ let inactivityTriggered = false;
1647
+ const clearInactivity = () => {
1648
+ if (inactivityTimer != null) {
1649
+ clearTimeout(inactivityTimer);
1650
+ inactivityTimer = null;
1651
+ }
1652
+ };
1653
+ const resetInactivity = () => {
1654
+ clearInactivity();
1655
+ const timeoutMs = this.reconnectConfig.liveInactivityTimeoutMs;
1656
+ if (timeoutMs != null && timeoutMs > 0 && !state.closed) inactivityTimer = setTimeout(() => {
1657
+ inactivityTriggered = true;
1658
+ capturedController?.abort();
1659
+ }, timeoutMs);
1660
+ };
1661
+ try {
1662
+ if (state.liveMode === "long-poll") {
1663
+ const requestOffset = state.cursor.nextOffset;
1664
+ const response = await this.client.readFromOffset(requestOffset, state.streamCursor, "long-poll", capturedController?.signal);
1665
+ if (response.kind === "gone") {
1666
+ const bootstrapped = await this.bootstrapState({
1667
+ ...state.cursor,
1668
+ nextOffset: "-1",
1669
+ serverLowerBoundVersion: this.adapter.emptyVersion()
1670
+ });
1671
+ state.cursor = bootstrapped.cursor;
1672
+ state.streamCursor = bootstrapped.streamCursor;
1673
+ state.retryAttempt = 0;
1674
+ continue;
1675
+ }
1676
+ const result = response.value;
1677
+ 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`);
1678
+ await this.enqueueExclusive(async () => {
1679
+ if (state.closed) return;
1680
+ state.cursor = await this.cursorManager.applyLiveResult(state.cursor, requestOffset, result, (previousVersion, nextVersion) => {
1681
+ this.snapshotManager.observeRemoteVersion(state, previousVersion, nextVersion);
1682
+ });
1683
+ if (result.streamCursor != null) state.streamCursor = result.streamCursor;
1601
1684
  });
1602
- state.cursor = bootstrapped.cursor;
1603
- state.streamCursor = bootstrapped.streamCursor;
1685
+ if (state.closed) return;
1604
1686
  state.retryAttempt = 0;
1687
+ this.setReadSubStatus(state, "ok");
1688
+ await sleep(0);
1605
1689
  continue;
1606
1690
  }
1607
- const result = response.value;
1608
- 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`);
1609
- await this.enqueueExclusive(async () => {
1610
- if (state.closed) return;
1611
- state.cursor = await this.cursorManager.applyLiveResult(state.cursor, requestOffset, result, (previousVersion, nextVersion) => {
1612
- this.snapshotManager.observeRemoteVersion(state, previousVersion, nextVersion);
1691
+ const sseRequestOffset = state.cursor.nextOffset;
1692
+ const pendingRemote = [];
1693
+ let flushChain = Promise.resolve();
1694
+ resetInactivity();
1695
+ const sseResult = await this.client.readFromOffsetSseItems(sseRequestOffset, state.streamCursor, (updates, nextOffset, _upToDate, streamCursor) => {
1696
+ resetInactivity();
1697
+ pendingRemote.push({
1698
+ updates,
1699
+ nextOffset,
1700
+ streamCursor
1613
1701
  });
1614
- if (result.streamCursor != null) state.streamCursor = result.streamCursor;
1615
- });
1616
- if (state.closed) return;
1617
- state.retryAttempt = 0;
1618
- this.setJoinStatus(state, "joined");
1619
- await sleep(0);
1620
- continue;
1621
- }
1622
- const sseRequestOffset = state.cursor.nextOffset;
1623
- const pendingRemote = [];
1624
- let flushChain = Promise.resolve();
1625
- const sseResult = await this.client.readFromOffsetSseItems(sseRequestOffset, state.streamCursor, (updates, nextOffset, _upToDate, streamCursor) => {
1626
- pendingRemote.push({
1627
- updates,
1628
- nextOffset,
1629
- streamCursor
1630
- });
1631
- flushChain = flushChain.then(async () => {
1632
- const events = pendingRemote.splice(0);
1633
- if (events.length === 0 || state.closed) return;
1634
- const allUpdates = events.flatMap((e) => e.updates);
1635
- const lastEvent = events[events.length - 1];
1636
- await this.enqueueExclusive(async () => {
1637
- if (state.closed) return;
1638
- state.cursor = await this.cursorManager.applyLiveResult(state.cursor, sseRequestOffset, {
1639
- nextOffset: lastEvent.nextOffset,
1640
- updates: allUpdates
1641
- }, (previousVersion, nextVersion) => {
1642
- this.snapshotManager.observeRemoteVersion(state, previousVersion, nextVersion);
1702
+ flushChain = flushChain.then(async () => {
1703
+ const events = pendingRemote.splice(0);
1704
+ if (events.length === 0 || state.closed) return;
1705
+ const allUpdates = events.flatMap((e) => e.updates);
1706
+ const lastEvent = events[events.length - 1];
1707
+ await this.enqueueExclusive(async () => {
1708
+ if (state.closed) return;
1709
+ state.cursor = await this.cursorManager.applyLiveResult(state.cursor, sseRequestOffset, {
1710
+ nextOffset: lastEvent.nextOffset,
1711
+ updates: allUpdates
1712
+ }, (previousVersion, nextVersion) => {
1713
+ this.snapshotManager.observeRemoteVersion(state, previousVersion, nextVersion);
1714
+ });
1715
+ for (let i = events.length - 1; i >= 0; i--) if (events[i].streamCursor != null) {
1716
+ state.streamCursor = events[i].streamCursor;
1717
+ break;
1718
+ }
1643
1719
  });
1644
- for (let i = events.length - 1; i >= 0; i--) if (events[i].streamCursor != null) {
1645
- state.streamCursor = events[i].streamCursor;
1646
- break;
1647
- }
1720
+ this.setReadSubStatus(state, "ok");
1648
1721
  });
1649
- this.setJoinStatus(state, "joined");
1722
+ }, () => {
1723
+ resetInactivity();
1724
+ this.setReadSubStatus(state, "ok");
1725
+ }, capturedController?.signal);
1726
+ clearInactivity();
1727
+ await flushChain;
1728
+ if (state.closed) return;
1729
+ if (sseResult.kind === "ok") {
1730
+ state.retryAttempt = 0;
1731
+ state.streamCursor = sseResult.latestCursor;
1732
+ await sleep(0);
1733
+ continue;
1734
+ }
1735
+ if (sseResult.kind === "unsupported") {
1736
+ state.liveMode = "long-poll";
1737
+ await sleep(0);
1738
+ continue;
1739
+ }
1740
+ if (sseResult.kind === "not-found") {
1741
+ this.logError("live read failed", new HttpError("stream not found", 404), {
1742
+ streamUrl: this.streamUrl,
1743
+ code: "stream_not_found"
1744
+ });
1745
+ this.setReadSubStatus(state, "error");
1746
+ return;
1747
+ }
1748
+ const bootstrapped = await this.bootstrapState({
1749
+ ...state.cursor,
1750
+ nextOffset: "-1",
1751
+ serverLowerBoundVersion: this.adapter.emptyVersion()
1650
1752
  });
1651
- }, () => {
1652
- this.setJoinStatus(state, "joined");
1653
- }, state.requestController?.signal);
1654
- await flushChain;
1655
- if (state.closed) return;
1656
- if (sseResult.kind === "ok") {
1753
+ state.cursor = bootstrapped.cursor;
1754
+ state.streamCursor = bootstrapped.streamCursor;
1657
1755
  state.retryAttempt = 0;
1658
- state.streamCursor = sseResult.latestCursor;
1659
- await sleep(0);
1660
- continue;
1661
- }
1662
- if (sseResult.kind === "unsupported") {
1663
- state.liveMode = "long-poll";
1664
- await sleep(0);
1665
- continue;
1666
- }
1667
- if (sseResult.kind === "not-found") {
1668
- this.logError("live read failed", new HttpError("stream not found", 404), {
1669
- streamUrl: this.streamUrl,
1670
- code: "stream_not_found"
1671
- });
1672
- this.setJoinStatus(state, "error");
1673
- return;
1674
- }
1675
- const bootstrapped = await this.bootstrapState({
1676
- ...state.cursor,
1677
- nextOffset: "-1",
1678
- serverLowerBoundVersion: this.adapter.emptyVersion()
1679
- });
1680
- state.cursor = bootstrapped.cursor;
1681
- state.streamCursor = bootstrapped.streamCursor;
1682
- state.retryAttempt = 0;
1683
- } catch (error) {
1684
- if (state.closed) return;
1685
- const classified = classifyFetchError(error);
1686
- if (!classified.retriable || isAuthOrProtocolError(error)) {
1687
- this.logError("live read failed", error, {
1688
- streamUrl: this.streamUrl,
1689
- code: classified.code
1690
- });
1691
- this.setJoinStatus(state, "error");
1692
- return;
1693
- }
1694
- this.setJoinStatus(state, "reconnecting");
1695
- const maxAttempts = this.reconnectConfig.maxAttempts;
1696
- if (state.retryAttempt >= maxAttempts) {
1697
- this.logError("live read reconnect exhausted", error, {
1698
- streamUrl: this.streamUrl,
1699
- attempts: state.retryAttempt
1700
- });
1701
- this.setJoinStatus(state, "disconnected");
1702
- return;
1756
+ } catch (error) {
1757
+ clearInactivity();
1758
+ if (state.closed) return;
1759
+ if (capturedController?.signal.aborted) {
1760
+ if (inactivityTriggered) {
1761
+ if (state.requestController === capturedController) state.requestController = new AbortController();
1762
+ this.logDebug("sse inactivity timeout", { streamUrl: this.streamUrl });
1763
+ this.setReadSubStatus(state, "reconnecting");
1764
+ const maxAttempts = this.reconnectConfig.maxAttempts;
1765
+ if (state.retryAttempt >= maxAttempts) {
1766
+ this.logError("live read reconnect exhausted (inactivity)", error, {
1767
+ streamUrl: this.streamUrl,
1768
+ attempts: state.retryAttempt
1769
+ });
1770
+ this.setReadSubStatus(state, "disconnected");
1771
+ return;
1772
+ }
1773
+ state.retryAttempt += 1;
1774
+ continue;
1775
+ }
1776
+ state.retryAttempt = 0;
1777
+ this.setReadSubStatus(state, "reconnecting");
1778
+ continue;
1779
+ }
1780
+ const classified = classifyFetchError(error);
1781
+ if (!classified.retriable || isAuthOrProtocolError(error)) {
1782
+ this.logError("live read failed", error, {
1783
+ streamUrl: this.streamUrl,
1784
+ code: classified.code
1785
+ });
1786
+ this.setReadSubStatus(state, "error");
1787
+ return;
1788
+ }
1789
+ this.setReadSubStatus(state, "reconnecting");
1790
+ const maxAttempts = this.reconnectConfig.maxAttempts;
1791
+ if (state.retryAttempt >= maxAttempts) {
1792
+ this.logError("live read reconnect exhausted", error, {
1793
+ streamUrl: this.streamUrl,
1794
+ attempts: state.retryAttempt
1795
+ });
1796
+ this.setReadSubStatus(state, "disconnected");
1797
+ return;
1798
+ }
1799
+ const delay = computeRetryDelay(state.retryAttempt, this.reconnectConfig);
1800
+ state.retryAttempt += 1;
1801
+ await sleep(delay, state.retrySleepController.signal);
1703
1802
  }
1704
- const delay = computeRetryDelay(state.retryAttempt, this.reconnectConfig);
1705
- state.retryAttempt += 1;
1706
- await sleep(delay, state.retrySleepController.signal);
1707
1803
  }
1708
1804
  }
1709
1805
  /** Maximum encoded size (bytes) for a single batched append. */
@@ -1755,7 +1851,7 @@ var StreamsCrdt = class StreamsCrdt {
1755
1851
  state.cursor = await this.enqueueExclusive(async () => this.appendLocalBatch(state.cursor, drained.batch));
1756
1852
  for (let i = 0; i < drained.count; i += 1) state.pendingLocal.popFront();
1757
1853
  this.notifySyncWaiters(state, drained.count);
1758
- this.setJoinStatus(state, "joined");
1854
+ this.setWriteSubStatus(state, "ok");
1759
1855
  this.snapshotManager.schedule(state);
1760
1856
  break;
1761
1857
  } catch (error) {
@@ -1766,16 +1862,16 @@ var StreamsCrdt = class StreamsCrdt {
1766
1862
  code: classified.code
1767
1863
  });
1768
1864
  this.rejectSyncWaiters(state, /* @__PURE__ */ new Error(`local append failed: ${classified.code}`));
1769
- this.setJoinStatus(state, "error");
1865
+ this.setWriteSubStatus(state, "error");
1770
1866
  return;
1771
1867
  }
1772
1868
  const maxAttempts = this.reconnectConfig.maxAttempts;
1773
1869
  if (attempts >= maxAttempts) {
1774
1870
  this.rejectSyncWaiters(state, /* @__PURE__ */ new Error("local append retry budget exhausted"));
1775
- this.setJoinStatus(state, "error");
1871
+ this.setWriteSubStatus(state, "error");
1776
1872
  return;
1777
1873
  }
1778
- this.setJoinStatus(state, "reconnecting");
1874
+ this.setWriteSubStatus(state, "reconnecting");
1779
1875
  const delay = computeRetryDelay(attempts, this.reconnectConfig);
1780
1876
  attempts += 1;
1781
1877
  await sleep(delay, state.retrySleepController.signal);
@@ -1812,11 +1908,30 @@ var StreamsCrdt = class StreamsCrdt {
1812
1908
  beforeRemoteCursorSave: this.beforeRemoteCursorSave
1813
1909
  });
1814
1910
  }
1815
- setJoinStatus(state, status) {
1911
+ setReadSubStatus(state, sub) {
1912
+ if (state.readSubStatus === sub) return;
1913
+ state.readSubStatus = sub;
1914
+ this.recomputeJoinStatus(state);
1915
+ }
1916
+ setWriteSubStatus(state, sub) {
1917
+ if (state.writeSubStatus === sub) return;
1918
+ state.writeSubStatus = sub;
1919
+ this.recomputeJoinStatus(state);
1920
+ }
1921
+ recomputeJoinStatus(state) {
1922
+ const status = this.computeOverallStatus(state);
1816
1923
  if (state.status === status) return;
1817
1924
  state.status = status;
1818
1925
  for (const listener of state.statusListeners) listener(status);
1819
1926
  }
1927
+ computeOverallStatus(state) {
1928
+ const { readSubStatus, writeSubStatus } = state;
1929
+ if (readSubStatus === "error" || writeSubStatus === "error") return "error";
1930
+ if (readSubStatus === "disconnected") return "disconnected";
1931
+ if (readSubStatus === "connecting") return "connecting";
1932
+ if (readSubStatus === "reconnecting" || writeSubStatus === "reconnecting") return "reconnecting";
1933
+ return "joined";
1934
+ }
1820
1935
  enqueueExclusive(task) {
1821
1936
  const run = this.opQueue.then(task, task);
1822
1937
  this.opQueue = run.then(() => void 0, () => void 0);
@@ -1875,4 +1990,4 @@ function createStreamUrl(input) {
1875
1990
  //#endregion
1876
1991
  export { InMemoryRemoteCursorStore as a, StreamsCrdt as i, isValidBucketId as n, IndexedDbRemoteCursorStore as o, isValidRillId as r, createInitialRemoteCursor as s, createStreamUrl as t };
1877
1992
 
1878
- //# sourceMappingURL=stream-id-DBym_2Mj.js.map
1993
+ //# sourceMappingURL=stream-id-0dTFsiaO.js.map