@loro-dev/streams-crdt 0.11.1 → 0.12.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 +55 -14
- 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 +1 -1
- package/dist/loro.js +1 -1
- package/dist/{stream-id-fOF9ZqCH.js → stream-id-D-UirMkk.js} +201 -53
- package/dist/stream-id-D-UirMkk.js.map +1 -0
- package/dist/{stream-id-DAAMJlvT.d.ts → stream-id-DJYQlTE2.d.ts} +68 -3
- package/package.json +1 -1
- package/dist/stream-id-fOF9ZqCH.js.map +0 -1
|
@@ -135,10 +135,20 @@ const DEFAULT_RECONNECT_CONFIG = {
|
|
|
135
135
|
jitterFraction: .2,
|
|
136
136
|
maxAttempts: Infinity,
|
|
137
137
|
connectTimeoutMs: 1e4,
|
|
138
|
+
initialSyncSlowAfterMs: 3e4,
|
|
139
|
+
initialSyncHardTimeoutMs: 12e4,
|
|
138
140
|
pollTimeoutMs: 3e4,
|
|
139
141
|
liveInactivityTimeoutMs: 45e3
|
|
140
142
|
};
|
|
141
143
|
/**
|
|
144
|
+
* Minimum time a live SSE must stay open before it counts as a "successful
|
|
145
|
+
* session". A read that stayed open at least this long before erroring is
|
|
146
|
+
* treated as a fresh disconnect (e.g. a routine server-side stream recycle)
|
|
147
|
+
* rather than a failing-to-connect retry, so its reconnect does not consume the
|
|
148
|
+
* backoff budget. Shared by the durable and ephemeral read loops.
|
|
149
|
+
*/
|
|
150
|
+
const MIN_SUCCESSFUL_SSE_DURATION_MS = 5e3;
|
|
151
|
+
/**
|
|
142
152
|
* Apply jitter of +/- fraction to a base delay.
|
|
143
153
|
* E.g. applyJitter(1000, 0.2) returns a value in [800, 1200].
|
|
144
154
|
*/
|
|
@@ -503,6 +513,17 @@ var TimeoutError = class extends Error {
|
|
|
503
513
|
this.timeoutMs = timeoutMs;
|
|
504
514
|
}
|
|
505
515
|
};
|
|
516
|
+
var InitialSyncTimeoutError = class extends Error {
|
|
517
|
+
operation;
|
|
518
|
+
timeoutMs;
|
|
519
|
+
elapsedMs;
|
|
520
|
+
constructor(operation, timeoutMs, elapsedMs) {
|
|
521
|
+
super(`initial ${operation} did not complete within ${timeoutMs}ms; the in-flight bootstrap/catch-up request was aborted at the hard timeout`);
|
|
522
|
+
this.operation = operation;
|
|
523
|
+
this.timeoutMs = timeoutMs;
|
|
524
|
+
this.elapsedMs = elapsedMs;
|
|
525
|
+
}
|
|
526
|
+
};
|
|
506
527
|
var ProtocolError = class extends Error {
|
|
507
528
|
status;
|
|
508
529
|
constructor(message, status) {
|
|
@@ -673,6 +694,12 @@ function classifyFetchError(err) {
|
|
|
673
694
|
retriable: true,
|
|
674
695
|
atMs: Date.now()
|
|
675
696
|
};
|
|
697
|
+
if (err instanceof InitialSyncTimeoutError) return {
|
|
698
|
+
code: "initial_sync_timeout",
|
|
699
|
+
message,
|
|
700
|
+
retriable: true,
|
|
701
|
+
atMs: Date.now()
|
|
702
|
+
};
|
|
676
703
|
if (err instanceof TypeError) return {
|
|
677
704
|
code: "network",
|
|
678
705
|
message,
|
|
@@ -748,6 +775,14 @@ function toTransportError(error) {
|
|
|
748
775
|
phase: error.timeoutType,
|
|
749
776
|
message: error.message
|
|
750
777
|
};
|
|
778
|
+
if (error instanceof InitialSyncTimeoutError) return {
|
|
779
|
+
code: "initial_sync_timeout",
|
|
780
|
+
retryable: true,
|
|
781
|
+
operation: error.operation,
|
|
782
|
+
timeoutMs: error.timeoutMs,
|
|
783
|
+
elapsedMs: error.elapsedMs,
|
|
784
|
+
message: error.message
|
|
785
|
+
};
|
|
751
786
|
if (error instanceof HttpError && error.status === 410) return {
|
|
752
787
|
code: "gone",
|
|
753
788
|
retryable: true,
|
|
@@ -903,9 +938,10 @@ function normalizeShardUrlsOptions(options) {
|
|
|
903
938
|
bootstrap: normalizeShardOrigins(options.bootstrap, "shardUrls.bootstrap"),
|
|
904
939
|
catchup: normalizeShardOrigins(options.catchup, "shardUrls.catchup"),
|
|
905
940
|
largePost: normalizeShardOrigins(options.largePost, "shardUrls.largePost"),
|
|
941
|
+
other: normalizeShardOrigins(options.other, "shardUrls.other"),
|
|
906
942
|
largePostMinBytes
|
|
907
943
|
};
|
|
908
|
-
if (normalized.bootstrap == null && normalized.catchup == null && normalized.largePost == null) return;
|
|
944
|
+
if (normalized.bootstrap == null && normalized.catchup == null && normalized.largePost == null && normalized.other == null) return;
|
|
909
945
|
return normalized;
|
|
910
946
|
}
|
|
911
947
|
function requestBodyByteLength(body) {
|
|
@@ -940,7 +976,9 @@ function normalizedStreamPath(streamUrl) {
|
|
|
940
976
|
var StreamsTransportClient = class {
|
|
941
977
|
streamUrl;
|
|
942
978
|
adapter;
|
|
979
|
+
streamTtlSeconds;
|
|
943
980
|
client;
|
|
981
|
+
initialSyncClient;
|
|
944
982
|
fetchImpl;
|
|
945
983
|
authProvider;
|
|
946
984
|
reconnectConfig;
|
|
@@ -950,6 +988,7 @@ var StreamsTransportClient = class {
|
|
|
950
988
|
constructor(options) {
|
|
951
989
|
this.streamUrl = options.streamUrl;
|
|
952
990
|
this.adapter = options.adapter;
|
|
991
|
+
this.streamTtlSeconds = normalizeStreamTtlSeconds(options.streamTtlSeconds);
|
|
953
992
|
this.fetchImpl = options.fetchImpl;
|
|
954
993
|
this.shardUrls = normalizeShardUrlsOptions(options.shardUrls);
|
|
955
994
|
this.client = new StreamsClient({
|
|
@@ -962,6 +1001,16 @@ var StreamsTransportClient = class {
|
|
|
962
1001
|
pollTimeoutMs: options.reconnectConfig.pollTimeoutMs
|
|
963
1002
|
}
|
|
964
1003
|
});
|
|
1004
|
+
this.initialSyncClient = new StreamsClient({
|
|
1005
|
+
url: options.streamUrl,
|
|
1006
|
+
auth: options.authProvider,
|
|
1007
|
+
fetch: this.fetchRouted.bind(this),
|
|
1008
|
+
retry: { maxAttempts: 0 },
|
|
1009
|
+
timeout: {
|
|
1010
|
+
connectTimeoutMs: 0,
|
|
1011
|
+
pollTimeoutMs: options.reconnectConfig.pollTimeoutMs
|
|
1012
|
+
}
|
|
1013
|
+
});
|
|
965
1014
|
this.authProvider = options.authProvider;
|
|
966
1015
|
this.reconnectConfig = options.reconnectConfig;
|
|
967
1016
|
}
|
|
@@ -972,17 +1021,20 @@ var StreamsTransportClient = class {
|
|
|
972
1021
|
const shardUrls = this.shardUrls;
|
|
973
1022
|
if (shardUrls == null) return input;
|
|
974
1023
|
const operation = this.classifyShardOperation(input, init, shardUrls);
|
|
975
|
-
|
|
976
|
-
const origins = operation === "bootstrap" ? shardUrls.bootstrap : operation === "catchup" ? shardUrls.catchup : shardUrls.largePost;
|
|
1024
|
+
const origins = this.originsForShardOperation(operation, shardUrls);
|
|
977
1025
|
if (origins == null || origins.length === 0) return input;
|
|
978
1026
|
const target = new URL(input);
|
|
979
|
-
const shardOrigin = new URL(this.nextShardOrigin(operation, origins));
|
|
1027
|
+
const shardOrigin = new URL(this.nextShardOrigin(operation ?? "other", origins));
|
|
980
1028
|
target.protocol = shardOrigin.protocol;
|
|
981
1029
|
target.host = shardOrigin.host;
|
|
982
1030
|
target.username = "";
|
|
983
1031
|
target.password = "";
|
|
984
1032
|
return target.toString();
|
|
985
1033
|
}
|
|
1034
|
+
originsForShardOperation(operation, shardUrls) {
|
|
1035
|
+
if (operation == null) return shardUrls.other;
|
|
1036
|
+
return (operation === "bootstrap" ? shardUrls.bootstrap : operation === "catchup" ? shardUrls.catchup : shardUrls.largePost) ?? shardUrls.other;
|
|
1037
|
+
}
|
|
986
1038
|
nextShardOrigin(operation, origins) {
|
|
987
1039
|
const currentIndex = this.shardCursorIndexes[operation] ?? createRandomShardStart(origins.length);
|
|
988
1040
|
this.shardCursorIndexes[operation] = (currentIndex + 1) % origins.length;
|
|
@@ -1002,7 +1054,10 @@ var StreamsTransportClient = class {
|
|
|
1002
1054
|
}
|
|
1003
1055
|
async createStreamRequest() {
|
|
1004
1056
|
const adapterHeaders = this.adapter.getCreateStreamHeaders ? await this.adapter.getCreateStreamHeaders() : void 0;
|
|
1005
|
-
const result = await this.client.create({
|
|
1057
|
+
const result = await this.client.create({
|
|
1058
|
+
headers: adapterHeaders,
|
|
1059
|
+
ttlSeconds: this.streamTtlSeconds
|
|
1060
|
+
});
|
|
1006
1061
|
if (result.ok) return result.result.created;
|
|
1007
1062
|
throw this.toTransportFailure(result.result);
|
|
1008
1063
|
}
|
|
@@ -1014,8 +1069,8 @@ var StreamsTransportClient = class {
|
|
|
1014
1069
|
async readFromOffsetSseItems(offset, cursor, onControl, onOpen, signal, onActivity) {
|
|
1015
1070
|
return this.readItemsSse(offset, cursor, onControl, onOpen, signal, onActivity);
|
|
1016
1071
|
}
|
|
1017
|
-
async readFromOffset(offset, cursor, live, signal) {
|
|
1018
|
-
const result = await this.client.readOnce({
|
|
1072
|
+
async readFromOffset(offset, cursor, live, signal, timeoutMode = "default") {
|
|
1073
|
+
const result = await (live == null && timeoutMode === "initial" ? this.initialSyncClient : this.client).readOnce({
|
|
1019
1074
|
offset,
|
|
1020
1075
|
cursor,
|
|
1021
1076
|
live,
|
|
@@ -1036,8 +1091,8 @@ var StreamsTransportClient = class {
|
|
|
1036
1091
|
}
|
|
1037
1092
|
};
|
|
1038
1093
|
}
|
|
1039
|
-
async bootstrap() {
|
|
1040
|
-
const result = await this.client.bootstrap();
|
|
1094
|
+
async bootstrap(signal, timeoutMode = "default") {
|
|
1095
|
+
const result = await (timeoutMode === "initial" ? this.initialSyncClient : this.client).bootstrap({ signal });
|
|
1041
1096
|
if (!result.ok) throw this.toTransportFailure(result.result);
|
|
1042
1097
|
const snapshot = result.result.snapshotOffset === "-1" ? new Uint8Array() : result.result.snapshot?.body ?? new Uint8Array();
|
|
1043
1098
|
return {
|
|
@@ -1178,15 +1233,16 @@ var StreamsTransportClient = class {
|
|
|
1178
1233
|
}
|
|
1179
1234
|
}
|
|
1180
1235
|
async fetchAuthorized(input, init, timeoutMs, phase) {
|
|
1236
|
+
const requestUrl = this.routeRequestUrl(input, init);
|
|
1181
1237
|
const firstToken = await this.resolveToken({ reason: "request" });
|
|
1182
|
-
const firstResponse = await this.fetchWithTimeout(
|
|
1238
|
+
const firstResponse = await this.fetchWithTimeout(requestUrl, {
|
|
1183
1239
|
...init,
|
|
1184
1240
|
headers: this.attachAuthorization(init.headers, firstToken)
|
|
1185
1241
|
}, timeoutMs, phase);
|
|
1186
1242
|
if (this.authProvider == null || firstResponse.status !== 401 && firstResponse.status !== 403) return firstResponse;
|
|
1187
1243
|
await firstResponse.body?.cancel().catch(() => {});
|
|
1188
1244
|
const refreshedToken = await this.refreshTokenAfterFailure(firstResponse.status, firstToken);
|
|
1189
|
-
return this.fetchWithTimeout(
|
|
1245
|
+
return this.fetchWithTimeout(requestUrl, {
|
|
1190
1246
|
...init,
|
|
1191
1247
|
headers: this.attachAuthorization(init.headers, refreshedToken)
|
|
1192
1248
|
}, timeoutMs, phase);
|
|
@@ -1375,6 +1431,11 @@ var StreamsTransportClient = class {
|
|
|
1375
1431
|
};
|
|
1376
1432
|
}
|
|
1377
1433
|
};
|
|
1434
|
+
function normalizeStreamTtlSeconds(value) {
|
|
1435
|
+
if (value == null) return;
|
|
1436
|
+
if (!Number.isSafeInteger(value) || value < 0) throw new Error("streamTtlSeconds must be a non-negative safe integer");
|
|
1437
|
+
return value;
|
|
1438
|
+
}
|
|
1378
1439
|
const REQUEST_ID_HEADER_NAMES = [
|
|
1379
1440
|
"x-request-id",
|
|
1380
1441
|
"x-amzn-trace-id",
|
|
@@ -1466,7 +1527,8 @@ var EphemeralStreamCrdt = class {
|
|
|
1466
1527
|
adapter: createNoopAdapter(),
|
|
1467
1528
|
authProvider: normalizeAuthProvider(options.auth),
|
|
1468
1529
|
fetchImpl: options.fetch ?? globalThis.fetch.bind(globalThis),
|
|
1469
|
-
reconnectConfig: this.reconnectConfig
|
|
1530
|
+
reconnectConfig: this.reconnectConfig,
|
|
1531
|
+
shardUrls: options.shardUrls
|
|
1470
1532
|
});
|
|
1471
1533
|
}
|
|
1472
1534
|
async join(params) {
|
|
@@ -1626,9 +1688,11 @@ var EphemeralStreamCrdt = class {
|
|
|
1626
1688
|
while (!state.closed) {
|
|
1627
1689
|
state.requestController ??= new AbortController();
|
|
1628
1690
|
const capturedController = state.requestController;
|
|
1691
|
+
let sseOpenedAtMs = 0;
|
|
1629
1692
|
try {
|
|
1630
1693
|
const result = await this.client.readEphemeralSse(async (bootstrap) => {
|
|
1631
1694
|
if (bootstrap.byteLength > 0) await this.adaptor.applyRemoteUpdates([bootstrap]);
|
|
1695
|
+
sseOpenedAtMs = Date.now();
|
|
1632
1696
|
this.setReadStatus(state, "ok");
|
|
1633
1697
|
this.resolveFirstOpen(state);
|
|
1634
1698
|
}, async (update) => {
|
|
@@ -1661,6 +1725,7 @@ var EphemeralStreamCrdt = class {
|
|
|
1661
1725
|
return;
|
|
1662
1726
|
}
|
|
1663
1727
|
this.setReadStatus(state, "reconnecting");
|
|
1728
|
+
if (sseOpenedAtMs > 0 && Date.now() - sseOpenedAtMs >= 5e3) state.retryAttempt = 0;
|
|
1664
1729
|
if (state.retryAttempt >= this.reconnectConfig.maxAttempts) {
|
|
1665
1730
|
this.logError("ephemeral live reconnect exhausted", error, {
|
|
1666
1731
|
streamUrl: this.streamUrl,
|
|
@@ -2218,13 +2283,12 @@ async function disposeJoinState(state, options) {
|
|
|
2218
2283
|
//#endregion
|
|
2219
2284
|
//#region src/transport-runtime.ts
|
|
2220
2285
|
const MIN_RECONNECT_DELAY_MS = 1e3;
|
|
2221
|
-
const MIN_SUCCESSFUL_SSE_DURATION_MS = 5e3;
|
|
2222
2286
|
function reconnectDelayWithFloor(attempt, config) {
|
|
2223
2287
|
return Math.max(computeRetryDelay(attempt, config), MIN_RECONNECT_DELAY_MS);
|
|
2224
2288
|
}
|
|
2225
2289
|
function fastSseCloseDelay(startedAtMs) {
|
|
2226
2290
|
const elapsedMs = Math.max(0, Date.now() - startedAtMs);
|
|
2227
|
-
if (elapsedMs >=
|
|
2291
|
+
if (elapsedMs >= 5e3) return 0;
|
|
2228
2292
|
return Math.max(MIN_RECONNECT_DELAY_MS, MIN_SUCCESSFUL_SSE_DURATION_MS - elapsedMs);
|
|
2229
2293
|
}
|
|
2230
2294
|
/**
|
|
@@ -2295,6 +2359,7 @@ var StreamsCrdt = class StreamsCrdt {
|
|
|
2295
2359
|
this.client = new StreamsTransportClient({
|
|
2296
2360
|
streamUrl: this.streamUrl,
|
|
2297
2361
|
adapter: this.adapter,
|
|
2362
|
+
streamTtlSeconds: options.streamTtlSeconds,
|
|
2298
2363
|
authProvider,
|
|
2299
2364
|
fetchImpl,
|
|
2300
2365
|
reconnectConfig: this.reconnectConfig,
|
|
@@ -2417,7 +2482,7 @@ var StreamsCrdt = class StreamsCrdt {
|
|
|
2417
2482
|
async uploadSnapshotForTesting() {
|
|
2418
2483
|
try {
|
|
2419
2484
|
await this.adapter.flushLocalExports?.();
|
|
2420
|
-
const cursor = await this.syncOnce();
|
|
2485
|
+
const cursor = await this.syncOnce("sync");
|
|
2421
2486
|
const rawSnapshot = this.adapter.exportSnapshot();
|
|
2422
2487
|
const snapshot = this.encodeSnapshot == null ? rawSnapshot : await this.encodeSnapshot(rawSnapshot);
|
|
2423
2488
|
await this.client.putSnapshotAt(cursor.nextOffset, snapshot);
|
|
@@ -2485,7 +2550,7 @@ var StreamsCrdt = class StreamsCrdt {
|
|
|
2485
2550
|
this.disableWriteOnlyMode("appendWriteOnly cannot be used after sync() on the same transport");
|
|
2486
2551
|
return {
|
|
2487
2552
|
ok: true,
|
|
2488
|
-
value: { cursor: await this.syncOnce() }
|
|
2553
|
+
value: { cursor: await this.syncOnce("sync") }
|
|
2489
2554
|
};
|
|
2490
2555
|
} catch (error) {
|
|
2491
2556
|
return {
|
|
@@ -2509,11 +2574,11 @@ var StreamsCrdt = class StreamsCrdt {
|
|
|
2509
2574
|
const state = this.joinState;
|
|
2510
2575
|
if (state != null && !state.closed) return {
|
|
2511
2576
|
ok: true,
|
|
2512
|
-
value: { cursor: await this.catchupJoinState(state, params) }
|
|
2577
|
+
value: { cursor: await this.catchupJoinState(state, params, state.requestController?.signal) }
|
|
2513
2578
|
};
|
|
2514
2579
|
return {
|
|
2515
2580
|
ok: true,
|
|
2516
|
-
value: { cursor: (await this.resolvePullOnlyRemoteState()).cursor }
|
|
2581
|
+
value: { cursor: (await this.resolvePullOnlyRemoteState("catchup")).cursor }
|
|
2517
2582
|
};
|
|
2518
2583
|
} catch (error) {
|
|
2519
2584
|
return {
|
|
@@ -2692,10 +2757,46 @@ var StreamsCrdt = class StreamsCrdt {
|
|
|
2692
2757
|
}
|
|
2693
2758
|
});
|
|
2694
2759
|
}
|
|
2760
|
+
async runInitialSyncWithDeadline(operation, task, parentSignal) {
|
|
2761
|
+
const startedAtMs = Date.now();
|
|
2762
|
+
const hardController = new AbortController();
|
|
2763
|
+
const signal = mergeAbortSignals(parentSignal, hardController.signal);
|
|
2764
|
+
const slowAfterMs = this.reconnectConfig.initialSyncSlowAfterMs;
|
|
2765
|
+
const hardTimeoutMs = this.reconnectConfig.initialSyncHardTimeoutMs;
|
|
2766
|
+
let slowTimer;
|
|
2767
|
+
let hardTimer;
|
|
2768
|
+
let hardTimeoutError;
|
|
2769
|
+
let hardTimeoutPromise;
|
|
2770
|
+
if (typeof slowAfterMs === "number" && Number.isFinite(slowAfterMs) && slowAfterMs > 0) slowTimer = setTimeout(() => {
|
|
2771
|
+
this.logDebug("initial sync still pending", {
|
|
2772
|
+
streamUrl: this.streamUrl,
|
|
2773
|
+
operation,
|
|
2774
|
+
elapsedMs: Date.now() - startedAtMs,
|
|
2775
|
+
slowAfterMs
|
|
2776
|
+
});
|
|
2777
|
+
}, slowAfterMs);
|
|
2778
|
+
if (typeof hardTimeoutMs === "number" && Number.isFinite(hardTimeoutMs) && hardTimeoutMs > 0) hardTimeoutPromise = new Promise((_, reject) => {
|
|
2779
|
+
hardTimer = setTimeout(() => {
|
|
2780
|
+
hardTimeoutError = new InitialSyncTimeoutError(operation, hardTimeoutMs, Date.now() - startedAtMs);
|
|
2781
|
+
hardController.abort(hardTimeoutError);
|
|
2782
|
+
reject(hardTimeoutError);
|
|
2783
|
+
}, hardTimeoutMs);
|
|
2784
|
+
});
|
|
2785
|
+
try {
|
|
2786
|
+
const taskPromise = task(signal);
|
|
2787
|
+
return hardTimeoutPromise == null ? await taskPromise : await Promise.race([taskPromise, hardTimeoutPromise]);
|
|
2788
|
+
} catch (error) {
|
|
2789
|
+
if (hardTimeoutError != null) throw hardTimeoutError;
|
|
2790
|
+
throw error;
|
|
2791
|
+
} finally {
|
|
2792
|
+
if (slowTimer != null) clearTimeout(slowTimer);
|
|
2793
|
+
if (hardTimer != null) clearTimeout(hardTimer);
|
|
2794
|
+
}
|
|
2795
|
+
}
|
|
2695
2796
|
async startJoin(state) {
|
|
2696
2797
|
try {
|
|
2697
2798
|
state.requestController ??= new AbortController();
|
|
2698
|
-
const joined = await this.performInitialJoinSync(state.liveMode);
|
|
2799
|
+
const joined = await this.performInitialJoinSync(state.liveMode, state.requestController?.signal);
|
|
2699
2800
|
state.cursor = joined.cursor;
|
|
2700
2801
|
state.streamCursor = joined.streamCursor;
|
|
2701
2802
|
state.liveMode = joined.liveMode;
|
|
@@ -2727,14 +2828,14 @@ var StreamsCrdt = class StreamsCrdt {
|
|
|
2727
2828
|
throw error;
|
|
2728
2829
|
}
|
|
2729
2830
|
}
|
|
2730
|
-
async syncOnce() {
|
|
2731
|
-
let cursor = (await this.finalizeDirectLocalAppendIfNeeded(await this.resolveInitialRemoteState())).cursor;
|
|
2831
|
+
async syncOnce(operation, parentSignal) {
|
|
2832
|
+
let cursor = (await this.finalizeDirectLocalAppendIfNeeded(await this.resolveInitialRemoteState(operation, parentSignal))).cursor;
|
|
2732
2833
|
const localBatch = this.exportUpdates(cursor.serverLowerBoundVersion);
|
|
2733
2834
|
if (localBatch.batch != null) cursor = (await this.appendLocalBatchDurably(cursor, localBatch.batch)).cursor;
|
|
2734
2835
|
return cursor;
|
|
2735
2836
|
}
|
|
2736
|
-
async performInitialJoinSync(preferredLiveMode) {
|
|
2737
|
-
const initial = await this.finalizeDirectLocalAppendIfNeeded(await this.resolveInitialRemoteState());
|
|
2837
|
+
async performInitialJoinSync(preferredLiveMode, parentSignal) {
|
|
2838
|
+
const initial = await this.finalizeDirectLocalAppendIfNeeded(await this.resolveInitialRemoteState("join", parentSignal));
|
|
2738
2839
|
let cursor = initial.cursor;
|
|
2739
2840
|
let localExportRefVersion = cursor.serverLowerBoundVersion;
|
|
2740
2841
|
let streamCursor = initial.streamCursor;
|
|
@@ -2761,33 +2862,51 @@ var StreamsCrdt = class StreamsCrdt {
|
|
|
2761
2862
|
serverLowerBoundVersion: this.adapter.emptyVersion()
|
|
2762
2863
|
});
|
|
2763
2864
|
}
|
|
2764
|
-
async resolveInitialRemoteState() {
|
|
2865
|
+
async resolveInitialRemoteState(operation, parentSignal) {
|
|
2765
2866
|
const loaded = await this.remoteCursorStore.load(this.streamUrl);
|
|
2766
2867
|
const cursor = loaded ?? this.createInitialCursor();
|
|
2868
|
+
const runInitial = (task) => this.runInitialSyncWithDeadline(operation, task, parentSignal);
|
|
2767
2869
|
try {
|
|
2768
|
-
if (loaded == null) return await this.bootstrapState(cursor
|
|
2769
|
-
|
|
2770
|
-
|
|
2870
|
+
if (loaded == null) return await runInitial((signal) => this.bootstrapState(cursor, {
|
|
2871
|
+
signal,
|
|
2872
|
+
timeoutMode: "initial"
|
|
2873
|
+
}));
|
|
2874
|
+
const caughtUp = await runInitial((signal) => this.catchupOrGone(cursor, {
|
|
2875
|
+
goneStrategy: "return-gone",
|
|
2876
|
+
signal,
|
|
2877
|
+
timeoutMode: "initial"
|
|
2878
|
+
}));
|
|
2879
|
+
if ("kind" in caughtUp) return await this.recoverFromGoneWithLocalBootstrap(cursor, operation, parentSignal);
|
|
2771
2880
|
return caughtUp;
|
|
2772
2881
|
} catch (error) {
|
|
2773
2882
|
if (!isStreamNotFoundError(error) || !this.createStreamIfMissing) throw error;
|
|
2774
2883
|
await this.ensureStreamExists();
|
|
2775
|
-
return await this.bootstrapState(this.createInitialCursor()
|
|
2884
|
+
return await runInitial((signal) => this.bootstrapState(this.createInitialCursor(), {
|
|
2885
|
+
signal,
|
|
2886
|
+
timeoutMode: "initial"
|
|
2887
|
+
}));
|
|
2776
2888
|
}
|
|
2777
2889
|
}
|
|
2778
|
-
async resolvePullOnlyRemoteState() {
|
|
2890
|
+
async resolvePullOnlyRemoteState(operation, parentSignal) {
|
|
2779
2891
|
const loaded = await this.remoteCursorStore.load(this.streamUrl);
|
|
2780
2892
|
const cursor = loaded ?? this.createInitialCursor();
|
|
2893
|
+
const runInitial = (task) => this.runInitialSyncWithDeadline(operation, task, parentSignal);
|
|
2781
2894
|
try {
|
|
2782
|
-
if (loaded == null) return await this.bootstrapState(cursor
|
|
2783
|
-
|
|
2895
|
+
if (loaded == null) return await runInitial((signal) => this.bootstrapState(cursor, {
|
|
2896
|
+
signal,
|
|
2897
|
+
timeoutMode: "initial"
|
|
2898
|
+
}));
|
|
2899
|
+
return await runInitial((signal) => this.catchupFromCursor(cursor, void 0, void 0, void 0, signal, "initial"));
|
|
2784
2900
|
} catch (error) {
|
|
2785
2901
|
if (!isStreamNotFoundError(error) || !this.createStreamIfMissing) throw error;
|
|
2786
2902
|
await this.ensureStreamExists();
|
|
2787
|
-
return await this.bootstrapState(this.createInitialCursor()
|
|
2903
|
+
return await runInitial((signal) => this.bootstrapState(this.createInitialCursor(), {
|
|
2904
|
+
signal,
|
|
2905
|
+
timeoutMode: "initial"
|
|
2906
|
+
}));
|
|
2788
2907
|
}
|
|
2789
2908
|
}
|
|
2790
|
-
async catchupJoinState(state, params) {
|
|
2909
|
+
async catchupJoinState(state, params, parentSignal) {
|
|
2791
2910
|
if (state.readSubStatus === "connecting") {
|
|
2792
2911
|
await state.started;
|
|
2793
2912
|
return state.cursor;
|
|
@@ -2795,9 +2914,9 @@ var StreamsCrdt = class StreamsCrdt {
|
|
|
2795
2914
|
if (state.readSubStatus === "ok" && params.force !== true) return state.cursor;
|
|
2796
2915
|
const cursor = await this.enqueueExclusive(async () => {
|
|
2797
2916
|
if (state.closed) throw new Error("subscription closed");
|
|
2798
|
-
const caughtUp = await this.catchupFromCursor(state.cursor, void 0, state.streamCursor, (previousVersion, nextVersion) => {
|
|
2917
|
+
const caughtUp = await this.runInitialSyncWithDeadline("catchup", (signal) => this.catchupFromCursor(state.cursor, void 0, state.streamCursor, (previousVersion, nextVersion) => {
|
|
2799
2918
|
this.snapshotManager.observeRemoteVersion(state, previousVersion, nextVersion);
|
|
2800
|
-
});
|
|
2919
|
+
}, signal, "initial"), parentSignal);
|
|
2801
2920
|
state.cursor = caughtUp.cursor;
|
|
2802
2921
|
state.streamCursor = caughtUp.streamCursor;
|
|
2803
2922
|
return state.cursor;
|
|
@@ -2811,13 +2930,15 @@ var StreamsCrdt = class StreamsCrdt {
|
|
|
2811
2930
|
}
|
|
2812
2931
|
async bootstrapState(cursor, options = {}) {
|
|
2813
2932
|
const cursorManager = options.cursorManager ?? this.cursorManager;
|
|
2814
|
-
const result = await this.client.bootstrap();
|
|
2933
|
+
const result = await this.client.bootstrap(options.signal, options.timeoutMode);
|
|
2815
2934
|
let saved = await cursorManager.applyBootstrapPayload(cursor, result.snapshot, result.updates, result.nextOffset, result.upToDate);
|
|
2816
2935
|
let bootstrapCursor = result.streamCursor;
|
|
2817
2936
|
if (!result.upToDate) {
|
|
2818
2937
|
const caughtUp = await this.catchupOrGone(saved, {
|
|
2819
2938
|
streamCursor: bootstrapCursor,
|
|
2820
|
-
cursorManager
|
|
2939
|
+
cursorManager,
|
|
2940
|
+
signal: options.signal,
|
|
2941
|
+
timeoutMode: options.timeoutMode
|
|
2821
2942
|
});
|
|
2822
2943
|
if ("kind" in caughtUp) throw new Error("bootstrap catch-up unexpectedly returned gone");
|
|
2823
2944
|
saved = caughtUp.cursor;
|
|
@@ -2828,11 +2949,13 @@ var StreamsCrdt = class StreamsCrdt {
|
|
|
2828
2949
|
streamCursor: bootstrapCursor
|
|
2829
2950
|
};
|
|
2830
2951
|
}
|
|
2831
|
-
async catchupFromCursor(cursor, live, streamCursor, onRemoteVersionApplied) {
|
|
2952
|
+
async catchupFromCursor(cursor, live, streamCursor, onRemoteVersionApplied, signal, timeoutMode) {
|
|
2832
2953
|
const result = await this.catchupOrGone(cursor, {
|
|
2833
2954
|
live,
|
|
2834
2955
|
streamCursor,
|
|
2835
|
-
onRemoteVersionApplied
|
|
2956
|
+
onRemoteVersionApplied,
|
|
2957
|
+
signal,
|
|
2958
|
+
timeoutMode
|
|
2836
2959
|
});
|
|
2837
2960
|
if ("kind" in result) throw new Error("catchup unexpectedly returned gone");
|
|
2838
2961
|
return result;
|
|
@@ -2844,14 +2967,18 @@ var StreamsCrdt = class StreamsCrdt {
|
|
|
2844
2967
|
const cursorManager = options.cursorManager ?? this.cursorManager;
|
|
2845
2968
|
while (true) {
|
|
2846
2969
|
const requestOffset = current.nextOffset;
|
|
2847
|
-
const response = await this.client.readFromOffset(requestOffset, currentStreamCursor, options.live);
|
|
2970
|
+
const response = await this.client.readFromOffset(requestOffset, currentStreamCursor, options.live, options.signal, options.timeoutMode);
|
|
2848
2971
|
if (response.kind === "gone") {
|
|
2849
2972
|
if (goneStrategy === "return-gone") return StreamsCrdt.GONE_CATCHUP;
|
|
2850
2973
|
return await this.bootstrapState({
|
|
2851
2974
|
...current,
|
|
2852
2975
|
nextOffset: "-1",
|
|
2853
2976
|
serverLowerBoundVersion: this.adapter.emptyVersion()
|
|
2854
|
-
}, {
|
|
2977
|
+
}, {
|
|
2978
|
+
cursorManager,
|
|
2979
|
+
signal: options.signal,
|
|
2980
|
+
timeoutMode: options.timeoutMode
|
|
2981
|
+
});
|
|
2855
2982
|
}
|
|
2856
2983
|
const result = response.value;
|
|
2857
2984
|
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`);
|
|
@@ -2862,7 +2989,11 @@ var StreamsCrdt = class StreamsCrdt {
|
|
|
2862
2989
|
...current,
|
|
2863
2990
|
nextOffset: "-1",
|
|
2864
2991
|
serverLowerBoundVersion: this.adapter.emptyVersion()
|
|
2865
|
-
}, {
|
|
2992
|
+
}, {
|
|
2993
|
+
cursorManager,
|
|
2994
|
+
signal: options.signal,
|
|
2995
|
+
timeoutMode: options.timeoutMode
|
|
2996
|
+
});
|
|
2866
2997
|
throw error;
|
|
2867
2998
|
}
|
|
2868
2999
|
currentStreamCursor = result.streamCursor;
|
|
@@ -2878,7 +3009,7 @@ var StreamsCrdt = class StreamsCrdt {
|
|
|
2878
3009
|
...state.cursor,
|
|
2879
3010
|
nextOffset: "-1",
|
|
2880
3011
|
serverLowerBoundVersion: this.adapter.emptyVersion()
|
|
2881
|
-
});
|
|
3012
|
+
}, { signal: state.requestController?.signal });
|
|
2882
3013
|
state.cursor = bootstrapped.cursor;
|
|
2883
3014
|
state.streamCursor = bootstrapped.streamCursor;
|
|
2884
3015
|
state.retryAttempt = 0;
|
|
@@ -3080,8 +3211,12 @@ var StreamsCrdt = class StreamsCrdt {
|
|
|
3080
3211
|
* exports and appends any local delta, then re-bootstraps to converge
|
|
3081
3212
|
* persisted cursor and local CRDT state on server truth.
|
|
3082
3213
|
*/
|
|
3083
|
-
async inferRemoteAndAppendLocal(bootstrapCursor, cursorManager) {
|
|
3084
|
-
const inferredRemote = await this.bootstrapState(bootstrapCursor, {
|
|
3214
|
+
async inferRemoteAndAppendLocal(bootstrapCursor, cursorManager, operation, parentSignal) {
|
|
3215
|
+
const inferredRemote = await this.runInitialSyncWithDeadline(operation, (signal) => this.bootstrapState(bootstrapCursor, {
|
|
3216
|
+
cursorManager,
|
|
3217
|
+
signal,
|
|
3218
|
+
timeoutMode: "initial"
|
|
3219
|
+
}), parentSignal);
|
|
3085
3220
|
let pendingAppend;
|
|
3086
3221
|
if (this.frozenLocalAppend != null) {
|
|
3087
3222
|
if (this.frozenLocalAppend.pendingCount != null) throw new Error("pending local append must be finalized by pending flush");
|
|
@@ -3090,22 +3225,29 @@ var StreamsCrdt = class StreamsCrdt {
|
|
|
3090
3225
|
const localBatch = this.exportUpdates(inferredRemote.cursor.serverLowerBoundVersion);
|
|
3091
3226
|
if (localBatch.batch != null) pendingAppend = await this.appendLocalBatchRemote(inferredRemote.cursor, localBatch.batch);
|
|
3092
3227
|
}
|
|
3093
|
-
const result = await this.bootstrapState(bootstrapCursor
|
|
3228
|
+
const result = await this.runInitialSyncWithDeadline(operation, (signal) => this.bootstrapState(bootstrapCursor, {
|
|
3229
|
+
signal,
|
|
3230
|
+
timeoutMode: "initial"
|
|
3231
|
+
}), parentSignal);
|
|
3094
3232
|
if (pendingAppend != null) this.commitProducerAck(pendingAppend.producerAck);
|
|
3095
3233
|
return result;
|
|
3096
3234
|
}
|
|
3097
|
-
async recoverFromGoneWithLocalBootstrap(cursor) {
|
|
3235
|
+
async recoverFromGoneWithLocalBootstrap(cursor, operation, parentSignal) {
|
|
3098
3236
|
const isolatedCursorManager = await this.createIsolatedCursorManager();
|
|
3099
3237
|
const bootstrapCursor = {
|
|
3100
3238
|
...cursor,
|
|
3101
3239
|
nextOffset: "-1",
|
|
3102
3240
|
serverLowerBoundVersion: this.adapter.emptyVersion()
|
|
3103
3241
|
};
|
|
3104
|
-
if (isolatedCursorManager != null) return await this.inferRemoteAndAppendLocal(bootstrapCursor, isolatedCursorManager);
|
|
3242
|
+
if (isolatedCursorManager != null) return await this.inferRemoteAndAppendLocal(bootstrapCursor, isolatedCursorManager, operation, parentSignal);
|
|
3105
3243
|
const preservedLocalSnapshot = this.adapter.exportSnapshot();
|
|
3106
3244
|
let restoredLocalSnapshot = false;
|
|
3107
3245
|
try {
|
|
3108
|
-
const inferredRemote = await this.
|
|
3246
|
+
const inferredRemote = await this.runInitialSyncWithDeadline(operation, (signal) => this.bootstrapState(bootstrapCursor, {
|
|
3247
|
+
cursorManager: this.volatileCursorManager,
|
|
3248
|
+
signal,
|
|
3249
|
+
timeoutMode: "initial"
|
|
3250
|
+
}), parentSignal);
|
|
3109
3251
|
await this.adapter.applySnapshot(preservedLocalSnapshot);
|
|
3110
3252
|
restoredLocalSnapshot = true;
|
|
3111
3253
|
let pendingAppend;
|
|
@@ -3116,7 +3258,10 @@ var StreamsCrdt = class StreamsCrdt {
|
|
|
3116
3258
|
const localBatch = this.exportUpdates(inferredRemote.cursor.serverLowerBoundVersion);
|
|
3117
3259
|
if (localBatch.batch != null) pendingAppend = await this.appendLocalBatchRemote(inferredRemote.cursor, localBatch.batch);
|
|
3118
3260
|
}
|
|
3119
|
-
const result = await this.bootstrapState(bootstrapCursor
|
|
3261
|
+
const result = await this.runInitialSyncWithDeadline(operation, (signal) => this.bootstrapState(bootstrapCursor, {
|
|
3262
|
+
signal,
|
|
3263
|
+
timeoutMode: "initial"
|
|
3264
|
+
}), parentSignal);
|
|
3120
3265
|
if (pendingAppend != null) this.commitProducerAck(pendingAppend.producerAck);
|
|
3121
3266
|
return result;
|
|
3122
3267
|
} catch (error) {
|
|
@@ -3130,6 +3275,7 @@ var StreamsCrdt = class StreamsCrdt {
|
|
|
3130
3275
|
const capturedController = state.requestController;
|
|
3131
3276
|
let inactivityTimer = null;
|
|
3132
3277
|
let inactivityTriggered = false;
|
|
3278
|
+
let sseOpenedAtMs = 0;
|
|
3133
3279
|
const clearInactivity = () => {
|
|
3134
3280
|
if (inactivityTimer != null) {
|
|
3135
3281
|
clearTimeout(inactivityTimer);
|
|
@@ -3153,7 +3299,7 @@ var StreamsCrdt = class StreamsCrdt {
|
|
|
3153
3299
|
...state.cursor,
|
|
3154
3300
|
nextOffset: "-1",
|
|
3155
3301
|
serverLowerBoundVersion: this.adapter.emptyVersion()
|
|
3156
|
-
});
|
|
3302
|
+
}, { signal: capturedController?.signal });
|
|
3157
3303
|
state.cursor = bootstrapped.cursor;
|
|
3158
3304
|
state.streamCursor = bootstrapped.streamCursor;
|
|
3159
3305
|
state.retryAttempt = 0;
|
|
@@ -3216,6 +3362,7 @@ var StreamsCrdt = class StreamsCrdt {
|
|
|
3216
3362
|
});
|
|
3217
3363
|
}, () => {
|
|
3218
3364
|
resetInactivity();
|
|
3365
|
+
sseOpenedAtMs = Date.now();
|
|
3219
3366
|
this.setReadSubStatus(state, "ok");
|
|
3220
3367
|
}, capturedController?.signal, resetInactivity);
|
|
3221
3368
|
clearInactivity();
|
|
@@ -3246,7 +3393,7 @@ var StreamsCrdt = class StreamsCrdt {
|
|
|
3246
3393
|
...state.cursor,
|
|
3247
3394
|
nextOffset: "-1",
|
|
3248
3395
|
serverLowerBoundVersion: this.adapter.emptyVersion()
|
|
3249
|
-
});
|
|
3396
|
+
}, { signal: capturedController?.signal });
|
|
3250
3397
|
state.cursor = bootstrapped.cursor;
|
|
3251
3398
|
state.streamCursor = bootstrapped.streamCursor;
|
|
3252
3399
|
state.retryAttempt = 0;
|
|
@@ -3316,6 +3463,7 @@ var StreamsCrdt = class StreamsCrdt {
|
|
|
3316
3463
|
return;
|
|
3317
3464
|
}
|
|
3318
3465
|
this.setReadSubStatus(state, "reconnecting");
|
|
3466
|
+
if (sseOpenedAtMs > 0 && Date.now() - sseOpenedAtMs >= 5e3) state.retryAttempt = 0;
|
|
3319
3467
|
const maxAttempts = this.reconnectConfig.maxAttempts;
|
|
3320
3468
|
if (state.retryAttempt >= maxAttempts) {
|
|
3321
3469
|
this.logError("live read reconnect exhausted", error, {
|
|
@@ -3544,4 +3692,4 @@ function createStreamUrl(input) {
|
|
|
3544
3692
|
//#endregion
|
|
3545
3693
|
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 };
|
|
3546
3694
|
|
|
3547
|
-
//# sourceMappingURL=stream-id-
|
|
3695
|
+
//# sourceMappingURL=stream-id-D-UirMkk.js.map
|