@loro-dev/streams-crdt 0.8.0 → 0.8.2
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 +83 -2
- package/dist/flock.d.ts +2 -2
- package/dist/flock.js +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/loro.d.ts +2 -4
- package/dist/loro.js +142 -22
- package/dist/loro.js.map +1 -1
- package/dist/{stream-id-BXJvwX_O.d.ts → stream-id-Bi5rAZfP.d.ts} +36 -19
- package/dist/{stream-id-CF1egQDz.js → stream-id-BjmGvObf.js} +183 -63
- package/dist/stream-id-BjmGvObf.js.map +1 -0
- package/package.json +1 -1
- package/dist/stream-id-CF1egQDz.js.map +0 -1
|
@@ -597,6 +597,12 @@ function mergeAbortSignals(...signals) {
|
|
|
597
597
|
function classifyFetchError(err) {
|
|
598
598
|
const message = err instanceof Error ? err.message : String(err);
|
|
599
599
|
const status = err instanceof HttpError ? err.status : void 0;
|
|
600
|
+
if (isCrdtApplyIncompleteError(err)) return {
|
|
601
|
+
code: "apply_incomplete",
|
|
602
|
+
message,
|
|
603
|
+
retriable: true,
|
|
604
|
+
atMs: Date.now()
|
|
605
|
+
};
|
|
600
606
|
if (status === 401 || status === 403) return {
|
|
601
607
|
code: "auth_failed",
|
|
602
608
|
message,
|
|
@@ -650,6 +656,12 @@ function isStreamNotFoundError(error) {
|
|
|
650
656
|
return error instanceof HttpError && error.status === 404;
|
|
651
657
|
}
|
|
652
658
|
function toTransportError(error) {
|
|
659
|
+
if (isCrdtApplyIncompleteError(error)) return {
|
|
660
|
+
code: "apply_incomplete",
|
|
661
|
+
retryable: true,
|
|
662
|
+
reason: "loro_pending_updates",
|
|
663
|
+
message: error instanceof Error ? error.message : String(error)
|
|
664
|
+
};
|
|
653
665
|
if (isStreamNotFoundError(error)) return {
|
|
654
666
|
code: "stream_not_found",
|
|
655
667
|
retryable: false
|
|
@@ -710,6 +722,9 @@ function toTransportError(error) {
|
|
|
710
722
|
message: String(error)
|
|
711
723
|
};
|
|
712
724
|
}
|
|
725
|
+
function isCrdtApplyIncompleteError(error) {
|
|
726
|
+
return error instanceof Error && Array.isArray(error.unresolved) && error.message.includes("CRDT apply incomplete");
|
|
727
|
+
}
|
|
713
728
|
function isReadNoProgress(requestOffset, result) {
|
|
714
729
|
return !result.upToDate && result.updates.length === 0 && result.nextOffset === requestOffset;
|
|
715
730
|
}
|
|
@@ -1231,8 +1246,6 @@ var EphemeralStreamCrdt = class {
|
|
|
1231
1246
|
client;
|
|
1232
1247
|
/** Retry policy shared by the read loop and local write queue. */
|
|
1233
1248
|
reconnectConfig;
|
|
1234
|
-
/** Period for republishing full local ephemeral state, or `false` to disable it. */
|
|
1235
|
-
heartbeatMs;
|
|
1236
1249
|
/** Enables diagnostic console errors for failed read/write loops. */
|
|
1237
1250
|
debug;
|
|
1238
1251
|
/** Current shared subscription state, if the stream is joined. */
|
|
@@ -1244,7 +1257,6 @@ var EphemeralStreamCrdt = class {
|
|
|
1244
1257
|
...DEFAULT_RECONNECT_CONFIG,
|
|
1245
1258
|
...options.reconnectConfig
|
|
1246
1259
|
};
|
|
1247
|
-
this.heartbeatMs = options.heartbeatMs ?? 15e3;
|
|
1248
1260
|
this.debug = options.debug ?? false;
|
|
1249
1261
|
this.client = new StreamsTransportClient({
|
|
1250
1262
|
streamUrl: options.streamUrl,
|
|
@@ -1325,18 +1337,15 @@ var EphemeralStreamCrdt = class {
|
|
|
1325
1337
|
};
|
|
1326
1338
|
}
|
|
1327
1339
|
async startJoin(state) {
|
|
1340
|
+
state.unsubscribeLocal = this.adaptor.subscribeLocalUpdates((update) => {
|
|
1341
|
+
this.enqueueLocal(state, normalizeEphemeralUpdate(update));
|
|
1342
|
+
});
|
|
1328
1343
|
state.requestController = new AbortController();
|
|
1329
1344
|
const firstOpen = this.waitForFirstOpen(state);
|
|
1330
1345
|
this.runReadLoop(state);
|
|
1331
1346
|
await firstOpen;
|
|
1332
1347
|
if (state.closed) return;
|
|
1333
|
-
state.unsubscribeLocal = this.adaptor.subscribeLocalUpdates((update) => {
|
|
1334
|
-
this.enqueueLocal(state, normalizeEphemeralUpdate(update));
|
|
1335
|
-
});
|
|
1336
1348
|
this.setWriteStatus(state, "ok");
|
|
1337
|
-
const fullState = normalizeEphemeralUpdate(await this.adaptor.encodeAll());
|
|
1338
|
-
if (fullState != null) state.pendingLocal.pushBack(fullState);
|
|
1339
|
-
this.startHeartbeat(state);
|
|
1340
1349
|
await this.flushPendingLocal(state);
|
|
1341
1350
|
if (state.status === "error" || state.status === "disconnected") throw new Error(`ephemeral stream failed to join: ${state.status}`);
|
|
1342
1351
|
}
|
|
@@ -1394,7 +1403,6 @@ var EphemeralStreamCrdt = class {
|
|
|
1394
1403
|
if (state.closed) return;
|
|
1395
1404
|
state.closed = true;
|
|
1396
1405
|
state.unsubscribeLocal?.();
|
|
1397
|
-
if (state.heartbeatTimer != null) clearInterval(state.heartbeatTimer);
|
|
1398
1406
|
state.requestController?.abort();
|
|
1399
1407
|
state.retrySleepController.abort();
|
|
1400
1408
|
this.rejectFirstOpen(state, closedSubscriptionError());
|
|
@@ -1420,7 +1428,6 @@ var EphemeralStreamCrdt = class {
|
|
|
1420
1428
|
if (bootstrap.byteLength > 0) await this.adaptor.applyRemoteUpdates([bootstrap]);
|
|
1421
1429
|
this.setReadStatus(state, "ok");
|
|
1422
1430
|
this.resolveFirstOpen(state);
|
|
1423
|
-
if (state.unsubscribeLocal != null) this.enqueueFullState(state);
|
|
1424
1431
|
}, async (update) => {
|
|
1425
1432
|
if (update.byteLength > 0) await this.adaptor.applyRemoteUpdates([update]);
|
|
1426
1433
|
}, capturedController.signal);
|
|
@@ -1466,25 +1473,10 @@ var EphemeralStreamCrdt = class {
|
|
|
1466
1473
|
}
|
|
1467
1474
|
}
|
|
1468
1475
|
}
|
|
1469
|
-
startHeartbeat(state) {
|
|
1470
|
-
if (this.heartbeatMs === false || this.heartbeatMs <= 0) return;
|
|
1471
|
-
state.heartbeatTimer = setInterval(() => {
|
|
1472
|
-
this.enqueueFullState(state);
|
|
1473
|
-
}, this.heartbeatMs);
|
|
1474
|
-
}
|
|
1475
|
-
async enqueueFullState(state) {
|
|
1476
|
-
try {
|
|
1477
|
-
const update = await this.adaptor.encodeAll();
|
|
1478
|
-
this.enqueueLocal(state, normalizeEphemeralUpdate(update));
|
|
1479
|
-
} catch (error) {
|
|
1480
|
-
this.logError("ephemeral encodeAll failed", error, { streamUrl: this.streamUrl });
|
|
1481
|
-
this.setWriteStatus(state, "error");
|
|
1482
|
-
}
|
|
1483
|
-
}
|
|
1484
1476
|
enqueueLocal(state, update) {
|
|
1485
1477
|
if (state.closed || update == null || update.byteLength === 0) return;
|
|
1486
1478
|
state.pendingLocal.pushBack(update);
|
|
1487
|
-
this.flushPendingLocal(state);
|
|
1479
|
+
if (state.writeStatus !== "connecting") this.flushPendingLocal(state);
|
|
1488
1480
|
}
|
|
1489
1481
|
async flushPendingLocal(state) {
|
|
1490
1482
|
if (state.flushingLocal || state.closed) return;
|
|
@@ -1737,67 +1729,115 @@ function createInitialRemoteCursor(input) {
|
|
|
1737
1729
|
}
|
|
1738
1730
|
//#endregion
|
|
1739
1731
|
//#region src/transport-cursor.ts
|
|
1732
|
+
var CrdtApplyIncompleteError = class extends Error {
|
|
1733
|
+
unresolved;
|
|
1734
|
+
constructor(unresolved) {
|
|
1735
|
+
super("CRDT apply incomplete: unresolved remote updates remain");
|
|
1736
|
+
this.unresolved = unresolved;
|
|
1737
|
+
}
|
|
1738
|
+
};
|
|
1739
|
+
function isApplyOutcome(value) {
|
|
1740
|
+
return typeof value === "object" && value != null && "version" in value && "complete" in value && typeof value.complete === "boolean";
|
|
1741
|
+
}
|
|
1742
|
+
function normalizeApplyReturn(value) {
|
|
1743
|
+
if (isApplyOutcome(value)) return value;
|
|
1744
|
+
return {
|
|
1745
|
+
version: value,
|
|
1746
|
+
complete: true
|
|
1747
|
+
};
|
|
1748
|
+
}
|
|
1740
1749
|
var TransportCursorManager = class {
|
|
1741
1750
|
adapter;
|
|
1742
1751
|
decodeSnapshot;
|
|
1743
1752
|
decodeUpdateItems;
|
|
1744
1753
|
saveRemoteCursor;
|
|
1754
|
+
incompleteCursors = /* @__PURE__ */ new WeakMap();
|
|
1745
1755
|
constructor(options) {
|
|
1746
1756
|
this.adapter = options.adapter;
|
|
1747
1757
|
this.decodeSnapshot = options.decodeSnapshot;
|
|
1748
1758
|
this.decodeUpdateItems = options.decodeUpdateItems;
|
|
1749
1759
|
this.saveRemoteCursor = options.saveRemoteCursor;
|
|
1750
1760
|
}
|
|
1751
|
-
|
|
1761
|
+
isCursorComplete(cursor) {
|
|
1762
|
+
return !this.incompleteCursors.has(cursor);
|
|
1763
|
+
}
|
|
1764
|
+
async applyBootstrapPayload(cursor, snapshot, updates, nextOffset, upToDate = false) {
|
|
1752
1765
|
let nextVersion = cursor.serverLowerBoundVersion;
|
|
1753
|
-
|
|
1766
|
+
let complete = true;
|
|
1767
|
+
let unresolved = [];
|
|
1768
|
+
if (snapshot.byteLength > 0) {
|
|
1769
|
+
const outcome = normalizeApplyReturn(await this.adapter.applySnapshot(this.decodeSnapshot == null ? snapshot : await this.decodeSnapshot(snapshot)));
|
|
1770
|
+
nextVersion = outcome.version;
|
|
1771
|
+
complete = outcome.complete;
|
|
1772
|
+
unresolved = outcome.unresolved ?? [];
|
|
1773
|
+
}
|
|
1754
1774
|
const clearUpdates = await this.decodeRemoteUpdateItems(updates);
|
|
1755
|
-
if (clearUpdates.length > 0)
|
|
1775
|
+
if (clearUpdates.length > 0) {
|
|
1776
|
+
const outcome = normalizeApplyReturn(await this.adapter.applyRemoteUpdates(clearUpdates, nextVersion));
|
|
1777
|
+
nextVersion = outcome.version;
|
|
1778
|
+
complete = outcome.complete;
|
|
1779
|
+
unresolved = outcome.unresolved ?? [];
|
|
1780
|
+
}
|
|
1756
1781
|
return await this.finalizeCursor({
|
|
1757
1782
|
...cursor,
|
|
1758
1783
|
nextOffset,
|
|
1759
1784
|
serverLowerBoundVersion: nextVersion
|
|
1760
|
-
}, "bootstrap");
|
|
1785
|
+
}, "bootstrap", complete, unresolved, upToDate);
|
|
1761
1786
|
}
|
|
1762
|
-
async applyBootstrapItems(cursor, items, nextOffset) {
|
|
1787
|
+
async applyBootstrapItems(cursor, items, nextOffset, upToDate = false) {
|
|
1763
1788
|
const snapshot = items[0] ?? new Uint8Array();
|
|
1764
|
-
return await this.applyBootstrapPayload(cursor, snapshot, items.slice(1), nextOffset);
|
|
1789
|
+
return await this.applyBootstrapPayload(cursor, snapshot, items.slice(1), nextOffset, upToDate);
|
|
1765
1790
|
}
|
|
1766
1791
|
async applyRemoteItems(cursor, items, nextOffset) {
|
|
1767
1792
|
return await this.applyRemotePayload(cursor, [...items], nextOffset, "remote", void 0);
|
|
1768
1793
|
}
|
|
1769
1794
|
async applyCatchupResult(cursor, result, onRemoteVersionApplied) {
|
|
1770
|
-
return await this.applyRemotePayload(cursor, result.updates, result.nextOffset, "remote", onRemoteVersionApplied);
|
|
1795
|
+
return await this.applyRemotePayload(cursor, result.updates, result.nextOffset, "remote", onRemoteVersionApplied, result.upToDate);
|
|
1771
1796
|
}
|
|
1772
1797
|
async applyLiveResult(cursor, requestOffset, result, onRemoteVersionApplied) {
|
|
1773
1798
|
const previousVersion = cursor.serverLowerBoundVersion;
|
|
1774
1799
|
let nextVersion = previousVersion;
|
|
1800
|
+
let complete = this.isCursorComplete(cursor);
|
|
1801
|
+
let unresolved = this.incompleteCursors.get(cursor) ?? [];
|
|
1775
1802
|
const clearUpdates = await this.decodeRemoteUpdateItems(result.updates);
|
|
1776
1803
|
if (clearUpdates.length > 0) {
|
|
1777
|
-
|
|
1804
|
+
const outcome = normalizeApplyReturn(await this.adapter.applyRemoteUpdates(clearUpdates, nextVersion));
|
|
1805
|
+
nextVersion = outcome.version;
|
|
1806
|
+
complete = outcome.complete;
|
|
1807
|
+
unresolved = outcome.unresolved ?? [];
|
|
1778
1808
|
await onRemoteVersionApplied?.(previousVersion, nextVersion);
|
|
1779
1809
|
}
|
|
1780
1810
|
return await this.finalizeCursor({
|
|
1781
1811
|
...cursor,
|
|
1782
1812
|
nextOffset: resolveNextOffsetAfterConcurrentAdvance(requestOffset, cursor.nextOffset, result.nextOffset),
|
|
1783
1813
|
serverLowerBoundVersion: nextVersion
|
|
1784
|
-
}, "remote");
|
|
1814
|
+
}, "remote", complete, unresolved, result.upToDate === true);
|
|
1785
1815
|
}
|
|
1786
|
-
async applyRemotePayload(cursor, updates, nextOffset, source, onRemoteVersionApplied) {
|
|
1816
|
+
async applyRemotePayload(cursor, updates, nextOffset, source, onRemoteVersionApplied, upToDate = false) {
|
|
1787
1817
|
const previousVersion = cursor.serverLowerBoundVersion;
|
|
1788
1818
|
let nextVersion = previousVersion;
|
|
1819
|
+
let complete = this.isCursorComplete(cursor);
|
|
1820
|
+
let unresolved = this.incompleteCursors.get(cursor) ?? [];
|
|
1789
1821
|
const clearUpdates = await this.decodeRemoteUpdateItems(updates);
|
|
1790
1822
|
if (clearUpdates.length > 0) {
|
|
1791
|
-
|
|
1823
|
+
const outcome = normalizeApplyReturn(await this.adapter.applyRemoteUpdates(clearUpdates, nextVersion));
|
|
1824
|
+
nextVersion = outcome.version;
|
|
1825
|
+
complete = outcome.complete;
|
|
1826
|
+
unresolved = outcome.unresolved ?? [];
|
|
1792
1827
|
await onRemoteVersionApplied?.(previousVersion, nextVersion);
|
|
1793
1828
|
}
|
|
1794
1829
|
return await this.finalizeCursor({
|
|
1795
1830
|
...cursor,
|
|
1796
1831
|
nextOffset,
|
|
1797
1832
|
serverLowerBoundVersion: nextVersion
|
|
1798
|
-
}, source);
|
|
1833
|
+
}, source, complete, unresolved, upToDate);
|
|
1799
1834
|
}
|
|
1800
|
-
async finalizeCursor(cursor, source) {
|
|
1835
|
+
async finalizeCursor(cursor, source, complete, unresolved, upToDate) {
|
|
1836
|
+
if (!complete) {
|
|
1837
|
+
if (upToDate) throw new CrdtApplyIncompleteError(unresolved);
|
|
1838
|
+
this.incompleteCursors.set(cursor, unresolved);
|
|
1839
|
+
return cursor;
|
|
1840
|
+
}
|
|
1801
1841
|
return await this.saveRemoteCursor(cursor, source);
|
|
1802
1842
|
}
|
|
1803
1843
|
async decodeRemoteUpdateItems(items) {
|
|
@@ -1830,6 +1870,7 @@ var TransportSnapshotManager = class {
|
|
|
1830
1870
|
logDebug;
|
|
1831
1871
|
logError;
|
|
1832
1872
|
isCurrentState;
|
|
1873
|
+
isCursorComplete;
|
|
1833
1874
|
constructor(options) {
|
|
1834
1875
|
this.adapter = options.adapter;
|
|
1835
1876
|
this.config = options.config;
|
|
@@ -1840,6 +1881,7 @@ var TransportSnapshotManager = class {
|
|
|
1840
1881
|
this.logDebug = options.logDebug;
|
|
1841
1882
|
this.logError = options.logError;
|
|
1842
1883
|
this.isCurrentState = options.isCurrentState;
|
|
1884
|
+
this.isCursorComplete = options.isCursorComplete;
|
|
1843
1885
|
}
|
|
1844
1886
|
dispose(state) {
|
|
1845
1887
|
this.clearDebounceTimer(state.snapshot);
|
|
@@ -1889,6 +1931,10 @@ var TransportSnapshotManager = class {
|
|
|
1889
1931
|
async maybeUpload(state) {
|
|
1890
1932
|
const config = await this.resolveUploadConfig(state);
|
|
1891
1933
|
if (config == null) return;
|
|
1934
|
+
if (!this.isCursorComplete(state.cursor)) {
|
|
1935
|
+
this.logDebug("skip snapshot upload: current cursor is incomplete", { offset: state.cursor.nextOffset });
|
|
1936
|
+
return;
|
|
1937
|
+
}
|
|
1892
1938
|
const tailBytes = parseSnapshotByteOffset(state.cursor.nextOffset);
|
|
1893
1939
|
if (tailBytes == null) {
|
|
1894
1940
|
this.logDebug("skip snapshot upload: non-numeric offset token", { offset: state.cursor.nextOffset });
|
|
@@ -2003,6 +2049,7 @@ var StreamsCrdt = class StreamsCrdt {
|
|
|
2003
2049
|
nextProducerSeq = 0;
|
|
2004
2050
|
frozenLocalAppend;
|
|
2005
2051
|
committedPendingCount;
|
|
2052
|
+
committedLocalStreamCursor;
|
|
2006
2053
|
pendingWriteOnly = new Deque();
|
|
2007
2054
|
writeOnlyCursor;
|
|
2008
2055
|
unsubscribeWriteOnlyLocal;
|
|
@@ -2060,7 +2107,8 @@ var StreamsCrdt = class StreamsCrdt {
|
|
|
2060
2107
|
enqueueExclusive: (task) => this.enqueueExclusive(task),
|
|
2061
2108
|
logDebug: (message, detail) => this.logDebug(message, detail),
|
|
2062
2109
|
logError: (message, error, detail) => this.logError(message, error, detail),
|
|
2063
|
-
isCurrentState: (state) => this.joinState === state
|
|
2110
|
+
isCurrentState: (state) => this.joinState === state,
|
|
2111
|
+
isCursorComplete: (cursor) => this.cursorManager.isCursorComplete(cursor)
|
|
2064
2112
|
});
|
|
2065
2113
|
this.writeOnlyCursor = this.createInitialCursor();
|
|
2066
2114
|
this.unsubscribeWriteOnlyLocal = this.adapter.subscribeLocalUpdates((batch) => {
|
|
@@ -2214,6 +2262,7 @@ var StreamsCrdt = class StreamsCrdt {
|
|
|
2214
2262
|
this.nextProducerSeq = 0;
|
|
2215
2263
|
this.frozenLocalAppend = void 0;
|
|
2216
2264
|
this.committedPendingCount = void 0;
|
|
2265
|
+
this.committedLocalStreamCursor = void 0;
|
|
2217
2266
|
return deleted;
|
|
2218
2267
|
}) }
|
|
2219
2268
|
};
|
|
@@ -2449,10 +2498,7 @@ var StreamsCrdt = class StreamsCrdt {
|
|
|
2449
2498
|
async syncOnce() {
|
|
2450
2499
|
let cursor = (await this.finalizeDirectLocalAppendIfNeeded(await this.resolveInitialRemoteState())).cursor;
|
|
2451
2500
|
const localBatch = this.exportUpdates(cursor.serverLowerBoundVersion);
|
|
2452
|
-
if (localBatch.batch != null)
|
|
2453
|
-
cursor = await this.appendLocalBatch(cursor, localBatch.batch);
|
|
2454
|
-
cursor = (await this.catchup(cursor)).cursor;
|
|
2455
|
-
}
|
|
2501
|
+
if (localBatch.batch != null) cursor = (await this.appendLocalBatchDurably(cursor, localBatch.batch)).cursor;
|
|
2456
2502
|
return cursor;
|
|
2457
2503
|
}
|
|
2458
2504
|
async performInitialJoinSync(preferredLiveMode) {
|
|
@@ -2464,10 +2510,9 @@ var StreamsCrdt = class StreamsCrdt {
|
|
|
2464
2510
|
const exportedLocal = this.exportUpdates(localExportRefVersion);
|
|
2465
2511
|
localExportRefVersion = exportedLocal.nextRefVersion;
|
|
2466
2512
|
if (exportedLocal.batch != null) {
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
streamCursor = caughtUp.streamCursor;
|
|
2513
|
+
const committed = await this.appendLocalBatchDurably(cursor, exportedLocal.batch, void 0, "direct", streamCursor);
|
|
2514
|
+
cursor = committed.cursor;
|
|
2515
|
+
streamCursor = committed.streamCursor;
|
|
2471
2516
|
}
|
|
2472
2517
|
localExportRefVersion = cursor.serverLowerBoundVersion;
|
|
2473
2518
|
return {
|
|
@@ -2501,7 +2546,7 @@ var StreamsCrdt = class StreamsCrdt {
|
|
|
2501
2546
|
async bootstrapState(cursor, options = {}) {
|
|
2502
2547
|
const cursorManager = options.cursorManager ?? this.cursorManager;
|
|
2503
2548
|
const result = await this.client.bootstrap();
|
|
2504
|
-
let saved = await cursorManager.applyBootstrapPayload(cursor, result.snapshot, result.updates, result.nextOffset);
|
|
2549
|
+
let saved = await cursorManager.applyBootstrapPayload(cursor, result.snapshot, result.updates, result.nextOffset, result.upToDate);
|
|
2505
2550
|
let bootstrapCursor = result.streamCursor;
|
|
2506
2551
|
if (!result.upToDate) {
|
|
2507
2552
|
const caughtUp = await this.catchupOrGone(saved, {
|
|
@@ -2544,7 +2589,16 @@ var StreamsCrdt = class StreamsCrdt {
|
|
|
2544
2589
|
}
|
|
2545
2590
|
const result = response.value;
|
|
2546
2591
|
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`);
|
|
2547
|
-
|
|
2592
|
+
try {
|
|
2593
|
+
current = await cursorManager.applyCatchupResult(current, result, options.onRemoteVersionApplied);
|
|
2594
|
+
} catch (error) {
|
|
2595
|
+
if (error instanceof CrdtApplyIncompleteError && result.upToDate) return await this.bootstrapState({
|
|
2596
|
+
...current,
|
|
2597
|
+
nextOffset: "-1",
|
|
2598
|
+
serverLowerBoundVersion: this.adapter.emptyVersion()
|
|
2599
|
+
}, { cursorManager });
|
|
2600
|
+
throw error;
|
|
2601
|
+
}
|
|
2548
2602
|
currentStreamCursor = result.streamCursor;
|
|
2549
2603
|
if (result.upToDate) return {
|
|
2550
2604
|
cursor: current,
|
|
@@ -2552,20 +2606,64 @@ var StreamsCrdt = class StreamsCrdt {
|
|
|
2552
2606
|
};
|
|
2553
2607
|
}
|
|
2554
2608
|
}
|
|
2555
|
-
async
|
|
2609
|
+
async recoverLiveIncompleteRemoteApply(state) {
|
|
2610
|
+
try {
|
|
2611
|
+
const bootstrapped = await this.bootstrapState({
|
|
2612
|
+
...state.cursor,
|
|
2613
|
+
nextOffset: "-1",
|
|
2614
|
+
serverLowerBoundVersion: this.adapter.emptyVersion()
|
|
2615
|
+
});
|
|
2616
|
+
state.cursor = bootstrapped.cursor;
|
|
2617
|
+
state.streamCursor = bootstrapped.streamCursor;
|
|
2618
|
+
state.retryAttempt = 0;
|
|
2619
|
+
state.lastError = void 0;
|
|
2620
|
+
this.setReadSubStatus(state, "ok");
|
|
2621
|
+
if (state.pendingLocal.length > 0) this.flushPendingLocal(state);
|
|
2622
|
+
return true;
|
|
2623
|
+
} catch (error) {
|
|
2624
|
+
const classified = classifyFetchError(error);
|
|
2625
|
+
this.logError("live incomplete apply recovery failed", error, {
|
|
2626
|
+
streamUrl: this.streamUrl,
|
|
2627
|
+
code: classified.code
|
|
2628
|
+
});
|
|
2629
|
+
state.lastError = toTransportError(error);
|
|
2630
|
+
this.setReadSubStatus(state, classified.retriable ? "disconnected" : "error");
|
|
2631
|
+
return false;
|
|
2632
|
+
}
|
|
2633
|
+
}
|
|
2634
|
+
async appendLocalBatch(cursor, batch, pendingCount, streamCursor) {
|
|
2556
2635
|
const source = pendingCount == null ? "direct" : "pending";
|
|
2557
|
-
const committed = await this.appendLocalBatchDurably(cursor, batch, pendingCount, source);
|
|
2636
|
+
const committed = await this.appendLocalBatchDurably(cursor, batch, pendingCount, source, streamCursor);
|
|
2558
2637
|
this.committedPendingCount = committed.pendingCount;
|
|
2638
|
+
this.committedLocalStreamCursor = committed.streamCursor;
|
|
2559
2639
|
return committed.cursor;
|
|
2560
2640
|
}
|
|
2561
|
-
async appendLocalBatchDurably(cursor, batch, pendingCount, source = pendingCount == null ? "direct" : "pending") {
|
|
2641
|
+
async appendLocalBatchDurably(cursor, batch, pendingCount, source = pendingCount == null ? "direct" : "pending", streamCursor) {
|
|
2562
2642
|
const frozen = await this.getOrCreateFrozenLocalAppend(cursor, batch, pendingCount, source);
|
|
2563
2643
|
const appended = await this.appendFrozenLocalBatchRemote(frozen);
|
|
2564
|
-
const
|
|
2644
|
+
const caughtUp = await this.catchupLocalAppend(frozen, streamCursor);
|
|
2565
2645
|
this.commitProducerAck(appended.producerAck);
|
|
2566
2646
|
return {
|
|
2567
|
-
cursor:
|
|
2568
|
-
pendingCount: frozen.pendingCount
|
|
2647
|
+
cursor: caughtUp.cursor,
|
|
2648
|
+
pendingCount: frozen.pendingCount,
|
|
2649
|
+
streamCursor: caughtUp.streamCursor
|
|
2650
|
+
};
|
|
2651
|
+
}
|
|
2652
|
+
localAppendReadCursor(frozen) {
|
|
2653
|
+
return {
|
|
2654
|
+
...frozen.baseCursor,
|
|
2655
|
+
serverLowerBoundVersion: this.adapter.mergeVersions(frozen.baseCursor.serverLowerBoundVersion, frozen.batch.version)
|
|
2656
|
+
};
|
|
2657
|
+
}
|
|
2658
|
+
async catchupLocalAppend(frozen, streamCursor) {
|
|
2659
|
+
const caughtUp = await this.catchupOrGone(this.localAppendReadCursor(frozen), {
|
|
2660
|
+
streamCursor,
|
|
2661
|
+
cursorManager: this.volatileCursorManager
|
|
2662
|
+
});
|
|
2663
|
+
if ("kind" in caughtUp) throw new Error("local append catch-up unexpectedly returned gone");
|
|
2664
|
+
return {
|
|
2665
|
+
cursor: await this.saveRemoteCursor(caughtUp.cursor, "local"),
|
|
2666
|
+
streamCursor: caughtUp.streamCursor
|
|
2569
2667
|
};
|
|
2570
2668
|
}
|
|
2571
2669
|
async appendLocalBatchRemote(cursor, batch, pendingCount, source = pendingCount == null ? "direct" : "pending") {
|
|
@@ -2639,9 +2737,9 @@ var StreamsCrdt = class StreamsCrdt {
|
|
|
2639
2737
|
const frozen = this.frozenLocalAppend;
|
|
2640
2738
|
if (frozen == null || frozen.pendingCount != null) return state;
|
|
2641
2739
|
const appended = await this.appendFrozenLocalBatchRemote(frozen);
|
|
2642
|
-
const
|
|
2740
|
+
const caughtUp = await this.catchupLocalAppend(frozen, state.streamCursor);
|
|
2643
2741
|
this.commitProducerAck(appended.producerAck);
|
|
2644
|
-
return
|
|
2742
|
+
return caughtUp;
|
|
2645
2743
|
}
|
|
2646
2744
|
createCurrentProducerSession() {
|
|
2647
2745
|
return {
|
|
@@ -2666,6 +2764,11 @@ var StreamsCrdt = class StreamsCrdt {
|
|
|
2666
2764
|
this.committedPendingCount = void 0;
|
|
2667
2765
|
return count;
|
|
2668
2766
|
}
|
|
2767
|
+
consumeCommittedLocalStreamCursor() {
|
|
2768
|
+
const streamCursor = this.committedLocalStreamCursor;
|
|
2769
|
+
this.committedLocalStreamCursor = void 0;
|
|
2770
|
+
return streamCursor;
|
|
2771
|
+
}
|
|
2669
2772
|
/**
|
|
2670
2773
|
* Ensures the target stream exists, creating it if necessary.
|
|
2671
2774
|
* Tolerates 409 Conflict (stream already created concurrently).
|
|
@@ -2802,6 +2905,7 @@ var StreamsCrdt = class StreamsCrdt {
|
|
|
2802
2905
|
if (state.closed) return;
|
|
2803
2906
|
state.retryAttempt = 0;
|
|
2804
2907
|
this.setReadSubStatus(state, "ok");
|
|
2908
|
+
if (state.pendingLocal.length > 0) this.flushPendingLocal(state);
|
|
2805
2909
|
await sleep(0);
|
|
2806
2910
|
continue;
|
|
2807
2911
|
}
|
|
@@ -2810,12 +2914,13 @@ var StreamsCrdt = class StreamsCrdt {
|
|
|
2810
2914
|
let flushChain = Promise.resolve();
|
|
2811
2915
|
resetInactivity();
|
|
2812
2916
|
const sseStartedAtMs = Date.now();
|
|
2813
|
-
const sseResult = await this.client.readFromOffsetSseItems(sseRequestOffset, state.streamCursor, (updates, nextOffset,
|
|
2917
|
+
const sseResult = await this.client.readFromOffsetSseItems(sseRequestOffset, state.streamCursor, (updates, nextOffset, upToDate, streamCursor) => {
|
|
2814
2918
|
if (capturedController?.signal.aborted) return "stop";
|
|
2815
2919
|
resetInactivity();
|
|
2816
2920
|
pendingRemote.push({
|
|
2817
2921
|
updates,
|
|
2818
2922
|
nextOffset,
|
|
2923
|
+
upToDate,
|
|
2819
2924
|
streamCursor
|
|
2820
2925
|
});
|
|
2821
2926
|
flushChain = flushChain.then(async () => {
|
|
@@ -2827,7 +2932,8 @@ var StreamsCrdt = class StreamsCrdt {
|
|
|
2827
2932
|
if (state.closed) return;
|
|
2828
2933
|
state.cursor = await this.cursorManager.applyLiveResult(state.cursor, sseRequestOffset, {
|
|
2829
2934
|
nextOffset: lastEvent.nextOffset,
|
|
2830
|
-
updates: allUpdates
|
|
2935
|
+
updates: allUpdates,
|
|
2936
|
+
upToDate: lastEvent.upToDate
|
|
2831
2937
|
}, (previousVersion, nextVersion) => {
|
|
2832
2938
|
this.snapshotManager.observeRemoteVersion(state, previousVersion, nextVersion);
|
|
2833
2939
|
});
|
|
@@ -2837,6 +2943,7 @@ var StreamsCrdt = class StreamsCrdt {
|
|
|
2837
2943
|
}
|
|
2838
2944
|
});
|
|
2839
2945
|
this.setReadSubStatus(state, "ok");
|
|
2946
|
+
if (state.pendingLocal.length > 0) this.flushPendingLocal(state);
|
|
2840
2947
|
}).catch((flushError) => {
|
|
2841
2948
|
this.logError("sse flush chain error", flushError, { streamUrl: this.streamUrl });
|
|
2842
2949
|
capturedController?.abort(flushError);
|
|
@@ -2880,6 +2987,11 @@ var StreamsCrdt = class StreamsCrdt {
|
|
|
2880
2987
|
} catch (error) {
|
|
2881
2988
|
clearInactivity();
|
|
2882
2989
|
if (state.closed) return;
|
|
2990
|
+
if (error instanceof CrdtApplyIncompleteError) {
|
|
2991
|
+
if (capturedController?.signal.aborted && state.requestController === capturedController) state.requestController = new AbortController();
|
|
2992
|
+
if (await this.recoverLiveIncompleteRemoteApply(state)) continue;
|
|
2993
|
+
return;
|
|
2994
|
+
}
|
|
2883
2995
|
if (capturedController?.signal.aborted) {
|
|
2884
2996
|
if (inactivityTriggered) {
|
|
2885
2997
|
if (state.requestController === capturedController) state.requestController = new AbortController();
|
|
@@ -2906,6 +3018,11 @@ var StreamsCrdt = class StreamsCrdt {
|
|
|
2906
3018
|
}
|
|
2907
3019
|
const abortReason = capturedController.signal.reason;
|
|
2908
3020
|
const flushError = abortReason instanceof Error ? abortReason : error;
|
|
3021
|
+
if (flushError instanceof CrdtApplyIncompleteError) {
|
|
3022
|
+
if (state.requestController === capturedController) state.requestController = new AbortController();
|
|
3023
|
+
if (await this.recoverLiveIncompleteRemoteApply(state)) continue;
|
|
3024
|
+
return;
|
|
3025
|
+
}
|
|
2909
3026
|
if (flushError instanceof Error && !(flushError instanceof DOMException && flushError.name === "AbortError")) {
|
|
2910
3027
|
const flushClassified = classifyFetchError(flushError);
|
|
2911
3028
|
if (!flushClassified.retriable || isAuthOrProtocolError(flushError)) {
|
|
@@ -2992,6 +3109,7 @@ var StreamsCrdt = class StreamsCrdt {
|
|
|
2992
3109
|
}
|
|
2993
3110
|
async flushPendingLocal(state) {
|
|
2994
3111
|
if (state.flushingLocal || state.closed) return;
|
|
3112
|
+
if (!this.cursorManager.isCursorComplete(state.cursor)) return;
|
|
2995
3113
|
state.flushingLocal = true;
|
|
2996
3114
|
try {
|
|
2997
3115
|
while (!state.closed) {
|
|
@@ -2999,7 +3117,9 @@ var StreamsCrdt = class StreamsCrdt {
|
|
|
2999
3117
|
if (drained == null) return;
|
|
3000
3118
|
let attempts = 0;
|
|
3001
3119
|
while (!state.closed) try {
|
|
3002
|
-
state.cursor = await this.enqueueExclusive(async () => this.appendLocalBatch(state.cursor, drained.batch, drained.count));
|
|
3120
|
+
state.cursor = await this.enqueueExclusive(async () => this.appendLocalBatch(state.cursor, drained.batch, drained.count, state.streamCursor));
|
|
3121
|
+
const committedStreamCursor = this.consumeCommittedLocalStreamCursor();
|
|
3122
|
+
if (committedStreamCursor != null) state.streamCursor = committedStreamCursor;
|
|
3003
3123
|
const committedCount = this.consumeCommittedPendingCount() ?? drained.count;
|
|
3004
3124
|
for (let i = 0; i < committedCount; i += 1) state.pendingLocal.popFront();
|
|
3005
3125
|
this.notifySyncWaiters(state, committedCount);
|
|
@@ -3158,4 +3278,4 @@ function createStreamUrl(input) {
|
|
|
3158
3278
|
//#endregion
|
|
3159
3279
|
export { InMemoryRemoteCursorStore as a, EphemeralStreamCrdt as c, StreamsCrdt as i, PayloadProtectionError as l, isValidBucketId as n, IndexedDbRemoteCursorStore as o, isValidRillId as r, createInitialRemoteCursor as s, createStreamUrl as t };
|
|
3160
3280
|
|
|
3161
|
-
//# sourceMappingURL=stream-id-
|
|
3281
|
+
//# sourceMappingURL=stream-id-BjmGvObf.js.map
|