@loro-dev/streams-crdt 0.4.0 → 0.5.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.
@@ -775,7 +775,7 @@ var TransportCursorManager = class {
775
775
  let nextVersion = cursor.serverLowerBoundVersion;
776
776
  if (snapshot.byteLength > 0) nextVersion = await this.adapter.applySnapshot(this.decodeSnapshot == null ? snapshot : await this.decodeSnapshot(snapshot));
777
777
  if (updates.length > 0) nextVersion = await this.adapter.applyRemoteUpdates([...updates], nextVersion);
778
- return await this.saveRemoteCursor({
778
+ return await this.finalizeCursor({
779
779
  ...cursor,
780
780
  nextOffset,
781
781
  serverLowerBoundVersion: nextVersion
@@ -786,7 +786,7 @@ var TransportCursorManager = class {
786
786
  return await this.applyBootstrapPayload(cursor, snapshot, items.slice(1), nextOffset);
787
787
  }
788
788
  async applyRemoteItems(cursor, items, nextOffset) {
789
- return await this.applyRemotePayload(cursor, [...items], nextOffset, "remote");
789
+ return await this.applyRemotePayload(cursor, [...items], nextOffset, "remote", void 0);
790
790
  }
791
791
  async applyCatchupResult(cursor, result, onRemoteVersionApplied) {
792
792
  return await this.applyRemotePayload(cursor, result.updates, result.nextOffset, "remote", onRemoteVersionApplied);
@@ -798,7 +798,7 @@ var TransportCursorManager = class {
798
798
  nextVersion = await this.adapter.applyRemoteUpdates([...result.updates], nextVersion);
799
799
  await onRemoteVersionApplied?.(previousVersion, nextVersion);
800
800
  }
801
- return await this.saveRemoteCursor({
801
+ return await this.finalizeCursor({
802
802
  ...cursor,
803
803
  nextOffset: resolveNextOffsetAfterConcurrentAdvance(requestOffset, cursor.nextOffset, result.nextOffset),
804
804
  serverLowerBoundVersion: nextVersion
@@ -811,12 +811,15 @@ var TransportCursorManager = class {
811
811
  nextVersion = await this.adapter.applyRemoteUpdates([...updates], nextVersion);
812
812
  await onRemoteVersionApplied?.(previousVersion, nextVersion);
813
813
  }
814
- return await this.saveRemoteCursor({
814
+ return await this.finalizeCursor({
815
815
  ...cursor,
816
816
  nextOffset,
817
817
  serverLowerBoundVersion: nextVersion
818
818
  }, source);
819
819
  }
820
+ async finalizeCursor(cursor, source) {
821
+ return await this.saveRemoteCursor(cursor, source);
822
+ }
820
823
  };
821
824
  //#endregion
822
825
  //#region src/deque.ts
@@ -1105,6 +1108,8 @@ var StreamsCrdt = class StreamsCrdt {
1105
1108
  createStreamIfMissing;
1106
1109
  client;
1107
1110
  cursorManager;
1111
+ volatileCursorManager;
1112
+ decodeSnapshot;
1108
1113
  reconnectConfig;
1109
1114
  debug;
1110
1115
  beforeRemoteCursorSave;
@@ -1115,6 +1120,11 @@ var StreamsCrdt = class StreamsCrdt {
1115
1120
  joinState;
1116
1121
  opQueue = Promise.resolve();
1117
1122
  /**
1123
+ * Internal catch-up result used when callers need to handle 410 by
1124
+ * preserving local state before rebuilding through bootstrap.
1125
+ */
1126
+ static GONE_CATCHUP = { kind: "gone" };
1127
+ /**
1118
1128
  * Creates a transport for one stream URL and one local adapter instance.
1119
1129
  */
1120
1130
  constructor(options) {
@@ -1132,6 +1142,7 @@ var StreamsCrdt = class StreamsCrdt {
1132
1142
  this.beforeRemoteCursorSave = composeBeforeRemoteCursorSaveHooks(options.beforeRemoteCursorSave, ...options.beforeRemoteCursorSaveHooks ?? []);
1133
1143
  const snapshotCodec = normalizeSnapshotCodec(options.snapshotCodec);
1134
1144
  const snapshotUploadConfig = normalizeSnapshotUploadConfig(options.snapshotUpload);
1145
+ this.decodeSnapshot = snapshotCodec?.decompress;
1135
1146
  this.client = new StreamsTransportClient({
1136
1147
  streamUrl: this.streamUrl,
1137
1148
  adapter: this.adapter,
@@ -1139,11 +1150,8 @@ var StreamsCrdt = class StreamsCrdt {
1139
1150
  fetchImpl,
1140
1151
  reconnectConfig: this.reconnectConfig
1141
1152
  });
1142
- this.cursorManager = new TransportCursorManager({
1143
- adapter: this.adapter,
1144
- decodeSnapshot: snapshotCodec?.decompress,
1145
- saveRemoteCursor: (cursor, source) => this.saveRemoteCursor(cursor, source)
1146
- });
1153
+ this.cursorManager = this.createCursorManager(this.adapter, (cursor, source) => this.saveRemoteCursor(cursor, source));
1154
+ this.volatileCursorManager = this.createCursorManager(this.adapter, async (cursor) => cursor);
1147
1155
  this.snapshotManager = new TransportSnapshotManager({
1148
1156
  adapter: this.adapter,
1149
1157
  config: snapshotUploadConfig,
@@ -1364,24 +1372,7 @@ var StreamsCrdt = class StreamsCrdt {
1364
1372
  }
1365
1373
  }
1366
1374
  async syncOnce() {
1367
- const loaded = await this.remoteCursorStore.load(this.streamUrl);
1368
- let cursor = loaded ?? createInitialRemoteCursor({
1369
- streamUrl: this.streamUrl,
1370
- nextOffset: "-1",
1371
- serverLowerBoundVersion: this.adapter.emptyVersion()
1372
- });
1373
- try {
1374
- if (loaded == null) cursor = await this.bootstrapIntoState(cursor);
1375
- else cursor = (await this.catchup(cursor)).cursor;
1376
- } catch (error) {
1377
- if (!isStreamNotFoundError(error) || !this.createStreamIfMissing) throw error;
1378
- await this.createStreamRequest();
1379
- cursor = await this.bootstrapIntoState({
1380
- ...cursor,
1381
- nextOffset: "-1",
1382
- serverLowerBoundVersion: this.adapter.emptyVersion()
1383
- });
1384
- }
1375
+ let cursor = (await this.resolveInitialRemoteState()).cursor;
1385
1376
  const localBatch = this.exportUpdates(cursor.serverLowerBoundVersion);
1386
1377
  if (localBatch.batch != null) {
1387
1378
  cursor = await this.appendLocalBatch(cursor, localBatch.batch);
@@ -1390,32 +1381,11 @@ var StreamsCrdt = class StreamsCrdt {
1390
1381
  return cursor;
1391
1382
  }
1392
1383
  async performInitialJoinSync(preferredLiveMode) {
1393
- const loaded = await this.remoteCursorStore.load(this.streamUrl);
1394
- let cursor = loaded ?? createInitialRemoteCursor({
1395
- streamUrl: this.streamUrl,
1396
- nextOffset: "-1",
1397
- serverLowerBoundVersion: this.adapter.emptyVersion()
1398
- });
1384
+ const initial = await this.resolveInitialRemoteState();
1385
+ let cursor = initial.cursor;
1399
1386
  let localExportRefVersion = cursor.serverLowerBoundVersion;
1400
- let streamCursor;
1387
+ let streamCursor = initial.streamCursor;
1401
1388
  let liveMode = preferredLiveMode;
1402
- try {
1403
- if (loaded == null) cursor = await this.bootstrapIntoState(cursor);
1404
- else {
1405
- const caughtUp = await this.catchup(cursor);
1406
- cursor = caughtUp.cursor;
1407
- streamCursor = caughtUp.streamCursor;
1408
- }
1409
- } catch (error) {
1410
- if (!isStreamNotFoundError(error) || !this.createStreamIfMissing) throw error;
1411
- await this.createStreamRequest();
1412
- cursor = await this.bootstrapIntoState({
1413
- ...cursor,
1414
- nextOffset: "-1",
1415
- serverLowerBoundVersion: this.adapter.emptyVersion()
1416
- });
1417
- }
1418
- localExportRefVersion = cursor.serverLowerBoundVersion;
1419
1389
  const exportedLocal = this.exportUpdates(localExportRefVersion);
1420
1390
  localExportRefVersion = exportedLocal.nextRefVersion;
1421
1391
  if (exportedLocal.batch != null) {
@@ -1432,26 +1402,74 @@ var StreamsCrdt = class StreamsCrdt {
1432
1402
  localExportRefVersion
1433
1403
  };
1434
1404
  }
1435
- async bootstrapIntoState(cursor) {
1405
+ createInitialCursor() {
1406
+ return createInitialRemoteCursor({
1407
+ streamUrl: this.streamUrl,
1408
+ nextOffset: "-1",
1409
+ serverLowerBoundVersion: this.adapter.emptyVersion()
1410
+ });
1411
+ }
1412
+ async resolveInitialRemoteState() {
1413
+ const loaded = await this.remoteCursorStore.load(this.streamUrl);
1414
+ const cursor = loaded ?? this.createInitialCursor();
1415
+ try {
1416
+ if (loaded == null) return await this.bootstrapState(cursor);
1417
+ const caughtUp = await this.catchupOrGone(cursor, { goneStrategy: "return-gone" });
1418
+ if ("kind" in caughtUp) return await this.recoverFromGoneWithLocalBootstrap(cursor);
1419
+ return caughtUp;
1420
+ } catch (error) {
1421
+ if (!isStreamNotFoundError(error) || !this.createStreamIfMissing) throw error;
1422
+ await this.createStreamRequest();
1423
+ return await this.bootstrapState(this.createInitialCursor());
1424
+ }
1425
+ }
1426
+ async bootstrapState(cursor, options = {}) {
1427
+ const cursorManager = options.cursorManager ?? this.cursorManager;
1436
1428
  const result = await this.bootstrap(this.streamUrl);
1437
- const saved = await this.cursorManager.applyBootstrapPayload(cursor, result.snapshot, result.updates, result.nextOffset);
1438
- if (!result.upToDate) return (await this.catchup(saved)).cursor;
1439
- return saved;
1429
+ let saved = await cursorManager.applyBootstrapPayload(cursor, result.snapshot, result.updates, result.nextOffset);
1430
+ let bootstrapCursor = result.streamCursor;
1431
+ if (!result.upToDate) {
1432
+ const caughtUp = await this.catchupOrGone(saved, {
1433
+ streamCursor: bootstrapCursor,
1434
+ cursorManager
1435
+ });
1436
+ if ("kind" in caughtUp) throw new Error("bootstrap catch-up unexpectedly returned gone");
1437
+ saved = caughtUp.cursor;
1438
+ bootstrapCursor = caughtUp.streamCursor;
1439
+ }
1440
+ return {
1441
+ cursor: saved,
1442
+ streamCursor: bootstrapCursor
1443
+ };
1440
1444
  }
1441
1445
  async catchup(cursor, live, streamCursor, onRemoteVersionApplied) {
1446
+ const result = await this.catchupOrGone(cursor, {
1447
+ live,
1448
+ streamCursor,
1449
+ onRemoteVersionApplied
1450
+ });
1451
+ if ("kind" in result) throw new Error("catchup unexpectedly returned gone");
1452
+ return result;
1453
+ }
1454
+ async catchupOrGone(cursor, options = {}) {
1442
1455
  let current = cursor;
1443
- let currentStreamCursor = streamCursor;
1456
+ let currentStreamCursor = options.streamCursor;
1457
+ const goneStrategy = options.goneStrategy ?? "bootstrap";
1458
+ const cursorManager = options.cursorManager ?? this.cursorManager;
1444
1459
  while (true) {
1445
1460
  const requestOffset = current.nextOffset;
1446
- const response = await this.readFromOffset(this.streamUrl, requestOffset, currentStreamCursor, live);
1447
- if (response.kind === "gone") return { cursor: await this.bootstrapIntoState({
1448
- ...current,
1449
- nextOffset: "-1",
1450
- serverLowerBoundVersion: this.adapter.emptyVersion()
1451
- }) };
1461
+ const response = await this.readFromOffset(this.streamUrl, requestOffset, currentStreamCursor, options.live);
1462
+ if (response.kind === "gone") {
1463
+ if (goneStrategy === "return-gone") return StreamsCrdt.GONE_CATCHUP;
1464
+ return await this.bootstrapState({
1465
+ ...current,
1466
+ nextOffset: "-1",
1467
+ serverLowerBoundVersion: this.adapter.emptyVersion()
1468
+ }, { cursorManager });
1469
+ }
1452
1470
  const result = response.value;
1453
1471
  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`);
1454
- current = await this.cursorManager.applyCatchupResult(current, result, onRemoteVersionApplied);
1472
+ current = await cursorManager.applyCatchupResult(current, result, options.onRemoteVersionApplied);
1455
1473
  currentStreamCursor = result.streamCursor;
1456
1474
  if (result.upToDate) return {
1457
1475
  cursor: current,
@@ -1459,7 +1477,7 @@ var StreamsCrdt = class StreamsCrdt {
1459
1477
  };
1460
1478
  }
1461
1479
  }
1462
- async appendLocalBatch(cursor, batch) {
1480
+ async appendLocalBatch(cursor, batch, persist = true) {
1463
1481
  let producerSession = {
1464
1482
  producerId: this.producerId,
1465
1483
  producerEpoch: this.producerEpoch,
@@ -1475,7 +1493,7 @@ var StreamsCrdt = class StreamsCrdt {
1475
1493
  nextOffset: result.nextOffset,
1476
1494
  serverLowerBoundVersion: this.adapter.mergeVersions(cursor.serverLowerBoundVersion, batch.version)
1477
1495
  };
1478
- return await this.saveRemoteCursor(next, "local");
1496
+ return persist ? await this.saveRemoteCursor(next, "local") : next;
1479
1497
  }
1480
1498
  if (result.kind === "seq-gap") {
1481
1499
  producerSession = {
@@ -1508,6 +1526,45 @@ var StreamsCrdt = class StreamsCrdt {
1508
1526
  async applyRemoteItems(cursor, items, nextOffset) {
1509
1527
  return await this.cursorManager.applyRemoteItems(cursor, items, nextOffset);
1510
1528
  }
1529
+ createCursorManager(adapter, saveRemoteCursor) {
1530
+ return new TransportCursorManager({
1531
+ adapter,
1532
+ decodeSnapshot: this.decodeSnapshot,
1533
+ saveRemoteCursor
1534
+ });
1535
+ }
1536
+ async createIsolatedCursorManager() {
1537
+ const isolated = await this.adapter.createIsolatedAdapter?.();
1538
+ if (isolated == null) return null;
1539
+ return this.createCursorManager(isolated, async (cursor) => cursor);
1540
+ }
1541
+ async recoverFromGoneWithLocalBootstrap(cursor) {
1542
+ const isolatedCursorManager = await this.createIsolatedCursorManager();
1543
+ const bootstrapCursor = {
1544
+ ...cursor,
1545
+ nextOffset: "-1",
1546
+ serverLowerBoundVersion: this.adapter.emptyVersion()
1547
+ };
1548
+ if (isolatedCursorManager != null) {
1549
+ const inferredRemote = await this.bootstrapState(bootstrapCursor, { cursorManager: isolatedCursorManager });
1550
+ const localBatch = this.exportUpdates(inferredRemote.cursor.serverLowerBoundVersion);
1551
+ if (localBatch.batch != null) await this.appendLocalBatch(inferredRemote.cursor, localBatch.batch, false);
1552
+ return await this.bootstrapState(bootstrapCursor);
1553
+ }
1554
+ const preservedLocalSnapshot = this.adapter.exportSnapshot();
1555
+ let restoredLocalSnapshot = false;
1556
+ try {
1557
+ const inferredRemote = await this.bootstrapState(bootstrapCursor, { cursorManager: this.volatileCursorManager });
1558
+ await this.adapter.applySnapshot(preservedLocalSnapshot);
1559
+ restoredLocalSnapshot = true;
1560
+ const localBatch = this.exportUpdates(inferredRemote.cursor.serverLowerBoundVersion);
1561
+ if (localBatch.batch != null) await this.appendLocalBatch(inferredRemote.cursor, localBatch.batch, false);
1562
+ return await this.bootstrapState(bootstrapCursor);
1563
+ } catch (error) {
1564
+ if (!restoredLocalSnapshot) await this.adapter.applySnapshot(preservedLocalSnapshot);
1565
+ throw error;
1566
+ }
1567
+ }
1511
1568
  async runReadLoop(state) {
1512
1569
  state.requestController ??= new AbortController();
1513
1570
  while (!state.closed) try {
@@ -1515,12 +1572,13 @@ var StreamsCrdt = class StreamsCrdt {
1515
1572
  const requestOffset = state.cursor.nextOffset;
1516
1573
  const response = await this.readFromOffset(this.streamUrl, requestOffset, state.streamCursor, "long-poll", state.requestController?.signal);
1517
1574
  if (response.kind === "gone") {
1518
- state.cursor = await this.bootstrapIntoState({
1575
+ const bootstrapped = await this.bootstrapState({
1519
1576
  ...state.cursor,
1520
1577
  nextOffset: "-1",
1521
1578
  serverLowerBoundVersion: this.adapter.emptyVersion()
1522
1579
  });
1523
- state.streamCursor = void 0;
1580
+ state.cursor = bootstrapped.cursor;
1581
+ state.streamCursor = bootstrapped.streamCursor;
1524
1582
  state.retryAttempt = 0;
1525
1583
  continue;
1526
1584
  }
@@ -1540,21 +1598,38 @@ var StreamsCrdt = class StreamsCrdt {
1540
1598
  continue;
1541
1599
  }
1542
1600
  const sseRequestOffset = state.cursor.nextOffset;
1543
- const sseResult = await this.readFromOffsetSseItems(this.streamUrl, sseRequestOffset, state.streamCursor, async (updates, nextOffset, _upToDate, streamCursor) => {
1544
- await this.enqueueExclusive(async () => {
1545
- if (state.closed) return;
1546
- state.cursor = await this.cursorManager.applyLiveResult(state.cursor, sseRequestOffset, {
1547
- nextOffset,
1548
- updates
1549
- }, (previousVersion, nextVersion) => {
1550
- this.snapshotManager.observeRemoteVersion(state, previousVersion, nextVersion);
1601
+ const pendingRemote = [];
1602
+ let flushChain = Promise.resolve();
1603
+ const sseResult = await this.readFromOffsetSseItems(this.streamUrl, sseRequestOffset, state.streamCursor, (updates, nextOffset, _upToDate, streamCursor) => {
1604
+ pendingRemote.push({
1605
+ updates,
1606
+ nextOffset,
1607
+ streamCursor
1608
+ });
1609
+ flushChain = flushChain.then(async () => {
1610
+ const events = pendingRemote.splice(0);
1611
+ if (events.length === 0 || state.closed) return;
1612
+ const allUpdates = events.flatMap((e) => e.updates);
1613
+ const lastEvent = events[events.length - 1];
1614
+ await this.enqueueExclusive(async () => {
1615
+ if (state.closed) return;
1616
+ state.cursor = await this.cursorManager.applyLiveResult(state.cursor, sseRequestOffset, {
1617
+ nextOffset: lastEvent.nextOffset,
1618
+ updates: allUpdates
1619
+ }, (previousVersion, nextVersion) => {
1620
+ this.snapshotManager.observeRemoteVersion(state, previousVersion, nextVersion);
1621
+ });
1622
+ for (let i = events.length - 1; i >= 0; i--) if (events[i].streamCursor != null) {
1623
+ state.streamCursor = events[i].streamCursor;
1624
+ break;
1625
+ }
1551
1626
  });
1552
- if (streamCursor != null) state.streamCursor = streamCursor;
1627
+ this.setJoinStatus(state, "joined");
1553
1628
  });
1554
- this.setJoinStatus(state, "joined");
1555
1629
  }, () => {
1556
1630
  this.setJoinStatus(state, "joined");
1557
1631
  }, state.requestController?.signal);
1632
+ await flushChain;
1558
1633
  if (state.closed) return;
1559
1634
  if (sseResult.kind === "ok") {
1560
1635
  state.retryAttempt = 0;
@@ -1575,12 +1650,13 @@ var StreamsCrdt = class StreamsCrdt {
1575
1650
  this.setJoinStatus(state, "error");
1576
1651
  return;
1577
1652
  }
1578
- state.cursor = await this.bootstrapIntoState({
1653
+ const bootstrapped = await this.bootstrapState({
1579
1654
  ...state.cursor,
1580
1655
  nextOffset: "-1",
1581
1656
  serverLowerBoundVersion: this.adapter.emptyVersion()
1582
1657
  });
1583
- state.streamCursor = void 0;
1658
+ state.cursor = bootstrapped.cursor;
1659
+ state.streamCursor = bootstrapped.streamCursor;
1584
1660
  state.retryAttempt = 0;
1585
1661
  } catch (error) {
1586
1662
  if (state.closed) return;
@@ -1779,4 +1855,4 @@ function createStreamUrl(input) {
1779
1855
  //#endregion
1780
1856
  export { InMemoryRemoteCursorStore as a, StreamsCrdt as i, isValidBucketId as n, IndexedDbRemoteCursorStore as o, isValidRillId as r, createInitialRemoteCursor as s, createStreamUrl as t };
1781
1857
 
1782
- //# sourceMappingURL=stream-id-TsPj50Jj.js.map
1858
+ //# sourceMappingURL=stream-id-nPEdFo7c.js.map