@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.
- package/README.md +2 -1
- package/dist/flock.d.ts +2 -2
- package/dist/flock.js +59 -10
- package/dist/flock.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/loro.d.ts +2 -2
- package/dist/loro.js +26 -3
- package/dist/loro.js.map +1 -1
- package/dist/{stream-id-TsPj50Jj.js → stream-id-DBym_2Mj.js} +209 -113
- package/dist/stream-id-DBym_2Mj.js.map +1 -0
- package/dist/{stream-id-CSottEoY.d.ts → stream-id-Do4h2lOD.d.ts} +71 -11
- package/package.json +1 -1
- package/dist/stream-id-TsPj50Jj.js.map +0 -1
|
@@ -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.
|
|
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.
|
|
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.
|
|
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 =
|
|
1143
|
-
|
|
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
|
-
|
|
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
|
|
1394
|
-
let cursor =
|
|
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
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
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(
|
|
1447
|
-
if (response.kind === "gone")
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
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
|
|
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(
|
|
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(
|
|
1595
|
+
const response = await this.client.readFromOffset(requestOffset, state.streamCursor, "long-poll", state.requestController?.signal);
|
|
1517
1596
|
if (response.kind === "gone") {
|
|
1518
|
-
|
|
1597
|
+
const bootstrapped = await this.bootstrapState({
|
|
1519
1598
|
...state.cursor,
|
|
1520
1599
|
nextOffset: "-1",
|
|
1521
1600
|
serverLowerBoundVersion: this.adapter.emptyVersion()
|
|
1522
1601
|
});
|
|
1523
|
-
state.
|
|
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
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1675
|
+
const bootstrapped = await this.bootstrapState({
|
|
1579
1676
|
...state.cursor,
|
|
1580
1677
|
nextOffset: "-1",
|
|
1581
1678
|
serverLowerBoundVersion: this.adapter.emptyVersion()
|
|
1582
1679
|
});
|
|
1583
|
-
state.
|
|
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-
|
|
1878
|
+
//# sourceMappingURL=stream-id-DBym_2Mj.js.map
|