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

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-DXmZ8kyI.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-DXmZ8kyI.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;
@@ -964,6 +984,25 @@ declare class SyncEngine {
964
984
  /** Resume cursor for paginated initial sync (persisted across reconnects) */
965
985
  private resumeDeltaCursor;
966
986
  private initialSyncTotalBatches;
987
+ /**
988
+ * The delivery watermark for the CURRENT sync view: the highest server delivery
989
+ * sequence up to which every operation in the current view (scope + active query
990
+ * subscriptions) has been contiguously applied. Advanced only through the server's
991
+ * gap-free delivery stream (never through live relay), so it is a durable lower bound
992
+ * on what this client holds for this view. Reported at handshake to resume the stream.
993
+ *
994
+ * The watermark is keyed by view: switching views (a scope or subscription change)
995
+ * saves the current view's watermark and loads the target view's, so returning to a
996
+ * previously-synced view resumes exactly where it left off instead of re-syncing. This
997
+ * is correct because a view's watermark is a lower bound specific to that view's
998
+ * filter; a widened view is simply a different (initially unsynced) view.
999
+ */
1000
+ private deliveryWatermark;
1001
+ /**
1002
+ * In-memory cache of watermark per view signature, preloaded from persistence at start
1003
+ * so a view switch resolves synchronously (no race with the reconnect it triggers).
1004
+ */
1005
+ private readonly deliverySignatureWatermarks;
967
1006
  constructor(options: SyncEngineOptions);
968
1007
  /**
969
1008
  * Start the sync engine: connect → handshake → delta exchange → streaming.
@@ -1056,6 +1095,41 @@ declare class SyncEngine {
1056
1095
  * @param scopeMap - New per-collection scope filters, or undefined to remove scope
1057
1096
  */
1058
1097
  updateScope(scopeMap: SyncScopeMap | undefined): void;
1098
+ /**
1099
+ * A stable identifier for the current sync view: the auth/tenant scope plus the set of
1100
+ * active query subscriptions, canonicalized so the same view always produces the same
1101
+ * string. The delivery watermark is keyed by this, so each distinct view resumes from
1102
+ * its own last position.
1103
+ */
1104
+ private deliverySignature;
1105
+ /**
1106
+ * Switch the active delivery watermark to the current view after a scope or subscription
1107
+ * change. The prior view's watermark is saved so returning to it resumes; the new view's
1108
+ * watermark is restored from the in-memory cache (0 for a never-synced view, which then
1109
+ * does a one-time resync under that view). No data is lost: a view's watermark is a lower
1110
+ * bound for that specific view's filter.
1111
+ */
1112
+ private switchDeliveryView;
1113
+ /**
1114
+ * Record a view's watermark in the in-memory cache as the most-recently-used entry (the
1115
+ * Map preserves insertion order, so re-inserting moves it to the tail), then evict cold
1116
+ * views past the retention cap. This is the single place the cache is written, so LRU
1117
+ * order and the size bound are always maintained together.
1118
+ */
1119
+ private setViewWatermark;
1120
+ /**
1121
+ * Trim the per-view watermark cache to `MAX_DELIVERY_VIEW_WATERMARKS`, evicting the
1122
+ * least-recently-used views. The default view ('') and the live view are never evicted
1123
+ * (evicting the live view would force an immediate re-scan of what we are actively
1124
+ * syncing). Each eviction also removes the persisted row, bounding storage. Safe by
1125
+ * construction: an evicted view simply back-fills from 0 (deduplicated) when next seen.
1126
+ */
1127
+ private evictColdViewWatermarks;
1128
+ /**
1129
+ * Remove a view's persisted watermark row (best-effort; a persistence layer that omits
1130
+ * the delete simply keeps the row, which is harmless).
1131
+ */
1132
+ private deleteDeliveryWatermark;
1059
1133
  /**
1060
1134
  * Get the currently active scope map. Returns undefined if no scope is configured.
1061
1135
  */
@@ -1119,6 +1193,8 @@ declare class SyncEngine {
1119
1193
  private markDeltaSendCompleteIfReady;
1120
1194
  private collectDelta;
1121
1195
  private handleOperationBatch;
1196
+ /** Acknowledge a delivery batch (used for duplicates) without re-applying it. */
1197
+ private sendDeliveryAck;
1122
1198
  private handleAcknowledgment;
1123
1199
  private handleError;
1124
1200
  /**
@@ -1166,7 +1242,11 @@ declare class SyncEngine {
1166
1242
  private transitionTo;
1167
1243
  private setSerializerWireFormat;
1168
1244
  private operationAllowedForSync;
1245
+ private matchesScopeAndSubsets;
1246
+ private readRecordForBackfill;
1247
+ private filterAllowedForSync;
1169
1248
  private persistDeltaCursor;
1249
+ private persistDeliveryWatermark;
1170
1250
  private scheduleQuerySubsetReconnect;
1171
1251
  private reconnectForQuerySubsets;
1172
1252
  }
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-DXmZ8kyI.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-DXmZ8kyI.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;
@@ -964,6 +984,25 @@ declare class SyncEngine {
964
984
  /** Resume cursor for paginated initial sync (persisted across reconnects) */
965
985
  private resumeDeltaCursor;
966
986
  private initialSyncTotalBatches;
987
+ /**
988
+ * The delivery watermark for the CURRENT sync view: the highest server delivery
989
+ * sequence up to which every operation in the current view (scope + active query
990
+ * subscriptions) has been contiguously applied. Advanced only through the server's
991
+ * gap-free delivery stream (never through live relay), so it is a durable lower bound
992
+ * on what this client holds for this view. Reported at handshake to resume the stream.
993
+ *
994
+ * The watermark is keyed by view: switching views (a scope or subscription change)
995
+ * saves the current view's watermark and loads the target view's, so returning to a
996
+ * previously-synced view resumes exactly where it left off instead of re-syncing. This
997
+ * is correct because a view's watermark is a lower bound specific to that view's
998
+ * filter; a widened view is simply a different (initially unsynced) view.
999
+ */
1000
+ private deliveryWatermark;
1001
+ /**
1002
+ * In-memory cache of watermark per view signature, preloaded from persistence at start
1003
+ * so a view switch resolves synchronously (no race with the reconnect it triggers).
1004
+ */
1005
+ private readonly deliverySignatureWatermarks;
967
1006
  constructor(options: SyncEngineOptions);
968
1007
  /**
969
1008
  * Start the sync engine: connect → handshake → delta exchange → streaming.
@@ -1056,6 +1095,41 @@ declare class SyncEngine {
1056
1095
  * @param scopeMap - New per-collection scope filters, or undefined to remove scope
1057
1096
  */
1058
1097
  updateScope(scopeMap: SyncScopeMap | undefined): void;
1098
+ /**
1099
+ * A stable identifier for the current sync view: the auth/tenant scope plus the set of
1100
+ * active query subscriptions, canonicalized so the same view always produces the same
1101
+ * string. The delivery watermark is keyed by this, so each distinct view resumes from
1102
+ * its own last position.
1103
+ */
1104
+ private deliverySignature;
1105
+ /**
1106
+ * Switch the active delivery watermark to the current view after a scope or subscription
1107
+ * change. The prior view's watermark is saved so returning to it resumes; the new view's
1108
+ * watermark is restored from the in-memory cache (0 for a never-synced view, which then
1109
+ * does a one-time resync under that view). No data is lost: a view's watermark is a lower
1110
+ * bound for that specific view's filter.
1111
+ */
1112
+ private switchDeliveryView;
1113
+ /**
1114
+ * Record a view's watermark in the in-memory cache as the most-recently-used entry (the
1115
+ * Map preserves insertion order, so re-inserting moves it to the tail), then evict cold
1116
+ * views past the retention cap. This is the single place the cache is written, so LRU
1117
+ * order and the size bound are always maintained together.
1118
+ */
1119
+ private setViewWatermark;
1120
+ /**
1121
+ * Trim the per-view watermark cache to `MAX_DELIVERY_VIEW_WATERMARKS`, evicting the
1122
+ * least-recently-used views. The default view ('') and the live view are never evicted
1123
+ * (evicting the live view would force an immediate re-scan of what we are actively
1124
+ * syncing). Each eviction also removes the persisted row, bounding storage. Safe by
1125
+ * construction: an evicted view simply back-fills from 0 (deduplicated) when next seen.
1126
+ */
1127
+ private evictColdViewWatermarks;
1128
+ /**
1129
+ * Remove a view's persisted watermark row (best-effort; a persistence layer that omits
1130
+ * the delete simply keeps the row, which is harmless).
1131
+ */
1132
+ private deleteDeliveryWatermark;
1059
1133
  /**
1060
1134
  * Get the currently active scope map. Returns undefined if no scope is configured.
1061
1135
  */
@@ -1119,6 +1193,8 @@ declare class SyncEngine {
1119
1193
  private markDeltaSendCompleteIfReady;
1120
1194
  private collectDelta;
1121
1195
  private handleOperationBatch;
1196
+ /** Acknowledge a delivery batch (used for duplicates) without re-applying it. */
1197
+ private sendDeliveryAck;
1122
1198
  private handleAcknowledgment;
1123
1199
  private handleError;
1124
1200
  /**
@@ -1166,7 +1242,11 @@ declare class SyncEngine {
1166
1242
  private transitionTo;
1167
1243
  private setSerializerWireFormat;
1168
1244
  private operationAllowedForSync;
1245
+ private matchesScopeAndSubsets;
1246
+ private readRecordForBackfill;
1247
+ private filterAllowedForSync;
1169
1248
  private persistDeltaCursor;
1249
+ private persistDeliveryWatermark;
1170
1250
  private scheduleQuerySubsetReconnect;
1171
1251
  private reconnectForQuerySubsets;
1172
1252
  }