@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.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-B2GQerqJ.cjs';
2
- export { A as AcknowledgmentMessage, p as AwarenessStateWire, q as AwarenessUpdateMessage, E as EncryptedPayload, r as ErrorMessage, H as HandshakeMessage, s as HandshakeResponseMessage, I as InvalidScopeError, M as MemoryRejectedOperationStorage, O as OperationBatchMessage, t as OperationRejectedMessage, u as SYNC_STATES, v as SYNC_STATUSES, w as ScopeViolationError, x as SyncEncryptionAlgorithm, y as SyncScopeContext, z as SyncStatus, C as dedupeQuerySubsets, F as isAcknowledgmentMessage, G as isAwarenessUpdateMessage, J as isBlobChunkPushMessage, K as isBlobChunkRequestMessage, L as isBlobChunkResponseMessage, N as isErrorMessage, P as isHandshakeMessage, U as isHandshakeResponseMessage, X as isOperationBatchMessage, Z as isOperationRejectedMessage, _ as isSyncMessage, $ as isYjsDocUpdateMessage, a0 as operationMatchesQuerySubsets } from './transport-B2GQerqJ.cjs';
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-B2GQerqJ.js';
2
- export { A as AcknowledgmentMessage, p as AwarenessStateWire, q as AwarenessUpdateMessage, E as EncryptedPayload, r as ErrorMessage, H as HandshakeMessage, s as HandshakeResponseMessage, I as InvalidScopeError, M as MemoryRejectedOperationStorage, O as OperationBatchMessage, t as OperationRejectedMessage, u as SYNC_STATES, v as SYNC_STATUSES, w as ScopeViolationError, x as SyncEncryptionAlgorithm, y as SyncScopeContext, z as SyncStatus, C as dedupeQuerySubsets, F as isAcknowledgmentMessage, G as isAwarenessUpdateMessage, J as isBlobChunkPushMessage, K as isBlobChunkRequestMessage, L as isBlobChunkResponseMessage, N as isErrorMessage, P as isHandshakeMessage, U as isHandshakeResponseMessage, X as isOperationBatchMessage, Z as isOperationRejectedMessage, _ as isSyncMessage, $ as isYjsDocUpdateMessage, a0 as operationMatchesQuerySubsets } from './transport-B2GQerqJ.js';
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] !== expected) {
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 seen = /* @__PURE__ */ new Set();
165
+ const normalized = subsets.map((subset) => ({
166
+ collection: subset.collection,
167
+ where: stableValue(subset.where)
168
+ }));
141
169
  const result = [];
142
- for (const subset of subsets) {
143
- const key = `${subset.collection}:${JSON.stringify(subset.where)}`;
144
- if (seen.has(key)) {
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
- seen.add(key);
148
- result.push(subset);
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 result;
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.switchDeliveryView(previousSignature);
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.switchDeliveryView(sigBeforeRemove);
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
- return dedupeQuerySubsets([...this.querySubsets.values()]);
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(transformed, "rejected", {
3568
- code: APPLY_FAILURE_CODES.CLOCK_DRIFT,
3569
- message: error.message,
3570
- retriable: false
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,