@korajs/sync 1.0.0-beta.1 → 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.
@@ -1,4 +1,4 @@
1
- import { c as SyncTransport, T as TransportOptions, a as SyncMessage, d as TransportMessageHandler, e as TransportCloseHandler, f as TransportErrorHandler, Q as QueueStorage } from './transport-CZLMdZ0k.cjs';
1
+ import { c as SyncTransport, T as TransportOptions, a as SyncMessage, d as TransportMessageHandler, e as TransportCloseHandler, f as TransportErrorHandler, Q as QueueStorage } from './transport-iXX0KA_W.cjs';
2
2
  import { Operation } from '@korajs/core';
3
3
 
4
4
  /**
@@ -1,4 +1,4 @@
1
- import { c as SyncTransport, T as TransportOptions, a as SyncMessage, d as TransportMessageHandler, e as TransportCloseHandler, f as TransportErrorHandler, Q as QueueStorage } from './transport-CZLMdZ0k.js';
1
+ import { c as SyncTransport, T as TransportOptions, a as SyncMessage, d as TransportMessageHandler, e as TransportCloseHandler, f as TransportErrorHandler, Q as QueueStorage } from './transport-iXX0KA_W.js';
2
2
  import { Operation } from '@korajs/core';
3
3
 
4
4
  /**
@@ -93,14 +93,29 @@ type SyncState = (typeof SYNC_STATES)[number];
93
93
  /**
94
94
  * Developer-facing sync status. Simplified view of the internal state.
95
95
  */
96
- declare const SYNC_STATUSES: readonly ["connected", "syncing", "synced", "offline", "clock-error", "error", "schema-mismatch"];
96
+ declare const SYNC_STATUSES: readonly ["connected", "reconnecting", "syncing", "synced", "offline", "clock-error", "error", "schema-mismatch", "auth-required"];
97
97
  type SyncStatus = (typeof SYNC_STATUSES)[number];
98
+ type SyncPhase = 'suspended' | 'offline' | 'connecting' | 'authenticating' | 'handshaking' | 'uploading' | 'receiving' | 'applying' | 'streaming' | 'blocked';
99
+ interface ActiveApplyFailure {
100
+ operationId: string;
101
+ collection: string;
102
+ recordId: string;
103
+ code: string;
104
+ message: string;
105
+ retriable: boolean;
106
+ firstSeenAt: number;
107
+ retryCount: number;
108
+ }
98
109
  /**
99
110
  * Sync status information exposed to developers.
100
111
  */
101
112
  interface SyncStatusInfo {
102
113
  /** Current developer-facing status */
103
114
  status: SyncStatus;
115
+ phase?: SyncPhase;
116
+ reason?: string;
117
+ /** True when the engine is actively trying to re-establish a transport session. */
118
+ reconnecting: boolean;
104
119
  /** Number of operations waiting to be sent */
105
120
  pendingOperations: number;
106
121
  /** Timestamp of last successful sync (null if never synced) */
@@ -113,7 +128,47 @@ interface SyncStatusInfo {
113
128
  conflicts: number;
114
129
  /** serverTime - localTime in ms measured at the last handshake, or null before first connect. Negative = this device's clock is fast. */
115
130
  clockSkewMs: number | null;
131
+ inFlightUploadOperations?: number;
132
+ hasInFlightDeliveryBatch?: boolean;
133
+ activeViewId?: string;
134
+ activeViewComplete?: boolean;
135
+ initialSync?: {
136
+ complete: boolean;
137
+ receivedBatches: number;
138
+ totalBatches: number | null;
139
+ progress: number | null;
140
+ };
141
+ deliveryWatermark?: number;
142
+ serverFrontier?: number | null;
143
+ blockedFailure?: ActiveApplyFailure | null;
116
144
  }
145
+ interface SyncSettlementOptions {
146
+ upload?: boolean;
147
+ download?: 'active-view' | false;
148
+ timeoutMs?: number;
149
+ signal?: AbortSignal;
150
+ }
151
+ type SyncSettlementResult = {
152
+ outcome: 'settled';
153
+ status: SyncStatusInfo;
154
+ } | {
155
+ outcome: 'offline';
156
+ status: SyncStatusInfo;
157
+ } | {
158
+ outcome: 'suspended';
159
+ reason: string;
160
+ status: SyncStatusInfo;
161
+ } | {
162
+ outcome: 'blocked';
163
+ failure: ActiveApplyFailure;
164
+ status: SyncStatusInfo;
165
+ } | {
166
+ outcome: 'timeout';
167
+ status: SyncStatusInfo;
168
+ } | {
169
+ outcome: 'aborted';
170
+ status: SyncStatusInfo;
171
+ };
117
172
  /**
118
173
  * Per-collection sync scope map. Maps collection names to field-value filters.
119
174
  * Empty filter `{}` means no restriction (all records visible).
@@ -132,6 +187,14 @@ interface SyncConfig {
132
187
  auth?: () => Promise<{
133
188
  token: string;
134
189
  }>;
190
+ /** Auth readiness gate. A suspended result prevents transport creation and retries. */
191
+ authState?: () => Promise<{
192
+ state: 'loading' | 'signed-out' | 'anonymous' | 'authenticated';
193
+ mayConnectAnonymously?: boolean;
194
+ }>;
195
+ querySubsets?: {
196
+ mode?: 'reactive' | 'static' | 'disabled';
197
+ };
135
198
  /** Sync scopes per collection. Limits which records sync to this client. */
136
199
  scopes?: Record<string, (ctx: SyncScopeContext) => Record<string, unknown>>;
137
200
  /**
@@ -141,6 +204,13 @@ interface SyncConfig {
141
204
  scopeMap?: SyncScopeMap;
142
205
  /** Number of operations per batch. Defaults to 100. */
143
206
  batchSize?: number;
207
+ /**
208
+ * Maximum time to wait for the server to acknowledge an outbound batch before
209
+ * treating the connection as stalled. The batch is returned to the durable
210
+ * outbound queue before disconnecting so it can be retried on reconnect.
211
+ * Defaults to 30000ms. Set to 0 to disable the watchdog.
212
+ */
213
+ outboundAckTimeoutMs?: number;
144
214
  /** Initial reconnection delay in ms. Defaults to 1000. */
145
215
  reconnectInterval?: number;
146
216
  /** Maximum reconnection delay in ms. Defaults to 30000. */
@@ -193,6 +263,23 @@ interface SyncStatePersistence {
193
263
  /** Resume position for paginated initial sync (optional). */
194
264
  loadDeltaCursor?(): Promise<DeltaCursor | null>;
195
265
  saveDeltaCursor?(cursor: DeltaCursor | null): Promise<void>;
266
+ /**
267
+ * The delivery watermark for a sync view, keyed by an opaque view signature (the auth
268
+ * scope plus active query subscriptions). Persisted per view so a restarted client
269
+ * resumes each view's gap-free stream from the right point, and switching views does not
270
+ * re-sync a view already synced. The empty-string signature is the default, unfiltered
271
+ * view. Optional: a persistence layer that omits these falls back to re-sync on restart.
272
+ */
273
+ loadDeliveryWatermark?(signature: string): Promise<number>;
274
+ saveDeliveryWatermark?(signature: string, watermark: number): Promise<void>;
275
+ /** Load every persisted view watermark, keyed by signature. */
276
+ loadAllDeliveryWatermarks?(): Promise<Record<string, number>>;
277
+ /**
278
+ * Delete a persisted view watermark. Used to bound the number of stored view watermarks
279
+ * (a cold view is evicted from the client's cache and its persisted row removed). Safe:
280
+ * an evicted view back-fills from 0 (deduplicated) when next visited.
281
+ */
282
+ deleteDeliveryWatermark?(signature: string): Promise<void>;
196
283
  }
197
284
  /**
198
285
  * Interface for persisting the outbound operation queue.
@@ -208,6 +295,48 @@ interface QueueStorage {
208
295
  /** Return number of operations in storage */
209
296
  count(): Promise<number>;
210
297
  }
298
+ /**
299
+ * A record of one outbound operation the server permanently refused. Preserved so
300
+ * a rejected op is explainable and reconcilable rather than silently lost.
301
+ */
302
+ interface RejectedOperation {
303
+ /** Content-addressed id of the rejected operation. */
304
+ operationId: string;
305
+ /** Collection the operation targeted. */
306
+ collection: string;
307
+ /** Record the operation targeted. */
308
+ recordId: string;
309
+ /** Stable, machine-readable reason code from the server. */
310
+ code: string;
311
+ /** Human-readable explanation. */
312
+ message: string;
313
+ /** Whether resubmitting the identical operation may later succeed. */
314
+ retriable: boolean;
315
+ /** Wall-clock time (ms since epoch) the rejection was recorded. Display only. */
316
+ rejectedAt: number;
317
+ }
318
+ /**
319
+ * Interface for durably persisting operations the server rejected, so the record
320
+ * survives a page refresh. Keyed by operation id (idempotent on re-rejection).
321
+ */
322
+ interface RejectedOperationStorage {
323
+ /** Record (or overwrite) a rejected operation. */
324
+ record(rejected: RejectedOperation): Promise<void>;
325
+ /** List all recorded rejected operations. */
326
+ list(): Promise<RejectedOperation[]>;
327
+ /** Remove rejected operations by their operation ids (after the app reconciles them). */
328
+ remove(operationIds: string[]): Promise<void>;
329
+ }
330
+ /**
331
+ * In-memory {@link RejectedOperationStorage}. The default when no durable store is
332
+ * provided; suitable for tests and ephemeral sessions.
333
+ */
334
+ declare class MemoryRejectedOperationStorage implements RejectedOperationStorage {
335
+ private readonly rejected;
336
+ record(rejected: RejectedOperation): Promise<void>;
337
+ list(): Promise<RejectedOperation[]>;
338
+ remove(operationIds: string[]): Promise<void>;
339
+ }
211
340
  /**
212
341
  * Thrown when an operation violates sync scope constraints.
213
342
  * This can happen when:
@@ -239,10 +368,10 @@ interface SyncQuerySubset {
239
368
  * for its collection. Collections without query subsets pass through.
240
369
  */
241
370
  declare function operationMatchesQuerySubsets(op: Operation, subsets: SyncQuerySubset[] | undefined, fullRecord?: Record<string, unknown> | null): boolean;
242
- /**
243
- * Deduplicate query subsets by collection + where JSON key.
244
- */
371
+ /** Canonicalize equality subsets and remove subsets already covered by a broader one. */
245
372
  declare function dedupeQuerySubsets(subsets: SyncQuerySubset[]): SyncQuerySubset[];
373
+ /** True when every record selected by `narrow` is also selected by `broad`. */
374
+ declare function querySubsetContains(broad: SyncQuerySubset[], narrow: SyncQuerySubset[]): boolean;
246
375
 
247
376
  type WireFormat = 'json' | 'protobuf';
248
377
  /**
@@ -286,6 +415,14 @@ interface HandshakeMessage {
286
415
  deltaCursor?: string;
287
416
  /** Live query filters that further narrow synced data within auth/schema scope */
288
417
  syncQueries?: SyncQuerySubset[];
418
+ /**
419
+ * The client's persisted delivery watermark: the highest server delivery sequence
420
+ * up to which it has contiguously applied every in-scope operation. Omitted (or 0)
421
+ * on first sync. When present, the server resumes the gap-free server->client
422
+ * stream from just after it, superseding the version-vector delta and the resume
423
+ * cursor for the server->client direction. Optional, so old servers ignore it.
424
+ */
425
+ lastDeliverySequence?: number;
289
426
  }
290
427
  /**
291
428
  * Server response to a handshake.
@@ -313,6 +450,13 @@ interface HandshakeResponseMessage {
313
450
  * available to other devices even after the authoring device goes offline.
314
451
  */
315
452
  blobStorageEnabled?: boolean;
453
+ /**
454
+ * The server's highest delivery sequence at handshake time. A client whose
455
+ * persisted delivery watermark exceeds this (the server's operation log was rolled
456
+ * back, for example by a backup restore) resets its watermark to 0 so it resyncs
457
+ * from the beginning instead of sitting above a frontier that no longer exists.
458
+ */
459
+ serverMaxDeliverySequence?: number;
316
460
  }
317
461
  /**
318
462
  * Batch of operations sent during delta exchange or streaming.
@@ -329,6 +473,20 @@ interface OperationBatchMessage {
329
473
  cursor?: string;
330
474
  /** Total batches in this delta exchange (for progress reporting) */
331
475
  totalBatches?: number;
476
+ /**
477
+ * Delivery-watermark chaining (server->client). `baseDeliverySequence` is the
478
+ * delivery sequence the client must already hold for this batch to apply in order
479
+ * (it equals the previous batch's `maxDeliverySequence`, or the client's reported
480
+ * watermark for the first batch after a handshake). `maxDeliverySequence` is the
481
+ * highest delivery sequence carried by this batch. The client applies the batch
482
+ * only when its watermark equals `baseDeliverySequence`, then advances the
483
+ * watermark to `maxDeliverySequence`. A dropped batch breaks the chain and stalls
484
+ * the watermark, so the reconnect resend recovers it. Present only on batches the
485
+ * server sends as part of the delivery stream; absent on legacy version-vector
486
+ * batches.
487
+ */
488
+ baseDeliverySequence?: number;
489
+ maxDeliverySequence?: number;
332
490
  }
333
491
  /**
334
492
  * Acknowledgment of a received message.
@@ -338,6 +496,12 @@ interface AcknowledgmentMessage {
338
496
  messageId: string;
339
497
  acknowledgedMessageId: string;
340
498
  lastSequenceNumber: number;
499
+ /**
500
+ * The acknowledged batch's `maxDeliverySequence`, echoed so the server can advance
501
+ * this client's durable delivery watermark. Present only when acking a delivery-
502
+ * stream batch. `lastSequenceNumber` stays for existing per-node behavior.
503
+ */
504
+ deliverySequence?: number;
341
505
  }
342
506
  /**
343
507
  * Error message from the server or client.
@@ -349,6 +513,33 @@ interface ErrorMessage {
349
513
  message: string;
350
514
  retriable: boolean;
351
515
  }
516
+ /**
517
+ * Server-to-client rejection of ONE specific operation the client submitted.
518
+ *
519
+ * Unlike {@link ErrorMessage}, which is connection-level, this is tied to an
520
+ * `operationId` so the client can divert exactly that operation out of its
521
+ * pending outbound queue and into a durable rejected-operations store — kept and
522
+ * explainable rather than silently retried forever or lost on the batch ack. The
523
+ * operation never entered the server's authoritative log, so no other replica
524
+ * ever sees it; the submitter is told so its optimistic local copy can be
525
+ * reconciled (rolled back or resubmitted) by the app.
526
+ */
527
+ interface OperationRejectedMessage {
528
+ type: 'operation-rejected';
529
+ messageId: string;
530
+ /** Content-addressed id of the rejected operation. */
531
+ operationId: string;
532
+ /** Collection the rejected operation targeted (for app routing / display). */
533
+ collection: string;
534
+ /** Record the rejected operation targeted. */
535
+ recordId: string;
536
+ /** Stable, machine-readable reason code. */
537
+ code: string;
538
+ /** Human-readable explanation. */
539
+ message: string;
540
+ /** Whether resubmitting the identical operation may later succeed. */
541
+ retriable: boolean;
542
+ }
352
543
  /**
353
544
  * Awareness state for a single client (cursor position, user info).
354
545
  * Wire-format representation for JSON transport.
@@ -441,7 +632,7 @@ interface BlobChunkPushMessage {
441
632
  /**
442
633
  * Union of all sync protocol messages.
443
634
  */
444
- type SyncMessage = HandshakeMessage | HandshakeResponseMessage | OperationBatchMessage | AcknowledgmentMessage | ErrorMessage | AwarenessUpdateMessage | YjsDocUpdateMessage | BlobChunkRequestMessage | BlobChunkResponseMessage | BlobChunkPushMessage;
635
+ type SyncMessage = HandshakeMessage | HandshakeResponseMessage | OperationBatchMessage | AcknowledgmentMessage | ErrorMessage | OperationRejectedMessage | AwarenessUpdateMessage | YjsDocUpdateMessage | BlobChunkRequestMessage | BlobChunkResponseMessage | BlobChunkPushMessage;
445
636
  /**
446
637
  * Check if an unknown value is a valid SyncMessage.
447
638
  */
@@ -466,6 +657,10 @@ declare function isAcknowledgmentMessage(value: unknown): value is Acknowledgmen
466
657
  * Check if a value is an ErrorMessage.
467
658
  */
468
659
  declare function isErrorMessage(value: unknown): value is ErrorMessage;
660
+ /**
661
+ * Check if a value is an OperationRejectedMessage.
662
+ */
663
+ declare function isOperationRejectedMessage(value: unknown): value is OperationRejectedMessage;
469
664
  /**
470
665
  * Check if a value is an AwarenessUpdateMessage.
471
666
  */
@@ -529,4 +724,4 @@ interface SyncTransport {
529
724
  isConnected(): boolean;
530
725
  }
531
726
 
532
- export { type AcknowledgmentMessage as A, type BlobChunkRequestMessage as B, isAwarenessUpdateMessage as C, type DeltaCursor as D, type EncryptedPayload as E, isBlobChunkPushMessage as F, isBlobChunkRequestMessage as G, type HandshakeMessage as H, InvalidScopeError as I, isBlobChunkResponseMessage as J, isErrorMessage as K, isHandshakeMessage as L, isHandshakeResponseMessage as M, isOperationBatchMessage as N, type OperationBatchMessage as O, isSyncMessage as P, type QueueStorage as Q, isYjsDocUpdateMessage as R, type SyncScopeMap as S, type TransportOptions as T, operationMatchesQuerySubsets as U, type VersionedKey as V, type WireFormat as W, type YjsDocUpdateMessage as Y, type SyncMessage as a, type SerializedOperation as b, type SyncTransport as c, type TransportMessageHandler as d, type TransportCloseHandler as e, type TransportErrorHandler as f, type BlobChunkResponseMessage as g, type BlobChunkPushMessage as h, type SyncEncryptionConfig as i, type SyncState as j, type SyncStatusInfo as k, type SyncConfig as l, type SyncStatePersistence as m, type SyncQuerySubset as n, type AwarenessStateWire as o, type AwarenessUpdateMessage as p, type ErrorMessage as q, type HandshakeResponseMessage as r, SYNC_STATES as s, SYNC_STATUSES as t, ScopeViolationError as u, type SyncEncryptionAlgorithm as v, type SyncScopeContext as w, type SyncStatus as x, dedupeQuerySubsets as y, isAcknowledgmentMessage as z };
727
+ export { isHandshakeResponseMessage as $, type AcknowledgmentMessage as A, type BlobChunkRequestMessage as B, type SyncScopeContext as C, type DeltaCursor as D, type EncryptedPayload as E, type SyncSettlementOptions as F, type SyncSettlementResult as G, type HandshakeMessage as H, InvalidScopeError as I, type SyncStatus as J, dedupeQuerySubsets as K, isAcknowledgmentMessage as L, MemoryRejectedOperationStorage as M, isAwarenessUpdateMessage as N, type OperationBatchMessage as O, isBlobChunkPushMessage as P, type QueueStorage as Q, type RejectedOperationStorage as R, type SyncScopeMap as S, type TransportOptions as T, isBlobChunkRequestMessage as U, type VersionedKey as V, type WireFormat as W, isBlobChunkResponseMessage as X, type YjsDocUpdateMessage as Y, isErrorMessage as Z, isHandshakeMessage as _, type SyncMessage as a, isOperationBatchMessage as a0, isOperationRejectedMessage as a1, isSyncMessage as a2, isYjsDocUpdateMessage as a3, operationMatchesQuerySubsets as a4, querySubsetContains as a5, type SerializedOperation as b, type SyncTransport as c, type TransportMessageHandler as d, type TransportCloseHandler as e, type TransportErrorHandler as f, type BlobChunkResponseMessage as g, type BlobChunkPushMessage as h, type SyncEncryptionConfig as i, type SyncState as j, type SyncStatusInfo as k, type SyncConfig as l, type SyncStatePersistence as m, type SyncQuerySubset as n, type RejectedOperation as o, type ActiveApplyFailure as p, type AwarenessStateWire as q, type AwarenessUpdateMessage as r, type ErrorMessage as s, type HandshakeResponseMessage as t, type OperationRejectedMessage as u, SYNC_STATES as v, SYNC_STATUSES as w, ScopeViolationError as x, type SyncEncryptionAlgorithm as y, type SyncPhase as z };
@@ -93,14 +93,29 @@ type SyncState = (typeof SYNC_STATES)[number];
93
93
  /**
94
94
  * Developer-facing sync status. Simplified view of the internal state.
95
95
  */
96
- declare const SYNC_STATUSES: readonly ["connected", "syncing", "synced", "offline", "clock-error", "error", "schema-mismatch"];
96
+ declare const SYNC_STATUSES: readonly ["connected", "reconnecting", "syncing", "synced", "offline", "clock-error", "error", "schema-mismatch", "auth-required"];
97
97
  type SyncStatus = (typeof SYNC_STATUSES)[number];
98
+ type SyncPhase = 'suspended' | 'offline' | 'connecting' | 'authenticating' | 'handshaking' | 'uploading' | 'receiving' | 'applying' | 'streaming' | 'blocked';
99
+ interface ActiveApplyFailure {
100
+ operationId: string;
101
+ collection: string;
102
+ recordId: string;
103
+ code: string;
104
+ message: string;
105
+ retriable: boolean;
106
+ firstSeenAt: number;
107
+ retryCount: number;
108
+ }
98
109
  /**
99
110
  * Sync status information exposed to developers.
100
111
  */
101
112
  interface SyncStatusInfo {
102
113
  /** Current developer-facing status */
103
114
  status: SyncStatus;
115
+ phase?: SyncPhase;
116
+ reason?: string;
117
+ /** True when the engine is actively trying to re-establish a transport session. */
118
+ reconnecting: boolean;
104
119
  /** Number of operations waiting to be sent */
105
120
  pendingOperations: number;
106
121
  /** Timestamp of last successful sync (null if never synced) */
@@ -113,7 +128,47 @@ interface SyncStatusInfo {
113
128
  conflicts: number;
114
129
  /** serverTime - localTime in ms measured at the last handshake, or null before first connect. Negative = this device's clock is fast. */
115
130
  clockSkewMs: number | null;
131
+ inFlightUploadOperations?: number;
132
+ hasInFlightDeliveryBatch?: boolean;
133
+ activeViewId?: string;
134
+ activeViewComplete?: boolean;
135
+ initialSync?: {
136
+ complete: boolean;
137
+ receivedBatches: number;
138
+ totalBatches: number | null;
139
+ progress: number | null;
140
+ };
141
+ deliveryWatermark?: number;
142
+ serverFrontier?: number | null;
143
+ blockedFailure?: ActiveApplyFailure | null;
116
144
  }
145
+ interface SyncSettlementOptions {
146
+ upload?: boolean;
147
+ download?: 'active-view' | false;
148
+ timeoutMs?: number;
149
+ signal?: AbortSignal;
150
+ }
151
+ type SyncSettlementResult = {
152
+ outcome: 'settled';
153
+ status: SyncStatusInfo;
154
+ } | {
155
+ outcome: 'offline';
156
+ status: SyncStatusInfo;
157
+ } | {
158
+ outcome: 'suspended';
159
+ reason: string;
160
+ status: SyncStatusInfo;
161
+ } | {
162
+ outcome: 'blocked';
163
+ failure: ActiveApplyFailure;
164
+ status: SyncStatusInfo;
165
+ } | {
166
+ outcome: 'timeout';
167
+ status: SyncStatusInfo;
168
+ } | {
169
+ outcome: 'aborted';
170
+ status: SyncStatusInfo;
171
+ };
117
172
  /**
118
173
  * Per-collection sync scope map. Maps collection names to field-value filters.
119
174
  * Empty filter `{}` means no restriction (all records visible).
@@ -132,6 +187,14 @@ interface SyncConfig {
132
187
  auth?: () => Promise<{
133
188
  token: string;
134
189
  }>;
190
+ /** Auth readiness gate. A suspended result prevents transport creation and retries. */
191
+ authState?: () => Promise<{
192
+ state: 'loading' | 'signed-out' | 'anonymous' | 'authenticated';
193
+ mayConnectAnonymously?: boolean;
194
+ }>;
195
+ querySubsets?: {
196
+ mode?: 'reactive' | 'static' | 'disabled';
197
+ };
135
198
  /** Sync scopes per collection. Limits which records sync to this client. */
136
199
  scopes?: Record<string, (ctx: SyncScopeContext) => Record<string, unknown>>;
137
200
  /**
@@ -141,6 +204,13 @@ interface SyncConfig {
141
204
  scopeMap?: SyncScopeMap;
142
205
  /** Number of operations per batch. Defaults to 100. */
143
206
  batchSize?: number;
207
+ /**
208
+ * Maximum time to wait for the server to acknowledge an outbound batch before
209
+ * treating the connection as stalled. The batch is returned to the durable
210
+ * outbound queue before disconnecting so it can be retried on reconnect.
211
+ * Defaults to 30000ms. Set to 0 to disable the watchdog.
212
+ */
213
+ outboundAckTimeoutMs?: number;
144
214
  /** Initial reconnection delay in ms. Defaults to 1000. */
145
215
  reconnectInterval?: number;
146
216
  /** Maximum reconnection delay in ms. Defaults to 30000. */
@@ -193,6 +263,23 @@ interface SyncStatePersistence {
193
263
  /** Resume position for paginated initial sync (optional). */
194
264
  loadDeltaCursor?(): Promise<DeltaCursor | null>;
195
265
  saveDeltaCursor?(cursor: DeltaCursor | null): Promise<void>;
266
+ /**
267
+ * The delivery watermark for a sync view, keyed by an opaque view signature (the auth
268
+ * scope plus active query subscriptions). Persisted per view so a restarted client
269
+ * resumes each view's gap-free stream from the right point, and switching views does not
270
+ * re-sync a view already synced. The empty-string signature is the default, unfiltered
271
+ * view. Optional: a persistence layer that omits these falls back to re-sync on restart.
272
+ */
273
+ loadDeliveryWatermark?(signature: string): Promise<number>;
274
+ saveDeliveryWatermark?(signature: string, watermark: number): Promise<void>;
275
+ /** Load every persisted view watermark, keyed by signature. */
276
+ loadAllDeliveryWatermarks?(): Promise<Record<string, number>>;
277
+ /**
278
+ * Delete a persisted view watermark. Used to bound the number of stored view watermarks
279
+ * (a cold view is evicted from the client's cache and its persisted row removed). Safe:
280
+ * an evicted view back-fills from 0 (deduplicated) when next visited.
281
+ */
282
+ deleteDeliveryWatermark?(signature: string): Promise<void>;
196
283
  }
197
284
  /**
198
285
  * Interface for persisting the outbound operation queue.
@@ -208,6 +295,48 @@ interface QueueStorage {
208
295
  /** Return number of operations in storage */
209
296
  count(): Promise<number>;
210
297
  }
298
+ /**
299
+ * A record of one outbound operation the server permanently refused. Preserved so
300
+ * a rejected op is explainable and reconcilable rather than silently lost.
301
+ */
302
+ interface RejectedOperation {
303
+ /** Content-addressed id of the rejected operation. */
304
+ operationId: string;
305
+ /** Collection the operation targeted. */
306
+ collection: string;
307
+ /** Record the operation targeted. */
308
+ recordId: string;
309
+ /** Stable, machine-readable reason code from the server. */
310
+ code: string;
311
+ /** Human-readable explanation. */
312
+ message: string;
313
+ /** Whether resubmitting the identical operation may later succeed. */
314
+ retriable: boolean;
315
+ /** Wall-clock time (ms since epoch) the rejection was recorded. Display only. */
316
+ rejectedAt: number;
317
+ }
318
+ /**
319
+ * Interface for durably persisting operations the server rejected, so the record
320
+ * survives a page refresh. Keyed by operation id (idempotent on re-rejection).
321
+ */
322
+ interface RejectedOperationStorage {
323
+ /** Record (or overwrite) a rejected operation. */
324
+ record(rejected: RejectedOperation): Promise<void>;
325
+ /** List all recorded rejected operations. */
326
+ list(): Promise<RejectedOperation[]>;
327
+ /** Remove rejected operations by their operation ids (after the app reconciles them). */
328
+ remove(operationIds: string[]): Promise<void>;
329
+ }
330
+ /**
331
+ * In-memory {@link RejectedOperationStorage}. The default when no durable store is
332
+ * provided; suitable for tests and ephemeral sessions.
333
+ */
334
+ declare class MemoryRejectedOperationStorage implements RejectedOperationStorage {
335
+ private readonly rejected;
336
+ record(rejected: RejectedOperation): Promise<void>;
337
+ list(): Promise<RejectedOperation[]>;
338
+ remove(operationIds: string[]): Promise<void>;
339
+ }
211
340
  /**
212
341
  * Thrown when an operation violates sync scope constraints.
213
342
  * This can happen when:
@@ -239,10 +368,10 @@ interface SyncQuerySubset {
239
368
  * for its collection. Collections without query subsets pass through.
240
369
  */
241
370
  declare function operationMatchesQuerySubsets(op: Operation, subsets: SyncQuerySubset[] | undefined, fullRecord?: Record<string, unknown> | null): boolean;
242
- /**
243
- * Deduplicate query subsets by collection + where JSON key.
244
- */
371
+ /** Canonicalize equality subsets and remove subsets already covered by a broader one. */
245
372
  declare function dedupeQuerySubsets(subsets: SyncQuerySubset[]): SyncQuerySubset[];
373
+ /** True when every record selected by `narrow` is also selected by `broad`. */
374
+ declare function querySubsetContains(broad: SyncQuerySubset[], narrow: SyncQuerySubset[]): boolean;
246
375
 
247
376
  type WireFormat = 'json' | 'protobuf';
248
377
  /**
@@ -286,6 +415,14 @@ interface HandshakeMessage {
286
415
  deltaCursor?: string;
287
416
  /** Live query filters that further narrow synced data within auth/schema scope */
288
417
  syncQueries?: SyncQuerySubset[];
418
+ /**
419
+ * The client's persisted delivery watermark: the highest server delivery sequence
420
+ * up to which it has contiguously applied every in-scope operation. Omitted (or 0)
421
+ * on first sync. When present, the server resumes the gap-free server->client
422
+ * stream from just after it, superseding the version-vector delta and the resume
423
+ * cursor for the server->client direction. Optional, so old servers ignore it.
424
+ */
425
+ lastDeliverySequence?: number;
289
426
  }
290
427
  /**
291
428
  * Server response to a handshake.
@@ -313,6 +450,13 @@ interface HandshakeResponseMessage {
313
450
  * available to other devices even after the authoring device goes offline.
314
451
  */
315
452
  blobStorageEnabled?: boolean;
453
+ /**
454
+ * The server's highest delivery sequence at handshake time. A client whose
455
+ * persisted delivery watermark exceeds this (the server's operation log was rolled
456
+ * back, for example by a backup restore) resets its watermark to 0 so it resyncs
457
+ * from the beginning instead of sitting above a frontier that no longer exists.
458
+ */
459
+ serverMaxDeliverySequence?: number;
316
460
  }
317
461
  /**
318
462
  * Batch of operations sent during delta exchange or streaming.
@@ -329,6 +473,20 @@ interface OperationBatchMessage {
329
473
  cursor?: string;
330
474
  /** Total batches in this delta exchange (for progress reporting) */
331
475
  totalBatches?: number;
476
+ /**
477
+ * Delivery-watermark chaining (server->client). `baseDeliverySequence` is the
478
+ * delivery sequence the client must already hold for this batch to apply in order
479
+ * (it equals the previous batch's `maxDeliverySequence`, or the client's reported
480
+ * watermark for the first batch after a handshake). `maxDeliverySequence` is the
481
+ * highest delivery sequence carried by this batch. The client applies the batch
482
+ * only when its watermark equals `baseDeliverySequence`, then advances the
483
+ * watermark to `maxDeliverySequence`. A dropped batch breaks the chain and stalls
484
+ * the watermark, so the reconnect resend recovers it. Present only on batches the
485
+ * server sends as part of the delivery stream; absent on legacy version-vector
486
+ * batches.
487
+ */
488
+ baseDeliverySequence?: number;
489
+ maxDeliverySequence?: number;
332
490
  }
333
491
  /**
334
492
  * Acknowledgment of a received message.
@@ -338,6 +496,12 @@ interface AcknowledgmentMessage {
338
496
  messageId: string;
339
497
  acknowledgedMessageId: string;
340
498
  lastSequenceNumber: number;
499
+ /**
500
+ * The acknowledged batch's `maxDeliverySequence`, echoed so the server can advance
501
+ * this client's durable delivery watermark. Present only when acking a delivery-
502
+ * stream batch. `lastSequenceNumber` stays for existing per-node behavior.
503
+ */
504
+ deliverySequence?: number;
341
505
  }
342
506
  /**
343
507
  * Error message from the server or client.
@@ -349,6 +513,33 @@ interface ErrorMessage {
349
513
  message: string;
350
514
  retriable: boolean;
351
515
  }
516
+ /**
517
+ * Server-to-client rejection of ONE specific operation the client submitted.
518
+ *
519
+ * Unlike {@link ErrorMessage}, which is connection-level, this is tied to an
520
+ * `operationId` so the client can divert exactly that operation out of its
521
+ * pending outbound queue and into a durable rejected-operations store — kept and
522
+ * explainable rather than silently retried forever or lost on the batch ack. The
523
+ * operation never entered the server's authoritative log, so no other replica
524
+ * ever sees it; the submitter is told so its optimistic local copy can be
525
+ * reconciled (rolled back or resubmitted) by the app.
526
+ */
527
+ interface OperationRejectedMessage {
528
+ type: 'operation-rejected';
529
+ messageId: string;
530
+ /** Content-addressed id of the rejected operation. */
531
+ operationId: string;
532
+ /** Collection the rejected operation targeted (for app routing / display). */
533
+ collection: string;
534
+ /** Record the rejected operation targeted. */
535
+ recordId: string;
536
+ /** Stable, machine-readable reason code. */
537
+ code: string;
538
+ /** Human-readable explanation. */
539
+ message: string;
540
+ /** Whether resubmitting the identical operation may later succeed. */
541
+ retriable: boolean;
542
+ }
352
543
  /**
353
544
  * Awareness state for a single client (cursor position, user info).
354
545
  * Wire-format representation for JSON transport.
@@ -441,7 +632,7 @@ interface BlobChunkPushMessage {
441
632
  /**
442
633
  * Union of all sync protocol messages.
443
634
  */
444
- type SyncMessage = HandshakeMessage | HandshakeResponseMessage | OperationBatchMessage | AcknowledgmentMessage | ErrorMessage | AwarenessUpdateMessage | YjsDocUpdateMessage | BlobChunkRequestMessage | BlobChunkResponseMessage | BlobChunkPushMessage;
635
+ type SyncMessage = HandshakeMessage | HandshakeResponseMessage | OperationBatchMessage | AcknowledgmentMessage | ErrorMessage | OperationRejectedMessage | AwarenessUpdateMessage | YjsDocUpdateMessage | BlobChunkRequestMessage | BlobChunkResponseMessage | BlobChunkPushMessage;
445
636
  /**
446
637
  * Check if an unknown value is a valid SyncMessage.
447
638
  */
@@ -466,6 +657,10 @@ declare function isAcknowledgmentMessage(value: unknown): value is Acknowledgmen
466
657
  * Check if a value is an ErrorMessage.
467
658
  */
468
659
  declare function isErrorMessage(value: unknown): value is ErrorMessage;
660
+ /**
661
+ * Check if a value is an OperationRejectedMessage.
662
+ */
663
+ declare function isOperationRejectedMessage(value: unknown): value is OperationRejectedMessage;
469
664
  /**
470
665
  * Check if a value is an AwarenessUpdateMessage.
471
666
  */
@@ -529,4 +724,4 @@ interface SyncTransport {
529
724
  isConnected(): boolean;
530
725
  }
531
726
 
532
- export { type AcknowledgmentMessage as A, type BlobChunkRequestMessage as B, isAwarenessUpdateMessage as C, type DeltaCursor as D, type EncryptedPayload as E, isBlobChunkPushMessage as F, isBlobChunkRequestMessage as G, type HandshakeMessage as H, InvalidScopeError as I, isBlobChunkResponseMessage as J, isErrorMessage as K, isHandshakeMessage as L, isHandshakeResponseMessage as M, isOperationBatchMessage as N, type OperationBatchMessage as O, isSyncMessage as P, type QueueStorage as Q, isYjsDocUpdateMessage as R, type SyncScopeMap as S, type TransportOptions as T, operationMatchesQuerySubsets as U, type VersionedKey as V, type WireFormat as W, type YjsDocUpdateMessage as Y, type SyncMessage as a, type SerializedOperation as b, type SyncTransport as c, type TransportMessageHandler as d, type TransportCloseHandler as e, type TransportErrorHandler as f, type BlobChunkResponseMessage as g, type BlobChunkPushMessage as h, type SyncEncryptionConfig as i, type SyncState as j, type SyncStatusInfo as k, type SyncConfig as l, type SyncStatePersistence as m, type SyncQuerySubset as n, type AwarenessStateWire as o, type AwarenessUpdateMessage as p, type ErrorMessage as q, type HandshakeResponseMessage as r, SYNC_STATES as s, SYNC_STATUSES as t, ScopeViolationError as u, type SyncEncryptionAlgorithm as v, type SyncScopeContext as w, type SyncStatus as x, dedupeQuerySubsets as y, isAcknowledgmentMessage as z };
727
+ export { isHandshakeResponseMessage as $, type AcknowledgmentMessage as A, type BlobChunkRequestMessage as B, type SyncScopeContext as C, type DeltaCursor as D, type EncryptedPayload as E, type SyncSettlementOptions as F, type SyncSettlementResult as G, type HandshakeMessage as H, InvalidScopeError as I, type SyncStatus as J, dedupeQuerySubsets as K, isAcknowledgmentMessage as L, MemoryRejectedOperationStorage as M, isAwarenessUpdateMessage as N, type OperationBatchMessage as O, isBlobChunkPushMessage as P, type QueueStorage as Q, type RejectedOperationStorage as R, type SyncScopeMap as S, type TransportOptions as T, isBlobChunkRequestMessage as U, type VersionedKey as V, type WireFormat as W, isBlobChunkResponseMessage as X, type YjsDocUpdateMessage as Y, isErrorMessage as Z, isHandshakeMessage as _, type SyncMessage as a, isOperationBatchMessage as a0, isOperationRejectedMessage as a1, isSyncMessage as a2, isYjsDocUpdateMessage as a3, operationMatchesQuerySubsets as a4, querySubsetContains as a5, type SerializedOperation as b, type SyncTransport as c, type TransportMessageHandler as d, type TransportCloseHandler as e, type TransportErrorHandler as f, type BlobChunkResponseMessage as g, type BlobChunkPushMessage as h, type SyncEncryptionConfig as i, type SyncState as j, type SyncStatusInfo as k, type SyncConfig as l, type SyncStatePersistence as m, type SyncQuerySubset as n, type RejectedOperation as o, type ActiveApplyFailure as p, type AwarenessStateWire as q, type AwarenessUpdateMessage as r, type ErrorMessage as s, type HandshakeResponseMessage as t, type OperationRejectedMessage as u, SYNC_STATES as v, SYNC_STATUSES as w, ScopeViolationError as x, type SyncEncryptionAlgorithm as y, type SyncPhase as z };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@korajs/sync",
3
- "version": "1.0.0-beta.1",
3
+ "version": "1.0.0-beta.11",
4
4
  "description": "Sync protocol and transports for Kora.js with delta sync, causal ordering, and automatic reconnection",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -34,8 +34,8 @@
34
34
  ],
35
35
  "dependencies": {
36
36
  "protobufjs": "^8.0.1",
37
- "@korajs/core": "1.0.0-beta.1",
38
- "@korajs/merge": "1.0.0-beta.1"
37
+ "@korajs/core": "1.0.0-beta.11",
38
+ "@korajs/merge": "1.0.0-beta.11"
39
39
  },
40
40
  "devDependencies": {
41
41
  "@fast-check/vitest": "0.2.0",