@loro-dev/streams-crdt 0.12.2 → 0.13.0

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.js CHANGED
@@ -1,4 +1,4 @@
1
- import { a as InMemoryRemoteCursorStore, c as EphemeralStreamCrdt, i as StreamsCrdt, l as PayloadProtectionError, n as isValidBucketId, o as IndexedDbRemoteCursorStore, r as isValidRillId, s as createInitialRemoteCursor, t as createStreamUrl } from "./stream-id-D-UirMkk.js";
1
+ import { a as InMemoryRemoteCursorStore, c as EphemeralStreamCrdt, i as StreamsCrdt, l as PayloadProtectionError, n as isValidBucketId, o as IndexedDbRemoteCursorStore, r as isValidRillId, s as createInitialRemoteCursor, t as createStreamUrl } from "./stream-id-CvJLq-0s.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.js CHANGED
@@ -1,2 +1,2 @@
1
- import { a as InMemoryRemoteCursorStore, c as EphemeralStreamCrdt, i as StreamsCrdt, l as PayloadProtectionError, n as isValidBucketId, o as IndexedDbRemoteCursorStore, r as isValidRillId, s as createInitialRemoteCursor, t as createStreamUrl } from "./stream-id-D-UirMkk.js";
1
+ import { a as InMemoryRemoteCursorStore, c as EphemeralStreamCrdt, i as StreamsCrdt, l as PayloadProtectionError, n as isValidBucketId, o as IndexedDbRemoteCursorStore, r as isValidRillId, s as createInitialRemoteCursor, t as createStreamUrl } from "./stream-id-CvJLq-0s.js";
2
2
  export { PayloadProtectionError as E2eeError, PayloadProtectionError, EphemeralStreamCrdt, InMemoryRemoteCursorStore, IndexedDbRemoteCursorStore, StreamsCrdt, createInitialRemoteCursor, createStreamUrl, isValidBucketId, isValidRillId };
package/dist/loro.js CHANGED
@@ -1,4 +1,4 @@
1
- import { a as InMemoryRemoteCursorStore, c as EphemeralStreamCrdt, i as StreamsCrdt, l as PayloadProtectionError, n as isValidBucketId, o as IndexedDbRemoteCursorStore, r as isValidRillId, s as createInitialRemoteCursor, t as createStreamUrl } from "./stream-id-D-UirMkk.js";
1
+ import { a as InMemoryRemoteCursorStore, c as EphemeralStreamCrdt, i as StreamsCrdt, l as PayloadProtectionError, n as isValidBucketId, o as IndexedDbRemoteCursorStore, r as isValidRillId, s as createInitialRemoteCursor, t as createStreamUrl } from "./stream-id-CvJLq-0s.js";
2
2
  import { LoroDoc, VersionVector, decodeImportBlobMeta } from "loro-crdt";
3
3
  //#region src/loro.ts
4
4
  function batchImportUpdates(doc, updates) {
@@ -1112,6 +1112,11 @@ var StreamsTransportClient = class {
1112
1112
  const snapshotOffset = result.result.snapshotOffset;
1113
1113
  return snapshotOffset == null || snapshotOffset.length === 0 ? null : snapshotOffset;
1114
1114
  }
1115
+ async streamSupportsAppendCas() {
1116
+ const result = await this.client.streamSupportsAppendCas();
1117
+ if (result.ok) return result.result;
1118
+ throw this.toTransportFailure(result.result);
1119
+ }
1115
1120
  async putSnapshotAt(snapshotOffset, snapshot) {
1116
1121
  const result = await this.client.putSnapshot({
1117
1122
  offset: snapshotOffset,
@@ -1151,6 +1156,43 @@ var StreamsTransportClient = class {
1151
1156
  };
1152
1157
  throw this.toTransportFailure(error);
1153
1158
  }
1159
+ async appendUpdatesCas(expectedOffset, updates, producerSession) {
1160
+ const result = await this.client.appendCas({
1161
+ expectedOffset,
1162
+ part: {
1163
+ contentType: BINARY_CONTENT_TYPE,
1164
+ body: encodeItems(updates)
1165
+ },
1166
+ producer: {
1167
+ producerId: producerSession.producerId,
1168
+ epoch: producerSession.producerEpoch,
1169
+ seq: producerSession.nextProducerSeq
1170
+ }
1171
+ });
1172
+ if (result.ok) {
1173
+ if (result.result.kind === "mismatch") return {
1174
+ kind: "cas-mismatch",
1175
+ expectedOffset: result.result.expectedOffset,
1176
+ currentOffset: result.result.currentOffset
1177
+ };
1178
+ return {
1179
+ kind: "ok",
1180
+ ackEpoch: result.result.value.producer?.epoch ?? producerSession.producerEpoch,
1181
+ ackSeq: result.result.value.producer?.seq ?? producerSession.nextProducerSeq,
1182
+ nextOffset: result.result.value.nextOffset
1183
+ };
1184
+ }
1185
+ const error = result.result;
1186
+ if (error.code === "conflict" && error.detail?.kind === "producer_seq_gap") return {
1187
+ kind: "seq-gap",
1188
+ expectedSeq: error.detail.expectedSeq
1189
+ };
1190
+ if (error.code === "forbidden" && error.detail?.kind === "stale_producer_epoch") return {
1191
+ kind: "stale-epoch",
1192
+ serverEpoch: error.detail.serverEpoch
1193
+ };
1194
+ throw this.toTransportFailure(error);
1195
+ }
1154
1196
  async appendItems(updates) {
1155
1197
  const result = await this.client.append({ part: {
1156
1198
  contentType: BINARY_CONTENT_TYPE,
@@ -1177,7 +1219,7 @@ var StreamsTransportClient = class {
1177
1219
  * the first event must be `bootstrap`, and all following events must be live
1178
1220
  * `data` updates.
1179
1221
  */
1180
- async readEphemeralSse(onBootstrap, onData, signal) {
1222
+ async readEphemeralSse(onBootstrap, onData, signal, onActivity) {
1181
1223
  const requestUrl = this.ephemeralReadEndpoint();
1182
1224
  const response = await this.fetchAuthorized(requestUrl, {
1183
1225
  method: "GET",
@@ -1198,7 +1240,7 @@ var StreamsTransportClient = class {
1198
1240
  const dataEncoding = response.headers.get("Stream-SSE-Data-Encoding") ?? void 0;
1199
1241
  const consume = async () => {
1200
1242
  let sawBootstrap = false;
1201
- for await (const event of readSseEvents(response.body)) {
1243
+ for await (const event of readSseEvents(response.body, { onActivity })) {
1202
1244
  if (!sawBootstrap) {
1203
1245
  if (event.event !== "bootstrap") throw new ProtocolError(`ephemeral sse must start with bootstrap, got '${event.event}'`);
1204
1246
  await onBootstrap(decodeSsePayload(event.data, dataEncoding));
@@ -1689,15 +1731,35 @@ var EphemeralStreamCrdt = class {
1689
1731
  state.requestController ??= new AbortController();
1690
1732
  const capturedController = state.requestController;
1691
1733
  let sseOpenedAtMs = 0;
1734
+ let inactivityTimer = null;
1735
+ let inactivityTriggered = false;
1736
+ const clearInactivity = () => {
1737
+ if (inactivityTimer != null) {
1738
+ clearTimeout(inactivityTimer);
1739
+ inactivityTimer = null;
1740
+ }
1741
+ };
1742
+ const resetInactivity = () => {
1743
+ clearInactivity();
1744
+ const timeoutMs = this.reconnectConfig.liveInactivityTimeoutMs;
1745
+ if (timeoutMs != null && timeoutMs > 0 && !state.closed) inactivityTimer = setTimeout(() => {
1746
+ inactivityTriggered = true;
1747
+ capturedController.abort();
1748
+ }, timeoutMs);
1749
+ };
1692
1750
  try {
1751
+ resetInactivity();
1693
1752
  const result = await this.client.readEphemeralSse(async (bootstrap) => {
1753
+ resetInactivity();
1694
1754
  if (bootstrap.byteLength > 0) await this.adaptor.applyRemoteUpdates([bootstrap]);
1695
1755
  sseOpenedAtMs = Date.now();
1696
1756
  this.setReadStatus(state, "ok");
1697
1757
  this.resolveFirstOpen(state);
1698
1758
  }, async (update) => {
1759
+ resetInactivity();
1699
1760
  if (update.byteLength > 0) await this.adaptor.applyRemoteUpdates([update]);
1700
- }, capturedController.signal);
1761
+ }, capturedController.signal, resetInactivity);
1762
+ clearInactivity();
1701
1763
  if (state.closed) return;
1702
1764
  if (result.kind === "ok") {
1703
1765
  state.retryAttempt = 0;
@@ -1708,10 +1770,25 @@ var EphemeralStreamCrdt = class {
1708
1770
  if (result.kind === "unsupported") throw new Error("ephemeral stream requires SSE support");
1709
1771
  if (result.kind === "not-found") throw new HttpError("ephemeral stream not found", 404);
1710
1772
  } catch (error) {
1773
+ clearInactivity();
1711
1774
  if (state.closed) return;
1712
1775
  if (capturedController.signal.aborted) {
1713
1776
  state.requestController = new AbortController();
1714
1777
  this.setReadStatus(state, "reconnecting");
1778
+ if (!inactivityTriggered) continue;
1779
+ if (sseOpenedAtMs > 0 && Date.now() - sseOpenedAtMs >= 5e3) state.retryAttempt = 0;
1780
+ if (state.retryAttempt >= this.reconnectConfig.maxAttempts) {
1781
+ this.logError("ephemeral live reconnect exhausted (inactivity)", error, {
1782
+ streamUrl: this.streamUrl,
1783
+ attempts: state.retryAttempt
1784
+ });
1785
+ this.setReadStatus(state, "disconnected");
1786
+ this.rejectFirstOpen(state, toError(error));
1787
+ return;
1788
+ }
1789
+ const delay = computeRetryDelay(state.retryAttempt, this.reconnectConfig);
1790
+ state.retryAttempt += 1;
1791
+ await sleep(delay, state.retrySleepController.signal);
1715
1792
  continue;
1716
1793
  }
1717
1794
  const classified = classifyFetchError(error);
@@ -3122,6 +3199,7 @@ var StreamsCrdt = class StreamsCrdt {
3122
3199
  };
3123
3200
  continue;
3124
3201
  }
3202
+ if (result.kind === "cas-mismatch") throw new Error("unexpected CAS mismatch from unconditional append");
3125
3203
  frozen.producerSession = {
3126
3204
  ...frozen.producerSession,
3127
3205
  producerEpoch: result.serverEpoch + 1,
@@ -3692,4 +3770,4 @@ function createStreamUrl(input) {
3692
3770
  //#endregion
3693
3771
  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 };
3694
3772
 
3695
- //# sourceMappingURL=stream-id-D-UirMkk.js.map
3773
+ //# sourceMappingURL=stream-id-CvJLq-0s.js.map