@korajs/sync 1.0.0-beta.11 → 1.0.0-beta.12

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-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';
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-CJvRO5ix.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-CJvRO5ix.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';
@@ -106,6 +106,16 @@ interface SyncStore {
106
106
  * @returns the record's fields, or null when it cannot be read.
107
107
  */
108
108
  readRecordFields?(collection: string, recordId: string): Promise<Record<string, unknown> | null>;
109
+ /**
110
+ * Remove a record from this client's materialized authorization view without
111
+ * writing a domain delete to the replicated operation log.
112
+ */
113
+ applyScopeRetraction?(collection: string, recordId: string): Promise<void>;
114
+ /** Retract every currently materialized row outside a newly accepted scope. */
115
+ applyScopeNarrowing?(scopes: Record<string, Record<string, unknown>>): Promise<Array<{
116
+ collection: string;
117
+ recordId: string;
118
+ }>>;
109
119
  }
110
120
 
111
121
  /** Prefix for handshake rejection when client schema version is unsupported. */
@@ -879,6 +889,8 @@ declare class OutboundQueue {
879
889
  * or null if it was not present (already acknowledged and removed).
880
890
  */
881
891
  reject(opId: string): Promise<Operation | null>;
892
+ /** Remove every pending/in-flight operation for a record so authorization loss cannot upload it. */
893
+ rejectRecord(collection: string, recordId: string): Promise<Operation[]>;
882
894
  /**
883
895
  * Remove operations by id from queue and persistent storage.
884
896
  * Used when ops were already sent during handshake delta exchange.
@@ -956,6 +968,8 @@ declare class SyncEngine {
956
968
  private readonly rejectedStorage;
957
969
  private readonly batchSize;
958
970
  private readonly outboundAckTimeoutMs;
971
+ private readonly outboundRetryBaseDelayMs;
972
+ private readonly outboundRetryMaxDelayMs;
959
973
  private readonly encryptor;
960
974
  private readonly awarenessManager;
961
975
  private readonly richtextDocChannel;
@@ -971,6 +985,9 @@ declare class SyncEngine {
971
985
  private conflictCount;
972
986
  private currentBatch;
973
987
  private currentBatchRequiresPartialAck;
988
+ private currentBatchRequiresRetryBackoff;
989
+ private outboundRetryAttempt;
990
+ private outboundRetryTimer;
974
991
  private reconnecting;
975
992
  private schemaBlocked;
976
993
  private clockBlocked;
@@ -998,6 +1015,9 @@ declare class SyncEngine {
998
1015
  * with the server-accepted scope (server is authoritative).
999
1016
  */
1000
1017
  private activeScope;
1018
+ /** Server-authoritative upload authorization, separate from the downloaded view. */
1019
+ private activeUplinkScope;
1020
+ private hasDirectionalScopes;
1001
1021
  /** Live query subsets registered from reactive subscriptions */
1002
1022
  private querySubsets;
1003
1023
  private staticQuerySubsets;
@@ -1169,6 +1189,8 @@ declare class SyncEngine {
1169
1189
  * Get the currently active scope map. Returns undefined if no scope is configured.
1170
1190
  */
1171
1191
  getActiveScope(): SyncScopeMap | undefined;
1192
+ /** Get the server-accepted upload authorization scope. */
1193
+ getActiveUplinkScope(): SyncScopeMap | undefined;
1172
1194
  /**
1173
1195
  * Register a live query subset that narrows synced data for a collection.
1174
1196
  * Takes effect on the next connection; reconnects when already connected.
@@ -1242,6 +1264,7 @@ declare class SyncEngine {
1242
1264
  * the app can reconcile. Unlike {@link handleError}, this is a normal per-op
1243
1265
  * signal, so the connection stays up.
1244
1266
  */
1267
+ private applyScopeRetraction;
1245
1268
  private handleOperationRejected;
1246
1269
  /**
1247
1270
  * Every operation the server rejected that has not yet been reconciled. The app
@@ -1276,6 +1299,8 @@ declare class SyncEngine {
1276
1299
  private flushQueue;
1277
1300
  private startOutboundAckTimer;
1278
1301
  private clearOutboundAckTimer;
1302
+ private scheduleOutboundRetry;
1303
+ private clearOutboundRetryTimer;
1279
1304
  private handleOutboundAckTimeout;
1280
1305
  private handleAwarenessUpdate;
1281
1306
  private handleTransportClose;
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-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';
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-CJvRO5ix.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-CJvRO5ix.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';
@@ -106,6 +106,16 @@ interface SyncStore {
106
106
  * @returns the record's fields, or null when it cannot be read.
107
107
  */
108
108
  readRecordFields?(collection: string, recordId: string): Promise<Record<string, unknown> | null>;
109
+ /**
110
+ * Remove a record from this client's materialized authorization view without
111
+ * writing a domain delete to the replicated operation log.
112
+ */
113
+ applyScopeRetraction?(collection: string, recordId: string): Promise<void>;
114
+ /** Retract every currently materialized row outside a newly accepted scope. */
115
+ applyScopeNarrowing?(scopes: Record<string, Record<string, unknown>>): Promise<Array<{
116
+ collection: string;
117
+ recordId: string;
118
+ }>>;
109
119
  }
110
120
 
111
121
  /** Prefix for handshake rejection when client schema version is unsupported. */
@@ -879,6 +889,8 @@ declare class OutboundQueue {
879
889
  * or null if it was not present (already acknowledged and removed).
880
890
  */
881
891
  reject(opId: string): Promise<Operation | null>;
892
+ /** Remove every pending/in-flight operation for a record so authorization loss cannot upload it. */
893
+ rejectRecord(collection: string, recordId: string): Promise<Operation[]>;
882
894
  /**
883
895
  * Remove operations by id from queue and persistent storage.
884
896
  * Used when ops were already sent during handshake delta exchange.
@@ -956,6 +968,8 @@ declare class SyncEngine {
956
968
  private readonly rejectedStorage;
957
969
  private readonly batchSize;
958
970
  private readonly outboundAckTimeoutMs;
971
+ private readonly outboundRetryBaseDelayMs;
972
+ private readonly outboundRetryMaxDelayMs;
959
973
  private readonly encryptor;
960
974
  private readonly awarenessManager;
961
975
  private readonly richtextDocChannel;
@@ -971,6 +985,9 @@ declare class SyncEngine {
971
985
  private conflictCount;
972
986
  private currentBatch;
973
987
  private currentBatchRequiresPartialAck;
988
+ private currentBatchRequiresRetryBackoff;
989
+ private outboundRetryAttempt;
990
+ private outboundRetryTimer;
974
991
  private reconnecting;
975
992
  private schemaBlocked;
976
993
  private clockBlocked;
@@ -998,6 +1015,9 @@ declare class SyncEngine {
998
1015
  * with the server-accepted scope (server is authoritative).
999
1016
  */
1000
1017
  private activeScope;
1018
+ /** Server-authoritative upload authorization, separate from the downloaded view. */
1019
+ private activeUplinkScope;
1020
+ private hasDirectionalScopes;
1001
1021
  /** Live query subsets registered from reactive subscriptions */
1002
1022
  private querySubsets;
1003
1023
  private staticQuerySubsets;
@@ -1169,6 +1189,8 @@ declare class SyncEngine {
1169
1189
  * Get the currently active scope map. Returns undefined if no scope is configured.
1170
1190
  */
1171
1191
  getActiveScope(): SyncScopeMap | undefined;
1192
+ /** Get the server-accepted upload authorization scope. */
1193
+ getActiveUplinkScope(): SyncScopeMap | undefined;
1172
1194
  /**
1173
1195
  * Register a live query subset that narrows synced data for a collection.
1174
1196
  * Takes effect on the next connection; reconnects when already connected.
@@ -1242,6 +1264,7 @@ declare class SyncEngine {
1242
1264
  * the app can reconcile. Unlike {@link handleError}, this is a normal per-op
1243
1265
  * signal, so the connection stays up.
1244
1266
  */
1267
+ private applyScopeRetraction;
1245
1268
  private handleOperationRejected;
1246
1269
  /**
1247
1270
  * Every operation the server rejected that has not yet been reconciled. The app
@@ -1276,6 +1299,8 @@ declare class SyncEngine {
1276
1299
  private flushQueue;
1277
1300
  private startOutboundAckTimer;
1278
1301
  private clearOutboundAckTimer;
1302
+ private scheduleOutboundRetry;
1303
+ private clearOutboundRetryTimer;
1279
1304
  private handleOutboundAckTimeout;
1280
1305
  private handleAwarenessUpdate;
1281
1306
  private handleTransportClose;
package/dist/index.js CHANGED
@@ -591,7 +591,8 @@ function toProtoEnvelope(message) {
591
591
  schemaVersion: message.schemaVersion,
592
592
  authToken: message.authToken,
593
593
  supportedWireFormats: message.supportedWireFormats,
594
- ...message.lastDeliverySequence !== void 0 ? { lastDeliverySequence: message.lastDeliverySequence } : {}
594
+ ...message.lastDeliverySequence !== void 0 ? { lastDeliverySequence: message.lastDeliverySequence } : {},
595
+ scopeExitPolicy: message.scopeExitPolicy
595
596
  };
596
597
  case "handshake-response":
597
598
  return {
@@ -608,7 +609,10 @@ function toProtoEnvelope(message) {
608
609
  selectedWireFormat: message.selectedWireFormat,
609
610
  serverTime: message.serverTime,
610
611
  blobStorageEnabled: message.blobStorageEnabled,
611
- ...message.serverMaxDeliverySequence !== void 0 ? { serverMaxDeliverySequence: message.serverMaxDeliverySequence } : {}
612
+ ...message.serverMaxDeliverySequence !== void 0 ? { serverMaxDeliverySequence: message.serverMaxDeliverySequence } : {},
613
+ acceptedScopeJson: message.acceptedScope ? JSON.stringify(message.acceptedScope) : void 0,
614
+ acceptedDownlinkScopesJson: message.acceptedDownlinkScopes ? JSON.stringify(message.acceptedDownlinkScopes) : void 0,
615
+ acceptedUplinkScopesJson: message.acceptedUplinkScopes ? JSON.stringify(message.acceptedUplinkScopes) : void 0
612
616
  };
613
617
  case "operation-batch":
614
618
  return {
@@ -618,7 +622,8 @@ function toProtoEnvelope(message) {
618
622
  isFinal: message.isFinal,
619
623
  batchIndex: message.batchIndex,
620
624
  ...message.baseDeliverySequence !== void 0 ? { baseDeliverySequence: message.baseDeliverySequence } : {},
621
- ...message.maxDeliverySequence !== void 0 ? { maxDeliverySequence: message.maxDeliverySequence } : {}
625
+ ...message.maxDeliverySequence !== void 0 ? { maxDeliverySequence: message.maxDeliverySequence } : {},
626
+ retractionsJson: message.retractions ? JSON.stringify(message.retractions) : void 0
622
627
  };
623
628
  case "acknowledgment":
624
629
  return {
@@ -692,7 +697,8 @@ function fromProtoEnvelope(envelope) {
692
697
  supportedWireFormats: envelope.supportedWireFormats?.filter(
693
698
  (format) => format === "json" || format === "protobuf"
694
699
  ),
695
- ...envelope.lastDeliverySequence !== void 0 ? { lastDeliverySequence: envelope.lastDeliverySequence } : {}
700
+ ...envelope.lastDeliverySequence !== void 0 ? { lastDeliverySequence: envelope.lastDeliverySequence } : {},
701
+ ...envelope.scopeExitPolicy === "retain" || envelope.scopeExitPolicy === "retract" ? { scopeExitPolicy: envelope.scopeExitPolicy } : {}
696
702
  };
697
703
  case "handshake-response":
698
704
  return {
@@ -710,7 +716,10 @@ function fromProtoEnvelope(envelope) {
710
716
  // automatic timestamp rebase work over the protobuf wire, not just JSON.
711
717
  ...envelope.serverTime !== void 0 ? { serverTime: envelope.serverTime } : {},
712
718
  ...envelope.blobStorageEnabled !== void 0 ? { blobStorageEnabled: envelope.blobStorageEnabled } : {},
713
- ...envelope.serverMaxDeliverySequence !== void 0 ? { serverMaxDeliverySequence: envelope.serverMaxDeliverySequence } : {}
719
+ ...envelope.serverMaxDeliverySequence !== void 0 ? { serverMaxDeliverySequence: envelope.serverMaxDeliverySequence } : {},
720
+ ...envelope.acceptedScopeJson ? { acceptedScope: JSON.parse(envelope.acceptedScopeJson) } : {},
721
+ ...envelope.acceptedDownlinkScopesJson ? { acceptedDownlinkScopes: JSON.parse(envelope.acceptedDownlinkScopesJson) } : {},
722
+ ...envelope.acceptedUplinkScopesJson ? { acceptedUplinkScopes: JSON.parse(envelope.acceptedUplinkScopesJson) } : {}
714
723
  };
715
724
  case "operation-batch":
716
725
  return {
@@ -720,7 +729,8 @@ function fromProtoEnvelope(envelope) {
720
729
  isFinal: envelope.isFinal ?? false,
721
730
  batchIndex: envelope.batchIndex ?? 0,
722
731
  ...envelope.baseDeliverySequence !== void 0 ? { baseDeliverySequence: envelope.baseDeliverySequence } : {},
723
- ...envelope.maxDeliverySequence !== void 0 ? { maxDeliverySequence: envelope.maxDeliverySequence } : {}
732
+ ...envelope.maxDeliverySequence !== void 0 ? { maxDeliverySequence: envelope.maxDeliverySequence } : {},
733
+ ...envelope.retractionsJson ? { retractions: JSON.parse(envelope.retractionsJson) } : {}
724
734
  };
725
735
  case "acknowledgment":
726
736
  return {
@@ -938,6 +948,13 @@ function encodeEnvelope(envelope) {
938
948
  if (envelope.deliverySequence !== void 0) writer.uint32(256).int64(envelope.deliverySequence);
939
949
  if (envelope.serverMaxDeliverySequence !== void 0)
940
950
  writer.uint32(264).int64(envelope.serverMaxDeliverySequence);
951
+ if (envelope.acceptedScopeJson) writer.uint32(274).string(envelope.acceptedScopeJson);
952
+ if (envelope.acceptedDownlinkScopesJson)
953
+ writer.uint32(282).string(envelope.acceptedDownlinkScopesJson);
954
+ if (envelope.acceptedUplinkScopesJson)
955
+ writer.uint32(290).string(envelope.acceptedUplinkScopesJson);
956
+ if (envelope.retractionsJson) writer.uint32(298).string(envelope.retractionsJson);
957
+ if (envelope.scopeExitPolicy) writer.uint32(306).string(envelope.scopeExitPolicy);
941
958
  return writer.finish();
942
959
  }
943
960
  function decodeEnvelope(bytes) {
@@ -1048,6 +1065,21 @@ function decodeEnvelope(bytes) {
1048
1065
  case 33:
1049
1066
  envelope.serverMaxDeliverySequence = longToNumber(reader.int64());
1050
1067
  break;
1068
+ case 34:
1069
+ envelope.acceptedScopeJson = reader.string();
1070
+ break;
1071
+ case 35:
1072
+ envelope.acceptedDownlinkScopesJson = reader.string();
1073
+ break;
1074
+ case 36:
1075
+ envelope.acceptedUplinkScopesJson = reader.string();
1076
+ break;
1077
+ case 37:
1078
+ envelope.retractionsJson = reader.string();
1079
+ break;
1080
+ case 38:
1081
+ envelope.scopeExitPolicy = reader.string();
1082
+ break;
1051
1083
  default:
1052
1084
  reader.skipType(tag & 7);
1053
1085
  }
@@ -2701,6 +2733,18 @@ var OutboundQueue = class {
2701
2733
  await this.storage.dequeue([opId]);
2702
2734
  return removed;
2703
2735
  }
2736
+ /** Remove every pending/in-flight operation for a record so authorization loss cannot upload it. */
2737
+ async rejectRecord(collection, recordId) {
2738
+ const matching = [...this.queue, ...[...this.inFlight.values()].flat()].filter(
2739
+ (op) => op.collection === collection && op.recordId === recordId
2740
+ );
2741
+ const removed = [];
2742
+ for (const operation of matching) {
2743
+ const rejected = await this.reject(operation.id);
2744
+ if (rejected) removed.push(rejected);
2745
+ }
2746
+ return removed;
2747
+ }
2704
2748
  /**
2705
2749
  * Remove operations by id from queue and persistent storage.
2706
2750
  * Used when ops were already sent during handshake delta exchange.
@@ -2720,6 +2764,8 @@ var OutboundQueue = class {
2720
2764
  var DEFAULT_BATCH_SIZE = 100;
2721
2765
  var DEFAULT_SCHEMA_VERSION = 1;
2722
2766
  var DEFAULT_OUTBOUND_ACK_TIMEOUT_MS = 3e4;
2767
+ var DEFAULT_OUTBOUND_RETRY_BASE_DELAY_MS = 250;
2768
+ var DEFAULT_OUTBOUND_RETRY_MAX_DELAY_MS = 3e4;
2723
2769
  var VALID_TRANSITIONS = {
2724
2770
  disconnected: ["connecting"],
2725
2771
  connecting: ["handshaking", "error", "disconnected"],
@@ -2755,6 +2801,8 @@ var SyncEngine = class {
2755
2801
  rejectedStorage;
2756
2802
  batchSize;
2757
2803
  outboundAckTimeoutMs;
2804
+ outboundRetryBaseDelayMs;
2805
+ outboundRetryMaxDelayMs;
2758
2806
  encryptor;
2759
2807
  awarenessManager;
2760
2808
  richtextDocChannel;
@@ -2770,6 +2818,9 @@ var SyncEngine = class {
2770
2818
  conflictCount = 0;
2771
2819
  currentBatch = null;
2772
2820
  currentBatchRequiresPartialAck = false;
2821
+ currentBatchRequiresRetryBackoff = false;
2822
+ outboundRetryAttempt = 0;
2823
+ outboundRetryTimer = null;
2773
2824
  reconnecting = false;
2774
2825
  schemaBlocked = false;
2775
2826
  clockBlocked = false;
@@ -2798,6 +2849,9 @@ var SyncEngine = class {
2798
2849
  * with the server-accepted scope (server is authoritative).
2799
2850
  */
2800
2851
  activeScope;
2852
+ /** Server-authoritative upload authorization, separate from the downloaded view. */
2853
+ activeUplinkScope;
2854
+ hasDirectionalScopes = false;
2801
2855
  /** Live query subsets registered from reactive subscriptions */
2802
2856
  querySubsets = /* @__PURE__ */ new Map();
2803
2857
  staticQuerySubsets = [];
@@ -2834,9 +2888,18 @@ var SyncEngine = class {
2834
2888
  this.emitter = options.emitter ?? null;
2835
2889
  this.batchSize = options.config.batchSize ?? DEFAULT_BATCH_SIZE;
2836
2890
  this.outboundAckTimeoutMs = options.config.outboundAckTimeoutMs ?? DEFAULT_OUTBOUND_ACK_TIMEOUT_MS;
2891
+ this.outboundRetryBaseDelayMs = Math.max(
2892
+ 0,
2893
+ options.config.outboundRetryBaseDelayMs ?? DEFAULT_OUTBOUND_RETRY_BASE_DELAY_MS
2894
+ );
2895
+ this.outboundRetryMaxDelayMs = Math.max(
2896
+ this.outboundRetryBaseDelayMs,
2897
+ options.config.outboundRetryMaxDelayMs ?? DEFAULT_OUTBOUND_RETRY_MAX_DELAY_MS
2898
+ );
2837
2899
  this.encryptor = options.encryptor ?? null;
2838
2900
  this.syncState = options.syncState ?? null;
2839
2901
  this.activeScope = options.config.scopeMap;
2902
+ this.activeUplinkScope = options.config.scopeMap;
2840
2903
  const queueStorage = options.queueStorage ?? new MemoryQueueStorage();
2841
2904
  this.outboundQueue = new OutboundQueue(queueStorage);
2842
2905
  this.rejectedStorage = options.rejectedStorage ?? new MemoryRejectedOperationStorage();
@@ -2959,6 +3022,7 @@ var SyncEngine = class {
2959
3022
  authToken,
2960
3023
  supportedWireFormats: ["json", "protobuf"],
2961
3024
  ...this.config.scopeMap ? { syncScope: this.config.scopeMap } : {},
3025
+ ...this.config.scopeExit ? { scopeExitPolicy: this.config.scopeExit } : {},
2962
3026
  ...activeQuerySubsets.length > 0 ? { syncQueries: activeQuerySubsets } : {},
2963
3027
  ...this.resumeDeltaCursor ? { deltaCursor: encodeDeltaCursor(this.resumeDeltaCursor) } : {},
2964
3028
  // Resume the server's gap-free delivery stream from our watermark. A server
@@ -2983,13 +3047,15 @@ var SyncEngine = class {
2983
3047
  return this.stopPromise;
2984
3048
  }
2985
3049
  async stopInternal() {
2986
- if (this.state === "disconnected") return;
2987
3050
  this.clearOutboundAckTimer();
3051
+ this.clearOutboundRetryTimer();
3052
+ if (this.state === "disconnected") return;
2988
3053
  this.awarenessManager.stopCleanupTimer();
2989
3054
  if (this.currentBatch) {
2990
3055
  this.outboundQueue.returnBatch(this.currentBatch.batchId);
2991
3056
  this.currentBatch = null;
2992
3057
  this.currentBatchRequiresPartialAck = false;
3058
+ this.currentBatchRequiresRetryBackoff = false;
2993
3059
  }
2994
3060
  try {
2995
3061
  await this.transport.disconnect();
@@ -3272,6 +3338,8 @@ var SyncEngine = class {
3272
3338
  updateScope(scopeMap) {
3273
3339
  const previousSignature = this.deliverySignature();
3274
3340
  this.activeScope = scopeMap;
3341
+ this.activeUplinkScope = scopeMap;
3342
+ this.hasDirectionalScopes = false;
3275
3343
  this.config.scopeMap = scopeMap;
3276
3344
  this.switchDeliveryView(previousSignature);
3277
3345
  }
@@ -3355,6 +3423,10 @@ var SyncEngine = class {
3355
3423
  getActiveScope() {
3356
3424
  return this.activeScope;
3357
3425
  }
3426
+ /** Get the server-accepted upload authorization scope. */
3427
+ getActiveUplinkScope() {
3428
+ return this.activeUplinkScope;
3429
+ }
3358
3430
  /**
3359
3431
  * Register a live query subset that narrows synced data for a collection.
3360
3432
  * Takes effect on the next connection; reconnects when already connected.
@@ -3557,8 +3629,17 @@ var SyncEngine = class {
3557
3629
  if (msg.selectedWireFormat) {
3558
3630
  this.setSerializerWireFormat(msg.selectedWireFormat);
3559
3631
  }
3560
- if (msg.acceptedScope) {
3561
- this.activeScope = msg.acceptedScope;
3632
+ if (msg.acceptedDownlinkScopes ?? msg.acceptedScope) {
3633
+ this.activeScope = msg.acceptedDownlinkScopes ?? msg.acceptedScope;
3634
+ }
3635
+ this.hasDirectionalScopes = msg.acceptedUplinkScopes !== void 0;
3636
+ this.activeUplinkScope = msg.acceptedUplinkScopes ?? msg.acceptedScope ?? this.activeScope;
3637
+ if (this.config.scopeExit === "retract" && this.activeScope && this.store.applyScopeNarrowing) {
3638
+ const narrowed = await this.store.applyScopeNarrowing(this.activeScope);
3639
+ for (const retraction of narrowed) {
3640
+ await this.applyScopeRetraction(retraction, false);
3641
+ }
3642
+ await this.refreshPendingCount();
3562
3643
  }
3563
3644
  this.emitter?.emit({ type: "sync:connected", nodeId: this.store.getNodeId() });
3564
3645
  this.metricsCollector.recordConnected();
@@ -3723,6 +3804,14 @@ var SyncEngine = class {
3723
3804
  const targetSchemaVersion = this.config.schemaVersion ?? DEFAULT_SCHEMA_VERSION;
3724
3805
  const transforms = this.config.operationTransforms ?? [];
3725
3806
  let fullyApplied = true;
3807
+ for (const retraction of msg.retractions ?? []) {
3808
+ try {
3809
+ await this.applyScopeRetraction(retraction, true);
3810
+ } catch {
3811
+ fullyApplied = false;
3812
+ }
3813
+ }
3814
+ await this.refreshPendingCount();
3726
3815
  for (const op of inScopeOps) {
3727
3816
  const transformed = transforms.length > 0 ? applyOperationTransforms(op, targetSchemaVersion, transforms) : op;
3728
3817
  if (transformed === null) {
@@ -3830,6 +3919,7 @@ var SyncEngine = class {
3830
3919
  }
3831
3920
  async handleAcknowledgment(msg) {
3832
3921
  this.clearOutboundAckTimer();
3922
+ const requiresRetryBackoff = this.currentBatchRequiresRetryBackoff;
3833
3923
  if (this.state === "syncing" && this.config.strictHandshake) {
3834
3924
  this.pendingDeltaBatchAcks.delete(msg.acknowledgedMessageId);
3835
3925
  this.markDeltaSendCompleteIfReady();
@@ -3845,6 +3935,7 @@ var SyncEngine = class {
3845
3935
  }
3846
3936
  this.currentBatch = null;
3847
3937
  this.currentBatchRequiresPartialAck = false;
3938
+ this.currentBatchRequiresRetryBackoff = false;
3848
3939
  const now = Date.now();
3849
3940
  this.lastSyncedAt = now;
3850
3941
  this.lastSuccessfulPush = now;
@@ -3852,7 +3943,14 @@ var SyncEngine = class {
3852
3943
  await this.advanceLastAckedForLocalNode(msg.lastSequenceNumber);
3853
3944
  await this.refreshPendingCount();
3854
3945
  if (this.state === "streaming" && this.outboundQueue.hasOperations) {
3855
- this.flushQueue();
3946
+ if (requiresRetryBackoff) {
3947
+ this.scheduleOutboundRetry();
3948
+ } else {
3949
+ this.outboundRetryAttempt = 0;
3950
+ this.flushQueue();
3951
+ }
3952
+ } else if (!requiresRetryBackoff) {
3953
+ this.outboundRetryAttempt = 0;
3856
3954
  }
3857
3955
  }
3858
3956
  handleError(msg) {
@@ -3861,6 +3959,7 @@ var SyncEngine = class {
3861
3959
  this.outboundQueue.returnBatch(this.currentBatch.batchId);
3862
3960
  this.currentBatch = null;
3863
3961
  this.currentBatchRequiresPartialAck = false;
3962
+ this.currentBatchRequiresRetryBackoff = false;
3864
3963
  }
3865
3964
  this.transitionTo("error");
3866
3965
  if (msg.code === "AUTH_FAILED" || msg.code === "DEVICE_REVOKED") {
@@ -3890,9 +3989,39 @@ var SyncEngine = class {
3890
3989
  * the app can reconcile. Unlike {@link handleError}, this is a normal per-op
3891
3990
  * signal, so the connection stays up.
3892
3991
  */
3992
+ async applyScopeRetraction(retraction, applyToStore) {
3993
+ if (applyToStore) {
3994
+ if (!this.store.applyScopeRetraction) {
3995
+ throw new Error("The configured sync store does not support scope retractions");
3996
+ }
3997
+ await this.store.applyScopeRetraction(retraction.collection, retraction.recordId);
3998
+ }
3999
+ const quarantined = await this.outboundQueue.rejectRecord(
4000
+ retraction.collection,
4001
+ retraction.recordId
4002
+ );
4003
+ for (const operation of quarantined) {
4004
+ await this.rejectedStorage.record({
4005
+ operationId: operation.id,
4006
+ collection: operation.collection,
4007
+ recordId: operation.recordId,
4008
+ code: "SCOPE_RETRACTED",
4009
+ message: "Authorization changed before this operation could be uploaded.",
4010
+ retriable: false,
4011
+ rejectedAt: Date.now()
4012
+ });
4013
+ }
4014
+ this.emitter?.emit({
4015
+ type: "sync:scope-retracted",
4016
+ collection: retraction.collection,
4017
+ recordId: retraction.recordId,
4018
+ quarantinedOperationIds: quarantined.map((operation) => operation.id)
4019
+ });
4020
+ }
3893
4021
  async handleOperationRejected(msg) {
3894
4022
  if (msg.retriable) {
3895
4023
  this.currentBatchRequiresPartialAck = true;
4024
+ this.currentBatchRequiresRetryBackoff = true;
3896
4025
  await this.refreshPendingCount();
3897
4026
  this.emitter?.emit({
3898
4027
  type: "sync:operation-rejected",
@@ -4046,11 +4175,13 @@ var SyncEngine = class {
4046
4175
  }
4047
4176
  flushQueue() {
4048
4177
  if (this.currentBatch) return;
4178
+ if (this.outboundRetryTimer) return;
4049
4179
  if (!this.outboundQueue.hasOperations) return;
4050
4180
  const batch = this.outboundQueue.takeBatch(this.batchSize);
4051
4181
  if (!batch) return;
4052
4182
  this.currentBatch = batch;
4053
4183
  this.currentBatchRequiresPartialAck = false;
4184
+ this.currentBatchRequiresRetryBackoff = false;
4054
4185
  if (this.encryptor) {
4055
4186
  this.encryptor.encryptBatch(batch.operations).then(
4056
4187
  (encrypted) => {
@@ -4074,6 +4205,7 @@ var SyncEngine = class {
4074
4205
  this.outboundQueue.returnBatch(batch.batchId);
4075
4206
  this.currentBatch = null;
4076
4207
  this.currentBatchRequiresPartialAck = false;
4208
+ this.currentBatchRequiresRetryBackoff = false;
4077
4209
  this.emitter?.emit({
4078
4210
  type: "sync:disconnected",
4079
4211
  reason: err instanceof Error ? err.message : "Encryption failed"
@@ -4110,11 +4242,29 @@ var SyncEngine = class {
4110
4242
  clearTimeout(this.outboundAckTimer);
4111
4243
  this.outboundAckTimer = null;
4112
4244
  }
4245
+ scheduleOutboundRetry() {
4246
+ if (this.outboundRetryTimer || !this.outboundQueue.hasOperations) return;
4247
+ const delay = Math.min(
4248
+ this.outboundRetryMaxDelayMs,
4249
+ this.outboundRetryBaseDelayMs * 2 ** this.outboundRetryAttempt
4250
+ );
4251
+ this.outboundRetryAttempt += 1;
4252
+ this.outboundRetryTimer = setTimeout(() => {
4253
+ this.outboundRetryTimer = null;
4254
+ if (this.state === "streaming") this.flushQueue();
4255
+ }, delay);
4256
+ }
4257
+ clearOutboundRetryTimer() {
4258
+ if (!this.outboundRetryTimer) return;
4259
+ clearTimeout(this.outboundRetryTimer);
4260
+ this.outboundRetryTimer = null;
4261
+ }
4113
4262
  async handleOutboundAckTimeout(batchId) {
4114
4263
  if (!this.currentBatch || this.currentBatch.batchId !== batchId) return;
4115
4264
  this.outboundQueue.returnBatch(batchId);
4116
4265
  this.currentBatch = null;
4117
4266
  this.currentBatchRequiresPartialAck = false;
4267
+ this.currentBatchRequiresRetryBackoff = false;
4118
4268
  this.clearOutboundAckTimer();
4119
4269
  await this.refreshPendingCount();
4120
4270
  if (this.state === "disconnected") return;
@@ -4136,10 +4286,12 @@ var SyncEngine = class {
4136
4286
  }
4137
4287
  handleTransportClose(reason) {
4138
4288
  this.clearOutboundAckTimer();
4289
+ this.clearOutboundRetryTimer();
4139
4290
  if (this.currentBatch) {
4140
4291
  this.outboundQueue.returnBatch(this.currentBatch.batchId);
4141
4292
  this.currentBatch = null;
4142
4293
  this.currentBatchRequiresPartialAck = false;
4294
+ this.currentBatchRequiresRetryBackoff = false;
4143
4295
  }
4144
4296
  if (this.schemaBlocked) {
4145
4297
  return;
@@ -4182,10 +4334,10 @@ var SyncEngine = class {
4182
4334
  return this.matchesScopeAndSubsets(op, fullRecord);
4183
4335
  }
4184
4336
  matchesScopeAndSubsets(op, fullRecord) {
4185
- if (!operationMatchesScope(op, this.activeScope, fullRecord)) {
4337
+ if (!operationMatchesScope(op, this.activeUplinkScope, fullRecord)) {
4186
4338
  return false;
4187
4339
  }
4188
- return operationMatchesQuerySubsets(op, this.getActiveQuerySubsets(), fullRecord);
4340
+ return this.hasDirectionalScopes || operationMatchesQuerySubsets(op, this.getActiveQuerySubsets(), fullRecord);
4189
4341
  }
4190
4342
  async readRecordForBackfill(op) {
4191
4343
  if (!this.store.readRecordFields) {