@korajs/sync 0.6.1 → 1.0.0-beta.1

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, g as SyncEncryptionConfig, V as VersionedKey, Y as YjsDocUpdateMessage, Q as QueueStorage, h as SyncState, i as SyncStatusInfo, j as SyncConfig, k as SyncStatePersistence, l as SyncQuerySubset } from './transport-BrdBj7rH.cjs';
2
- export { A as AcknowledgmentMessage, m as AwarenessStateWire, n as AwarenessUpdateMessage, E as EncryptedPayload, o as ErrorMessage, H as HandshakeMessage, p as HandshakeResponseMessage, I as InvalidScopeError, O as OperationBatchMessage, q as SYNC_STATES, r as SYNC_STATUSES, s as ScopeViolationError, t as SyncEncryptionAlgorithm, u as SyncScopeContext, v as SyncStatus, w as dedupeQuerySubsets, x as isAcknowledgmentMessage, y as isAwarenessUpdateMessage, z as isErrorMessage, B as isHandshakeMessage, C as isHandshakeResponseMessage, F as isOperationBatchMessage, G as isSyncMessage, J as isYjsDocUpdateMessage, K as operationMatchesQuerySubsets } from './transport-BrdBj7rH.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, m as SyncStatePersistence, n as SyncQuerySubset } from './transport-CZLMdZ0k.cjs';
2
+ export { A as AcknowledgmentMessage, o as AwarenessStateWire, p as AwarenessUpdateMessage, E as EncryptedPayload, q as ErrorMessage, H as HandshakeMessage, r as HandshakeResponseMessage, I as InvalidScopeError, O as OperationBatchMessage, s as SYNC_STATES, t as SYNC_STATUSES, u as ScopeViolationError, v as SyncEncryptionAlgorithm, w as SyncScopeContext, x as SyncStatus, y as dedupeQuerySubsets, z as isAcknowledgmentMessage, C as isAwarenessUpdateMessage, F as isBlobChunkPushMessage, G as isBlobChunkRequestMessage, J as isBlobChunkResponseMessage, K as isErrorMessage, L as isHandshakeMessage, M as isHandshakeResponseMessage, N as isOperationBatchMessage, P as isSyncMessage, R as isYjsDocUpdateMessage, U as operationMatchesQuerySubsets } from './transport-CZLMdZ0k.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';
@@ -81,6 +81,20 @@ interface SyncStore {
81
81
  * @param toSeq - End sequence number (inclusive)
82
82
  */
83
83
  getOperationRange(nodeId: string, fromSeq: number, toSeq: number): Promise<Operation[]>;
84
+ /**
85
+ * Optional: re-stamp never-acknowledged local operations after a fast device
86
+ * clock was corrected (timestamp rebase). Optional so hand-rolled SyncStore
87
+ * implementations keep working; when absent the engine simply skips the
88
+ * rebase and falls back to the clock-block behavior.
89
+ *
90
+ * @param ids - Operation ids that are candidates for re-stamping
91
+ * @param correctedNowMs - Trusted "now" (server time at handshake) in ms
92
+ */
93
+ rebaseUnsyncedOperations?(ids: string[], correctedNowMs: number): Promise<{
94
+ operations: Operation[];
95
+ idMapping: Record<string, string>;
96
+ rebasedCount: number;
97
+ }>;
84
98
  }
85
99
 
86
100
  /** Prefix for handshake rejection when client schema version is unsupported. */
@@ -275,7 +289,7 @@ declare class ChaosTransport implements SyncTransport {
275
289
  private readonly random;
276
290
  private messageHandler;
277
291
  private reorderBuffer;
278
- private timers;
292
+ private pending;
279
293
  constructor(inner: SyncTransport, config?: ChaosConfig);
280
294
  connect(url: string, options?: TransportOptions): Promise<void>;
281
295
  disconnect(): Promise<void>;
@@ -456,6 +470,71 @@ declare class AwarenessManager {
456
470
  private emitAwarenessEvent;
457
471
  }
458
472
 
473
+ /**
474
+ * A blob-chunk message in decoded form (chunk bytes as a `Uint8Array` rather
475
+ * than the base64 wire string). This is the shape callers of the channel work
476
+ * with; the channel handles base64 encode/decode at the wire boundary.
477
+ *
478
+ * The shape mirrors `@korajs/store`'s `ChunkMessage` so an app-layer adapter can
479
+ * bridge the two with no field translation.
480
+ */
481
+ type BlobChunkChannelMessage = {
482
+ type: 'blob-chunk-request';
483
+ requestId: string;
484
+ hash: string;
485
+ } | {
486
+ type: 'blob-chunk-response';
487
+ requestId: string;
488
+ bytes: Uint8Array | null;
489
+ } | {
490
+ type: 'blob-chunk-push';
491
+ hash: string;
492
+ bytes: Uint8Array;
493
+ };
494
+ /** Encode chunk bytes to a base64 string for the JSON wire. */
495
+ declare function encodeBlobChunkBytes(bytes: Uint8Array): string;
496
+ /** Decode base64 chunk bytes from the wire back to a `Uint8Array`. */
497
+ declare function decodeBlobChunkBytes(encoded: string): Uint8Array;
498
+ interface BlobChunkChannelOptions {
499
+ /**
500
+ * Called when a local blob-chunk message should go out on the wire. The
501
+ * caller (the sync engine) decides whether the connection is ready to send.
502
+ */
503
+ onSend?: (message: BlobChunkRequestMessage | BlobChunkResponseMessage | BlobChunkPushMessage) => void;
504
+ }
505
+ /**
506
+ * Ephemeral side channel for out-of-band blob chunk transfer over the sync
507
+ * connection. Symmetric: a peer can both request chunks it needs and answer
508
+ * requests for chunks it holds. Never persisted in the operation log — durable
509
+ * state is the `BlobRef` inside a record's fields; this channel only moves the
510
+ * referenced bytes.
511
+ *
512
+ * The channel owns the base64 wire encoding so callers work purely in
513
+ * `Uint8Array`, and its {@link BlobChunkChannelMessage} shape mirrors
514
+ * `@korajs/store`'s `ChunkMessage` so the app layer can expose it as a
515
+ * `ChunkMessagePort` without translating fields.
516
+ */
517
+ declare class BlobChunkChannel {
518
+ private readonly onSend;
519
+ private readonly handlers;
520
+ constructor(options?: BlobChunkChannelOptions);
521
+ /**
522
+ * Send a blob-chunk message to the peer. Response bytes are base64-encoded
523
+ * onto the wire here; callers pass a `Uint8Array`.
524
+ */
525
+ send(message: BlobChunkChannelMessage): void;
526
+ /**
527
+ * Register a handler for inbound blob-chunk messages. Multiple handlers are
528
+ * allowed; each receives every delivered message.
529
+ */
530
+ onMessage(handler: (message: BlobChunkChannelMessage) => void): () => void;
531
+ /**
532
+ * Deliver an inbound wire message to registered handlers, decoding response
533
+ * bytes from base64 back to a `Uint8Array` first.
534
+ */
535
+ deliver(message: BlobChunkRequestMessage | BlobChunkResponseMessage | BlobChunkPushMessage): void;
536
+ }
537
+
459
538
  /**
460
539
  * Configuration for the SyncMetricsCollector.
461
540
  */
@@ -529,11 +608,13 @@ declare class SyncEncryptor {
529
608
  * @param config - Encryption configuration with passphrase
530
609
  * @param salt - Optional salt for deterministic key derivation (mainly for testing).
531
610
  * If omitted, a random salt is generated.
611
+ * @param iterations - Optional PBKDF2 iteration count. Defaults to the
612
+ * production-strength value. Lower it only in tests.
532
613
  * @returns A configured SyncEncryptor instance
533
614
  * @throws {EncryptionError} If configuration is invalid
534
615
  * @throws {KeyDerivationError} If key derivation fails
535
616
  */
536
- static create(config: SyncEncryptionConfig, salt?: Uint8Array): Promise<SyncEncryptor>;
617
+ static create(config: SyncEncryptionConfig, salt?: Uint8Array, iterations?: number): Promise<SyncEncryptor>;
537
618
  /**
538
619
  * Creates a SyncEncryptor from pre-derived versioned keys.
539
620
  *
@@ -745,6 +826,20 @@ declare class OutboundQueue {
745
826
  * Peek at the first `count` operations without removing them.
746
827
  */
747
828
  peek(count: number): Operation[];
829
+ /**
830
+ * All queued operations (not counting in-flight), in causal order.
831
+ */
832
+ getAll(): Operation[];
833
+ /**
834
+ * Atomically replace the entire queue with a new set of operations.
835
+ *
836
+ * Used after a timestamp rebase rewrote the queued operations under new
837
+ * content-addressed ids: the old entries must vanish from memory AND from
838
+ * persistent storage in one step, or a page refresh could resurrect
839
+ * stale-stamped ops the server would reject. Resets the seen set to exactly
840
+ * the new ids so the rewritten ops are not treated as duplicates.
841
+ */
842
+ replaceAll(ops: Operation[]): Promise<void>;
748
843
  /**
749
844
  * Whether initialize() has been called.
750
845
  */
@@ -820,6 +915,7 @@ declare class SyncEngine {
820
915
  private readonly encryptor;
821
916
  private readonly awarenessManager;
822
917
  private readonly richtextDocChannel;
918
+ private readonly blobChunkChannel;
823
919
  private readonly metricsCollector;
824
920
  private readonly syncState;
825
921
  private remoteVector;
@@ -832,6 +928,9 @@ declare class SyncEngine {
832
928
  private currentBatch;
833
929
  private reconnecting;
834
930
  private schemaBlocked;
931
+ private clockBlocked;
932
+ private clockSkewMs;
933
+ private blobStorageEnabled;
835
934
  private deltaBatchesReceived;
836
935
  private deltaReceiveComplete;
837
936
  private deltaSendComplete;
@@ -885,6 +984,18 @@ declare class SyncEngine {
885
984
  * Sync stays blocked until the app schema is upgraded or sync config changes.
886
985
  */
887
986
  isSchemaBlocked(): boolean;
987
+ /**
988
+ * True when sync is blocked because this device's clock is too far ahead.
989
+ * Local writes continue to work and queue; only sync is paused.
990
+ */
991
+ isClockBlocked(): boolean;
992
+ /** serverTime - localTime measured at the last handshake, or null before first connect. */
993
+ getClockSkewMs(): number | null;
994
+ /**
995
+ * Clears the clock block after the user corrects the device clock.
996
+ * Moves the engine back to `disconnected` so `start()` can run again.
997
+ */
998
+ clearClockBlock(): void;
888
999
  /**
889
1000
  * Clears schema-mismatch block after upgrading the local schema / sync config.
890
1001
  * Moves the engine back to `disconnected` so `start()` can run again.
@@ -954,11 +1065,42 @@ declare class SyncEngine {
954
1065
  * Optional side channel for incremental Yjs updates on large richtext fields.
955
1066
  */
956
1067
  getRichtextDocChannel(): RichtextDocChannel;
1068
+ /**
1069
+ * Side channel for out-of-band blob chunk transfer over the sync connection.
1070
+ * The app layer bridges this to a `ChunkMessagePort` to pull/serve blob bytes.
1071
+ */
1072
+ getBlobChunkChannel(): BlobChunkChannel;
1073
+ /**
1074
+ * Whether the connected server persists blob bytes centrally (learned from the
1075
+ * handshake response). When true, the app uploads the bytes behind `blob`
1076
+ * fields so they stay available after the authoring device goes offline.
1077
+ */
1078
+ isBlobStorageEnabled(): boolean;
1079
+ /**
1080
+ * Upload a blob chunk (or manifest) to the server for central persistence.
1081
+ * A no-op unless connected and the server advertised blob storage.
1082
+ */
1083
+ uploadBlobChunk(hash: string, bytes: Uint8Array): void;
957
1084
  private messageChain;
958
1085
  private enqueueMessage;
959
1086
  private handleMessageAsync;
960
1087
  private handleMessageFailure;
1088
+ /**
1089
+ * Compares server wall-clock time from the handshake with local time.
1090
+ * Negative skew = this device's clock is fast (dangerous for LWW and rejected
1091
+ * by server ingest beyond 60s). Positive skew = slow (accepted but surfaced).
1092
+ * Zero developer work required: the result flows into sync status and events.
1093
+ */
1094
+ private evaluateClockSkew;
961
1095
  private handleHandshakeResponse;
1096
+ /**
1097
+ * Re-stamps queued (never-acknowledged) operations whose timestamps are far
1098
+ * enough in the future that the server would reject them, using the server's
1099
+ * own handshake time as the trusted "now". This is the automatic recovery
1100
+ * path after a user corrects a fast device clock: the queue drains
1101
+ * immediately instead of waiting for real time to catch up.
1102
+ */
1103
+ private maybeRebaseQueuedOperations;
962
1104
  private sendDelta;
963
1105
  private markDeltaSendCompleteIfReady;
964
1106
  private collectDelta;
@@ -1164,6 +1306,9 @@ declare function generateSalt(): Uint8Array;
1164
1306
  *
1165
1307
  * @param passphrase - The user's passphrase (must be non-empty)
1166
1308
  * @param salt - Optional salt bytes. If omitted, a random 32-byte salt is generated.
1309
+ * @param iterations - Optional PBKDF2 iteration count. Defaults to
1310
+ * {@link DEFAULT_PBKDF2_ITERATIONS}. Lower it only in tests; production must
1311
+ * keep the default so derived keys stay brute-force resistant.
1167
1312
  * @returns The derived CryptoKey and the salt used
1168
1313
  * @throws {KeyDerivationError} If the passphrase is empty, crypto is unavailable, or derivation fails
1169
1314
  *
@@ -1176,7 +1321,7 @@ declare function generateSalt(): Uint8Array;
1176
1321
  * const { key: sameKey } = await deriveKey('my-secure-passphrase', salt)
1177
1322
  * ```
1178
1323
  */
1179
- declare function deriveKey(passphrase: string, salt?: Uint8Array): Promise<{
1324
+ declare function deriveKey(passphrase: string, salt?: Uint8Array, iterations?: number): Promise<{
1180
1325
  key: CryptoKey;
1181
1326
  salt: Uint8Array;
1182
1327
  }>;
@@ -1190,10 +1335,12 @@ declare function deriveKey(passphrase: string, salt?: Uint8Array): Promise<{
1190
1335
  * @param passphrase - The user's passphrase
1191
1336
  * @param version - Key version number (must be a positive integer)
1192
1337
  * @param salt - Optional salt bytes. If omitted, a random salt is generated.
1338
+ * @param iterations - Optional PBKDF2 iteration count. Defaults to
1339
+ * {@link DEFAULT_PBKDF2_ITERATIONS}. Lower it only in tests.
1193
1340
  * @returns A {@link VersionedKey} containing the key, version, and salt
1194
1341
  * @throws {KeyDerivationError} If parameters are invalid or derivation fails
1195
1342
  */
1196
- declare function deriveVersionedKey(passphrase: string, version: number, salt?: Uint8Array): Promise<VersionedKey>;
1343
+ declare function deriveVersionedKey(passphrase: string, version: number, salt?: Uint8Array, iterations?: number): Promise<VersionedKey>;
1197
1344
 
1198
1345
  /** Default status when sync is not configured or the engine is unavailable. */
1199
1346
  declare const OFFLINE_SYNC_STATUS: SyncStatusInfo;
@@ -1228,4 +1375,4 @@ declare function getRemoteAwarenessStates(awareness: AwarenessManager): Awarenes
1228
1375
  */
1229
1376
  declare function subscribeRemoteAwarenessStates(awareness: AwarenessManager, listener: (states: AwarenessState[]) => void): () => void;
1230
1377
 
1231
- export { type AwarenessChange, type AwarenessCursor, AwarenessManager, type AwarenessMessage, type AwarenessState, type AwarenessUser, type ChaosConfig, ChaosTransport, ConnectionMonitor, type ConnectionMonitorConfig, type CursorInfo, DEFAULT_RICHTEXT_DOC_CHANNEL_THRESHOLD, DecryptionError, DeltaCursor, EncryptionError, HttpLongPollingTransport, type HttpLongPollingTransportOptions, JsonMessageSerializer, KeyDerivationError, type MessageSerializer, NegotiatedMessageSerializer, OFFLINE_SYNC_STATUS, type OutboundBatch, OutboundQueue, ProtobufMessageSerializer, QueueStorage, type ReconnectionConfig, ReconnectionManager, RichtextDocChannel, type RichtextDocChannelOptions, type RichtextDocListener, SCHEMA_MISMATCH_PREFIX, SerializedOperation, SyncConfig, type SyncDiagnostics, SyncEncryptionConfig, SyncEncryptor, SyncEngine, type SyncEngineOptions, SyncMessage, SyncQuerySubset, SyncScopeMap, SyncState, SyncStatePersistence, type SyncStatusController, type SyncStatusControllerOptions, SyncStatusInfo, type SyncStore, SyncTransport, TransportCloseHandler, TransportErrorHandler, TransportMessageHandler, TransportOptions, VersionedKey, type WebSocketConstructor, type WebSocketLike, WebSocketTransport, type WebSocketTransportOptions, WireFormat, YjsDocUpdateMessage, createDeltaCursorFromBatch, createSyncStatusController, decodeDeltaCursor, decodeYjsUpdate, deriveKey, deriveVersionedKey, encodeDeltaCursor, encodeYjsUpdate, filterOperationsByScope, generateSalt, getRemoteAwarenessStates, isClientSchemaVersionSupported, isEncryptedPayload, isSchemaMismatchReject, operationMatchesScope, richtextDocKey, sliceOperationsAfterCursor, subscribeRemoteAwarenessStates, versionVectorToWire, wireToVersionVector };
1378
+ export { type AwarenessChange, type AwarenessCursor, AwarenessManager, type AwarenessMessage, type AwarenessState, type AwarenessUser, BlobChunkChannel, type BlobChunkChannelMessage, type BlobChunkChannelOptions, BlobChunkPushMessage, BlobChunkRequestMessage, BlobChunkResponseMessage, type ChaosConfig, ChaosTransport, ConnectionMonitor, type ConnectionMonitorConfig, type CursorInfo, DEFAULT_RICHTEXT_DOC_CHANNEL_THRESHOLD, DecryptionError, DeltaCursor, EncryptionError, HttpLongPollingTransport, type HttpLongPollingTransportOptions, JsonMessageSerializer, KeyDerivationError, type MessageSerializer, NegotiatedMessageSerializer, OFFLINE_SYNC_STATUS, type OutboundBatch, OutboundQueue, ProtobufMessageSerializer, QueueStorage, type ReconnectionConfig, ReconnectionManager, RichtextDocChannel, type RichtextDocChannelOptions, type RichtextDocListener, SCHEMA_MISMATCH_PREFIX, SerializedOperation, SyncConfig, type SyncDiagnostics, SyncEncryptionConfig, SyncEncryptor, SyncEngine, type SyncEngineOptions, SyncMessage, SyncQuerySubset, SyncScopeMap, SyncState, SyncStatePersistence, type SyncStatusController, type SyncStatusControllerOptions, SyncStatusInfo, type SyncStore, SyncTransport, TransportCloseHandler, TransportErrorHandler, TransportMessageHandler, TransportOptions, VersionedKey, type WebSocketConstructor, type WebSocketLike, WebSocketTransport, type WebSocketTransportOptions, WireFormat, YjsDocUpdateMessage, createDeltaCursorFromBatch, createSyncStatusController, decodeBlobChunkBytes, decodeDeltaCursor, decodeYjsUpdate, deriveKey, deriveVersionedKey, encodeBlobChunkBytes, encodeDeltaCursor, encodeYjsUpdate, filterOperationsByScope, generateSalt, getRemoteAwarenessStates, isClientSchemaVersionSupported, isEncryptedPayload, isSchemaMismatchReject, operationMatchesScope, richtextDocKey, sliceOperationsAfterCursor, subscribeRemoteAwarenessStates, versionVectorToWire, wireToVersionVector };
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, g as SyncEncryptionConfig, V as VersionedKey, Y as YjsDocUpdateMessage, Q as QueueStorage, h as SyncState, i as SyncStatusInfo, j as SyncConfig, k as SyncStatePersistence, l as SyncQuerySubset } from './transport-BrdBj7rH.js';
2
- export { A as AcknowledgmentMessage, m as AwarenessStateWire, n as AwarenessUpdateMessage, E as EncryptedPayload, o as ErrorMessage, H as HandshakeMessage, p as HandshakeResponseMessage, I as InvalidScopeError, O as OperationBatchMessage, q as SYNC_STATES, r as SYNC_STATUSES, s as ScopeViolationError, t as SyncEncryptionAlgorithm, u as SyncScopeContext, v as SyncStatus, w as dedupeQuerySubsets, x as isAcknowledgmentMessage, y as isAwarenessUpdateMessage, z as isErrorMessage, B as isHandshakeMessage, C as isHandshakeResponseMessage, F as isOperationBatchMessage, G as isSyncMessage, J as isYjsDocUpdateMessage, K as operationMatchesQuerySubsets } from './transport-BrdBj7rH.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, m as SyncStatePersistence, n as SyncQuerySubset } from './transport-CZLMdZ0k.js';
2
+ export { A as AcknowledgmentMessage, o as AwarenessStateWire, p as AwarenessUpdateMessage, E as EncryptedPayload, q as ErrorMessage, H as HandshakeMessage, r as HandshakeResponseMessage, I as InvalidScopeError, O as OperationBatchMessage, s as SYNC_STATES, t as SYNC_STATUSES, u as ScopeViolationError, v as SyncEncryptionAlgorithm, w as SyncScopeContext, x as SyncStatus, y as dedupeQuerySubsets, z as isAcknowledgmentMessage, C as isAwarenessUpdateMessage, F as isBlobChunkPushMessage, G as isBlobChunkRequestMessage, J as isBlobChunkResponseMessage, K as isErrorMessage, L as isHandshakeMessage, M as isHandshakeResponseMessage, N as isOperationBatchMessage, P as isSyncMessage, R as isYjsDocUpdateMessage, U as operationMatchesQuerySubsets } from './transport-CZLMdZ0k.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';
@@ -81,6 +81,20 @@ interface SyncStore {
81
81
  * @param toSeq - End sequence number (inclusive)
82
82
  */
83
83
  getOperationRange(nodeId: string, fromSeq: number, toSeq: number): Promise<Operation[]>;
84
+ /**
85
+ * Optional: re-stamp never-acknowledged local operations after a fast device
86
+ * clock was corrected (timestamp rebase). Optional so hand-rolled SyncStore
87
+ * implementations keep working; when absent the engine simply skips the
88
+ * rebase and falls back to the clock-block behavior.
89
+ *
90
+ * @param ids - Operation ids that are candidates for re-stamping
91
+ * @param correctedNowMs - Trusted "now" (server time at handshake) in ms
92
+ */
93
+ rebaseUnsyncedOperations?(ids: string[], correctedNowMs: number): Promise<{
94
+ operations: Operation[];
95
+ idMapping: Record<string, string>;
96
+ rebasedCount: number;
97
+ }>;
84
98
  }
85
99
 
86
100
  /** Prefix for handshake rejection when client schema version is unsupported. */
@@ -275,7 +289,7 @@ declare class ChaosTransport implements SyncTransport {
275
289
  private readonly random;
276
290
  private messageHandler;
277
291
  private reorderBuffer;
278
- private timers;
292
+ private pending;
279
293
  constructor(inner: SyncTransport, config?: ChaosConfig);
280
294
  connect(url: string, options?: TransportOptions): Promise<void>;
281
295
  disconnect(): Promise<void>;
@@ -456,6 +470,71 @@ declare class AwarenessManager {
456
470
  private emitAwarenessEvent;
457
471
  }
458
472
 
473
+ /**
474
+ * A blob-chunk message in decoded form (chunk bytes as a `Uint8Array` rather
475
+ * than the base64 wire string). This is the shape callers of the channel work
476
+ * with; the channel handles base64 encode/decode at the wire boundary.
477
+ *
478
+ * The shape mirrors `@korajs/store`'s `ChunkMessage` so an app-layer adapter can
479
+ * bridge the two with no field translation.
480
+ */
481
+ type BlobChunkChannelMessage = {
482
+ type: 'blob-chunk-request';
483
+ requestId: string;
484
+ hash: string;
485
+ } | {
486
+ type: 'blob-chunk-response';
487
+ requestId: string;
488
+ bytes: Uint8Array | null;
489
+ } | {
490
+ type: 'blob-chunk-push';
491
+ hash: string;
492
+ bytes: Uint8Array;
493
+ };
494
+ /** Encode chunk bytes to a base64 string for the JSON wire. */
495
+ declare function encodeBlobChunkBytes(bytes: Uint8Array): string;
496
+ /** Decode base64 chunk bytes from the wire back to a `Uint8Array`. */
497
+ declare function decodeBlobChunkBytes(encoded: string): Uint8Array;
498
+ interface BlobChunkChannelOptions {
499
+ /**
500
+ * Called when a local blob-chunk message should go out on the wire. The
501
+ * caller (the sync engine) decides whether the connection is ready to send.
502
+ */
503
+ onSend?: (message: BlobChunkRequestMessage | BlobChunkResponseMessage | BlobChunkPushMessage) => void;
504
+ }
505
+ /**
506
+ * Ephemeral side channel for out-of-band blob chunk transfer over the sync
507
+ * connection. Symmetric: a peer can both request chunks it needs and answer
508
+ * requests for chunks it holds. Never persisted in the operation log — durable
509
+ * state is the `BlobRef` inside a record's fields; this channel only moves the
510
+ * referenced bytes.
511
+ *
512
+ * The channel owns the base64 wire encoding so callers work purely in
513
+ * `Uint8Array`, and its {@link BlobChunkChannelMessage} shape mirrors
514
+ * `@korajs/store`'s `ChunkMessage` so the app layer can expose it as a
515
+ * `ChunkMessagePort` without translating fields.
516
+ */
517
+ declare class BlobChunkChannel {
518
+ private readonly onSend;
519
+ private readonly handlers;
520
+ constructor(options?: BlobChunkChannelOptions);
521
+ /**
522
+ * Send a blob-chunk message to the peer. Response bytes are base64-encoded
523
+ * onto the wire here; callers pass a `Uint8Array`.
524
+ */
525
+ send(message: BlobChunkChannelMessage): void;
526
+ /**
527
+ * Register a handler for inbound blob-chunk messages. Multiple handlers are
528
+ * allowed; each receives every delivered message.
529
+ */
530
+ onMessage(handler: (message: BlobChunkChannelMessage) => void): () => void;
531
+ /**
532
+ * Deliver an inbound wire message to registered handlers, decoding response
533
+ * bytes from base64 back to a `Uint8Array` first.
534
+ */
535
+ deliver(message: BlobChunkRequestMessage | BlobChunkResponseMessage | BlobChunkPushMessage): void;
536
+ }
537
+
459
538
  /**
460
539
  * Configuration for the SyncMetricsCollector.
461
540
  */
@@ -529,11 +608,13 @@ declare class SyncEncryptor {
529
608
  * @param config - Encryption configuration with passphrase
530
609
  * @param salt - Optional salt for deterministic key derivation (mainly for testing).
531
610
  * If omitted, a random salt is generated.
611
+ * @param iterations - Optional PBKDF2 iteration count. Defaults to the
612
+ * production-strength value. Lower it only in tests.
532
613
  * @returns A configured SyncEncryptor instance
533
614
  * @throws {EncryptionError} If configuration is invalid
534
615
  * @throws {KeyDerivationError} If key derivation fails
535
616
  */
536
- static create(config: SyncEncryptionConfig, salt?: Uint8Array): Promise<SyncEncryptor>;
617
+ static create(config: SyncEncryptionConfig, salt?: Uint8Array, iterations?: number): Promise<SyncEncryptor>;
537
618
  /**
538
619
  * Creates a SyncEncryptor from pre-derived versioned keys.
539
620
  *
@@ -745,6 +826,20 @@ declare class OutboundQueue {
745
826
  * Peek at the first `count` operations without removing them.
746
827
  */
747
828
  peek(count: number): Operation[];
829
+ /**
830
+ * All queued operations (not counting in-flight), in causal order.
831
+ */
832
+ getAll(): Operation[];
833
+ /**
834
+ * Atomically replace the entire queue with a new set of operations.
835
+ *
836
+ * Used after a timestamp rebase rewrote the queued operations under new
837
+ * content-addressed ids: the old entries must vanish from memory AND from
838
+ * persistent storage in one step, or a page refresh could resurrect
839
+ * stale-stamped ops the server would reject. Resets the seen set to exactly
840
+ * the new ids so the rewritten ops are not treated as duplicates.
841
+ */
842
+ replaceAll(ops: Operation[]): Promise<void>;
748
843
  /**
749
844
  * Whether initialize() has been called.
750
845
  */
@@ -820,6 +915,7 @@ declare class SyncEngine {
820
915
  private readonly encryptor;
821
916
  private readonly awarenessManager;
822
917
  private readonly richtextDocChannel;
918
+ private readonly blobChunkChannel;
823
919
  private readonly metricsCollector;
824
920
  private readonly syncState;
825
921
  private remoteVector;
@@ -832,6 +928,9 @@ declare class SyncEngine {
832
928
  private currentBatch;
833
929
  private reconnecting;
834
930
  private schemaBlocked;
931
+ private clockBlocked;
932
+ private clockSkewMs;
933
+ private blobStorageEnabled;
835
934
  private deltaBatchesReceived;
836
935
  private deltaReceiveComplete;
837
936
  private deltaSendComplete;
@@ -885,6 +984,18 @@ declare class SyncEngine {
885
984
  * Sync stays blocked until the app schema is upgraded or sync config changes.
886
985
  */
887
986
  isSchemaBlocked(): boolean;
987
+ /**
988
+ * True when sync is blocked because this device's clock is too far ahead.
989
+ * Local writes continue to work and queue; only sync is paused.
990
+ */
991
+ isClockBlocked(): boolean;
992
+ /** serverTime - localTime measured at the last handshake, or null before first connect. */
993
+ getClockSkewMs(): number | null;
994
+ /**
995
+ * Clears the clock block after the user corrects the device clock.
996
+ * Moves the engine back to `disconnected` so `start()` can run again.
997
+ */
998
+ clearClockBlock(): void;
888
999
  /**
889
1000
  * Clears schema-mismatch block after upgrading the local schema / sync config.
890
1001
  * Moves the engine back to `disconnected` so `start()` can run again.
@@ -954,11 +1065,42 @@ declare class SyncEngine {
954
1065
  * Optional side channel for incremental Yjs updates on large richtext fields.
955
1066
  */
956
1067
  getRichtextDocChannel(): RichtextDocChannel;
1068
+ /**
1069
+ * Side channel for out-of-band blob chunk transfer over the sync connection.
1070
+ * The app layer bridges this to a `ChunkMessagePort` to pull/serve blob bytes.
1071
+ */
1072
+ getBlobChunkChannel(): BlobChunkChannel;
1073
+ /**
1074
+ * Whether the connected server persists blob bytes centrally (learned from the
1075
+ * handshake response). When true, the app uploads the bytes behind `blob`
1076
+ * fields so they stay available after the authoring device goes offline.
1077
+ */
1078
+ isBlobStorageEnabled(): boolean;
1079
+ /**
1080
+ * Upload a blob chunk (or manifest) to the server for central persistence.
1081
+ * A no-op unless connected and the server advertised blob storage.
1082
+ */
1083
+ uploadBlobChunk(hash: string, bytes: Uint8Array): void;
957
1084
  private messageChain;
958
1085
  private enqueueMessage;
959
1086
  private handleMessageAsync;
960
1087
  private handleMessageFailure;
1088
+ /**
1089
+ * Compares server wall-clock time from the handshake with local time.
1090
+ * Negative skew = this device's clock is fast (dangerous for LWW and rejected
1091
+ * by server ingest beyond 60s). Positive skew = slow (accepted but surfaced).
1092
+ * Zero developer work required: the result flows into sync status and events.
1093
+ */
1094
+ private evaluateClockSkew;
961
1095
  private handleHandshakeResponse;
1096
+ /**
1097
+ * Re-stamps queued (never-acknowledged) operations whose timestamps are far
1098
+ * enough in the future that the server would reject them, using the server's
1099
+ * own handshake time as the trusted "now". This is the automatic recovery
1100
+ * path after a user corrects a fast device clock: the queue drains
1101
+ * immediately instead of waiting for real time to catch up.
1102
+ */
1103
+ private maybeRebaseQueuedOperations;
962
1104
  private sendDelta;
963
1105
  private markDeltaSendCompleteIfReady;
964
1106
  private collectDelta;
@@ -1164,6 +1306,9 @@ declare function generateSalt(): Uint8Array;
1164
1306
  *
1165
1307
  * @param passphrase - The user's passphrase (must be non-empty)
1166
1308
  * @param salt - Optional salt bytes. If omitted, a random 32-byte salt is generated.
1309
+ * @param iterations - Optional PBKDF2 iteration count. Defaults to
1310
+ * {@link DEFAULT_PBKDF2_ITERATIONS}. Lower it only in tests; production must
1311
+ * keep the default so derived keys stay brute-force resistant.
1167
1312
  * @returns The derived CryptoKey and the salt used
1168
1313
  * @throws {KeyDerivationError} If the passphrase is empty, crypto is unavailable, or derivation fails
1169
1314
  *
@@ -1176,7 +1321,7 @@ declare function generateSalt(): Uint8Array;
1176
1321
  * const { key: sameKey } = await deriveKey('my-secure-passphrase', salt)
1177
1322
  * ```
1178
1323
  */
1179
- declare function deriveKey(passphrase: string, salt?: Uint8Array): Promise<{
1324
+ declare function deriveKey(passphrase: string, salt?: Uint8Array, iterations?: number): Promise<{
1180
1325
  key: CryptoKey;
1181
1326
  salt: Uint8Array;
1182
1327
  }>;
@@ -1190,10 +1335,12 @@ declare function deriveKey(passphrase: string, salt?: Uint8Array): Promise<{
1190
1335
  * @param passphrase - The user's passphrase
1191
1336
  * @param version - Key version number (must be a positive integer)
1192
1337
  * @param salt - Optional salt bytes. If omitted, a random salt is generated.
1338
+ * @param iterations - Optional PBKDF2 iteration count. Defaults to
1339
+ * {@link DEFAULT_PBKDF2_ITERATIONS}. Lower it only in tests.
1193
1340
  * @returns A {@link VersionedKey} containing the key, version, and salt
1194
1341
  * @throws {KeyDerivationError} If parameters are invalid or derivation fails
1195
1342
  */
1196
- declare function deriveVersionedKey(passphrase: string, version: number, salt?: Uint8Array): Promise<VersionedKey>;
1343
+ declare function deriveVersionedKey(passphrase: string, version: number, salt?: Uint8Array, iterations?: number): Promise<VersionedKey>;
1197
1344
 
1198
1345
  /** Default status when sync is not configured or the engine is unavailable. */
1199
1346
  declare const OFFLINE_SYNC_STATUS: SyncStatusInfo;
@@ -1228,4 +1375,4 @@ declare function getRemoteAwarenessStates(awareness: AwarenessManager): Awarenes
1228
1375
  */
1229
1376
  declare function subscribeRemoteAwarenessStates(awareness: AwarenessManager, listener: (states: AwarenessState[]) => void): () => void;
1230
1377
 
1231
- export { type AwarenessChange, type AwarenessCursor, AwarenessManager, type AwarenessMessage, type AwarenessState, type AwarenessUser, type ChaosConfig, ChaosTransport, ConnectionMonitor, type ConnectionMonitorConfig, type CursorInfo, DEFAULT_RICHTEXT_DOC_CHANNEL_THRESHOLD, DecryptionError, DeltaCursor, EncryptionError, HttpLongPollingTransport, type HttpLongPollingTransportOptions, JsonMessageSerializer, KeyDerivationError, type MessageSerializer, NegotiatedMessageSerializer, OFFLINE_SYNC_STATUS, type OutboundBatch, OutboundQueue, ProtobufMessageSerializer, QueueStorage, type ReconnectionConfig, ReconnectionManager, RichtextDocChannel, type RichtextDocChannelOptions, type RichtextDocListener, SCHEMA_MISMATCH_PREFIX, SerializedOperation, SyncConfig, type SyncDiagnostics, SyncEncryptionConfig, SyncEncryptor, SyncEngine, type SyncEngineOptions, SyncMessage, SyncQuerySubset, SyncScopeMap, SyncState, SyncStatePersistence, type SyncStatusController, type SyncStatusControllerOptions, SyncStatusInfo, type SyncStore, SyncTransport, TransportCloseHandler, TransportErrorHandler, TransportMessageHandler, TransportOptions, VersionedKey, type WebSocketConstructor, type WebSocketLike, WebSocketTransport, type WebSocketTransportOptions, WireFormat, YjsDocUpdateMessage, createDeltaCursorFromBatch, createSyncStatusController, decodeDeltaCursor, decodeYjsUpdate, deriveKey, deriveVersionedKey, encodeDeltaCursor, encodeYjsUpdate, filterOperationsByScope, generateSalt, getRemoteAwarenessStates, isClientSchemaVersionSupported, isEncryptedPayload, isSchemaMismatchReject, operationMatchesScope, richtextDocKey, sliceOperationsAfterCursor, subscribeRemoteAwarenessStates, versionVectorToWire, wireToVersionVector };
1378
+ export { type AwarenessChange, type AwarenessCursor, AwarenessManager, type AwarenessMessage, type AwarenessState, type AwarenessUser, BlobChunkChannel, type BlobChunkChannelMessage, type BlobChunkChannelOptions, BlobChunkPushMessage, BlobChunkRequestMessage, BlobChunkResponseMessage, type ChaosConfig, ChaosTransport, ConnectionMonitor, type ConnectionMonitorConfig, type CursorInfo, DEFAULT_RICHTEXT_DOC_CHANNEL_THRESHOLD, DecryptionError, DeltaCursor, EncryptionError, HttpLongPollingTransport, type HttpLongPollingTransportOptions, JsonMessageSerializer, KeyDerivationError, type MessageSerializer, NegotiatedMessageSerializer, OFFLINE_SYNC_STATUS, type OutboundBatch, OutboundQueue, ProtobufMessageSerializer, QueueStorage, type ReconnectionConfig, ReconnectionManager, RichtextDocChannel, type RichtextDocChannelOptions, type RichtextDocListener, SCHEMA_MISMATCH_PREFIX, SerializedOperation, SyncConfig, type SyncDiagnostics, SyncEncryptionConfig, SyncEncryptor, SyncEngine, type SyncEngineOptions, SyncMessage, SyncQuerySubset, SyncScopeMap, SyncState, SyncStatePersistence, type SyncStatusController, type SyncStatusControllerOptions, SyncStatusInfo, type SyncStore, SyncTransport, TransportCloseHandler, TransportErrorHandler, TransportMessageHandler, TransportOptions, VersionedKey, type WebSocketConstructor, type WebSocketLike, WebSocketTransport, type WebSocketTransportOptions, WireFormat, YjsDocUpdateMessage, createDeltaCursorFromBatch, createSyncStatusController, decodeBlobChunkBytes, decodeDeltaCursor, decodeYjsUpdate, deriveKey, deriveVersionedKey, encodeBlobChunkBytes, encodeDeltaCursor, encodeYjsUpdate, filterOperationsByScope, generateSalt, getRemoteAwarenessStates, isClientSchemaVersionSupported, isEncryptedPayload, isSchemaMismatchReject, operationMatchesScope, richtextDocKey, sliceOperationsAfterCursor, subscribeRemoteAwarenessStates, versionVectorToWire, wireToVersionVector };