@loro-dev/streams-crdt 0.4.1 → 0.6.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
@@ -1079,7 +1082,8 @@ function createJoinState(cursor, liveMode = "sse") {
1079
1082
  cursor,
1080
1083
  liveMode,
1081
1084
  snapshot: createTransportSnapshotState(),
1082
- started: Promise.resolve()
1085
+ started: Promise.resolve(),
1086
+ syncWaiters: []
1083
1087
  };
1084
1088
  }
1085
1089
  async function disposeJoinState(state, options) {
@@ -1091,6 +1095,8 @@ async function disposeJoinState(state, options) {
1091
1095
  state.unsubscribeLocal = void 0;
1092
1096
  requestController?.abort();
1093
1097
  unsubscribeLocal?.();
1098
+ const disposedError = /* @__PURE__ */ new Error("subscription closed");
1099
+ for (const waiter of state.syncWaiters.splice(0)) waiter.reject(disposedError);
1094
1100
  options.onDisposed?.();
1095
1101
  }
1096
1102
  //#endregion
@@ -1105,6 +1111,8 @@ var StreamsCrdt = class StreamsCrdt {
1105
1111
  createStreamIfMissing;
1106
1112
  client;
1107
1113
  cursorManager;
1114
+ volatileCursorManager;
1115
+ decodeSnapshot;
1108
1116
  reconnectConfig;
1109
1117
  debug;
1110
1118
  beforeRemoteCursorSave;
@@ -1115,6 +1123,11 @@ var StreamsCrdt = class StreamsCrdt {
1115
1123
  joinState;
1116
1124
  opQueue = Promise.resolve();
1117
1125
  /**
1126
+ * Internal catch-up result used when callers need to handle 410 by
1127
+ * preserving local state before rebuilding through bootstrap.
1128
+ */
1129
+ static GONE_CATCHUP = { kind: "gone" };
1130
+ /**
1118
1131
  * Creates a transport for one stream URL and one local adapter instance.
1119
1132
  */
1120
1133
  constructor(options) {
@@ -1132,6 +1145,7 @@ var StreamsCrdt = class StreamsCrdt {
1132
1145
  this.beforeRemoteCursorSave = composeBeforeRemoteCursorSaveHooks(options.beforeRemoteCursorSave, ...options.beforeRemoteCursorSaveHooks ?? []);
1133
1146
  const snapshotCodec = normalizeSnapshotCodec(options.snapshotCodec);
1134
1147
  const snapshotUploadConfig = normalizeSnapshotUploadConfig(options.snapshotUpload);
1148
+ this.decodeSnapshot = snapshotCodec?.decompress;
1135
1149
  this.client = new StreamsTransportClient({
1136
1150
  streamUrl: this.streamUrl,
1137
1151
  adapter: this.adapter,
@@ -1139,17 +1153,14 @@ var StreamsCrdt = class StreamsCrdt {
1139
1153
  fetchImpl,
1140
1154
  reconnectConfig: this.reconnectConfig
1141
1155
  });
1142
- this.cursorManager = new TransportCursorManager({
1143
- adapter: this.adapter,
1144
- decodeSnapshot: snapshotCodec?.decompress,
1145
- saveRemoteCursor: (cursor, source) => this.saveRemoteCursor(cursor, source)
1146
- });
1156
+ this.cursorManager = this.createCursorManager(this.adapter, (cursor, source) => this.saveRemoteCursor(cursor, source));
1157
+ this.volatileCursorManager = this.createCursorManager(this.adapter, async (cursor) => cursor);
1147
1158
  this.snapshotManager = new TransportSnapshotManager({
1148
1159
  adapter: this.adapter,
1149
1160
  config: snapshotUploadConfig,
1150
1161
  encodeSnapshot: snapshotCodec?.compress,
1151
- readSnapshotOffsetValue: () => this.readSnapshotOffsetValue(),
1152
- putSnapshotAt: (snapshotOffset, snapshot) => this.putSnapshotAt(snapshotOffset, snapshot),
1162
+ readSnapshotOffsetValue: () => this.client.readSnapshotOffsetValue(),
1163
+ putSnapshotAt: (snapshotOffset, snapshot) => this.client.putSnapshotAt(snapshotOffset, snapshot),
1153
1164
  enqueueExclusive: (task) => this.enqueueExclusive(task),
1154
1165
  logDebug: (message, detail) => this.logDebug(message, detail),
1155
1166
  logError: (message, error, detail) => this.logError(message, error, detail),
@@ -1191,7 +1202,7 @@ var StreamsCrdt = class StreamsCrdt {
1191
1202
  try {
1192
1203
  return {
1193
1204
  ok: true,
1194
- value: await this.readSnapshotOffsetValue()
1205
+ value: await this.client.readSnapshotOffsetValue()
1195
1206
  };
1196
1207
  } catch (error) {
1197
1208
  return {
@@ -1207,7 +1218,7 @@ var StreamsCrdt = class StreamsCrdt {
1207
1218
  try {
1208
1219
  return {
1209
1220
  ok: true,
1210
- value: { created: await this.createStreamRequest() }
1221
+ value: { created: await this.client.createStreamRequest() }
1211
1222
  };
1212
1223
  } catch (error) {
1213
1224
  return {
@@ -1225,7 +1236,7 @@ var StreamsCrdt = class StreamsCrdt {
1225
1236
  return {
1226
1237
  ok: true,
1227
1238
  value: { deleted: await this.enqueueExclusive(async () => {
1228
- const deleted = await this.deleteStreamRequest();
1239
+ const deleted = await this.client.deleteStreamRequest();
1229
1240
  await this.remoteCursorStore.delete?.(this.streamUrl);
1230
1241
  this.producerEpoch = 0;
1231
1242
  this.nextProducerSeq = 0;
@@ -1322,6 +1333,19 @@ var StreamsCrdt = class StreamsCrdt {
1322
1333
  return () => {
1323
1334
  state.statusListeners.delete(listener);
1324
1335
  };
1336
+ },
1337
+ waitUntilSynced: async () => {
1338
+ await this.adapter.flushLocalExports?.();
1339
+ if (state.closed) throw new Error("subscription closed");
1340
+ const remaining = state.pendingLocal.length;
1341
+ if (remaining === 0) return;
1342
+ return new Promise((resolve, reject) => {
1343
+ state.syncWaiters.push({
1344
+ remaining,
1345
+ resolve,
1346
+ reject
1347
+ });
1348
+ });
1325
1349
  }
1326
1350
  };
1327
1351
  }
@@ -1364,24 +1388,7 @@ var StreamsCrdt = class StreamsCrdt {
1364
1388
  }
1365
1389
  }
1366
1390
  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
- }
1391
+ let cursor = (await this.resolveInitialRemoteState()).cursor;
1385
1392
  const localBatch = this.exportUpdates(cursor.serverLowerBoundVersion);
1386
1393
  if (localBatch.batch != null) {
1387
1394
  cursor = await this.appendLocalBatch(cursor, localBatch.batch);
@@ -1390,32 +1397,11 @@ var StreamsCrdt = class StreamsCrdt {
1390
1397
  return cursor;
1391
1398
  }
1392
1399
  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
- });
1400
+ const initial = await this.resolveInitialRemoteState();
1401
+ let cursor = initial.cursor;
1399
1402
  let localExportRefVersion = cursor.serverLowerBoundVersion;
1400
- let streamCursor;
1403
+ let streamCursor = initial.streamCursor;
1401
1404
  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
1405
  const exportedLocal = this.exportUpdates(localExportRefVersion);
1420
1406
  localExportRefVersion = exportedLocal.nextRefVersion;
1421
1407
  if (exportedLocal.batch != null) {
@@ -1432,26 +1418,74 @@ var StreamsCrdt = class StreamsCrdt {
1432
1418
  localExportRefVersion
1433
1419
  };
1434
1420
  }
1435
- async bootstrapIntoState(cursor) {
1436
- 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;
1421
+ createInitialCursor() {
1422
+ return createInitialRemoteCursor({
1423
+ streamUrl: this.streamUrl,
1424
+ nextOffset: "-1",
1425
+ serverLowerBoundVersion: this.adapter.emptyVersion()
1426
+ });
1427
+ }
1428
+ async resolveInitialRemoteState() {
1429
+ const loaded = await this.remoteCursorStore.load(this.streamUrl);
1430
+ const cursor = loaded ?? this.createInitialCursor();
1431
+ try {
1432
+ if (loaded == null) return await this.bootstrapState(cursor);
1433
+ const caughtUp = await this.catchupOrGone(cursor, { goneStrategy: "return-gone" });
1434
+ if ("kind" in caughtUp) return await this.recoverFromGoneWithLocalBootstrap(cursor);
1435
+ return caughtUp;
1436
+ } catch (error) {
1437
+ if (!isStreamNotFoundError(error) || !this.createStreamIfMissing) throw error;
1438
+ await this.client.createStreamRequest();
1439
+ return await this.bootstrapState(this.createInitialCursor());
1440
+ }
1441
+ }
1442
+ async bootstrapState(cursor, options = {}) {
1443
+ const cursorManager = options.cursorManager ?? this.cursorManager;
1444
+ const result = await this.client.bootstrap();
1445
+ let saved = await cursorManager.applyBootstrapPayload(cursor, result.snapshot, result.updates, result.nextOffset);
1446
+ let bootstrapCursor = result.streamCursor;
1447
+ if (!result.upToDate) {
1448
+ const caughtUp = await this.catchupOrGone(saved, {
1449
+ streamCursor: bootstrapCursor,
1450
+ cursorManager
1451
+ });
1452
+ if ("kind" in caughtUp) throw new Error("bootstrap catch-up unexpectedly returned gone");
1453
+ saved = caughtUp.cursor;
1454
+ bootstrapCursor = caughtUp.streamCursor;
1455
+ }
1456
+ return {
1457
+ cursor: saved,
1458
+ streamCursor: bootstrapCursor
1459
+ };
1440
1460
  }
1441
1461
  async catchup(cursor, live, streamCursor, onRemoteVersionApplied) {
1462
+ const result = await this.catchupOrGone(cursor, {
1463
+ live,
1464
+ streamCursor,
1465
+ onRemoteVersionApplied
1466
+ });
1467
+ if ("kind" in result) throw new Error("catchup unexpectedly returned gone");
1468
+ return result;
1469
+ }
1470
+ async catchupOrGone(cursor, options = {}) {
1442
1471
  let current = cursor;
1443
- let currentStreamCursor = streamCursor;
1472
+ let currentStreamCursor = options.streamCursor;
1473
+ const goneStrategy = options.goneStrategy ?? "bootstrap";
1474
+ const cursorManager = options.cursorManager ?? this.cursorManager;
1444
1475
  while (true) {
1445
1476
  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
- }) };
1477
+ const response = await this.client.readFromOffset(requestOffset, currentStreamCursor, options.live);
1478
+ if (response.kind === "gone") {
1479
+ if (goneStrategy === "return-gone") return StreamsCrdt.GONE_CATCHUP;
1480
+ return await this.bootstrapState({
1481
+ ...current,
1482
+ nextOffset: "-1",
1483
+ serverLowerBoundVersion: this.adapter.emptyVersion()
1484
+ }, { cursorManager });
1485
+ }
1452
1486
  const result = response.value;
1453
1487
  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);
1488
+ current = await cursorManager.applyCatchupResult(current, result, options.onRemoteVersionApplied);
1455
1489
  currentStreamCursor = result.streamCursor;
1456
1490
  if (result.upToDate) return {
1457
1491
  cursor: current,
@@ -1459,14 +1493,14 @@ var StreamsCrdt = class StreamsCrdt {
1459
1493
  };
1460
1494
  }
1461
1495
  }
1462
- async appendLocalBatch(cursor, batch) {
1496
+ async appendLocalBatch(cursor, batch, persist = true) {
1463
1497
  let producerSession = {
1464
1498
  producerId: this.producerId,
1465
1499
  producerEpoch: this.producerEpoch,
1466
1500
  nextProducerSeq: this.nextProducerSeq
1467
1501
  };
1468
1502
  for (let attempt = 0; attempt < 3; attempt += 1) {
1469
- const result = await this.appendUpdates(this.streamUrl, batch.updates, producerSession);
1503
+ const result = await this.client.appendUpdates(batch.updates, producerSession);
1470
1504
  if (result.kind === "ok") {
1471
1505
  this.producerEpoch = result.ackEpoch;
1472
1506
  this.nextProducerSeq = result.ackSeq + 1;
@@ -1475,7 +1509,7 @@ var StreamsCrdt = class StreamsCrdt {
1475
1509
  nextOffset: result.nextOffset,
1476
1510
  serverLowerBoundVersion: this.adapter.mergeVersions(cursor.serverLowerBoundVersion, batch.version)
1477
1511
  };
1478
- return await this.saveRemoteCursor(next, "local");
1512
+ return persist ? await this.saveRemoteCursor(next, "local") : next;
1479
1513
  }
1480
1514
  if (result.kind === "seq-gap") {
1481
1515
  producerSession = {
@@ -1508,19 +1542,65 @@ var StreamsCrdt = class StreamsCrdt {
1508
1542
  async applyRemoteItems(cursor, items, nextOffset) {
1509
1543
  return await this.cursorManager.applyRemoteItems(cursor, items, nextOffset);
1510
1544
  }
1545
+ createCursorManager(adapter, saveRemoteCursor) {
1546
+ return new TransportCursorManager({
1547
+ adapter,
1548
+ decodeSnapshot: this.decodeSnapshot,
1549
+ saveRemoteCursor
1550
+ });
1551
+ }
1552
+ async createIsolatedCursorManager() {
1553
+ const isolated = await this.adapter.createIsolatedAdapter?.();
1554
+ if (isolated == null) return null;
1555
+ return this.createCursorManager(isolated, async (cursor) => cursor);
1556
+ }
1557
+ /**
1558
+ * Infers remote state via bootstrap with the given cursor manager,
1559
+ * exports and appends any local delta, then re-bootstraps to converge
1560
+ * persisted cursor and local CRDT state on server truth.
1561
+ */
1562
+ async inferRemoteAndAppendLocal(bootstrapCursor, cursorManager) {
1563
+ const inferredRemote = await this.bootstrapState(bootstrapCursor, { cursorManager });
1564
+ const localBatch = this.exportUpdates(inferredRemote.cursor.serverLowerBoundVersion);
1565
+ if (localBatch.batch != null) await this.appendLocalBatch(inferredRemote.cursor, localBatch.batch, false);
1566
+ return await this.bootstrapState(bootstrapCursor);
1567
+ }
1568
+ async recoverFromGoneWithLocalBootstrap(cursor) {
1569
+ const isolatedCursorManager = await this.createIsolatedCursorManager();
1570
+ const bootstrapCursor = {
1571
+ ...cursor,
1572
+ nextOffset: "-1",
1573
+ serverLowerBoundVersion: this.adapter.emptyVersion()
1574
+ };
1575
+ if (isolatedCursorManager != null) return await this.inferRemoteAndAppendLocal(bootstrapCursor, isolatedCursorManager);
1576
+ const preservedLocalSnapshot = this.adapter.exportSnapshot();
1577
+ let restoredLocalSnapshot = false;
1578
+ try {
1579
+ const inferredRemote = await this.bootstrapState(bootstrapCursor, { cursorManager: this.volatileCursorManager });
1580
+ await this.adapter.applySnapshot(preservedLocalSnapshot);
1581
+ restoredLocalSnapshot = true;
1582
+ const localBatch = this.exportUpdates(inferredRemote.cursor.serverLowerBoundVersion);
1583
+ if (localBatch.batch != null) await this.appendLocalBatch(inferredRemote.cursor, localBatch.batch, false);
1584
+ return await this.bootstrapState(bootstrapCursor);
1585
+ } catch (error) {
1586
+ if (!restoredLocalSnapshot) await this.adapter.applySnapshot(preservedLocalSnapshot);
1587
+ throw error;
1588
+ }
1589
+ }
1511
1590
  async runReadLoop(state) {
1512
1591
  state.requestController ??= new AbortController();
1513
1592
  while (!state.closed) try {
1514
1593
  if (state.liveMode === "long-poll") {
1515
1594
  const requestOffset = state.cursor.nextOffset;
1516
- const response = await this.readFromOffset(this.streamUrl, requestOffset, state.streamCursor, "long-poll", state.requestController?.signal);
1595
+ const response = await this.client.readFromOffset(requestOffset, state.streamCursor, "long-poll", state.requestController?.signal);
1517
1596
  if (response.kind === "gone") {
1518
- state.cursor = await this.bootstrapIntoState({
1597
+ const bootstrapped = await this.bootstrapState({
1519
1598
  ...state.cursor,
1520
1599
  nextOffset: "-1",
1521
1600
  serverLowerBoundVersion: this.adapter.emptyVersion()
1522
1601
  });
1523
- state.streamCursor = void 0;
1602
+ state.cursor = bootstrapped.cursor;
1603
+ state.streamCursor = bootstrapped.streamCursor;
1524
1604
  state.retryAttempt = 0;
1525
1605
  continue;
1526
1606
  }
@@ -1540,21 +1620,38 @@ var StreamsCrdt = class StreamsCrdt {
1540
1620
  continue;
1541
1621
  }
1542
1622
  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);
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);
1643
+ });
1644
+ for (let i = events.length - 1; i >= 0; i--) if (events[i].streamCursor != null) {
1645
+ state.streamCursor = events[i].streamCursor;
1646
+ break;
1647
+ }
1551
1648
  });
1552
- if (streamCursor != null) state.streamCursor = streamCursor;
1649
+ this.setJoinStatus(state, "joined");
1553
1650
  });
1554
- this.setJoinStatus(state, "joined");
1555
1651
  }, () => {
1556
1652
  this.setJoinStatus(state, "joined");
1557
1653
  }, state.requestController?.signal);
1654
+ await flushChain;
1558
1655
  if (state.closed) return;
1559
1656
  if (sseResult.kind === "ok") {
1560
1657
  state.retryAttempt = 0;
@@ -1575,12 +1672,13 @@ var StreamsCrdt = class StreamsCrdt {
1575
1672
  this.setJoinStatus(state, "error");
1576
1673
  return;
1577
1674
  }
1578
- state.cursor = await this.bootstrapIntoState({
1675
+ const bootstrapped = await this.bootstrapState({
1579
1676
  ...state.cursor,
1580
1677
  nextOffset: "-1",
1581
1678
  serverLowerBoundVersion: this.adapter.emptyVersion()
1582
1679
  });
1583
- state.streamCursor = void 0;
1680
+ state.cursor = bootstrapped.cursor;
1681
+ state.streamCursor = bootstrapped.streamCursor;
1584
1682
  state.retryAttempt = 0;
1585
1683
  } catch (error) {
1586
1684
  if (state.closed) return;
@@ -1656,6 +1754,7 @@ var StreamsCrdt = class StreamsCrdt {
1656
1754
  while (!state.closed) try {
1657
1755
  state.cursor = await this.enqueueExclusive(async () => this.appendLocalBatch(state.cursor, drained.batch));
1658
1756
  for (let i = 0; i < drained.count; i += 1) state.pendingLocal.popFront();
1757
+ this.notifySyncWaiters(state, drained.count);
1659
1758
  this.setJoinStatus(state, "joined");
1660
1759
  this.snapshotManager.schedule(state);
1661
1760
  break;
@@ -1666,11 +1765,13 @@ var StreamsCrdt = class StreamsCrdt {
1666
1765
  streamUrl: this.streamUrl,
1667
1766
  code: classified.code
1668
1767
  });
1768
+ this.rejectSyncWaiters(state, /* @__PURE__ */ new Error(`local append failed: ${classified.code}`));
1669
1769
  this.setJoinStatus(state, "error");
1670
1770
  return;
1671
1771
  }
1672
1772
  const maxAttempts = this.reconnectConfig.maxAttempts;
1673
1773
  if (attempts >= maxAttempts) {
1774
+ this.rejectSyncWaiters(state, /* @__PURE__ */ new Error("local append retry budget exhausted"));
1674
1775
  this.setJoinStatus(state, "error");
1675
1776
  return;
1676
1777
  }
@@ -1684,6 +1785,25 @@ var StreamsCrdt = class StreamsCrdt {
1684
1785
  state.flushingLocal = false;
1685
1786
  }
1686
1787
  }
1788
+ /**
1789
+ * Decrements all sync waiters by the number of flushed batches and
1790
+ * resolves any that have reached zero.
1791
+ */
1792
+ notifySyncWaiters(state, flushedCount) {
1793
+ let i = 0;
1794
+ while (i < state.syncWaiters.length) {
1795
+ const waiter = state.syncWaiters[i];
1796
+ waiter.remaining -= flushedCount;
1797
+ if (waiter.remaining <= 0) {
1798
+ state.syncWaiters.splice(i, 1);
1799
+ waiter.resolve();
1800
+ } else i += 1;
1801
+ }
1802
+ }
1803
+ /** Rejects and removes all sync waiters. */
1804
+ rejectSyncWaiters(state, error) {
1805
+ for (const waiter of state.syncWaiters.splice(0)) waiter.reject(error);
1806
+ }
1687
1807
  async saveRemoteCursor(cursor, source) {
1688
1808
  return await persistRemoteCursor({
1689
1809
  remoteCursorStore: this.remoteCursorStore,
@@ -1697,30 +1817,6 @@ var StreamsCrdt = class StreamsCrdt {
1697
1817
  state.status = status;
1698
1818
  for (const listener of state.statusListeners) listener(status);
1699
1819
  }
1700
- async createStreamRequest() {
1701
- return await this.client.createStreamRequest();
1702
- }
1703
- async deleteStreamRequest() {
1704
- return await this.client.deleteStreamRequest();
1705
- }
1706
- async readFromOffsetSseItems(_streamUrl, offset, cursor, onControl, onOpen, signal) {
1707
- return await this.client.readFromOffsetSseItems(offset, cursor, onControl, onOpen, signal);
1708
- }
1709
- async readFromOffset(_streamUrl, offset, cursor, live, signal) {
1710
- return await this.client.readFromOffset(offset, cursor, live, signal);
1711
- }
1712
- async bootstrap(_streamUrl) {
1713
- return await this.client.bootstrap();
1714
- }
1715
- async readSnapshotOffsetValue() {
1716
- return await this.client.readSnapshotOffsetValue();
1717
- }
1718
- async putSnapshotAt(snapshotOffset, snapshot) {
1719
- await this.client.putSnapshotAt(snapshotOffset, snapshot);
1720
- }
1721
- async appendUpdates(_streamUrl, updates, producerSession) {
1722
- return await this.client.appendUpdates(updates, producerSession);
1723
- }
1724
1820
  enqueueExclusive(task) {
1725
1821
  const run = this.opQueue.then(task, task);
1726
1822
  this.opQueue = run.then(() => void 0, () => void 0);
@@ -1779,4 +1875,4 @@ function createStreamUrl(input) {
1779
1875
  //#endregion
1780
1876
  export { InMemoryRemoteCursorStore as a, StreamsCrdt as i, isValidBucketId as n, IndexedDbRemoteCursorStore as o, isValidRillId as r, createInitialRemoteCursor as s, createStreamUrl as t };
1781
1877
 
1782
- //# sourceMappingURL=stream-id-TsPj50Jj.js.map
1878
+ //# sourceMappingURL=stream-id-DBym_2Mj.js.map