@korajs/sync 1.0.0-beta.4 → 1.0.0-beta.6

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-BGr-_eH8.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-BGr-_eH8.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-ByWfWBbB.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-ByWfWBbB.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';
@@ -95,6 +95,17 @@ interface SyncStore {
95
95
  idMapping: Record<string, string>;
96
96
  rebasedCount: number;
97
97
  }>;
98
+ /**
99
+ * Optional: read a record's current materialized field values, used to backfill
100
+ * scope / query-subset fields that a partial update (or a delete) does not carry
101
+ * in its own data. Without it, such an operation would be judged out of scope by
102
+ * its changed fields alone and wrongly dropped from sync. Optional so hand-rolled
103
+ * SyncStore implementations keep working; when absent, the engine falls back to
104
+ * judging visibility from the operation's own data.
105
+ *
106
+ * @returns the record's fields, or null when it cannot be read.
107
+ */
108
+ readRecordFields?(collection: string, recordId: string): Promise<Record<string, unknown> | null>;
98
109
  }
99
110
 
100
111
  /** Prefix for handshake rejection when client schema version is unsupported. */
@@ -272,6 +283,14 @@ interface ChaosConfig {
272
283
  maxLatency?: number;
273
284
  /** Injectable random source for deterministic testing. Returns value in [0, 1). */
274
285
  randomSource?: () => number;
286
+ /**
287
+ * Deterministic targeted drop. When it returns true for a message and direction,
288
+ * that message is dropped unconditionally (independent of `dropRate`). Enables
289
+ * reproducing an exact gap (e.g. drop one specific operation) instead of relying
290
+ * on probabilistic drops. `direction` is 'outgoing' for `send` and 'incoming' for
291
+ * messages arriving from the inner transport.
292
+ */
293
+ dropPredicate?: (message: SyncMessage, direction: 'outgoing' | 'incoming') => boolean;
275
294
  }
276
295
  /**
277
296
  * Chaos transport that wraps another transport and injects faults.
@@ -287,6 +306,7 @@ declare class ChaosTransport implements SyncTransport {
287
306
  private readonly reorderRate;
288
307
  private readonly maxLatency;
289
308
  private readonly random;
309
+ private readonly dropPredicate;
290
310
  private messageHandler;
291
311
  private reorderBuffer;
292
312
  private pending;
@@ -926,6 +946,7 @@ declare class SyncEngine {
926
946
  private readonly outboundQueue;
927
947
  private readonly rejectedStorage;
928
948
  private readonly batchSize;
949
+ private readonly outboundAckTimeoutMs;
929
950
  private readonly encryptor;
930
951
  private readonly awarenessManager;
931
952
  private readonly richtextDocChannel;
@@ -945,6 +966,9 @@ declare class SyncEngine {
945
966
  private clockBlocked;
946
967
  private clockSkewMs;
947
968
  private blobStorageEnabled;
969
+ private outboundAckTimer;
970
+ private startPromise;
971
+ private stopPromise;
948
972
  private deltaBatchesReceived;
949
973
  private deltaReceiveComplete;
950
974
  private deltaSendComplete;
@@ -964,15 +988,36 @@ declare class SyncEngine {
964
988
  /** Resume cursor for paginated initial sync (persisted across reconnects) */
965
989
  private resumeDeltaCursor;
966
990
  private initialSyncTotalBatches;
991
+ /**
992
+ * The delivery watermark for the CURRENT sync view: the highest server delivery
993
+ * sequence up to which every operation in the current view (scope + active query
994
+ * subscriptions) has been contiguously applied. Advanced only through the server's
995
+ * gap-free delivery stream (never through live relay), so it is a durable lower bound
996
+ * on what this client holds for this view. Reported at handshake to resume the stream.
997
+ *
998
+ * The watermark is keyed by view: switching views (a scope or subscription change)
999
+ * saves the current view's watermark and loads the target view's, so returning to a
1000
+ * previously-synced view resumes exactly where it left off instead of re-syncing. This
1001
+ * is correct because a view's watermark is a lower bound specific to that view's
1002
+ * filter; a widened view is simply a different (initially unsynced) view.
1003
+ */
1004
+ private deliveryWatermark;
1005
+ /**
1006
+ * In-memory cache of watermark per view signature, preloaded from persistence at start
1007
+ * so a view switch resolves synchronously (no race with the reconnect it triggers).
1008
+ */
1009
+ private readonly deliverySignatureWatermarks;
967
1010
  constructor(options: SyncEngineOptions);
968
1011
  /**
969
1012
  * Start the sync engine: connect → handshake → delta exchange → streaming.
970
1013
  */
971
1014
  start(): Promise<void>;
1015
+ private startInternal;
972
1016
  /**
973
1017
  * Stop the sync engine. Disconnects the transport.
974
1018
  */
975
1019
  stop(): Promise<void>;
1020
+ private stopInternal;
976
1021
  private ensureDisconnected;
977
1022
  /**
978
1023
  * Push a local operation to the outbound queue.
@@ -1056,6 +1101,41 @@ declare class SyncEngine {
1056
1101
  * @param scopeMap - New per-collection scope filters, or undefined to remove scope
1057
1102
  */
1058
1103
  updateScope(scopeMap: SyncScopeMap | undefined): void;
1104
+ /**
1105
+ * A stable identifier for the current sync view: the auth/tenant scope plus the set of
1106
+ * active query subscriptions, canonicalized so the same view always produces the same
1107
+ * string. The delivery watermark is keyed by this, so each distinct view resumes from
1108
+ * its own last position.
1109
+ */
1110
+ private deliverySignature;
1111
+ /**
1112
+ * Switch the active delivery watermark to the current view after a scope or subscription
1113
+ * change. The prior view's watermark is saved so returning to it resumes; the new view's
1114
+ * watermark is restored from the in-memory cache (0 for a never-synced view, which then
1115
+ * does a one-time resync under that view). No data is lost: a view's watermark is a lower
1116
+ * bound for that specific view's filter.
1117
+ */
1118
+ private switchDeliveryView;
1119
+ /**
1120
+ * Record a view's watermark in the in-memory cache as the most-recently-used entry (the
1121
+ * Map preserves insertion order, so re-inserting moves it to the tail), then evict cold
1122
+ * views past the retention cap. This is the single place the cache is written, so LRU
1123
+ * order and the size bound are always maintained together.
1124
+ */
1125
+ private setViewWatermark;
1126
+ /**
1127
+ * Trim the per-view watermark cache to `MAX_DELIVERY_VIEW_WATERMARKS`, evicting the
1128
+ * least-recently-used views. The default view ('') and the live view are never evicted
1129
+ * (evicting the live view would force an immediate re-scan of what we are actively
1130
+ * syncing). Each eviction also removes the persisted row, bounding storage. Safe by
1131
+ * construction: an evicted view simply back-fills from 0 (deduplicated) when next seen.
1132
+ */
1133
+ private evictColdViewWatermarks;
1134
+ /**
1135
+ * Remove a view's persisted watermark row (best-effort; a persistence layer that omits
1136
+ * the delete simply keeps the row, which is harmless).
1137
+ */
1138
+ private deleteDeliveryWatermark;
1059
1139
  /**
1060
1140
  * Get the currently active scope map. Returns undefined if no scope is configured.
1061
1141
  */
@@ -1119,6 +1199,8 @@ declare class SyncEngine {
1119
1199
  private markDeltaSendCompleteIfReady;
1120
1200
  private collectDelta;
1121
1201
  private handleOperationBatch;
1202
+ /** Acknowledge a delivery batch (used for duplicates) without re-applying it. */
1203
+ private sendDeliveryAck;
1122
1204
  private handleAcknowledgment;
1123
1205
  private handleError;
1124
1206
  /**
@@ -1160,13 +1242,20 @@ declare class SyncEngine {
1160
1242
  */
1161
1243
  private reconcileOutboundFromOpLog;
1162
1244
  private flushQueue;
1245
+ private startOutboundAckTimer;
1246
+ private clearOutboundAckTimer;
1247
+ private handleOutboundAckTimeout;
1163
1248
  private handleAwarenessUpdate;
1164
1249
  private handleTransportClose;
1165
1250
  private handleTransportError;
1166
1251
  private transitionTo;
1167
1252
  private setSerializerWireFormat;
1168
1253
  private operationAllowedForSync;
1254
+ private matchesScopeAndSubsets;
1255
+ private readRecordForBackfill;
1256
+ private filterAllowedForSync;
1169
1257
  private persistDeltaCursor;
1258
+ private persistDeliveryWatermark;
1170
1259
  private scheduleQuerySubsetReconnect;
1171
1260
  private reconnectForQuerySubsets;
1172
1261
  }
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-BGr-_eH8.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-BGr-_eH8.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-ByWfWBbB.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-ByWfWBbB.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';
@@ -95,6 +95,17 @@ interface SyncStore {
95
95
  idMapping: Record<string, string>;
96
96
  rebasedCount: number;
97
97
  }>;
98
+ /**
99
+ * Optional: read a record's current materialized field values, used to backfill
100
+ * scope / query-subset fields that a partial update (or a delete) does not carry
101
+ * in its own data. Without it, such an operation would be judged out of scope by
102
+ * its changed fields alone and wrongly dropped from sync. Optional so hand-rolled
103
+ * SyncStore implementations keep working; when absent, the engine falls back to
104
+ * judging visibility from the operation's own data.
105
+ *
106
+ * @returns the record's fields, or null when it cannot be read.
107
+ */
108
+ readRecordFields?(collection: string, recordId: string): Promise<Record<string, unknown> | null>;
98
109
  }
99
110
 
100
111
  /** Prefix for handshake rejection when client schema version is unsupported. */
@@ -272,6 +283,14 @@ interface ChaosConfig {
272
283
  maxLatency?: number;
273
284
  /** Injectable random source for deterministic testing. Returns value in [0, 1). */
274
285
  randomSource?: () => number;
286
+ /**
287
+ * Deterministic targeted drop. When it returns true for a message and direction,
288
+ * that message is dropped unconditionally (independent of `dropRate`). Enables
289
+ * reproducing an exact gap (e.g. drop one specific operation) instead of relying
290
+ * on probabilistic drops. `direction` is 'outgoing' for `send` and 'incoming' for
291
+ * messages arriving from the inner transport.
292
+ */
293
+ dropPredicate?: (message: SyncMessage, direction: 'outgoing' | 'incoming') => boolean;
275
294
  }
276
295
  /**
277
296
  * Chaos transport that wraps another transport and injects faults.
@@ -287,6 +306,7 @@ declare class ChaosTransport implements SyncTransport {
287
306
  private readonly reorderRate;
288
307
  private readonly maxLatency;
289
308
  private readonly random;
309
+ private readonly dropPredicate;
290
310
  private messageHandler;
291
311
  private reorderBuffer;
292
312
  private pending;
@@ -926,6 +946,7 @@ declare class SyncEngine {
926
946
  private readonly outboundQueue;
927
947
  private readonly rejectedStorage;
928
948
  private readonly batchSize;
949
+ private readonly outboundAckTimeoutMs;
929
950
  private readonly encryptor;
930
951
  private readonly awarenessManager;
931
952
  private readonly richtextDocChannel;
@@ -945,6 +966,9 @@ declare class SyncEngine {
945
966
  private clockBlocked;
946
967
  private clockSkewMs;
947
968
  private blobStorageEnabled;
969
+ private outboundAckTimer;
970
+ private startPromise;
971
+ private stopPromise;
948
972
  private deltaBatchesReceived;
949
973
  private deltaReceiveComplete;
950
974
  private deltaSendComplete;
@@ -964,15 +988,36 @@ declare class SyncEngine {
964
988
  /** Resume cursor for paginated initial sync (persisted across reconnects) */
965
989
  private resumeDeltaCursor;
966
990
  private initialSyncTotalBatches;
991
+ /**
992
+ * The delivery watermark for the CURRENT sync view: the highest server delivery
993
+ * sequence up to which every operation in the current view (scope + active query
994
+ * subscriptions) has been contiguously applied. Advanced only through the server's
995
+ * gap-free delivery stream (never through live relay), so it is a durable lower bound
996
+ * on what this client holds for this view. Reported at handshake to resume the stream.
997
+ *
998
+ * The watermark is keyed by view: switching views (a scope or subscription change)
999
+ * saves the current view's watermark and loads the target view's, so returning to a
1000
+ * previously-synced view resumes exactly where it left off instead of re-syncing. This
1001
+ * is correct because a view's watermark is a lower bound specific to that view's
1002
+ * filter; a widened view is simply a different (initially unsynced) view.
1003
+ */
1004
+ private deliveryWatermark;
1005
+ /**
1006
+ * In-memory cache of watermark per view signature, preloaded from persistence at start
1007
+ * so a view switch resolves synchronously (no race with the reconnect it triggers).
1008
+ */
1009
+ private readonly deliverySignatureWatermarks;
967
1010
  constructor(options: SyncEngineOptions);
968
1011
  /**
969
1012
  * Start the sync engine: connect → handshake → delta exchange → streaming.
970
1013
  */
971
1014
  start(): Promise<void>;
1015
+ private startInternal;
972
1016
  /**
973
1017
  * Stop the sync engine. Disconnects the transport.
974
1018
  */
975
1019
  stop(): Promise<void>;
1020
+ private stopInternal;
976
1021
  private ensureDisconnected;
977
1022
  /**
978
1023
  * Push a local operation to the outbound queue.
@@ -1056,6 +1101,41 @@ declare class SyncEngine {
1056
1101
  * @param scopeMap - New per-collection scope filters, or undefined to remove scope
1057
1102
  */
1058
1103
  updateScope(scopeMap: SyncScopeMap | undefined): void;
1104
+ /**
1105
+ * A stable identifier for the current sync view: the auth/tenant scope plus the set of
1106
+ * active query subscriptions, canonicalized so the same view always produces the same
1107
+ * string. The delivery watermark is keyed by this, so each distinct view resumes from
1108
+ * its own last position.
1109
+ */
1110
+ private deliverySignature;
1111
+ /**
1112
+ * Switch the active delivery watermark to the current view after a scope or subscription
1113
+ * change. The prior view's watermark is saved so returning to it resumes; the new view's
1114
+ * watermark is restored from the in-memory cache (0 for a never-synced view, which then
1115
+ * does a one-time resync under that view). No data is lost: a view's watermark is a lower
1116
+ * bound for that specific view's filter.
1117
+ */
1118
+ private switchDeliveryView;
1119
+ /**
1120
+ * Record a view's watermark in the in-memory cache as the most-recently-used entry (the
1121
+ * Map preserves insertion order, so re-inserting moves it to the tail), then evict cold
1122
+ * views past the retention cap. This is the single place the cache is written, so LRU
1123
+ * order and the size bound are always maintained together.
1124
+ */
1125
+ private setViewWatermark;
1126
+ /**
1127
+ * Trim the per-view watermark cache to `MAX_DELIVERY_VIEW_WATERMARKS`, evicting the
1128
+ * least-recently-used views. The default view ('') and the live view are never evicted
1129
+ * (evicting the live view would force an immediate re-scan of what we are actively
1130
+ * syncing). Each eviction also removes the persisted row, bounding storage. Safe by
1131
+ * construction: an evicted view simply back-fills from 0 (deduplicated) when next seen.
1132
+ */
1133
+ private evictColdViewWatermarks;
1134
+ /**
1135
+ * Remove a view's persisted watermark row (best-effort; a persistence layer that omits
1136
+ * the delete simply keeps the row, which is harmless).
1137
+ */
1138
+ private deleteDeliveryWatermark;
1059
1139
  /**
1060
1140
  * Get the currently active scope map. Returns undefined if no scope is configured.
1061
1141
  */
@@ -1119,6 +1199,8 @@ declare class SyncEngine {
1119
1199
  private markDeltaSendCompleteIfReady;
1120
1200
  private collectDelta;
1121
1201
  private handleOperationBatch;
1202
+ /** Acknowledge a delivery batch (used for duplicates) without re-applying it. */
1203
+ private sendDeliveryAck;
1122
1204
  private handleAcknowledgment;
1123
1205
  private handleError;
1124
1206
  /**
@@ -1160,13 +1242,20 @@ declare class SyncEngine {
1160
1242
  */
1161
1243
  private reconcileOutboundFromOpLog;
1162
1244
  private flushQueue;
1245
+ private startOutboundAckTimer;
1246
+ private clearOutboundAckTimer;
1247
+ private handleOutboundAckTimeout;
1163
1248
  private handleAwarenessUpdate;
1164
1249
  private handleTransportClose;
1165
1250
  private handleTransportError;
1166
1251
  private transitionTo;
1167
1252
  private setSerializerWireFormat;
1168
1253
  private operationAllowedForSync;
1254
+ private matchesScopeAndSubsets;
1255
+ private readRecordForBackfill;
1256
+ private filterAllowedForSync;
1169
1257
  private persistDeltaCursor;
1258
+ private persistDeliveryWatermark;
1170
1259
  private scheduleQuerySubsetReconnect;
1171
1260
  private reconnectForQuerySubsets;
1172
1261
  }