@loro-dev/streams-crdt 0.5.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 +1 -1
- package/dist/flock.js +16 -25
- package/dist/flock.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/loro.d.ts +1 -1
- package/dist/loro.js +1 -1
- package/dist/loro.js.map +1 -1
- package/dist/{stream-id-nPEdFo7c.js → stream-id-0dTFsiaO.js} +302 -167
- package/dist/stream-id-0dTFsiaO.js.map +1 -0
- package/dist/{stream-id-BCCfzKNm.d.ts → stream-id-Dr8ZBpUQ.d.ts} +53 -13
- package/package.json +1 -1
- package/dist/stream-id-nPEdFo7c.js.map +0 -1
|
@@ -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
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
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,
|
|
@@ -1082,7 +1101,8 @@ function createJoinState(cursor, liveMode = "sse") {
|
|
|
1082
1101
|
cursor,
|
|
1083
1102
|
liveMode,
|
|
1084
1103
|
snapshot: createTransportSnapshotState(),
|
|
1085
|
-
started: Promise.resolve()
|
|
1104
|
+
started: Promise.resolve(),
|
|
1105
|
+
syncWaiters: []
|
|
1086
1106
|
};
|
|
1087
1107
|
}
|
|
1088
1108
|
async function disposeJoinState(state, options) {
|
|
@@ -1094,6 +1114,8 @@ async function disposeJoinState(state, options) {
|
|
|
1094
1114
|
state.unsubscribeLocal = void 0;
|
|
1095
1115
|
requestController?.abort();
|
|
1096
1116
|
unsubscribeLocal?.();
|
|
1117
|
+
const disposedError = /* @__PURE__ */ new Error("subscription closed");
|
|
1118
|
+
for (const waiter of state.syncWaiters.splice(0)) waiter.reject(disposedError);
|
|
1097
1119
|
options.onDisposed?.();
|
|
1098
1120
|
}
|
|
1099
1121
|
//#endregion
|
|
@@ -1156,8 +1178,8 @@ var StreamsCrdt = class StreamsCrdt {
|
|
|
1156
1178
|
adapter: this.adapter,
|
|
1157
1179
|
config: snapshotUploadConfig,
|
|
1158
1180
|
encodeSnapshot: snapshotCodec?.compress,
|
|
1159
|
-
readSnapshotOffsetValue: () => this.readSnapshotOffsetValue(),
|
|
1160
|
-
putSnapshotAt: (snapshotOffset, snapshot) => this.putSnapshotAt(snapshotOffset, snapshot),
|
|
1181
|
+
readSnapshotOffsetValue: () => this.client.readSnapshotOffsetValue(),
|
|
1182
|
+
putSnapshotAt: (snapshotOffset, snapshot) => this.client.putSnapshotAt(snapshotOffset, snapshot),
|
|
1161
1183
|
enqueueExclusive: (task) => this.enqueueExclusive(task),
|
|
1162
1184
|
logDebug: (message, detail) => this.logDebug(message, detail),
|
|
1163
1185
|
logError: (message, error, detail) => this.logError(message, error, detail),
|
|
@@ -1175,22 +1197,31 @@ var StreamsCrdt = class StreamsCrdt {
|
|
|
1175
1197
|
* Resets the retry counter and immediately attempts to reconnect.
|
|
1176
1198
|
*
|
|
1177
1199
|
* Use this after device wake or network recovery to skip any pending
|
|
1178
|
-
* backoff delay. No-op when the room is already
|
|
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.
|
|
1179
1202
|
*
|
|
1180
|
-
* If the room reached `"disconnected"`
|
|
1181
|
-
*
|
|
1203
|
+
* If the room reached `"disconnected"` or `"error"` status, this
|
|
1204
|
+
* restarts the affected loop(s).
|
|
1182
1205
|
*/
|
|
1183
1206
|
rejoin() {
|
|
1184
1207
|
const state = this.joinState;
|
|
1185
|
-
if (state == null || state.closed
|
|
1186
|
-
|
|
1208
|
+
if (state == null || state.closed) return;
|
|
1209
|
+
if (state.readSubStatus === "ok" && state.writeSubStatus === "ok") return;
|
|
1187
1210
|
state.retryAttempt = 0;
|
|
1188
1211
|
state.retrySleepController.abort();
|
|
1189
1212
|
state.retrySleepController = new AbortController();
|
|
1190
|
-
|
|
1191
|
-
|
|
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");
|
|
1192
1218
|
this.runReadLoop(state);
|
|
1193
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
|
+
}
|
|
1194
1225
|
}
|
|
1195
1226
|
/**
|
|
1196
1227
|
* Reads the latest remote snapshot offset, if the server exposes one.
|
|
@@ -1199,7 +1230,7 @@ var StreamsCrdt = class StreamsCrdt {
|
|
|
1199
1230
|
try {
|
|
1200
1231
|
return {
|
|
1201
1232
|
ok: true,
|
|
1202
|
-
value: await this.readSnapshotOffsetValue()
|
|
1233
|
+
value: await this.client.readSnapshotOffsetValue()
|
|
1203
1234
|
};
|
|
1204
1235
|
} catch (error) {
|
|
1205
1236
|
return {
|
|
@@ -1215,7 +1246,7 @@ var StreamsCrdt = class StreamsCrdt {
|
|
|
1215
1246
|
try {
|
|
1216
1247
|
return {
|
|
1217
1248
|
ok: true,
|
|
1218
|
-
value: { created: await this.createStreamRequest() }
|
|
1249
|
+
value: { created: await this.client.createStreamRequest() }
|
|
1219
1250
|
};
|
|
1220
1251
|
} catch (error) {
|
|
1221
1252
|
return {
|
|
@@ -1233,7 +1264,7 @@ var StreamsCrdt = class StreamsCrdt {
|
|
|
1233
1264
|
return {
|
|
1234
1265
|
ok: true,
|
|
1235
1266
|
value: { deleted: await this.enqueueExclusive(async () => {
|
|
1236
|
-
const deleted = await this.deleteStreamRequest();
|
|
1267
|
+
const deleted = await this.client.deleteStreamRequest();
|
|
1237
1268
|
await this.remoteCursorStore.delete?.(this.streamUrl);
|
|
1238
1269
|
this.producerEpoch = 0;
|
|
1239
1270
|
this.nextProducerSeq = 0;
|
|
@@ -1330,6 +1361,19 @@ var StreamsCrdt = class StreamsCrdt {
|
|
|
1330
1361
|
return () => {
|
|
1331
1362
|
state.statusListeners.delete(listener);
|
|
1332
1363
|
};
|
|
1364
|
+
},
|
|
1365
|
+
waitUntilSynced: async () => {
|
|
1366
|
+
await this.adapter.flushLocalExports?.();
|
|
1367
|
+
if (state.closed) throw new Error("subscription closed");
|
|
1368
|
+
const remaining = state.pendingLocal.length;
|
|
1369
|
+
if (remaining === 0) return;
|
|
1370
|
+
return new Promise((resolve, reject) => {
|
|
1371
|
+
state.syncWaiters.push({
|
|
1372
|
+
remaining,
|
|
1373
|
+
resolve,
|
|
1374
|
+
reject
|
|
1375
|
+
});
|
|
1376
|
+
});
|
|
1333
1377
|
}
|
|
1334
1378
|
};
|
|
1335
1379
|
}
|
|
@@ -1361,13 +1405,16 @@ var StreamsCrdt = class StreamsCrdt {
|
|
|
1361
1405
|
if (state.status === "error" || state.status === "disconnected") throw new Error("join local sync failed before subscription became ready");
|
|
1362
1406
|
}
|
|
1363
1407
|
if (state.closed) return;
|
|
1364
|
-
this.
|
|
1408
|
+
this.setReadSubStatus(state, "ok");
|
|
1409
|
+
this.setWriteSubStatus(state, "ok");
|
|
1365
1410
|
this.runReadLoop(state);
|
|
1366
1411
|
} catch (error) {
|
|
1367
1412
|
if (state.closed) return;
|
|
1368
1413
|
this.snapshotManager.dispose(state);
|
|
1369
1414
|
this.logError("join first sync failed", error, { streamUrl: this.streamUrl });
|
|
1370
|
-
|
|
1415
|
+
state.readSubStatus = "error";
|
|
1416
|
+
state.writeSubStatus = "error";
|
|
1417
|
+
this.recomputeJoinStatus(state);
|
|
1371
1418
|
throw error;
|
|
1372
1419
|
}
|
|
1373
1420
|
}
|
|
@@ -1419,13 +1466,13 @@ var StreamsCrdt = class StreamsCrdt {
|
|
|
1419
1466
|
return caughtUp;
|
|
1420
1467
|
} catch (error) {
|
|
1421
1468
|
if (!isStreamNotFoundError(error) || !this.createStreamIfMissing) throw error;
|
|
1422
|
-
await this.
|
|
1469
|
+
await this.ensureStreamExists();
|
|
1423
1470
|
return await this.bootstrapState(this.createInitialCursor());
|
|
1424
1471
|
}
|
|
1425
1472
|
}
|
|
1426
1473
|
async bootstrapState(cursor, options = {}) {
|
|
1427
1474
|
const cursorManager = options.cursorManager ?? this.cursorManager;
|
|
1428
|
-
const result = await this.bootstrap(
|
|
1475
|
+
const result = await this.client.bootstrap();
|
|
1429
1476
|
let saved = await cursorManager.applyBootstrapPayload(cursor, result.snapshot, result.updates, result.nextOffset);
|
|
1430
1477
|
let bootstrapCursor = result.streamCursor;
|
|
1431
1478
|
if (!result.upToDate) {
|
|
@@ -1458,7 +1505,7 @@ var StreamsCrdt = class StreamsCrdt {
|
|
|
1458
1505
|
const cursorManager = options.cursorManager ?? this.cursorManager;
|
|
1459
1506
|
while (true) {
|
|
1460
1507
|
const requestOffset = current.nextOffset;
|
|
1461
|
-
const response = await this.readFromOffset(
|
|
1508
|
+
const response = await this.client.readFromOffset(requestOffset, currentStreamCursor, options.live);
|
|
1462
1509
|
if (response.kind === "gone") {
|
|
1463
1510
|
if (goneStrategy === "return-gone") return StreamsCrdt.GONE_CATCHUP;
|
|
1464
1511
|
return await this.bootstrapState({
|
|
@@ -1484,7 +1531,16 @@ var StreamsCrdt = class StreamsCrdt {
|
|
|
1484
1531
|
nextProducerSeq: this.nextProducerSeq
|
|
1485
1532
|
};
|
|
1486
1533
|
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
1487
|
-
|
|
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
|
+
}
|
|
1488
1544
|
if (result.kind === "ok") {
|
|
1489
1545
|
this.producerEpoch = result.ackEpoch;
|
|
1490
1546
|
this.nextProducerSeq = result.ackSeq + 1;
|
|
@@ -1510,6 +1566,17 @@ var StreamsCrdt = class StreamsCrdt {
|
|
|
1510
1566
|
}
|
|
1511
1567
|
throw new Error("append retry budget exhausted");
|
|
1512
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
|
+
}
|
|
1513
1580
|
exportUpdates(fromVersion) {
|
|
1514
1581
|
const exported = this.adapter.exportUpdates(fromVersion);
|
|
1515
1582
|
if (exported == null) return { nextRefVersion: fromVersion };
|
|
@@ -1538,6 +1605,17 @@ var StreamsCrdt = class StreamsCrdt {
|
|
|
1538
1605
|
if (isolated == null) return null;
|
|
1539
1606
|
return this.createCursorManager(isolated, async (cursor) => cursor);
|
|
1540
1607
|
}
|
|
1608
|
+
/**
|
|
1609
|
+
* Infers remote state via bootstrap with the given cursor manager,
|
|
1610
|
+
* exports and appends any local delta, then re-bootstraps to converge
|
|
1611
|
+
* persisted cursor and local CRDT state on server truth.
|
|
1612
|
+
*/
|
|
1613
|
+
async inferRemoteAndAppendLocal(bootstrapCursor, cursorManager) {
|
|
1614
|
+
const inferredRemote = await this.bootstrapState(bootstrapCursor, { cursorManager });
|
|
1615
|
+
const localBatch = this.exportUpdates(inferredRemote.cursor.serverLowerBoundVersion);
|
|
1616
|
+
if (localBatch.batch != null) await this.appendLocalBatch(inferredRemote.cursor, localBatch.batch, false);
|
|
1617
|
+
return await this.bootstrapState(bootstrapCursor);
|
|
1618
|
+
}
|
|
1541
1619
|
async recoverFromGoneWithLocalBootstrap(cursor) {
|
|
1542
1620
|
const isolatedCursorManager = await this.createIsolatedCursorManager();
|
|
1543
1621
|
const bootstrapCursor = {
|
|
@@ -1545,12 +1623,7 @@ var StreamsCrdt = class StreamsCrdt {
|
|
|
1545
1623
|
nextOffset: "-1",
|
|
1546
1624
|
serverLowerBoundVersion: this.adapter.emptyVersion()
|
|
1547
1625
|
};
|
|
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
|
-
}
|
|
1626
|
+
if (isolatedCursorManager != null) return await this.inferRemoteAndAppendLocal(bootstrapCursor, isolatedCursorManager);
|
|
1554
1627
|
const preservedLocalSnapshot = this.adapter.exportSnapshot();
|
|
1555
1628
|
let restoredLocalSnapshot = false;
|
|
1556
1629
|
try {
|
|
@@ -1567,121 +1640,166 @@ var StreamsCrdt = class StreamsCrdt {
|
|
|
1567
1640
|
}
|
|
1568
1641
|
async runReadLoop(state) {
|
|
1569
1642
|
state.requestController ??= new AbortController();
|
|
1570
|
-
while (!state.closed)
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
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;
|
|
1579
1684
|
});
|
|
1580
|
-
state.
|
|
1581
|
-
state.streamCursor = bootstrapped.streamCursor;
|
|
1685
|
+
if (state.closed) return;
|
|
1582
1686
|
state.retryAttempt = 0;
|
|
1687
|
+
this.setReadSubStatus(state, "ok");
|
|
1688
|
+
await sleep(0);
|
|
1583
1689
|
continue;
|
|
1584
1690
|
}
|
|
1585
|
-
const
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
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
|
|
1591
1701
|
});
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
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);
|
|
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
|
+
}
|
|
1621
1719
|
});
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1720
|
+
this.setReadSubStatus(state, "ok");
|
|
1721
|
+
});
|
|
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"
|
|
1626
1744
|
});
|
|
1627
|
-
this.
|
|
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()
|
|
1628
1752
|
});
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
}, state.requestController?.signal);
|
|
1632
|
-
await flushChain;
|
|
1633
|
-
if (state.closed) return;
|
|
1634
|
-
if (sseResult.kind === "ok") {
|
|
1753
|
+
state.cursor = bootstrapped.cursor;
|
|
1754
|
+
state.streamCursor = bootstrapped.streamCursor;
|
|
1635
1755
|
state.retryAttempt = 0;
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
}
|
|
1669
|
-
this.
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
}
|
|
1679
|
-
|
|
1680
|
-
|
|
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);
|
|
1681
1802
|
}
|
|
1682
|
-
const delay = computeRetryDelay(state.retryAttempt, this.reconnectConfig);
|
|
1683
|
-
state.retryAttempt += 1;
|
|
1684
|
-
await sleep(delay, state.retrySleepController.signal);
|
|
1685
1803
|
}
|
|
1686
1804
|
}
|
|
1687
1805
|
/** Maximum encoded size (bytes) for a single batched append. */
|
|
@@ -1732,7 +1850,8 @@ var StreamsCrdt = class StreamsCrdt {
|
|
|
1732
1850
|
while (!state.closed) try {
|
|
1733
1851
|
state.cursor = await this.enqueueExclusive(async () => this.appendLocalBatch(state.cursor, drained.batch));
|
|
1734
1852
|
for (let i = 0; i < drained.count; i += 1) state.pendingLocal.popFront();
|
|
1735
|
-
this.
|
|
1853
|
+
this.notifySyncWaiters(state, drained.count);
|
|
1854
|
+
this.setWriteSubStatus(state, "ok");
|
|
1736
1855
|
this.snapshotManager.schedule(state);
|
|
1737
1856
|
break;
|
|
1738
1857
|
} catch (error) {
|
|
@@ -1742,15 +1861,17 @@ var StreamsCrdt = class StreamsCrdt {
|
|
|
1742
1861
|
streamUrl: this.streamUrl,
|
|
1743
1862
|
code: classified.code
|
|
1744
1863
|
});
|
|
1745
|
-
this.
|
|
1864
|
+
this.rejectSyncWaiters(state, /* @__PURE__ */ new Error(`local append failed: ${classified.code}`));
|
|
1865
|
+
this.setWriteSubStatus(state, "error");
|
|
1746
1866
|
return;
|
|
1747
1867
|
}
|
|
1748
1868
|
const maxAttempts = this.reconnectConfig.maxAttempts;
|
|
1749
1869
|
if (attempts >= maxAttempts) {
|
|
1750
|
-
this.
|
|
1870
|
+
this.rejectSyncWaiters(state, /* @__PURE__ */ new Error("local append retry budget exhausted"));
|
|
1871
|
+
this.setWriteSubStatus(state, "error");
|
|
1751
1872
|
return;
|
|
1752
1873
|
}
|
|
1753
|
-
this.
|
|
1874
|
+
this.setWriteSubStatus(state, "reconnecting");
|
|
1754
1875
|
const delay = computeRetryDelay(attempts, this.reconnectConfig);
|
|
1755
1876
|
attempts += 1;
|
|
1756
1877
|
await sleep(delay, state.retrySleepController.signal);
|
|
@@ -1760,6 +1881,25 @@ var StreamsCrdt = class StreamsCrdt {
|
|
|
1760
1881
|
state.flushingLocal = false;
|
|
1761
1882
|
}
|
|
1762
1883
|
}
|
|
1884
|
+
/**
|
|
1885
|
+
* Decrements all sync waiters by the number of flushed batches and
|
|
1886
|
+
* resolves any that have reached zero.
|
|
1887
|
+
*/
|
|
1888
|
+
notifySyncWaiters(state, flushedCount) {
|
|
1889
|
+
let i = 0;
|
|
1890
|
+
while (i < state.syncWaiters.length) {
|
|
1891
|
+
const waiter = state.syncWaiters[i];
|
|
1892
|
+
waiter.remaining -= flushedCount;
|
|
1893
|
+
if (waiter.remaining <= 0) {
|
|
1894
|
+
state.syncWaiters.splice(i, 1);
|
|
1895
|
+
waiter.resolve();
|
|
1896
|
+
} else i += 1;
|
|
1897
|
+
}
|
|
1898
|
+
}
|
|
1899
|
+
/** Rejects and removes all sync waiters. */
|
|
1900
|
+
rejectSyncWaiters(state, error) {
|
|
1901
|
+
for (const waiter of state.syncWaiters.splice(0)) waiter.reject(error);
|
|
1902
|
+
}
|
|
1763
1903
|
async saveRemoteCursor(cursor, source) {
|
|
1764
1904
|
return await persistRemoteCursor({
|
|
1765
1905
|
remoteCursorStore: this.remoteCursorStore,
|
|
@@ -1768,34 +1908,29 @@ var StreamsCrdt = class StreamsCrdt {
|
|
|
1768
1908
|
beforeRemoteCursorSave: this.beforeRemoteCursorSave
|
|
1769
1909
|
});
|
|
1770
1910
|
}
|
|
1771
|
-
|
|
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);
|
|
1772
1923
|
if (state.status === status) return;
|
|
1773
1924
|
state.status = status;
|
|
1774
1925
|
for (const listener of state.statusListeners) listener(status);
|
|
1775
1926
|
}
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
return await this.client.readFromOffsetSseItems(offset, cursor, onControl, onOpen, signal);
|
|
1784
|
-
}
|
|
1785
|
-
async readFromOffset(_streamUrl, offset, cursor, live, signal) {
|
|
1786
|
-
return await this.client.readFromOffset(offset, cursor, live, signal);
|
|
1787
|
-
}
|
|
1788
|
-
async bootstrap(_streamUrl) {
|
|
1789
|
-
return await this.client.bootstrap();
|
|
1790
|
-
}
|
|
1791
|
-
async readSnapshotOffsetValue() {
|
|
1792
|
-
return await this.client.readSnapshotOffsetValue();
|
|
1793
|
-
}
|
|
1794
|
-
async putSnapshotAt(snapshotOffset, snapshot) {
|
|
1795
|
-
await this.client.putSnapshotAt(snapshotOffset, snapshot);
|
|
1796
|
-
}
|
|
1797
|
-
async appendUpdates(_streamUrl, updates, producerSession) {
|
|
1798
|
-
return await this.client.appendUpdates(updates, producerSession);
|
|
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";
|
|
1799
1934
|
}
|
|
1800
1935
|
enqueueExclusive(task) {
|
|
1801
1936
|
const run = this.opQueue.then(task, task);
|
|
@@ -1855,4 +1990,4 @@ function createStreamUrl(input) {
|
|
|
1855
1990
|
//#endregion
|
|
1856
1991
|
export { InMemoryRemoteCursorStore as a, StreamsCrdt as i, isValidBucketId as n, IndexedDbRemoteCursorStore as o, isValidRillId as r, createInitialRemoteCursor as s, createStreamUrl as t };
|
|
1857
1992
|
|
|
1858
|
-
//# sourceMappingURL=stream-id-
|
|
1993
|
+
//# sourceMappingURL=stream-id-0dTFsiaO.js.map
|