@loro-dev/streams-crdt 0.15.1 → 0.16.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/README.md +11 -8
- package/dist/flock.d.ts +1 -1
- package/dist/flock.js +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/loro.d.ts +9 -11
- package/dist/loro.js +43 -3
- package/dist/loro.js.map +1 -1
- package/dist/{stream-id-BygPTNvQ.js → stream-id-BERJQKYV.js} +278 -94
- package/dist/stream-id-BERJQKYV.js.map +1 -0
- package/dist/{stream-id-CVCOLgSp.d.ts → stream-id-DEOH5LmQ.d.ts} +42 -3
- package/package.json +2 -2
- package/dist/stream-id-BygPTNvQ.js.map +0 -1
|
@@ -261,27 +261,29 @@ var PayloadProtectionRuntime = class {
|
|
|
261
261
|
}
|
|
262
262
|
return out;
|
|
263
263
|
}
|
|
264
|
-
async encodeSnapshot(snapshot) {
|
|
264
|
+
async encodeSnapshot(snapshot, continuationOffset) {
|
|
265
|
+
checkContinuationOffset(continuationOffset);
|
|
265
266
|
if (this.writePolicy === "plaintext") return snapshot;
|
|
266
|
-
return await this.sealPayload("snapshot", snapshot);
|
|
267
|
+
return await this.sealPayload("snapshot", snapshot, continuationOffset);
|
|
267
268
|
}
|
|
268
|
-
async decodeSnapshot(snapshot) {
|
|
269
|
-
|
|
269
|
+
async decodeSnapshot(snapshot, continuationOffset) {
|
|
270
|
+
checkContinuationOffset(continuationOffset);
|
|
271
|
+
if (isEncryptedEnvelope(snapshot)) return await this.openPayload("snapshot", snapshot, continuationOffset);
|
|
270
272
|
this.assertPlaintextAllowed("snapshot");
|
|
271
273
|
return snapshot;
|
|
272
274
|
}
|
|
273
|
-
async sealPayload(kind, plaintext) {
|
|
274
|
-
return await this.sealProviderPayload(kind, plaintext);
|
|
275
|
+
async sealPayload(kind, plaintext, continuationOffset) {
|
|
276
|
+
return await this.sealProviderPayload(kind, plaintext, continuationOffset);
|
|
275
277
|
}
|
|
276
|
-
async openPayload(expectedKind, envelope) {
|
|
278
|
+
async openPayload(expectedKind, envelope, continuationOffset) {
|
|
277
279
|
const version = envelope[4];
|
|
278
|
-
if (version === PROVIDER_ENVELOPE_VERSION) return await this.openProviderPayload(expectedKind, envelope);
|
|
280
|
+
if (version === PROVIDER_ENVELOPE_VERSION) return await this.openProviderPayload(expectedKind, envelope, continuationOffset);
|
|
279
281
|
throw new PayloadProtectionError("invalid_envelope", `unsupported streams-crdt protected payload envelope version '${String(version)}'`);
|
|
280
282
|
}
|
|
281
|
-
async sealProviderPayload(kind, plaintext) {
|
|
283
|
+
async sealProviderPayload(kind, plaintext, continuationOffset) {
|
|
282
284
|
const provider = this.provider;
|
|
283
285
|
if (provider == null) throw new PayloadProtectionError("encrypt_failed", "provider-backed E2EE writes require a provider");
|
|
284
|
-
const context = createProviderContext(kind);
|
|
286
|
+
const context = createProviderContext(kind, continuationOffset);
|
|
285
287
|
let authenticatedHeader;
|
|
286
288
|
let additionalDataCalls = 0;
|
|
287
289
|
let result;
|
|
@@ -314,12 +316,12 @@ var PayloadProtectionRuntime = class {
|
|
|
314
316
|
throw new PayloadProtectionError(reason, payloadProtectionFailureMessage(reason));
|
|
315
317
|
}
|
|
316
318
|
}
|
|
317
|
-
async openProviderPayload(expectedKind, envelope) {
|
|
319
|
+
async openProviderPayload(expectedKind, envelope, continuationOffset) {
|
|
318
320
|
const parsed = parseProviderEnvelope(envelope);
|
|
319
321
|
if (parsed.kind !== expectedKind) throw new PayloadProtectionError("wrong_payload_kind", `expected ${expectedKind} payload, got ${parsed.kind}`);
|
|
320
322
|
const provider = this.provider;
|
|
321
323
|
if (provider == null) throw new PayloadProtectionError("missing_read_key", "provider-backed E2EE reads require a provider");
|
|
322
|
-
const context = createProviderContext(parsed.kind);
|
|
324
|
+
const context = createProviderContext(parsed.kind, continuationOffset);
|
|
323
325
|
try {
|
|
324
326
|
const plaintext = await provider.open({
|
|
325
327
|
sealed: parsed.sealed,
|
|
@@ -370,13 +372,26 @@ function payloadProtectionFailureMessage(reason) {
|
|
|
370
372
|
function isEncryptedEnvelope(payload) {
|
|
371
373
|
return payload.byteLength >= MAGIC.byteLength && MAGIC.every((byte, index) => payload[index] === byte);
|
|
372
374
|
}
|
|
373
|
-
function createProviderContext(kind) {
|
|
375
|
+
function createProviderContext(kind, continuationOffset) {
|
|
376
|
+
if (kind === "snapshot") {
|
|
377
|
+
checkContinuationOffset(continuationOffset);
|
|
378
|
+
return {
|
|
379
|
+
protocol: PAYLOAD_PROTECTION_PROTOCOL,
|
|
380
|
+
version: PROVIDER_ENVELOPE_VERSION,
|
|
381
|
+
kind,
|
|
382
|
+
continuationOffset
|
|
383
|
+
};
|
|
384
|
+
}
|
|
374
385
|
return {
|
|
375
386
|
protocol: PAYLOAD_PROTECTION_PROTOCOL,
|
|
376
387
|
version: PROVIDER_ENVELOPE_VERSION,
|
|
377
388
|
kind
|
|
378
389
|
};
|
|
379
390
|
}
|
|
391
|
+
/** Opaque offset token only. Not a timestamp, Org, bucket, or role. */
|
|
392
|
+
function checkContinuationOffset(value) {
|
|
393
|
+
if (typeof value !== "string" || value.length === 0 || value.length > 1024 || value === "now") throw new PayloadProtectionError("invalid_envelope", "invalid continuation offset");
|
|
394
|
+
}
|
|
380
395
|
function createProviderEnvelopeHeader(kind, providerHeader) {
|
|
381
396
|
const normalizedHeader = normalizeProviderHeader(providerHeader);
|
|
382
397
|
const header = new Uint8Array(PROVIDER_PREFIX_BYTES + normalizedHeader.byteLength);
|
|
@@ -1083,11 +1098,13 @@ var StreamsTransportClient = class {
|
|
|
1083
1098
|
async bootstrap(signal, timeoutMode = "default") {
|
|
1084
1099
|
const result = await (timeoutMode === "initial" ? this.initialSyncClient : this.client).bootstrap({ signal });
|
|
1085
1100
|
if (!result.ok) throw this.toTransportFailure(result.result);
|
|
1086
|
-
const
|
|
1101
|
+
const snapshotOffset = result.result.snapshotOffset;
|
|
1102
|
+
const snapshot = snapshotOffset === "-1" ? new Uint8Array() : result.result.snapshot?.body ?? new Uint8Array();
|
|
1087
1103
|
return {
|
|
1088
1104
|
nextOffset: result.result.nextOffset,
|
|
1089
1105
|
upToDate: result.result.upToDate,
|
|
1090
1106
|
snapshot,
|
|
1107
|
+
snapshotOffset,
|
|
1091
1108
|
updates: result.result.updates.flatMap((part) => decodeMultipartPartItems(part.body)),
|
|
1092
1109
|
streamCursor: result.result.cursor
|
|
1093
1110
|
};
|
|
@@ -1265,14 +1282,14 @@ var StreamsTransportClient = class {
|
|
|
1265
1282
|
}
|
|
1266
1283
|
async fetchAuthorized(input, init, timeoutMs, phase) {
|
|
1267
1284
|
const requestUrl = this.routeRequestUrl(input, init);
|
|
1268
|
-
const firstToken = await this.resolveToken({ reason: "request" });
|
|
1285
|
+
const firstToken = this.authProvider == null ? void 0 : await withTokenTimeout(this.resolveToken({ reason: "request" }), timeoutMs, phase);
|
|
1269
1286
|
const firstResponse = await this.fetchWithTimeout(requestUrl, {
|
|
1270
1287
|
...init,
|
|
1271
1288
|
headers: this.attachAuthorization(init.headers, firstToken)
|
|
1272
1289
|
}, timeoutMs, phase);
|
|
1273
1290
|
if (this.authProvider == null || firstResponse.status !== 401 && firstResponse.status !== 403) return firstResponse;
|
|
1274
1291
|
await firstResponse.body?.cancel().catch(() => {});
|
|
1275
|
-
const refreshedToken = await this.refreshTokenAfterFailure(firstResponse.status, firstToken);
|
|
1292
|
+
const refreshedToken = await this.refreshTokenAfterFailure(firstResponse.status, firstToken, timeoutMs, phase);
|
|
1276
1293
|
return this.fetchWithTimeout(requestUrl, {
|
|
1277
1294
|
...init,
|
|
1278
1295
|
headers: this.attachAuthorization(init.headers, refreshedToken)
|
|
@@ -1299,16 +1316,25 @@ var StreamsTransportClient = class {
|
|
|
1299
1316
|
async resolveToken(context) {
|
|
1300
1317
|
return await this.authProvider?.(context);
|
|
1301
1318
|
}
|
|
1302
|
-
async refreshTokenAfterFailure(status, previousToken) {
|
|
1319
|
+
async refreshTokenAfterFailure(status, previousToken, timeoutMs, phase) {
|
|
1303
1320
|
if (this.authProvider == null) return;
|
|
1304
|
-
if (this.authRefreshInFlight == null)
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1321
|
+
if (this.authRefreshInFlight == null) {
|
|
1322
|
+
const inFlight = this.resolveToken({
|
|
1323
|
+
reason: "unauthorized",
|
|
1324
|
+
status,
|
|
1325
|
+
previousToken
|
|
1326
|
+
}).finally(() => {
|
|
1327
|
+
if (this.authRefreshInFlight === inFlight) this.authRefreshInFlight = void 0;
|
|
1328
|
+
});
|
|
1329
|
+
this.authRefreshInFlight = inFlight;
|
|
1330
|
+
}
|
|
1331
|
+
const inFlight = this.authRefreshInFlight;
|
|
1332
|
+
try {
|
|
1333
|
+
return await withTokenTimeout(inFlight, timeoutMs, phase);
|
|
1334
|
+
} catch (error) {
|
|
1335
|
+
if (error instanceof TimeoutError && this.authRefreshInFlight === inFlight) this.authRefreshInFlight = void 0;
|
|
1336
|
+
throw error;
|
|
1337
|
+
}
|
|
1312
1338
|
}
|
|
1313
1339
|
attachAuthorization(base, token) {
|
|
1314
1340
|
const headers = new Headers(base);
|
|
@@ -1502,8 +1528,26 @@ function parseRetryAfterMs(value) {
|
|
|
1502
1528
|
const delta = parsed - Date.now();
|
|
1503
1529
|
return delta > 0 ? delta : void 0;
|
|
1504
1530
|
}
|
|
1531
|
+
async function withTokenTimeout(promise, timeoutMs, phase) {
|
|
1532
|
+
if (!(timeoutMs > 0)) return await promise;
|
|
1533
|
+
let timer;
|
|
1534
|
+
try {
|
|
1535
|
+
return await Promise.race([promise, new Promise((_, reject) => {
|
|
1536
|
+
timer = setTimeout(() => {
|
|
1537
|
+
reject(new TimeoutError(phase, timeoutMs));
|
|
1538
|
+
}, timeoutMs);
|
|
1539
|
+
})]);
|
|
1540
|
+
} finally {
|
|
1541
|
+
clearTimeout(timer);
|
|
1542
|
+
}
|
|
1543
|
+
}
|
|
1505
1544
|
//#endregion
|
|
1506
1545
|
//#region src/ephemeral-runtime.ts
|
|
1546
|
+
/**
|
|
1547
|
+
* Superseded entries are skipped lazily; once they outnumber live entries
|
|
1548
|
+
* (and exceed this floor) the queue is rebuilt so it stays O(live entries).
|
|
1549
|
+
*/
|
|
1550
|
+
const PENDING_LOCAL_REBUILD_MIN_SUPERSEDED = 64;
|
|
1507
1551
|
function createNoopAdapter() {
|
|
1508
1552
|
return {
|
|
1509
1553
|
emptyVersion: () => ({}),
|
|
@@ -1624,6 +1668,11 @@ var EphemeralStreamCrdt = class {
|
|
|
1624
1668
|
writeStatus: "connecting",
|
|
1625
1669
|
statusListeners: /* @__PURE__ */ new Set(),
|
|
1626
1670
|
pendingLocal: new Deque(),
|
|
1671
|
+
pendingLocalByKey: /* @__PURE__ */ new Map(),
|
|
1672
|
+
pendingLocalSuperseded: 0,
|
|
1673
|
+
pendingLocalCount: 0,
|
|
1674
|
+
lastChainId: 0,
|
|
1675
|
+
liveChains: 0,
|
|
1627
1676
|
syncWaiters: [],
|
|
1628
1677
|
flushingLocal: false,
|
|
1629
1678
|
retryAttempt: 0,
|
|
@@ -1674,15 +1723,17 @@ var EphemeralStreamCrdt = class {
|
|
|
1674
1723
|
return state.status;
|
|
1675
1724
|
},
|
|
1676
1725
|
get pendingLocalCount() {
|
|
1677
|
-
return state.
|
|
1726
|
+
return state.pendingLocalCount;
|
|
1678
1727
|
},
|
|
1679
1728
|
onStatusChange: (listener) => this.onStatusChange(state, listener),
|
|
1680
1729
|
waitUntilSynced: async () => {
|
|
1681
1730
|
if (state.closed) throw closedSubscriptionError();
|
|
1682
|
-
const remaining = state.
|
|
1731
|
+
const remaining = state.liveChains;
|
|
1732
|
+
const lastChainId = state.lastChainId;
|
|
1683
1733
|
if (remaining === 0) return;
|
|
1684
1734
|
return new Promise((resolve, reject) => {
|
|
1685
1735
|
state.syncWaiters.push({
|
|
1736
|
+
lastChainId,
|
|
1686
1737
|
remaining,
|
|
1687
1738
|
resolve,
|
|
1688
1739
|
reject
|
|
@@ -1812,7 +1863,33 @@ var EphemeralStreamCrdt = class {
|
|
|
1812
1863
|
}
|
|
1813
1864
|
enqueueLocal(state, update) {
|
|
1814
1865
|
if (state.closed || update == null || update.byteLength === 0) return;
|
|
1815
|
-
|
|
1866
|
+
const key = this.localUpdateKey(update);
|
|
1867
|
+
const previous = key == null ? void 0 : state.pendingLocalByKey.get(key);
|
|
1868
|
+
let chainId;
|
|
1869
|
+
if (previous == null) {
|
|
1870
|
+
state.lastChainId += 1;
|
|
1871
|
+
state.liveChains += 1;
|
|
1872
|
+
chainId = state.lastChainId;
|
|
1873
|
+
} else chainId = previous.chainId;
|
|
1874
|
+
const entry = {
|
|
1875
|
+
update,
|
|
1876
|
+
key,
|
|
1877
|
+
count: 1,
|
|
1878
|
+
chainId,
|
|
1879
|
+
superseded: false,
|
|
1880
|
+
inFlight: false
|
|
1881
|
+
};
|
|
1882
|
+
if (previous != null) {
|
|
1883
|
+
previous.superseded = true;
|
|
1884
|
+
previous.update = null;
|
|
1885
|
+
entry.count += previous.count;
|
|
1886
|
+
previous.count = 0;
|
|
1887
|
+
state.pendingLocalSuperseded += 1;
|
|
1888
|
+
}
|
|
1889
|
+
if (key != null) state.pendingLocalByKey.set(key, entry);
|
|
1890
|
+
state.pendingLocal.pushBack(entry);
|
|
1891
|
+
state.pendingLocalCount += 1;
|
|
1892
|
+
this.maybeRebuildPendingLocal(state);
|
|
1816
1893
|
if (state.writeStatus !== "connecting") this.flushPendingLocal(state);
|
|
1817
1894
|
}
|
|
1818
1895
|
async flushPendingLocal(state) {
|
|
@@ -1820,42 +1897,94 @@ var EphemeralStreamCrdt = class {
|
|
|
1820
1897
|
state.flushingLocal = true;
|
|
1821
1898
|
try {
|
|
1822
1899
|
while (!state.closed) {
|
|
1823
|
-
const update = state.pendingLocal.peekFront();
|
|
1824
|
-
if (update == null) return;
|
|
1825
1900
|
let attempt = 0;
|
|
1826
|
-
while (!state.closed)
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
this.
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1901
|
+
while (!state.closed) {
|
|
1902
|
+
const entry = this.nextPendingLocal(state);
|
|
1903
|
+
if (entry == null) return;
|
|
1904
|
+
try {
|
|
1905
|
+
await this.client.appendEphemeralUpdate(entry.update);
|
|
1906
|
+
state.pendingLocal.popFront();
|
|
1907
|
+
state.pendingLocalCount -= entry.count;
|
|
1908
|
+
state.liveChains -= 1;
|
|
1909
|
+
this.notifyChainAcknowledged(state, entry.chainId);
|
|
1910
|
+
this.setWriteStatus(state, "ok");
|
|
1911
|
+
break;
|
|
1912
|
+
} catch (error) {
|
|
1913
|
+
const classified = classifyFetchError(error);
|
|
1914
|
+
if (!classified.retriable || isAuthOrProtocolError(error)) {
|
|
1915
|
+
this.rejectSyncWaiters(state, /* @__PURE__ */ new Error(`ephemeral append failed: ${classified.code}`));
|
|
1916
|
+
this.setWriteStatus(state, "error");
|
|
1917
|
+
return;
|
|
1918
|
+
}
|
|
1919
|
+
if (attempt >= this.reconnectConfig.maxAttempts) {
|
|
1920
|
+
this.rejectSyncWaiters(state, /* @__PURE__ */ new Error("ephemeral append retry budget exhausted"));
|
|
1921
|
+
this.setWriteStatus(state, "error");
|
|
1922
|
+
return;
|
|
1923
|
+
}
|
|
1924
|
+
this.setWriteStatus(state, "reconnecting");
|
|
1925
|
+
const delay = computeRetryDelay(attempt, this.reconnectConfig);
|
|
1926
|
+
attempt += 1;
|
|
1927
|
+
await sleep(delay, state.retrySleepController.signal);
|
|
1843
1928
|
}
|
|
1844
|
-
this.setWriteStatus(state, "reconnecting");
|
|
1845
|
-
const delay = computeRetryDelay(attempt, this.reconnectConfig);
|
|
1846
|
-
attempt += 1;
|
|
1847
|
-
await sleep(delay, state.retrySleepController.signal);
|
|
1848
1929
|
}
|
|
1849
1930
|
}
|
|
1850
1931
|
} finally {
|
|
1851
1932
|
state.flushingLocal = false;
|
|
1852
1933
|
}
|
|
1853
1934
|
}
|
|
1854
|
-
|
|
1935
|
+
localUpdateKey(update) {
|
|
1936
|
+
const adaptor = this.adaptor;
|
|
1937
|
+
if (adaptor.localUpdateKey == null) return;
|
|
1938
|
+
try {
|
|
1939
|
+
return adaptor.localUpdateKey(update) ?? void 0;
|
|
1940
|
+
} catch (error) {
|
|
1941
|
+
this.logError("ephemeral local update key lookup failed", error);
|
|
1942
|
+
return;
|
|
1943
|
+
}
|
|
1944
|
+
}
|
|
1945
|
+
/**
|
|
1946
|
+
* Pops superseded entries off the head and returns the next entry to send,
|
|
1947
|
+
* marking it in-flight so later same-key updates queue behind it.
|
|
1948
|
+
*/
|
|
1949
|
+
nextPendingLocal(state) {
|
|
1950
|
+
for (;;) {
|
|
1951
|
+
const head = state.pendingLocal.peekFront();
|
|
1952
|
+
if (head == null) return;
|
|
1953
|
+
if (head.superseded) {
|
|
1954
|
+
state.pendingLocal.popFront();
|
|
1955
|
+
state.pendingLocalSuperseded -= 1;
|
|
1956
|
+
continue;
|
|
1957
|
+
}
|
|
1958
|
+
if (!head.inFlight) {
|
|
1959
|
+
head.inFlight = true;
|
|
1960
|
+
if (head.key != null && state.pendingLocalByKey.get(head.key) === head) state.pendingLocalByKey.delete(head.key);
|
|
1961
|
+
}
|
|
1962
|
+
return head;
|
|
1963
|
+
}
|
|
1964
|
+
}
|
|
1965
|
+
/** Drops superseded markers once they dominate the queue (amortized O(1)). */
|
|
1966
|
+
maybeRebuildPendingLocal(state) {
|
|
1967
|
+
if (state.pendingLocalSuperseded < PENDING_LOCAL_REBUILD_MIN_SUPERSEDED || state.pendingLocalSuperseded * 2 <= state.pendingLocal.length) return;
|
|
1968
|
+
const live = state.pendingLocal.toArray().filter((entry) => !entry.superseded);
|
|
1969
|
+
state.pendingLocal.clear();
|
|
1970
|
+
for (const entry of live) state.pendingLocal.pushBack(entry);
|
|
1971
|
+
state.pendingLocalSuperseded = 0;
|
|
1972
|
+
}
|
|
1973
|
+
/**
|
|
1974
|
+
* Completes waiters that tracked `chainId`. Each chain is acknowledged
|
|
1975
|
+
* exactly once (its entry is never superseded after it goes in flight), so
|
|
1976
|
+
* a waiter resolves only after every chain live at call time has had its
|
|
1977
|
+
* latest value acknowledged.
|
|
1978
|
+
*/
|
|
1979
|
+
notifyChainAcknowledged(state, chainId) {
|
|
1855
1980
|
let i = 0;
|
|
1856
1981
|
while (i < state.syncWaiters.length) {
|
|
1857
1982
|
const waiter = state.syncWaiters[i];
|
|
1858
|
-
waiter.
|
|
1983
|
+
if (chainId > waiter.lastChainId) {
|
|
1984
|
+
i += 1;
|
|
1985
|
+
continue;
|
|
1986
|
+
}
|
|
1987
|
+
waiter.remaining -= 1;
|
|
1859
1988
|
if (waiter.remaining <= 0) {
|
|
1860
1989
|
state.syncWaiters.splice(i, 1);
|
|
1861
1990
|
waiter.resolve();
|
|
@@ -2135,12 +2264,13 @@ var TransportCursorManager = class {
|
|
|
2135
2264
|
markCursorIncomplete(cursor, unresolved) {
|
|
2136
2265
|
this.incompleteCursors.set(cursor, unresolved);
|
|
2137
2266
|
}
|
|
2138
|
-
async applyBootstrapPayload(cursor, snapshot, updates, nextOffset, upToDate = false) {
|
|
2267
|
+
async applyBootstrapPayload(cursor, snapshot, updates, nextOffset, upToDate = false, snapshotOffset = "-1") {
|
|
2139
2268
|
let nextVersion = cursor.serverLowerBoundVersion;
|
|
2140
2269
|
let complete = true;
|
|
2141
2270
|
let unresolved = [];
|
|
2142
2271
|
if (snapshot.byteLength > 0) {
|
|
2143
|
-
|
|
2272
|
+
if (typeof snapshotOffset !== "string" || snapshotOffset.length === 0 || snapshotOffset.length > 1024 || snapshotOffset === "now" || snapshotOffset === "-1") throw new Error("bootstrap snapshot offset does not match an admitted continuation");
|
|
2273
|
+
const outcome = normalizeApplyReturn(await this.adapter.applySnapshot(this.decodeSnapshot == null ? snapshot : await this.decodeSnapshot(snapshot, snapshotOffset)));
|
|
2144
2274
|
nextVersion = outcome.version;
|
|
2145
2275
|
complete = outcome.complete;
|
|
2146
2276
|
unresolved = outcome.unresolved ?? [];
|
|
@@ -2158,9 +2288,9 @@ var TransportCursorManager = class {
|
|
|
2158
2288
|
serverLowerBoundVersion: nextVersion
|
|
2159
2289
|
}, "bootstrap", complete, unresolved, upToDate);
|
|
2160
2290
|
}
|
|
2161
|
-
async applyBootstrapItems(cursor, items, nextOffset, upToDate = false) {
|
|
2291
|
+
async applyBootstrapItems(cursor, items, nextOffset, upToDate = false, snapshotOffset = "-1") {
|
|
2162
2292
|
const snapshot = items[0] ?? new Uint8Array();
|
|
2163
|
-
return await this.applyBootstrapPayload(cursor, snapshot, items.slice(1), nextOffset, upToDate);
|
|
2293
|
+
return await this.applyBootstrapPayload(cursor, snapshot, items.slice(1), nextOffset, upToDate, snapshotOffset);
|
|
2164
2294
|
}
|
|
2165
2295
|
async applyRemoteItems(cursor, items, nextOffset) {
|
|
2166
2296
|
return await this.applyRemotePayload(cursor, [...items], nextOffset, "remote", void 0);
|
|
@@ -2324,7 +2454,7 @@ var TransportSnapshotManager = class {
|
|
|
2324
2454
|
if (tailBytes <= snapshotBytes) return;
|
|
2325
2455
|
if (tailBytes - snapshotBytes < config.minBytesSinceRemoteSnapshot) return;
|
|
2326
2456
|
const snapshot = this.adapter.exportSnapshot();
|
|
2327
|
-
await this.putSnapshotAt(state.cursor.nextOffset, this.encodeSnapshot == null ? snapshot : await this.encodeSnapshot(snapshot));
|
|
2457
|
+
await this.putSnapshotAt(state.cursor.nextOffset, this.encodeSnapshot == null ? snapshot : await this.encodeSnapshot(snapshot, state.cursor.nextOffset));
|
|
2328
2458
|
} catch (error) {
|
|
2329
2459
|
this.logError("snapshot upload failed", error);
|
|
2330
2460
|
}
|
|
@@ -2506,16 +2636,16 @@ var StreamsCrdt = class StreamsCrdt {
|
|
|
2506
2636
|
}
|
|
2507
2637
|
composeSnapshotDecoder(payloadProtection, decompressSnapshot) {
|
|
2508
2638
|
if (payloadProtection == null && decompressSnapshot == null) return;
|
|
2509
|
-
return async (snapshot) => {
|
|
2510
|
-
const clearSnapshot = payloadProtection == null ? snapshot : await payloadProtection.decodeSnapshot(snapshot);
|
|
2639
|
+
return async (snapshot, continuationOffset) => {
|
|
2640
|
+
const clearSnapshot = payloadProtection == null ? snapshot : await payloadProtection.decodeSnapshot(snapshot, continuationOffset);
|
|
2511
2641
|
return decompressSnapshot == null ? clearSnapshot : await decompressSnapshot(clearSnapshot);
|
|
2512
2642
|
};
|
|
2513
2643
|
}
|
|
2514
2644
|
composeSnapshotEncoder(payloadProtection, compressSnapshot) {
|
|
2515
2645
|
if (payloadProtection == null && compressSnapshot == null) return;
|
|
2516
|
-
return async (snapshot) => {
|
|
2646
|
+
return async (snapshot, continuationOffset) => {
|
|
2517
2647
|
const compressedSnapshot = compressSnapshot == null ? snapshot : await compressSnapshot(snapshot);
|
|
2518
|
-
return payloadProtection == null ? compressedSnapshot : await payloadProtection.encodeSnapshot(compressedSnapshot);
|
|
2648
|
+
return payloadProtection == null ? compressedSnapshot : await payloadProtection.encodeSnapshot(compressedSnapshot, continuationOffset);
|
|
2519
2649
|
};
|
|
2520
2650
|
}
|
|
2521
2651
|
/**
|
|
@@ -2624,7 +2754,7 @@ var StreamsCrdt = class StreamsCrdt {
|
|
|
2624
2754
|
await this.adapter.flushLocalExports?.();
|
|
2625
2755
|
const cursor = await this.syncOnce("sync");
|
|
2626
2756
|
const rawSnapshot = this.adapter.exportSnapshot();
|
|
2627
|
-
const snapshot = this.encodeSnapshot == null ? rawSnapshot : await this.encodeSnapshot(rawSnapshot);
|
|
2757
|
+
const snapshot = this.encodeSnapshot == null ? rawSnapshot : await this.encodeSnapshot(rawSnapshot, cursor.nextOffset);
|
|
2628
2758
|
await this.client.putSnapshotAt(cursor.nextOffset, snapshot);
|
|
2629
2759
|
return {
|
|
2630
2760
|
ok: true,
|
|
@@ -3045,12 +3175,46 @@ var StreamsCrdt = class StreamsCrdt {
|
|
|
3045
3175
|
}
|
|
3046
3176
|
}
|
|
3047
3177
|
async syncOnce(operation, parentSignal) {
|
|
3048
|
-
|
|
3178
|
+
return this.enqueueExclusive(async () => {
|
|
3179
|
+
await this.adapter.flushLocalExports?.();
|
|
3180
|
+
return this.syncOnceExclusive(operation, parentSignal);
|
|
3181
|
+
});
|
|
3182
|
+
}
|
|
3183
|
+
/** Caller owns opQueue; never wait for the background flush from here. */
|
|
3184
|
+
async syncOnceExclusive(operation, parentSignal) {
|
|
3185
|
+
const state = this.joinState;
|
|
3186
|
+
if (state != null && !state.closed) while (!state.closed && state.pendingLocal.length > 0) await this.flushPendingLocalBatchExclusive(state);
|
|
3187
|
+
if ((state == null || state.closed) && this.frozenLocalAppend?.source === "pending") {
|
|
3188
|
+
const frozen = this.frozenLocalAppend;
|
|
3189
|
+
const appended = await this.appendFrozenLocalBatchRemote(frozen);
|
|
3190
|
+
await this.catchupLocalAppend(frozen);
|
|
3191
|
+
this.commitProducerAck(appended.producerAck);
|
|
3192
|
+
}
|
|
3193
|
+
const initial = await this.finalizeDirectLocalAppendIfNeeded(await this.resolveInitialRemoteState(operation, parentSignal));
|
|
3194
|
+
let cursor = initial.cursor;
|
|
3195
|
+
if (state != null && !state.closed) {
|
|
3196
|
+
state.cursor = cursor;
|
|
3197
|
+
state.streamCursor = initial.streamCursor;
|
|
3198
|
+
while (!state.closed && state.pendingLocal.length > 0) {
|
|
3199
|
+
await this.flushPendingLocalBatchExclusive(state);
|
|
3200
|
+
cursor = state.cursor;
|
|
3201
|
+
}
|
|
3202
|
+
}
|
|
3049
3203
|
const localBatch = this.exportUpdates(cursor.serverLowerBoundVersion);
|
|
3050
|
-
if (localBatch.batch != null)
|
|
3204
|
+
if (localBatch.batch != null) {
|
|
3205
|
+
const appended = await this.appendLocalBatchDurably(cursor, localBatch.batch);
|
|
3206
|
+
cursor = appended.cursor;
|
|
3207
|
+
if (state != null && !state.closed) {
|
|
3208
|
+
state.cursor = cursor;
|
|
3209
|
+
state.streamCursor = appended.streamCursor;
|
|
3210
|
+
}
|
|
3211
|
+
}
|
|
3051
3212
|
return cursor;
|
|
3052
3213
|
}
|
|
3053
3214
|
async performInitialJoinSync(preferredLiveMode, parentSignal) {
|
|
3215
|
+
return this.enqueueExclusive(() => this.performInitialJoinSyncExclusive(preferredLiveMode, parentSignal));
|
|
3216
|
+
}
|
|
3217
|
+
async performInitialJoinSyncExclusive(preferredLiveMode, parentSignal) {
|
|
3054
3218
|
const initial = await this.finalizeDirectLocalAppendIfNeeded(await this.resolveInitialRemoteState("join", parentSignal));
|
|
3055
3219
|
let cursor = initial.cursor;
|
|
3056
3220
|
let localExportRefVersion = cursor.serverLowerBoundVersion;
|
|
@@ -3060,9 +3224,7 @@ var StreamsCrdt = class StreamsCrdt {
|
|
|
3060
3224
|
localExportRefVersion = exportedLocal.nextRefVersion;
|
|
3061
3225
|
const exportedBatch = exportedLocal.batch;
|
|
3062
3226
|
if (exportedBatch != null) {
|
|
3063
|
-
const
|
|
3064
|
-
const appendStreamCursor = streamCursor;
|
|
3065
|
-
const committed = await this.enqueueExclusive(() => this.appendLocalBatchDurably(appendCursor, exportedBatch, void 0, "direct", appendStreamCursor));
|
|
3227
|
+
const committed = await this.appendLocalBatchDurably(cursor, exportedBatch, void 0, "direct", streamCursor);
|
|
3066
3228
|
cursor = committed.cursor;
|
|
3067
3229
|
streamCursor = committed.streamCursor;
|
|
3068
3230
|
}
|
|
@@ -3150,7 +3312,7 @@ var StreamsCrdt = class StreamsCrdt {
|
|
|
3150
3312
|
async bootstrapState(cursor, options = {}) {
|
|
3151
3313
|
const cursorManager = options.cursorManager ?? this.cursorManager;
|
|
3152
3314
|
const result = await this.client.bootstrap(options.signal, options.timeoutMode);
|
|
3153
|
-
let saved = await cursorManager.applyBootstrapPayload(cursor, result.snapshot, result.updates, result.nextOffset, result.upToDate);
|
|
3315
|
+
let saved = await cursorManager.applyBootstrapPayload(cursor, result.snapshot, result.updates, result.nextOffset, result.upToDate, result.snapshotOffset);
|
|
3154
3316
|
let bootstrapCursor = result.streamCursor;
|
|
3155
3317
|
if (!result.upToDate) {
|
|
3156
3318
|
const caughtUp = await this.catchupOrGone(saved, {
|
|
@@ -3841,6 +4003,50 @@ var StreamsCrdt = class StreamsCrdt {
|
|
|
3841
4003
|
return this.drainQueuedBatch(this.pendingWriteOnly);
|
|
3842
4004
|
}
|
|
3843
4005
|
/**
|
|
4006
|
+
* One FIFO transaction, called only while holding opQueue. Selecting the
|
|
4007
|
+
* head and committing its ACK/count must share the lock: sync may drain
|
|
4008
|
+
* the queue while a background writer is queued or sleeping between retries.
|
|
4009
|
+
*/
|
|
4010
|
+
async flushPendingLocalBatchExclusive(state) {
|
|
4011
|
+
while (!state.closed) {
|
|
4012
|
+
const drained = await this.drainPendingBatch(state);
|
|
4013
|
+
if (drained == null || state.closed) return false;
|
|
4014
|
+
try {
|
|
4015
|
+
let committedCount;
|
|
4016
|
+
const initial = await this.finalizeDirectLocalAppendIfNeeded({
|
|
4017
|
+
cursor: state.cursor,
|
|
4018
|
+
streamCursor: state.streamCursor
|
|
4019
|
+
});
|
|
4020
|
+
state.cursor = initial.cursor;
|
|
4021
|
+
state.streamCursor = initial.streamCursor;
|
|
4022
|
+
if (this.cursorManager.isCursorComplete(state.cursor)) {
|
|
4023
|
+
state.cursor = await this.appendLocalBatch(state.cursor, drained.batch, drained.count, state.streamCursor);
|
|
4024
|
+
const streamCursor = this.consumeCommittedLocalStreamCursor();
|
|
4025
|
+
if (streamCursor != null) state.streamCursor = streamCursor;
|
|
4026
|
+
committedCount = this.consumeCommittedPendingCount() ?? drained.count;
|
|
4027
|
+
if (!state.closed) this.snapshotManager.schedule(state);
|
|
4028
|
+
} else {
|
|
4029
|
+
const appended = await this.appendLocalBatchRemote(state.cursor, drained.batch, drained.count, "pending");
|
|
4030
|
+
this.commitProducerAck(appended.producerAck);
|
|
4031
|
+
committedCount = appended.pendingCount ?? drained.count;
|
|
4032
|
+
}
|
|
4033
|
+
for (let i = 0; i < committedCount; i += 1) state.pendingLocal.popFront();
|
|
4034
|
+
this.notifySyncWaiters(state, committedCount);
|
|
4035
|
+
state.flushHeadBatchOnly = false;
|
|
4036
|
+
state.lastWriteError = void 0;
|
|
4037
|
+
this.setWriteSubStatus(state, "ok");
|
|
4038
|
+
return true;
|
|
4039
|
+
} catch (error) {
|
|
4040
|
+
if (isPayloadTooLargeError(error) && drained.count > 1 && this.abandonPendingFrozenLocalAppend()) {
|
|
4041
|
+
state.flushHeadBatchOnly = true;
|
|
4042
|
+
continue;
|
|
4043
|
+
}
|
|
4044
|
+
throw error;
|
|
4045
|
+
}
|
|
4046
|
+
}
|
|
4047
|
+
return false;
|
|
4048
|
+
}
|
|
4049
|
+
/**
|
|
3844
4050
|
* Drains `pendingLocal` to the server in strict FIFO order.
|
|
3845
4051
|
*
|
|
3846
4052
|
* FIFO invariant: batches are only popped after a durable append, and a
|
|
@@ -3865,34 +4071,12 @@ var StreamsCrdt = class StreamsCrdt {
|
|
|
3865
4071
|
this.clearWriteRetryTimer(state);
|
|
3866
4072
|
state.flushingLocal = true;
|
|
3867
4073
|
try {
|
|
3868
|
-
|
|
3869
|
-
const drained = await this.drainPendingBatch(state);
|
|
3870
|
-
if (drained == null) return;
|
|
4074
|
+
while (!state.closed) {
|
|
3871
4075
|
let attempts = 0;
|
|
3872
4076
|
while (!state.closed) try {
|
|
3873
|
-
|
|
3874
|
-
if (this.cursorManager.isCursorComplete(state.cursor)) {
|
|
3875
|
-
state.cursor = await this.enqueueExclusive(async () => this.appendLocalBatch(state.cursor, drained.batch, drained.count, state.streamCursor));
|
|
3876
|
-
const committedStreamCursor = this.consumeCommittedLocalStreamCursor();
|
|
3877
|
-
if (committedStreamCursor != null) state.streamCursor = committedStreamCursor;
|
|
3878
|
-
committedCount = this.consumeCommittedPendingCount() ?? drained.count;
|
|
3879
|
-
this.snapshotManager.schedule(state);
|
|
3880
|
-
} else {
|
|
3881
|
-
const appended = await this.enqueueExclusive(async () => this.appendLocalBatchRemote(state.cursor, drained.batch, drained.count, "pending"));
|
|
3882
|
-
this.commitProducerAck(appended.producerAck);
|
|
3883
|
-
committedCount = appended.pendingCount ?? drained.count;
|
|
3884
|
-
}
|
|
3885
|
-
for (let i = 0; i < committedCount; i += 1) state.pendingLocal.popFront();
|
|
3886
|
-
this.notifySyncWaiters(state, committedCount);
|
|
3887
|
-
state.flushHeadBatchOnly = false;
|
|
3888
|
-
state.lastWriteError = void 0;
|
|
3889
|
-
this.setWriteSubStatus(state, "ok");
|
|
4077
|
+
if (!await this.enqueueExclusive(() => this.flushPendingLocalBatchExclusive(state))) return;
|
|
3890
4078
|
break;
|
|
3891
4079
|
} catch (error) {
|
|
3892
|
-
if (isPayloadTooLargeError(error) && drained.count > 1 && this.abandonPendingFrozenLocalAppend()) {
|
|
3893
|
-
state.flushHeadBatchOnly = true;
|
|
3894
|
-
continue outer;
|
|
3895
|
-
}
|
|
3896
4080
|
const classified = classifyFetchError(error);
|
|
3897
4081
|
const transportError = toTransportError(error);
|
|
3898
4082
|
if (!classified.retriable || isAuthOrProtocolError(error)) {
|
|
@@ -4071,4 +4255,4 @@ function isValidRillId(id) {
|
|
|
4071
4255
|
//#endregion
|
|
4072
4256
|
export { createInitialRemoteCursor as a, PayloadProtectionError as c, IndexedDbRemoteCursorStore as i, StreamsCrdt as n, EphemeralStreamCrdt as o, InMemoryRemoteCursorStore as r, LocalAppendFailedError as s, isValidRillId as t };
|
|
4073
4257
|
|
|
4074
|
-
//# sourceMappingURL=stream-id-
|
|
4258
|
+
//# sourceMappingURL=stream-id-BERJQKYV.js.map
|