@korajs/sync 1.0.0-beta.10 → 1.0.0-beta.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +225 -24
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +14 -2
- package/dist/index.d.ts +14 -2
- package/dist/index.js +224 -24
- package/dist/index.js.map +1 -1
- package/dist/internal.d.cts +1 -1
- package/dist/internal.d.ts +1 -1
- package/dist/{transport-B2GQerqJ.d.cts → transport-iXX0KA_W.d.cts} +66 -5
- package/dist/{transport-B2GQerqJ.d.ts → transport-iXX0KA_W.d.ts} +66 -5
- package/package.json +3 -3
package/dist/index.d.cts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { S as SyncScopeMap, D as DeltaCursor, a as SyncMessage, b as SerializedOperation, W as WireFormat, c as SyncTransport, T as TransportOptions, d as TransportMessageHandler, e as TransportCloseHandler, f as TransportErrorHandler, B as BlobChunkRequestMessage, g as BlobChunkResponseMessage, h as BlobChunkPushMessage, i as SyncEncryptionConfig, V as VersionedKey, Y as YjsDocUpdateMessage, Q as QueueStorage, j as SyncState, k as SyncStatusInfo, l as SyncConfig, R as RejectedOperationStorage, m as SyncStatePersistence, n as SyncQuerySubset, o as RejectedOperation } from './transport-
|
|
2
|
-
export { A as AcknowledgmentMessage, p as
|
|
1
|
+
import { S as SyncScopeMap, D as DeltaCursor, a as SyncMessage, b as SerializedOperation, W as WireFormat, c as SyncTransport, T as TransportOptions, d as TransportMessageHandler, e as TransportCloseHandler, f as TransportErrorHandler, B as BlobChunkRequestMessage, g as BlobChunkResponseMessage, h as BlobChunkPushMessage, i as SyncEncryptionConfig, V as VersionedKey, Y as YjsDocUpdateMessage, Q as QueueStorage, j as SyncState, k as SyncStatusInfo, l as SyncConfig, R as RejectedOperationStorage, m as SyncStatePersistence, n as SyncQuerySubset, o as RejectedOperation } from './transport-iXX0KA_W.cjs';
|
|
2
|
+
export { A as AcknowledgmentMessage, p as ActiveApplyFailure, q as AwarenessStateWire, r as AwarenessUpdateMessage, E as EncryptedPayload, s as ErrorMessage, H as HandshakeMessage, t as HandshakeResponseMessage, I as InvalidScopeError, M as MemoryRejectedOperationStorage, O as OperationBatchMessage, u as OperationRejectedMessage, v as SYNC_STATES, w as SYNC_STATUSES, x as ScopeViolationError, y as SyncEncryptionAlgorithm, z as SyncPhase, C as SyncScopeContext, F as SyncSettlementOptions, G as SyncSettlementResult, J as SyncStatus, K as dedupeQuerySubsets, L as isAcknowledgmentMessage, N as isAwarenessUpdateMessage, P as isBlobChunkPushMessage, U as isBlobChunkRequestMessage, X as isBlobChunkResponseMessage, Z as isErrorMessage, _ as isHandshakeMessage, $ as isHandshakeResponseMessage, a0 as isOperationBatchMessage, a1 as isOperationRejectedMessage, a2 as isSyncMessage, a3 as isYjsDocUpdateMessage, a4 as operationMatchesQuerySubsets, a5 as querySubsetContains } from './transport-iXX0KA_W.cjs';
|
|
3
3
|
import * as _korajs_core from '@korajs/core';
|
|
4
4
|
import { Operation, VersionVector, ApplyResult, KoraEventEmitter, TimeSource, SyncError, ConnectionQuality } from '@korajs/core';
|
|
5
5
|
export { ApplyFailureReason, ApplyResult } from '@korajs/core';
|
|
@@ -976,6 +976,11 @@ declare class SyncEngine {
|
|
|
976
976
|
private clockBlocked;
|
|
977
977
|
private clockSkewMs;
|
|
978
978
|
private blobStorageEnabled;
|
|
979
|
+
private suspensionReason;
|
|
980
|
+
private authRejected;
|
|
981
|
+
private serverFrontier;
|
|
982
|
+
private hasInFlightDeliveryBatch;
|
|
983
|
+
private blockedFailure;
|
|
979
984
|
private outboundAckTimer;
|
|
980
985
|
private startPromise;
|
|
981
986
|
private stopPromise;
|
|
@@ -995,6 +1000,7 @@ declare class SyncEngine {
|
|
|
995
1000
|
private activeScope;
|
|
996
1001
|
/** Live query subsets registered from reactive subscriptions */
|
|
997
1002
|
private querySubsets;
|
|
1003
|
+
private staticQuerySubsets;
|
|
998
1004
|
private querySubsetReconnectTimer;
|
|
999
1005
|
/** Resume cursor for paginated initial sync (persisted across reconnects) */
|
|
1000
1006
|
private resumeDeltaCursor;
|
|
@@ -1051,6 +1057,7 @@ declare class SyncEngine {
|
|
|
1051
1057
|
* Get the current developer-facing sync status.
|
|
1052
1058
|
*/
|
|
1053
1059
|
getStatus(): SyncStatusInfo;
|
|
1060
|
+
private resolvePhase;
|
|
1054
1061
|
/**
|
|
1055
1062
|
* True when the server rejected the client's schema version at handshake.
|
|
1056
1063
|
* Sync stays blocked until the app schema is upgraded or sync config changes.
|
|
@@ -1088,6 +1095,8 @@ declare class SyncEngine {
|
|
|
1088
1095
|
* or in error state, restarts the sync. If already connected, no-op.
|
|
1089
1096
|
*/
|
|
1090
1097
|
retryNow(): Promise<void>;
|
|
1098
|
+
/** Wake a permanent auth suspension after the binding reports a new auth state. */
|
|
1099
|
+
notifyAuthChanged(): void;
|
|
1091
1100
|
/**
|
|
1092
1101
|
* Atomically refresh the transport session. Overlapping callers share one
|
|
1093
1102
|
* reconnect, in-flight outbound batches are returned to the queue by stop(), and the
|
|
@@ -1165,6 +1174,9 @@ declare class SyncEngine {
|
|
|
1165
1174
|
* Takes effect on the next connection; reconnects when already connected.
|
|
1166
1175
|
*/
|
|
1167
1176
|
registerQuerySubset(subset: SyncQuerySubset): () => void;
|
|
1177
|
+
private switchDeliveryViewWithInheritance;
|
|
1178
|
+
/** Atomically replace the manifest used by static query-subset mode. */
|
|
1179
|
+
setQuerySubsets(subsets: SyncQuerySubset[]): void;
|
|
1168
1180
|
/**
|
|
1169
1181
|
* Returns deduplicated active query subsets from registered subscriptions.
|
|
1170
1182
|
*/
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { S as SyncScopeMap, D as DeltaCursor, a as SyncMessage, b as SerializedOperation, W as WireFormat, c as SyncTransport, T as TransportOptions, d as TransportMessageHandler, e as TransportCloseHandler, f as TransportErrorHandler, B as BlobChunkRequestMessage, g as BlobChunkResponseMessage, h as BlobChunkPushMessage, i as SyncEncryptionConfig, V as VersionedKey, Y as YjsDocUpdateMessage, Q as QueueStorage, j as SyncState, k as SyncStatusInfo, l as SyncConfig, R as RejectedOperationStorage, m as SyncStatePersistence, n as SyncQuerySubset, o as RejectedOperation } from './transport-
|
|
2
|
-
export { A as AcknowledgmentMessage, p as
|
|
1
|
+
import { S as SyncScopeMap, D as DeltaCursor, a as SyncMessage, b as SerializedOperation, W as WireFormat, c as SyncTransport, T as TransportOptions, d as TransportMessageHandler, e as TransportCloseHandler, f as TransportErrorHandler, B as BlobChunkRequestMessage, g as BlobChunkResponseMessage, h as BlobChunkPushMessage, i as SyncEncryptionConfig, V as VersionedKey, Y as YjsDocUpdateMessage, Q as QueueStorage, j as SyncState, k as SyncStatusInfo, l as SyncConfig, R as RejectedOperationStorage, m as SyncStatePersistence, n as SyncQuerySubset, o as RejectedOperation } from './transport-iXX0KA_W.js';
|
|
2
|
+
export { A as AcknowledgmentMessage, p as ActiveApplyFailure, q as AwarenessStateWire, r as AwarenessUpdateMessage, E as EncryptedPayload, s as ErrorMessage, H as HandshakeMessage, t as HandshakeResponseMessage, I as InvalidScopeError, M as MemoryRejectedOperationStorage, O as OperationBatchMessage, u as OperationRejectedMessage, v as SYNC_STATES, w as SYNC_STATUSES, x as ScopeViolationError, y as SyncEncryptionAlgorithm, z as SyncPhase, C as SyncScopeContext, F as SyncSettlementOptions, G as SyncSettlementResult, J as SyncStatus, K as dedupeQuerySubsets, L as isAcknowledgmentMessage, N as isAwarenessUpdateMessage, P as isBlobChunkPushMessage, U as isBlobChunkRequestMessage, X as isBlobChunkResponseMessage, Z as isErrorMessage, _ as isHandshakeMessage, $ as isHandshakeResponseMessage, a0 as isOperationBatchMessage, a1 as isOperationRejectedMessage, a2 as isSyncMessage, a3 as isYjsDocUpdateMessage, a4 as operationMatchesQuerySubsets, a5 as querySubsetContains } from './transport-iXX0KA_W.js';
|
|
3
3
|
import * as _korajs_core from '@korajs/core';
|
|
4
4
|
import { Operation, VersionVector, ApplyResult, KoraEventEmitter, TimeSource, SyncError, ConnectionQuality } from '@korajs/core';
|
|
5
5
|
export { ApplyFailureReason, ApplyResult } from '@korajs/core';
|
|
@@ -976,6 +976,11 @@ declare class SyncEngine {
|
|
|
976
976
|
private clockBlocked;
|
|
977
977
|
private clockSkewMs;
|
|
978
978
|
private blobStorageEnabled;
|
|
979
|
+
private suspensionReason;
|
|
980
|
+
private authRejected;
|
|
981
|
+
private serverFrontier;
|
|
982
|
+
private hasInFlightDeliveryBatch;
|
|
983
|
+
private blockedFailure;
|
|
979
984
|
private outboundAckTimer;
|
|
980
985
|
private startPromise;
|
|
981
986
|
private stopPromise;
|
|
@@ -995,6 +1000,7 @@ declare class SyncEngine {
|
|
|
995
1000
|
private activeScope;
|
|
996
1001
|
/** Live query subsets registered from reactive subscriptions */
|
|
997
1002
|
private querySubsets;
|
|
1003
|
+
private staticQuerySubsets;
|
|
998
1004
|
private querySubsetReconnectTimer;
|
|
999
1005
|
/** Resume cursor for paginated initial sync (persisted across reconnects) */
|
|
1000
1006
|
private resumeDeltaCursor;
|
|
@@ -1051,6 +1057,7 @@ declare class SyncEngine {
|
|
|
1051
1057
|
* Get the current developer-facing sync status.
|
|
1052
1058
|
*/
|
|
1053
1059
|
getStatus(): SyncStatusInfo;
|
|
1060
|
+
private resolvePhase;
|
|
1054
1061
|
/**
|
|
1055
1062
|
* True when the server rejected the client's schema version at handshake.
|
|
1056
1063
|
* Sync stays blocked until the app schema is upgraded or sync config changes.
|
|
@@ -1088,6 +1095,8 @@ declare class SyncEngine {
|
|
|
1088
1095
|
* or in error state, restarts the sync. If already connected, no-op.
|
|
1089
1096
|
*/
|
|
1090
1097
|
retryNow(): Promise<void>;
|
|
1098
|
+
/** Wake a permanent auth suspension after the binding reports a new auth state. */
|
|
1099
|
+
notifyAuthChanged(): void;
|
|
1091
1100
|
/**
|
|
1092
1101
|
* Atomically refresh the transport session. Overlapping callers share one
|
|
1093
1102
|
* reconnect, in-flight outbound batches are returned to the queue by stop(), and the
|
|
@@ -1165,6 +1174,9 @@ declare class SyncEngine {
|
|
|
1165
1174
|
* Takes effect on the next connection; reconnects when already connected.
|
|
1166
1175
|
*/
|
|
1167
1176
|
registerQuerySubset(subset: SyncQuerySubset): () => void;
|
|
1177
|
+
private switchDeliveryViewWithInheritance;
|
|
1178
|
+
/** Atomically replace the manifest used by static query-subset mode. */
|
|
1179
|
+
setQuerySubsets(subsets: SyncQuerySubset[]): void;
|
|
1168
1180
|
/**
|
|
1169
1181
|
* Returns deduplicated active query subsets from registered subscriptions.
|
|
1170
1182
|
*/
|
package/dist/index.js
CHANGED
|
@@ -20,7 +20,8 @@ var SYNC_STATUSES = [
|
|
|
20
20
|
"offline",
|
|
21
21
|
"clock-error",
|
|
22
22
|
"error",
|
|
23
|
-
"schema-mismatch"
|
|
23
|
+
"schema-mismatch",
|
|
24
|
+
"auth-required"
|
|
24
25
|
];
|
|
25
26
|
var MemoryRejectedOperationStorage = class {
|
|
26
27
|
rejected = /* @__PURE__ */ new Map();
|
|
@@ -68,12 +69,19 @@ function operationMatchesScope(op, scopeMap, fullRecord) {
|
|
|
68
69
|
const snapshot = buildSnapshot(op, fullRecord ?? void 0);
|
|
69
70
|
if (!snapshot) return false;
|
|
70
71
|
for (const [field, expected] of Object.entries(collectionScope)) {
|
|
71
|
-
if (snapshot[field]
|
|
72
|
+
if (!matchesPredicate(snapshot[field], expected)) {
|
|
72
73
|
return false;
|
|
73
74
|
}
|
|
74
75
|
}
|
|
75
76
|
return true;
|
|
76
77
|
}
|
|
78
|
+
function matchesPredicate(actual, expected) {
|
|
79
|
+
if (expected && typeof expected === "object" && !Array.isArray(expected) && "$in" in expected) {
|
|
80
|
+
const values = expected.$in;
|
|
81
|
+
return Array.isArray(values) && values.some((value) => Object.is(actual, value));
|
|
82
|
+
}
|
|
83
|
+
return Object.is(actual, expected);
|
|
84
|
+
}
|
|
77
85
|
function filterOperationsByScope(operations, scopeMap) {
|
|
78
86
|
if (!scopeMap) return operations;
|
|
79
87
|
return operations.filter((op) => operationMatchesScope(op, scopeMap));
|
|
@@ -130,24 +138,77 @@ function operationMatchesQuerySubsets(op, subsets, fullRecord) {
|
|
|
130
138
|
if (collectionSubsets.length === 0) {
|
|
131
139
|
return true;
|
|
132
140
|
}
|
|
141
|
+
if (collectionSubsets.some((subset) => Object.keys(subset.where).length === 0)) {
|
|
142
|
+
return true;
|
|
143
|
+
}
|
|
133
144
|
const snapshot = buildSnapshot2(op, fullRecord);
|
|
134
145
|
if (!snapshot) {
|
|
135
146
|
return false;
|
|
136
147
|
}
|
|
137
148
|
return collectionSubsets.some((subset) => recordMatchesWhere(snapshot, subset.where));
|
|
138
149
|
}
|
|
150
|
+
function stableValue(value) {
|
|
151
|
+
if (Array.isArray(value)) return value.map(stableValue);
|
|
152
|
+
if (value && typeof value === "object") {
|
|
153
|
+
return Object.fromEntries(
|
|
154
|
+
Object.entries(value).filter(([, entry]) => entry !== void 0).sort(([a], [b]) => a.localeCompare(b)).map(([key, entry]) => [key, stableValue(entry)])
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
return value;
|
|
158
|
+
}
|
|
159
|
+
function whereContains(broad, narrow) {
|
|
160
|
+
return Object.entries(broad).every(
|
|
161
|
+
([field, expected]) => JSON.stringify(stableValue(narrow[field])) === JSON.stringify(stableValue(expected))
|
|
162
|
+
);
|
|
163
|
+
}
|
|
139
164
|
function dedupeQuerySubsets(subsets) {
|
|
140
|
-
const
|
|
165
|
+
const normalized = subsets.map((subset) => ({
|
|
166
|
+
collection: subset.collection,
|
|
167
|
+
where: stableValue(subset.where)
|
|
168
|
+
}));
|
|
141
169
|
const result = [];
|
|
142
|
-
for (const
|
|
143
|
-
|
|
144
|
-
|
|
170
|
+
for (const candidate of normalized) {
|
|
171
|
+
if (normalized.some(
|
|
172
|
+
(other) => other !== candidate && other.collection === candidate.collection && whereContains(other.where, candidate.where) && (Object.keys(other.where).length < Object.keys(candidate.where).length || JSON.stringify(other.where) === JSON.stringify(candidate.where))
|
|
173
|
+
)) {
|
|
174
|
+
const duplicateAlreadyKept = result.some(
|
|
175
|
+
(item) => item.collection === candidate.collection && JSON.stringify(item.where) === JSON.stringify(candidate.where)
|
|
176
|
+
);
|
|
177
|
+
if (duplicateAlreadyKept || normalized.some(
|
|
178
|
+
(other) => other !== candidate && other.collection === candidate.collection && Object.keys(other.where).length < Object.keys(candidate.where).length && whereContains(other.where, candidate.where)
|
|
179
|
+
))
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
if (!result.some(
|
|
183
|
+
(item) => item.collection === candidate.collection && JSON.stringify(item.where) === JSON.stringify(candidate.where)
|
|
184
|
+
))
|
|
185
|
+
result.push(candidate);
|
|
186
|
+
}
|
|
187
|
+
return result.sort(
|
|
188
|
+
(a, b) => `${a.collection}:${JSON.stringify(a.where)}`.localeCompare(
|
|
189
|
+
`${b.collection}:${JSON.stringify(b.where)}`
|
|
190
|
+
)
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
function querySubsetContains(broad, narrow) {
|
|
194
|
+
const collections = /* @__PURE__ */ new Set([
|
|
195
|
+
...broad.map((subset) => subset.collection),
|
|
196
|
+
...narrow.map((subset) => subset.collection)
|
|
197
|
+
]);
|
|
198
|
+
for (const collection of collections) {
|
|
199
|
+
const broadForCollection = broad.filter((subset) => subset.collection === collection);
|
|
200
|
+
const narrowForCollection = narrow.filter((subset) => subset.collection === collection);
|
|
201
|
+
if (narrowForCollection.length === 0) {
|
|
202
|
+
if (broadForCollection.length > 0) return false;
|
|
145
203
|
continue;
|
|
146
204
|
}
|
|
147
|
-
|
|
148
|
-
|
|
205
|
+
if (broadForCollection.length === 0) continue;
|
|
206
|
+
if (!narrowForCollection.every(
|
|
207
|
+
(candidate) => broadForCollection.some((other) => whereContains(other.where, candidate.where))
|
|
208
|
+
))
|
|
209
|
+
return false;
|
|
149
210
|
}
|
|
150
|
-
return
|
|
211
|
+
return true;
|
|
151
212
|
}
|
|
152
213
|
|
|
153
214
|
// src/delta/delta-cursor.ts
|
|
@@ -2714,6 +2775,11 @@ var SyncEngine = class {
|
|
|
2714
2775
|
clockBlocked = false;
|
|
2715
2776
|
clockSkewMs = null;
|
|
2716
2777
|
blobStorageEnabled = false;
|
|
2778
|
+
suspensionReason = null;
|
|
2779
|
+
authRejected = false;
|
|
2780
|
+
serverFrontier = null;
|
|
2781
|
+
hasInFlightDeliveryBatch = false;
|
|
2782
|
+
blockedFailure = null;
|
|
2717
2783
|
outboundAckTimer = null;
|
|
2718
2784
|
startPromise = null;
|
|
2719
2785
|
stopPromise = null;
|
|
@@ -2734,6 +2800,7 @@ var SyncEngine = class {
|
|
|
2734
2800
|
activeScope;
|
|
2735
2801
|
/** Live query subsets registered from reactive subscriptions */
|
|
2736
2802
|
querySubsets = /* @__PURE__ */ new Map();
|
|
2803
|
+
staticQuerySubsets = [];
|
|
2737
2804
|
querySubsetReconnectTimer = null;
|
|
2738
2805
|
/** Resume cursor for paginated initial sync (persisted across reconnects) */
|
|
2739
2806
|
resumeDeltaCursor = null;
|
|
@@ -2827,6 +2894,22 @@ var SyncEngine = class {
|
|
|
2827
2894
|
if (this.state !== "disconnected") {
|
|
2828
2895
|
await this.stop();
|
|
2829
2896
|
}
|
|
2897
|
+
if (this.authRejected) {
|
|
2898
|
+
this.emitter?.emit({
|
|
2899
|
+
type: "sync:suspended",
|
|
2900
|
+
reason: this.suspensionReason ?? "auth-rejected"
|
|
2901
|
+
});
|
|
2902
|
+
return;
|
|
2903
|
+
}
|
|
2904
|
+
if (this.config.authState) {
|
|
2905
|
+
const authState = await this.config.authState();
|
|
2906
|
+
if (authState.state === "loading" || authState.state === "signed-out") {
|
|
2907
|
+
this.suspensionReason = authState.state === "loading" ? "auth-loading" : "auth-required";
|
|
2908
|
+
this.emitter?.emit({ type: "sync:suspended", reason: this.suspensionReason });
|
|
2909
|
+
return;
|
|
2910
|
+
}
|
|
2911
|
+
}
|
|
2912
|
+
this.suspensionReason = null;
|
|
2830
2913
|
await this.outboundQueue.initialize();
|
|
2831
2914
|
if (this.syncState) {
|
|
2832
2915
|
this.lastAckedServerVector = await this.syncState.loadLastAckedServerVector();
|
|
@@ -2956,14 +3039,30 @@ var SyncEngine = class {
|
|
|
2956
3039
|
getStatus() {
|
|
2957
3040
|
const pendingOperations = this.syncState ? this.cachedUnsyncedCount : this.outboundQueue.totalPending;
|
|
2958
3041
|
const base = {
|
|
3042
|
+
phase: this.resolvePhase(),
|
|
3043
|
+
...this.suspensionReason ? { reason: this.suspensionReason } : {},
|
|
2959
3044
|
reconnecting: this.reconnecting,
|
|
2960
3045
|
pendingOperations,
|
|
2961
3046
|
lastSyncedAt: this.lastSyncedAt,
|
|
2962
3047
|
lastSuccessfulPush: this.lastSuccessfulPush,
|
|
2963
3048
|
lastSuccessfulPull: this.lastSuccessfulPull,
|
|
2964
3049
|
conflicts: this.conflictCount,
|
|
2965
|
-
clockSkewMs: this.clockSkewMs
|
|
3050
|
+
clockSkewMs: this.clockSkewMs,
|
|
3051
|
+
inFlightUploadOperations: this.currentBatch?.operations.length ?? 0,
|
|
3052
|
+
hasInFlightDeliveryBatch: this.hasInFlightDeliveryBatch,
|
|
3053
|
+
activeViewId: this.deliverySignature(),
|
|
3054
|
+
activeViewComplete: this.state === "streaming" && !this.hasInFlightDeliveryBatch && this.blockedFailure === null && (this.serverFrontier === null || this.deliveryWatermark >= this.serverFrontier),
|
|
3055
|
+
initialSync: {
|
|
3056
|
+
complete: this.deltaReceiveComplete && this.deltaSendComplete,
|
|
3057
|
+
receivedBatches: this.deltaBatchesReceived,
|
|
3058
|
+
totalBatches: this.initialSyncTotalBatches || null,
|
|
3059
|
+
progress: this.initialSyncTotalBatches > 0 ? Math.min(1, this.deltaBatchesReceived / this.initialSyncTotalBatches) : null
|
|
3060
|
+
},
|
|
3061
|
+
deliveryWatermark: this.deliveryWatermark,
|
|
3062
|
+
serverFrontier: this.serverFrontier,
|
|
3063
|
+
blockedFailure: this.blockedFailure
|
|
2966
3064
|
};
|
|
3065
|
+
if (this.suspensionReason) return { ...base, status: "auth-required" };
|
|
2967
3066
|
switch (this.state) {
|
|
2968
3067
|
case "disconnected":
|
|
2969
3068
|
if (this.clockBlocked) {
|
|
@@ -2986,6 +3085,26 @@ var SyncEngine = class {
|
|
|
2986
3085
|
return { ...base, status: this.schemaBlocked ? "schema-mismatch" : "error" };
|
|
2987
3086
|
}
|
|
2988
3087
|
}
|
|
3088
|
+
resolvePhase() {
|
|
3089
|
+
if (this.suspensionReason) return "suspended";
|
|
3090
|
+
if (this.blockedFailure || this.schemaBlocked || this.clockBlocked) return "blocked";
|
|
3091
|
+
if (this.hasInFlightDeliveryBatch) return "applying";
|
|
3092
|
+
if (this.currentBatch) return "uploading";
|
|
3093
|
+
switch (this.state) {
|
|
3094
|
+
case "disconnected":
|
|
3095
|
+
return "offline";
|
|
3096
|
+
case "connecting":
|
|
3097
|
+
return "connecting";
|
|
3098
|
+
case "handshaking":
|
|
3099
|
+
return "handshaking";
|
|
3100
|
+
case "syncing":
|
|
3101
|
+
return "receiving";
|
|
3102
|
+
case "streaming":
|
|
3103
|
+
return "streaming";
|
|
3104
|
+
case "error":
|
|
3105
|
+
return "blocked";
|
|
3106
|
+
}
|
|
3107
|
+
}
|
|
2989
3108
|
/**
|
|
2990
3109
|
* True when the server rejected the client's schema version at handshake.
|
|
2991
3110
|
* Sync stays blocked until the app schema is upgraded or sync config changes.
|
|
@@ -3038,8 +3157,25 @@ var SyncEngine = class {
|
|
|
3038
3157
|
await this.refreshPendingCount();
|
|
3039
3158
|
return this.getStatus().pendingOperations;
|
|
3040
3159
|
}
|
|
3041
|
-
emitApplyFailure(op, result, overrides) {
|
|
3160
|
+
emitApplyFailure(op, result, overrides, blocking = false) {
|
|
3042
3161
|
const reason = defaultApplyFailureReason(result, overrides);
|
|
3162
|
+
if (blocking || reason.retriable) {
|
|
3163
|
+
const prior = this.blockedFailure;
|
|
3164
|
+
this.blockedFailure = {
|
|
3165
|
+
operationId: op.id,
|
|
3166
|
+
collection: op.collection,
|
|
3167
|
+
recordId: op.recordId,
|
|
3168
|
+
code: reason.code,
|
|
3169
|
+
message: reason.message,
|
|
3170
|
+
retriable: reason.retriable,
|
|
3171
|
+
firstSeenAt: prior?.operationId === op.id ? prior.firstSeenAt : Date.now(),
|
|
3172
|
+
retryCount: prior?.operationId === op.id ? prior.retryCount + 1 : 0
|
|
3173
|
+
};
|
|
3174
|
+
this.emitter?.emit({
|
|
3175
|
+
type: prior?.operationId === op.id ? "sync:apply-retrying" : "sync:apply-blocked",
|
|
3176
|
+
failure: this.blockedFailure
|
|
3177
|
+
});
|
|
3178
|
+
}
|
|
3043
3179
|
this.emitter?.emit({
|
|
3044
3180
|
type: "sync:apply-failed",
|
|
3045
3181
|
operationId: op.id,
|
|
@@ -3056,11 +3192,18 @@ var SyncEngine = class {
|
|
|
3056
3192
|
*/
|
|
3057
3193
|
async retryNow() {
|
|
3058
3194
|
if (this.schemaBlocked) return;
|
|
3195
|
+
this.authRejected = false;
|
|
3196
|
+
this.suspensionReason = null;
|
|
3059
3197
|
if (this.state === "disconnected" || this.state === "error") {
|
|
3060
3198
|
this.reconnecting = false;
|
|
3061
3199
|
await this.start();
|
|
3062
3200
|
}
|
|
3063
3201
|
}
|
|
3202
|
+
/** Wake a permanent auth suspension after the binding reports a new auth state. */
|
|
3203
|
+
notifyAuthChanged() {
|
|
3204
|
+
this.authRejected = false;
|
|
3205
|
+
this.suspensionReason = null;
|
|
3206
|
+
}
|
|
3064
3207
|
/**
|
|
3065
3208
|
* Atomically refresh the transport session. Overlapping callers share one
|
|
3066
3209
|
* reconnect, in-flight outbound batches are returned to the queue by stop(), and the
|
|
@@ -3217,23 +3360,49 @@ var SyncEngine = class {
|
|
|
3217
3360
|
* Takes effect on the next connection; reconnects when already connected.
|
|
3218
3361
|
*/
|
|
3219
3362
|
registerQuerySubset(subset) {
|
|
3363
|
+
if ((this.config.querySubsets?.mode ?? "reactive") !== "reactive") return () => {
|
|
3364
|
+
};
|
|
3220
3365
|
const id = `query-${nextQuerySubsetId++}`;
|
|
3221
3366
|
const previousSignature = this.deliverySignature();
|
|
3367
|
+
const previousEffective = this.getActiveQuerySubsets();
|
|
3222
3368
|
this.querySubsets.set(id, subset);
|
|
3223
|
-
this.
|
|
3224
|
-
this.scheduleQuerySubsetReconnect();
|
|
3369
|
+
this.switchDeliveryViewWithInheritance(previousSignature, previousEffective);
|
|
3370
|
+
if (this.deliverySignature() !== previousSignature) this.scheduleQuerySubsetReconnect();
|
|
3225
3371
|
return () => {
|
|
3226
3372
|
const sigBeforeRemove = this.deliverySignature();
|
|
3373
|
+
const effectiveBeforeRemove = this.getActiveQuerySubsets();
|
|
3227
3374
|
this.querySubsets.delete(id);
|
|
3228
|
-
this.
|
|
3229
|
-
this.scheduleQuerySubsetReconnect();
|
|
3375
|
+
this.switchDeliveryViewWithInheritance(sigBeforeRemove, effectiveBeforeRemove);
|
|
3376
|
+
if (this.deliverySignature() !== sigBeforeRemove) this.scheduleQuerySubsetReconnect();
|
|
3230
3377
|
};
|
|
3231
3378
|
}
|
|
3379
|
+
switchDeliveryViewWithInheritance(previousSignature, previousSubsets) {
|
|
3380
|
+
const nextSubsets = this.getActiveQuerySubsets();
|
|
3381
|
+
const previousWatermark = this.deliveryWatermark;
|
|
3382
|
+
this.switchDeliveryView(previousSignature);
|
|
3383
|
+
if (this.deliveryWatermark === 0 && querySubsetContains(previousSubsets, nextSubsets)) {
|
|
3384
|
+
this.deliveryWatermark = previousWatermark;
|
|
3385
|
+
this.setViewWatermark(this.deliverySignature(), previousWatermark);
|
|
3386
|
+
void this.persistDeliveryWatermark(previousWatermark);
|
|
3387
|
+
}
|
|
3388
|
+
}
|
|
3389
|
+
/** Atomically replace the manifest used by static query-subset mode. */
|
|
3390
|
+
setQuerySubsets(subsets) {
|
|
3391
|
+
if ((this.config.querySubsets?.mode ?? "reactive") !== "static")
|
|
3392
|
+
throw new Error('setQuerySubsets() requires sync.querySubsets.mode = "static"');
|
|
3393
|
+
const previousSignature = this.deliverySignature();
|
|
3394
|
+
const previousEffective = this.getActiveQuerySubsets();
|
|
3395
|
+
this.staticQuerySubsets = dedupeQuerySubsets(subsets);
|
|
3396
|
+
this.switchDeliveryViewWithInheritance(previousSignature, previousEffective);
|
|
3397
|
+
if (this.deliverySignature() !== previousSignature) this.scheduleQuerySubsetReconnect();
|
|
3398
|
+
}
|
|
3232
3399
|
/**
|
|
3233
3400
|
* Returns deduplicated active query subsets from registered subscriptions.
|
|
3234
3401
|
*/
|
|
3235
3402
|
getActiveQuerySubsets() {
|
|
3236
|
-
|
|
3403
|
+
const mode = this.config.querySubsets?.mode ?? "reactive";
|
|
3404
|
+
if (mode === "disabled") return [];
|
|
3405
|
+
return mode === "static" ? this.staticQuerySubsets : dedupeQuerySubsets([...this.querySubsets.values()]);
|
|
3237
3406
|
}
|
|
3238
3407
|
/**
|
|
3239
3408
|
* Get the awareness manager for collaborative presence.
|
|
@@ -3310,6 +3479,7 @@ var SyncEngine = class {
|
|
|
3310
3479
|
}
|
|
3311
3480
|
}
|
|
3312
3481
|
handleMessageFailure(error) {
|
|
3482
|
+
this.hasInFlightDeliveryBatch = false;
|
|
3313
3483
|
const reason = error instanceof Error ? error.message : "Message handling failed";
|
|
3314
3484
|
this.handleTransportClose(reason);
|
|
3315
3485
|
}
|
|
@@ -3383,6 +3553,7 @@ var SyncEngine = class {
|
|
|
3383
3553
|
this.setViewWatermark(this.deliverySignature(), 0);
|
|
3384
3554
|
await this.persistDeliveryWatermark(0);
|
|
3385
3555
|
}
|
|
3556
|
+
this.serverFrontier = msg.serverMaxDeliverySequence ?? null;
|
|
3386
3557
|
if (msg.selectedWireFormat) {
|
|
3387
3558
|
this.setSerializerWireFormat(msg.selectedWireFormat);
|
|
3388
3559
|
}
|
|
@@ -3544,6 +3715,7 @@ var SyncEngine = class {
|
|
|
3544
3715
|
this.sendDeliveryAck(msg.messageId, this.deliveryWatermark);
|
|
3545
3716
|
return;
|
|
3546
3717
|
}
|
|
3718
|
+
this.hasInFlightDeliveryBatch = true;
|
|
3547
3719
|
}
|
|
3548
3720
|
const deserialized = msg.operations.map((s) => this.serializer.decodeOperation(s));
|
|
3549
3721
|
const operations = this.encryptor ? await this.encryptor.decryptBatch(deserialized) : deserialized;
|
|
@@ -3564,17 +3736,22 @@ var SyncEngine = class {
|
|
|
3564
3736
|
} catch (error) {
|
|
3565
3737
|
fullyApplied = false;
|
|
3566
3738
|
if (error instanceof ClockDriftError) {
|
|
3567
|
-
this.emitApplyFailure(
|
|
3568
|
-
|
|
3569
|
-
|
|
3570
|
-
|
|
3571
|
-
|
|
3739
|
+
this.emitApplyFailure(
|
|
3740
|
+
transformed,
|
|
3741
|
+
"rejected",
|
|
3742
|
+
{
|
|
3743
|
+
code: APPLY_FAILURE_CODES.CLOCK_DRIFT,
|
|
3744
|
+
message: error.message,
|
|
3745
|
+
retriable: false
|
|
3746
|
+
},
|
|
3747
|
+
true
|
|
3748
|
+
);
|
|
3572
3749
|
continue;
|
|
3573
3750
|
}
|
|
3574
3751
|
const message = error instanceof Error ? error.message : "Apply failed";
|
|
3575
3752
|
const code = error instanceof SyncError4 ? error.code : error instanceof KoraError2 ? error.code : error instanceof Error && "code" in error && typeof error.code === "string" ? error.code : APPLY_FAILURE_CODES.APPLY_FAILED;
|
|
3576
3753
|
const retriable = code !== APPLY_FAILURE_CODES.REFERENTIAL_INTEGRITY;
|
|
3577
|
-
this.emitApplyFailure(transformed, "rejected", { code, message, retriable });
|
|
3754
|
+
this.emitApplyFailure(transformed, "rejected", { code, message, retriable }, true);
|
|
3578
3755
|
}
|
|
3579
3756
|
}
|
|
3580
3757
|
if (inScopeOps.length > 0) {
|
|
@@ -3607,7 +3784,13 @@ var SyncEngine = class {
|
|
|
3607
3784
|
this.deliveryWatermark = msg.maxDeliverySequence;
|
|
3608
3785
|
this.setViewWatermark(this.deliverySignature(), this.deliveryWatermark);
|
|
3609
3786
|
await this.persistDeliveryWatermark(this.deliveryWatermark);
|
|
3787
|
+
if (this.blockedFailure && inScopeOps.some((op) => op.id === this.blockedFailure?.operationId)) {
|
|
3788
|
+
const recovered = this.blockedFailure;
|
|
3789
|
+
this.blockedFailure = null;
|
|
3790
|
+
this.emitter?.emit({ type: "sync:apply-recovered", failure: recovered });
|
|
3791
|
+
}
|
|
3610
3792
|
}
|
|
3793
|
+
this.hasInFlightDeliveryBatch = false;
|
|
3611
3794
|
if (this.state === "syncing") {
|
|
3612
3795
|
this.deltaBatchesReceived++;
|
|
3613
3796
|
const totalBatches = msg.totalBatches ?? this.initialSyncTotalBatches;
|
|
@@ -3680,8 +3863,11 @@ var SyncEngine = class {
|
|
|
3680
3863
|
this.currentBatchRequiresPartialAck = false;
|
|
3681
3864
|
}
|
|
3682
3865
|
this.transitionTo("error");
|
|
3683
|
-
if (msg.code === "AUTH_FAILED") {
|
|
3866
|
+
if (msg.code === "AUTH_FAILED" || msg.code === "DEVICE_REVOKED") {
|
|
3867
|
+
this.authRejected = true;
|
|
3868
|
+
this.suspensionReason = msg.code === "DEVICE_REVOKED" ? "device-revoked" : "auth-rejected";
|
|
3684
3869
|
this.emitter?.emit({ type: "sync:auth-failed", reason: msg.message });
|
|
3870
|
+
this.emitter?.emit({ type: "sync:suspended", reason: this.suspensionReason });
|
|
3685
3871
|
}
|
|
3686
3872
|
if (msg.code === "INVALID_TIMESTAMP") {
|
|
3687
3873
|
this.clockBlocked = true;
|
|
@@ -4725,13 +4911,22 @@ function fromBase642(str) {
|
|
|
4725
4911
|
// src/reactivity/sync-status-controller.ts
|
|
4726
4912
|
var OFFLINE_SYNC_STATUS = Object.freeze({
|
|
4727
4913
|
status: "offline",
|
|
4914
|
+
phase: "offline",
|
|
4728
4915
|
reconnecting: false,
|
|
4729
4916
|
pendingOperations: 0,
|
|
4730
4917
|
lastSyncedAt: null,
|
|
4731
4918
|
lastSuccessfulPush: null,
|
|
4732
4919
|
lastSuccessfulPull: null,
|
|
4733
4920
|
conflicts: 0,
|
|
4734
|
-
clockSkewMs: null
|
|
4921
|
+
clockSkewMs: null,
|
|
4922
|
+
inFlightUploadOperations: 0,
|
|
4923
|
+
hasInFlightDeliveryBatch: false,
|
|
4924
|
+
activeViewId: "",
|
|
4925
|
+
activeViewComplete: false,
|
|
4926
|
+
initialSync: { complete: false, receivedBatches: 0, totalBatches: null, progress: null },
|
|
4927
|
+
deliveryWatermark: 0,
|
|
4928
|
+
serverFrontier: null,
|
|
4929
|
+
blockedFailure: null
|
|
4735
4930
|
});
|
|
4736
4931
|
var SYNC_STATUS_EVENT_TYPES = [
|
|
4737
4932
|
"sync:connected",
|
|
@@ -4743,6 +4938,10 @@ var SYNC_STATUS_EVENT_TYPES = [
|
|
|
4743
4938
|
"sync:received",
|
|
4744
4939
|
"sync:acknowledged",
|
|
4745
4940
|
"sync:apply-failed",
|
|
4941
|
+
"sync:suspended",
|
|
4942
|
+
"sync:apply-blocked",
|
|
4943
|
+
"sync:apply-retrying",
|
|
4944
|
+
"sync:apply-recovered",
|
|
4746
4945
|
"sync:delivery-gap",
|
|
4747
4946
|
"sync:diagnostics",
|
|
4748
4947
|
"sync:initial-sync-progress"
|
|
@@ -4934,6 +5133,7 @@ export {
|
|
|
4934
5133
|
isYjsDocUpdateMessage,
|
|
4935
5134
|
operationMatchesQuerySubsets,
|
|
4936
5135
|
operationMatchesScope,
|
|
5136
|
+
querySubsetContains,
|
|
4937
5137
|
richtextDocKey,
|
|
4938
5138
|
sliceOperationsAfterCursor,
|
|
4939
5139
|
subscribeRemoteAwarenessStates,
|