@korajs/sync 0.3.2 → 0.5.0
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.cjs +2350 -199
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +684 -7
- package/dist/index.d.ts +684 -7
- package/dist/index.js +2334 -205
- package/dist/index.js.map +1 -1
- package/dist/internal.d.cts +1 -1
- package/dist/internal.d.ts +1 -1
- package/dist/transport-BrdBj7rH.d.cts +465 -0
- package/dist/transport-BrdBj7rH.d.ts +465 -0
- package/package.json +5 -4
- package/dist/transport-B5EFsr5F.d.cts +0 -215
- package/dist/transport-B5EFsr5F.d.ts +0 -215
package/dist/index.d.cts
CHANGED
|
@@ -1,11 +1,57 @@
|
|
|
1
|
-
import { S as
|
|
2
|
-
export { A as AcknowledgmentMessage, E as ErrorMessage, H as HandshakeMessage,
|
|
3
|
-
import
|
|
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';
|
|
3
|
+
import * as _korajs_core from '@korajs/core';
|
|
4
|
+
import { Operation, VersionVector, ApplyResult, KoraEventEmitter, TimeSource, SyncError, ConnectionQuality } from '@korajs/core';
|
|
5
|
+
export { ApplyFailureReason, ApplyResult } from '@korajs/core';
|
|
4
6
|
|
|
5
7
|
/**
|
|
6
|
-
*
|
|
8
|
+
* Check whether an operation matches the given scope map.
|
|
9
|
+
*
|
|
10
|
+
* Rules:
|
|
11
|
+
* - No scope map configured: operation is always in scope.
|
|
12
|
+
* - Collection not present in scope map: operation is out of scope.
|
|
13
|
+
* - Empty scope for a collection `{}`: no field restrictions, operation is in scope.
|
|
14
|
+
* - Scope has field/value pairs: all must match in the operation's data snapshot.
|
|
15
|
+
*
|
|
16
|
+
* For updates, the snapshot is built by merging `previousData` and `data` (data wins),
|
|
17
|
+
* which represents the record's state after the operation is applied. When an optional
|
|
18
|
+
* `fullRecord` is provided (e.g., from the local store), its values fill in scope fields
|
|
19
|
+
* that weren't included in the operation's data (critical for update operations where
|
|
20
|
+
* `data` only contains changed fields).
|
|
21
|
+
*
|
|
22
|
+
* @param op - The operation to check
|
|
23
|
+
* @param scopeMap - Per-collection scope filters, or undefined for no filtering
|
|
24
|
+
* @param fullRecord - Optional full record state from the store, used to fill in scope
|
|
25
|
+
* fields not present in the operation's partial data
|
|
26
|
+
* @returns true if the operation is within scope
|
|
27
|
+
*/
|
|
28
|
+
declare function operationMatchesScope(op: Operation, scopeMap: SyncScopeMap | undefined, fullRecord?: Record<string, unknown> | null): boolean;
|
|
29
|
+
/**
|
|
30
|
+
* Filter operations to only those matching the given scope map.
|
|
31
|
+
*
|
|
32
|
+
* @param operations - Array of operations to filter
|
|
33
|
+
* @param scopeMap - Per-collection scope filters
|
|
34
|
+
* @returns Operations that match the scope
|
|
35
|
+
*/
|
|
36
|
+
declare function filterOperationsByScope(operations: Operation[], scopeMap: SyncScopeMap | undefined): Operation[];
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Encode a delta cursor for wire transfer or `_kora_meta` persistence.
|
|
40
|
+
*/
|
|
41
|
+
declare function encodeDeltaCursor(cursor: DeltaCursor): string;
|
|
42
|
+
/**
|
|
43
|
+
* Decode a delta cursor string. Returns null when malformed.
|
|
44
|
+
*/
|
|
45
|
+
declare function decodeDeltaCursor(value: string | undefined | null): DeltaCursor | null;
|
|
46
|
+
/**
|
|
47
|
+
* Slice a causally sorted operation list to resume after a cursor.
|
|
48
|
+
*/
|
|
49
|
+
declare function sliceOperationsAfterCursor(operations: _korajs_core.Operation[], cursor: DeltaCursor | null): _korajs_core.Operation[];
|
|
50
|
+
/**
|
|
51
|
+
* Build a cursor from the last operation in a delta batch.
|
|
7
52
|
*/
|
|
8
|
-
|
|
53
|
+
declare function createDeltaCursorFromBatch(operations: _korajs_core.Operation[], batchIndex: number): DeltaCursor | null;
|
|
54
|
+
|
|
9
55
|
/**
|
|
10
56
|
* Interface that the local store must implement for sync.
|
|
11
57
|
* This decouples @korajs/sync from @korajs/store — the store satisfies this interface.
|
|
@@ -37,6 +83,20 @@ interface SyncStore {
|
|
|
37
83
|
getOperationRange(nodeId: string, fromSeq: number, toSeq: number): Promise<Operation[]>;
|
|
38
84
|
}
|
|
39
85
|
|
|
86
|
+
/** Prefix for handshake rejection when client schema version is unsupported. */
|
|
87
|
+
declare const SCHEMA_MISMATCH_PREFIX = "SCHEMA_MISMATCH";
|
|
88
|
+
/**
|
|
89
|
+
* Returns true when a handshake `rejectReason` indicates unsupported schema version.
|
|
90
|
+
*/
|
|
91
|
+
declare function isSchemaMismatchReject(rejectReason: string | undefined): boolean;
|
|
92
|
+
/**
|
|
93
|
+
* Returns true when the client schema version is within the inclusive supported range.
|
|
94
|
+
*/
|
|
95
|
+
declare function isClientSchemaVersionSupported(clientVersion: number, supported: {
|
|
96
|
+
min: number;
|
|
97
|
+
max: number;
|
|
98
|
+
}): boolean;
|
|
99
|
+
|
|
40
100
|
type EncodedMessage = string | Uint8Array;
|
|
41
101
|
/**
|
|
42
102
|
* Interface for encoding/decoding sync protocol messages.
|
|
@@ -229,6 +289,396 @@ declare class ChaosTransport implements SyncTransport {
|
|
|
229
289
|
private flushReorderBuffer;
|
|
230
290
|
}
|
|
231
291
|
|
|
292
|
+
/**
|
|
293
|
+
* Cursor position within a richtext field.
|
|
294
|
+
* Uses Yjs-compatible anchor/head positions for editor interop.
|
|
295
|
+
*/
|
|
296
|
+
interface AwarenessCursor {
|
|
297
|
+
/** Collection containing the record being edited */
|
|
298
|
+
collection: string;
|
|
299
|
+
/** ID of the record being edited */
|
|
300
|
+
recordId: string;
|
|
301
|
+
/** Field name of the richtext field */
|
|
302
|
+
field: string;
|
|
303
|
+
/** Cursor anchor position in Y.Text (start of selection) */
|
|
304
|
+
anchor: number;
|
|
305
|
+
/** Cursor head position in Y.Text (end of selection, same as anchor if no selection) */
|
|
306
|
+
head: number;
|
|
307
|
+
}
|
|
308
|
+
/**
|
|
309
|
+
* User identity information for presence display.
|
|
310
|
+
*/
|
|
311
|
+
interface AwarenessUser {
|
|
312
|
+
/** Display name of the user */
|
|
313
|
+
name: string;
|
|
314
|
+
/** Hex color for cursor/selection rendering (e.g. '#ff0000') */
|
|
315
|
+
color: string;
|
|
316
|
+
/** Optional avatar URL */
|
|
317
|
+
avatar?: string;
|
|
318
|
+
}
|
|
319
|
+
/**
|
|
320
|
+
* Per-client awareness state. Ephemeral -- not persisted, only shared with connected peers.
|
|
321
|
+
* Compatible with Yjs awareness protocol format for interop with existing editors.
|
|
322
|
+
*/
|
|
323
|
+
interface AwarenessState {
|
|
324
|
+
/** User identity information */
|
|
325
|
+
user: AwarenessUser;
|
|
326
|
+
/** Current cursor position, if any */
|
|
327
|
+
cursor?: AwarenessCursor;
|
|
328
|
+
}
|
|
329
|
+
/**
|
|
330
|
+
* Internal awareness message format used between AwarenessManager and transport layer.
|
|
331
|
+
*/
|
|
332
|
+
interface AwarenessMessage {
|
|
333
|
+
type: 'awareness';
|
|
334
|
+
/** Client ID of the sender */
|
|
335
|
+
clientId: number;
|
|
336
|
+
/** All known awareness states. null value means removal. */
|
|
337
|
+
states: Record<number, AwarenessState | null>;
|
|
338
|
+
}
|
|
339
|
+
/**
|
|
340
|
+
* Describes a change in awareness states.
|
|
341
|
+
* Emitted when remote clients update or remove their presence.
|
|
342
|
+
*/
|
|
343
|
+
interface AwarenessChange {
|
|
344
|
+
/** Client IDs whose states were added */
|
|
345
|
+
added: number[];
|
|
346
|
+
/** Client IDs whose states were updated */
|
|
347
|
+
updated: number[];
|
|
348
|
+
/** Client IDs whose states were removed */
|
|
349
|
+
removed: number[];
|
|
350
|
+
}
|
|
351
|
+
/**
|
|
352
|
+
* Cursor information for rendering in editors.
|
|
353
|
+
* This is the developer-facing type -- simplified from internal awareness state.
|
|
354
|
+
* Editor-agnostic: provides data that can be rendered by TipTap, ProseMirror, Quill, etc.
|
|
355
|
+
*/
|
|
356
|
+
interface CursorInfo {
|
|
357
|
+
/** Unique client ID */
|
|
358
|
+
clientId: number;
|
|
359
|
+
/** User display name */
|
|
360
|
+
userName: string;
|
|
361
|
+
/** Hex color for cursor rendering */
|
|
362
|
+
color: string;
|
|
363
|
+
/** Cursor anchor position (start of selection) */
|
|
364
|
+
anchor: number;
|
|
365
|
+
/** Cursor head position (end of selection) */
|
|
366
|
+
head: number;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Callback for awareness change events.
|
|
371
|
+
*/
|
|
372
|
+
type AwarenessChangeListener = (change: AwarenessChange) => void;
|
|
373
|
+
/**
|
|
374
|
+
* Manages collaborative awareness state (cursor positions, user presence).
|
|
375
|
+
*
|
|
376
|
+
* Awareness is ephemeral -- states are never persisted, only shared with
|
|
377
|
+
* currently connected peers via the sync transport. This implements a
|
|
378
|
+
* lightweight protocol compatible with Yjs awareness semantics.
|
|
379
|
+
*
|
|
380
|
+
* Each AwarenessManager has a unique client ID and tracks both its own
|
|
381
|
+
* local state and all remote clients' states.
|
|
382
|
+
*/
|
|
383
|
+
declare class AwarenessManager {
|
|
384
|
+
/** Unique client ID for this instance */
|
|
385
|
+
readonly clientId: number;
|
|
386
|
+
private localState;
|
|
387
|
+
private readonly remoteStates;
|
|
388
|
+
private readonly listeners;
|
|
389
|
+
private readonly emitter;
|
|
390
|
+
private readonly timeoutMs;
|
|
391
|
+
private readonly lastUpdated;
|
|
392
|
+
private cleanupTimer;
|
|
393
|
+
private sendHandler;
|
|
394
|
+
private destroyed;
|
|
395
|
+
constructor(options?: {
|
|
396
|
+
clientId?: number;
|
|
397
|
+
emitter?: KoraEventEmitter;
|
|
398
|
+
timeoutMs?: number;
|
|
399
|
+
});
|
|
400
|
+
/**
|
|
401
|
+
* Set the local user's awareness state and broadcast to peers.
|
|
402
|
+
*
|
|
403
|
+
* @param state - The awareness state to share. Pass null to clear presence.
|
|
404
|
+
*/
|
|
405
|
+
setLocalState(state: AwarenessState | null): void;
|
|
406
|
+
/**
|
|
407
|
+
* Get the local awareness state.
|
|
408
|
+
*/
|
|
409
|
+
getLocalState(): AwarenessState | null;
|
|
410
|
+
/**
|
|
411
|
+
* Get all known awareness states (local + remote).
|
|
412
|
+
* Returns a new Map on each call.
|
|
413
|
+
*/
|
|
414
|
+
getStates(): Map<number, AwarenessState>;
|
|
415
|
+
/**
|
|
416
|
+
* Handle an incoming awareness message from the transport.
|
|
417
|
+
* This processes remote state updates and notifies listeners.
|
|
418
|
+
*/
|
|
419
|
+
handleRemoteMessage(message: AwarenessMessage): void;
|
|
420
|
+
/**
|
|
421
|
+
* Remove a specific remote client's awareness state.
|
|
422
|
+
* Called when the server notifies that a client has disconnected.
|
|
423
|
+
*/
|
|
424
|
+
removeClient(clientId: number): void;
|
|
425
|
+
/**
|
|
426
|
+
* Register a handler for sending awareness messages through the transport.
|
|
427
|
+
* The sync engine calls this to wire outgoing awareness messages to the transport.
|
|
428
|
+
*/
|
|
429
|
+
onSend(handler: (message: AwarenessMessage) => void): void;
|
|
430
|
+
/**
|
|
431
|
+
* Register a listener for awareness state changes.
|
|
432
|
+
* Returns an unsubscribe function.
|
|
433
|
+
*/
|
|
434
|
+
on(_event: 'change', listener: AwarenessChangeListener): () => void;
|
|
435
|
+
/**
|
|
436
|
+
* Remove a specific change listener.
|
|
437
|
+
*/
|
|
438
|
+
off(_event: 'change', listener: AwarenessChangeListener): void;
|
|
439
|
+
/**
|
|
440
|
+
* Start the cleanup timer that removes stale remote states.
|
|
441
|
+
* Called when the sync engine transitions to streaming state.
|
|
442
|
+
*/
|
|
443
|
+
startCleanupTimer(): void;
|
|
444
|
+
/**
|
|
445
|
+
* Stop the cleanup timer.
|
|
446
|
+
*/
|
|
447
|
+
stopCleanupTimer(): void;
|
|
448
|
+
/**
|
|
449
|
+
* Clean up all resources. After calling destroy(), the manager
|
|
450
|
+
* will no longer send or receive awareness updates.
|
|
451
|
+
* Broadcasts removal of local state before shutting down.
|
|
452
|
+
*/
|
|
453
|
+
destroy(): void;
|
|
454
|
+
private cleanupStaleStates;
|
|
455
|
+
private notifyListeners;
|
|
456
|
+
private emitAwarenessEvent;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/**
|
|
460
|
+
* Configuration for the SyncMetricsCollector.
|
|
461
|
+
*/
|
|
462
|
+
interface MetricsCollectorConfig {
|
|
463
|
+
/** Number of RTT samples to keep for percentile calculations. Defaults to 100. */
|
|
464
|
+
rttWindowSize?: number;
|
|
465
|
+
/** Number of bandwidth samples to keep. Defaults to 20. */
|
|
466
|
+
bandwidthWindowSize?: number;
|
|
467
|
+
/** Interval in ms between periodic diagnostics emissions. Defaults to 5000. */
|
|
468
|
+
diagnosticsInterval?: number;
|
|
469
|
+
/** Injectable time source for deterministic testing. */
|
|
470
|
+
timeSource?: TimeSource;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
/**
|
|
474
|
+
* Thrown when encryption of operation data fails.
|
|
475
|
+
*/
|
|
476
|
+
declare class EncryptionError extends SyncError {
|
|
477
|
+
constructor(message: string, context?: Record<string, unknown>);
|
|
478
|
+
}
|
|
479
|
+
/**
|
|
480
|
+
* Thrown when decryption of operation data fails.
|
|
481
|
+
* This typically indicates a wrong key, tampered ciphertext, or corrupted data.
|
|
482
|
+
*/
|
|
483
|
+
declare class DecryptionError extends SyncError {
|
|
484
|
+
constructor(message: string, context?: Record<string, unknown>);
|
|
485
|
+
}
|
|
486
|
+
/**
|
|
487
|
+
* Encrypts and decrypts operation `data` and `previousData` fields for
|
|
488
|
+
* end-to-end encryption in the sync layer.
|
|
489
|
+
*
|
|
490
|
+
* **Design principles:**
|
|
491
|
+
* - Only `data` and `previousData` are encrypted. Metadata (id, nodeId,
|
|
492
|
+
* collection, timestamps, causalDeps, etc.) stays in cleartext so the
|
|
493
|
+
* server can route, deduplicate, and order operations.
|
|
494
|
+
* - Each field encryption uses a unique random IV (12 bytes for AES-GCM),
|
|
495
|
+
* ensuring that encrypting the same data twice produces different ciphertext.
|
|
496
|
+
* - Key rotation is supported via versioned keys. The key version is embedded
|
|
497
|
+
* in the {@link EncryptedPayload} so the decryptor can select the correct key.
|
|
498
|
+
* - Unencrypted fields pass through during decryption (backward compatibility).
|
|
499
|
+
*
|
|
500
|
+
* @example
|
|
501
|
+
* ```typescript
|
|
502
|
+
* const encryptor = await SyncEncryptor.create({
|
|
503
|
+
* enabled: true,
|
|
504
|
+
* key: 'user-passphrase'
|
|
505
|
+
* })
|
|
506
|
+
*
|
|
507
|
+
* // Encrypt before sending
|
|
508
|
+
* const encrypted = await encryptor.encryptOperation(operation)
|
|
509
|
+
*
|
|
510
|
+
* // Decrypt after receiving
|
|
511
|
+
* const decrypted = await encryptor.decryptOperation(encrypted)
|
|
512
|
+
* ```
|
|
513
|
+
*/
|
|
514
|
+
declare class SyncEncryptor {
|
|
515
|
+
/**
|
|
516
|
+
* Map of key version -> VersionedKey. The current (latest) version is used
|
|
517
|
+
* for encryption. All versions are available for decryption (key rotation).
|
|
518
|
+
*/
|
|
519
|
+
private readonly keys;
|
|
520
|
+
/** The current key version used for encryption. */
|
|
521
|
+
private currentVersion;
|
|
522
|
+
private constructor();
|
|
523
|
+
/**
|
|
524
|
+
* Creates a SyncEncryptor from a {@link SyncEncryptionConfig}.
|
|
525
|
+
*
|
|
526
|
+
* Derives the encryption key from the passphrase using PBKDF2. The key
|
|
527
|
+
* derivation is async because it uses the Web Crypto API.
|
|
528
|
+
*
|
|
529
|
+
* @param config - Encryption configuration with passphrase
|
|
530
|
+
* @param salt - Optional salt for deterministic key derivation (mainly for testing).
|
|
531
|
+
* If omitted, a random salt is generated.
|
|
532
|
+
* @returns A configured SyncEncryptor instance
|
|
533
|
+
* @throws {EncryptionError} If configuration is invalid
|
|
534
|
+
* @throws {KeyDerivationError} If key derivation fails
|
|
535
|
+
*/
|
|
536
|
+
static create(config: SyncEncryptionConfig, salt?: Uint8Array): Promise<SyncEncryptor>;
|
|
537
|
+
/**
|
|
538
|
+
* Creates a SyncEncryptor from pre-derived versioned keys.
|
|
539
|
+
*
|
|
540
|
+
* Use this when you need to support multiple key versions for key rotation,
|
|
541
|
+
* or when you have already derived the keys externally.
|
|
542
|
+
*
|
|
543
|
+
* @param versionedKeys - Array of versioned keys. The highest version is used for encryption.
|
|
544
|
+
* @returns A configured SyncEncryptor instance
|
|
545
|
+
* @throws {EncryptionError} If no keys are provided
|
|
546
|
+
*/
|
|
547
|
+
static fromKeys(versionedKeys: VersionedKey[]): SyncEncryptor;
|
|
548
|
+
/**
|
|
549
|
+
* Add a new key version for key rotation.
|
|
550
|
+
*
|
|
551
|
+
* After adding, the new key becomes the current version used for encryption
|
|
552
|
+
* if its version number is higher than the current version. Previously-versioned
|
|
553
|
+
* keys remain available for decrypting older operations.
|
|
554
|
+
*
|
|
555
|
+
* @param key - The new versioned key to add
|
|
556
|
+
* @throws {EncryptionError} If the key version already exists
|
|
557
|
+
*/
|
|
558
|
+
addKey(key: VersionedKey): void;
|
|
559
|
+
/**
|
|
560
|
+
* Get the current encryption key version number.
|
|
561
|
+
*/
|
|
562
|
+
getCurrentKeyVersion(): number;
|
|
563
|
+
/**
|
|
564
|
+
* Encrypt an operation's `data` and `previousData` fields.
|
|
565
|
+
*
|
|
566
|
+
* Returns a new Operation with encrypted field values. The original
|
|
567
|
+
* operation is not mutated (operations are immutable).
|
|
568
|
+
*
|
|
569
|
+
* Fields that are null (e.g., delete operations) remain null.
|
|
570
|
+
*
|
|
571
|
+
* @param operation - The operation to encrypt
|
|
572
|
+
* @returns A new operation with encrypted data fields
|
|
573
|
+
* @throws {EncryptionError} If encryption fails
|
|
574
|
+
*/
|
|
575
|
+
encryptOperation(operation: Operation): Promise<Operation>;
|
|
576
|
+
/**
|
|
577
|
+
* Decrypt an operation's `data` and `previousData` fields.
|
|
578
|
+
*
|
|
579
|
+
* Returns a new Operation with the original field values restored.
|
|
580
|
+
* The original operation is not mutated.
|
|
581
|
+
*
|
|
582
|
+
* If a field is null or not encrypted (no marker), it passes through unchanged.
|
|
583
|
+
* This enables mixed plaintext/encrypted operations during migration.
|
|
584
|
+
*
|
|
585
|
+
* @param operation - The operation to decrypt
|
|
586
|
+
* @returns A new operation with decrypted data fields
|
|
587
|
+
* @throws {DecryptionError} If decryption fails (wrong key, tampered data, unsupported version)
|
|
588
|
+
*/
|
|
589
|
+
decryptOperation(operation: Operation): Promise<Operation>;
|
|
590
|
+
/**
|
|
591
|
+
* Encrypt a serialized operation's `data` and `previousData` fields.
|
|
592
|
+
*
|
|
593
|
+
* Same as {@link encryptOperation} but works with the wire-format
|
|
594
|
+
* {@link SerializedOperation} type used by the serializer.
|
|
595
|
+
*
|
|
596
|
+
* @param serialized - The serialized operation to encrypt
|
|
597
|
+
* @returns A new serialized operation with encrypted data fields
|
|
598
|
+
* @throws {EncryptionError} If encryption fails
|
|
599
|
+
*/
|
|
600
|
+
encryptSerializedOperation(serialized: SerializedOperation): Promise<SerializedOperation>;
|
|
601
|
+
/**
|
|
602
|
+
* Decrypt a serialized operation's `data` and `previousData` fields.
|
|
603
|
+
*
|
|
604
|
+
* Same as {@link decryptOperation} but works with the wire-format
|
|
605
|
+
* {@link SerializedOperation} type used by the serializer.
|
|
606
|
+
*
|
|
607
|
+
* @param serialized - The serialized operation to decrypt
|
|
608
|
+
* @returns A new serialized operation with decrypted data fields
|
|
609
|
+
* @throws {DecryptionError} If decryption fails
|
|
610
|
+
*/
|
|
611
|
+
decryptSerializedOperation(serialized: SerializedOperation): Promise<SerializedOperation>;
|
|
612
|
+
/**
|
|
613
|
+
* Encrypt a batch of operations in parallel.
|
|
614
|
+
*
|
|
615
|
+
* @param operations - Operations to encrypt
|
|
616
|
+
* @returns New operations with encrypted data fields
|
|
617
|
+
*/
|
|
618
|
+
encryptBatch(operations: Operation[]): Promise<Operation[]>;
|
|
619
|
+
/**
|
|
620
|
+
* Decrypt a batch of operations in parallel.
|
|
621
|
+
*
|
|
622
|
+
* @param operations - Operations to decrypt
|
|
623
|
+
* @returns New operations with decrypted data fields
|
|
624
|
+
*/
|
|
625
|
+
decryptBatch(operations: Operation[]): Promise<Operation[]>;
|
|
626
|
+
/**
|
|
627
|
+
* Check if a field value contains an encrypted payload.
|
|
628
|
+
*
|
|
629
|
+
* @param field - An operation's `data` or `previousData` field
|
|
630
|
+
* @returns true if the field contains an encrypted payload
|
|
631
|
+
*/
|
|
632
|
+
static isEncryptedPayload(field: Record<string, unknown> | null): boolean;
|
|
633
|
+
private encryptField;
|
|
634
|
+
private decryptField;
|
|
635
|
+
}
|
|
636
|
+
/**
|
|
637
|
+
* Check if a field value contains an encrypted payload.
|
|
638
|
+
*
|
|
639
|
+
* @param field - An operation's `data` or `previousData` field
|
|
640
|
+
* @returns true if the field contains an encrypted payload
|
|
641
|
+
*/
|
|
642
|
+
declare function isEncryptedPayload(field: Record<string, unknown> | null): boolean;
|
|
643
|
+
|
|
644
|
+
/** Default snapshot size above which the doc channel is preferred. */
|
|
645
|
+
declare const DEFAULT_RICHTEXT_DOC_CHANNEL_THRESHOLD = 4096;
|
|
646
|
+
type RichtextDocListener = (update: Uint8Array) => void;
|
|
647
|
+
interface RichtextDocChannelOptions {
|
|
648
|
+
/** Snapshot byte length at which incremental channel sync is recommended. */
|
|
649
|
+
largeDocThreshold?: number;
|
|
650
|
+
/** Called when a local update should be sent on the wire. */
|
|
651
|
+
onSend?: (message: YjsDocUpdateMessage) => void;
|
|
652
|
+
}
|
|
653
|
+
/**
|
|
654
|
+
* Side channel for incremental Yjs updates on large richtext fields.
|
|
655
|
+
* Ephemeral — not persisted in the operation log (durable state still flows via ops).
|
|
656
|
+
*/
|
|
657
|
+
declare class RichtextDocChannel {
|
|
658
|
+
private readonly threshold;
|
|
659
|
+
private readonly onSend;
|
|
660
|
+
private readonly listeners;
|
|
661
|
+
constructor(options?: RichtextDocChannelOptions);
|
|
662
|
+
/**
|
|
663
|
+
* Whether the doc channel should be used for a field.
|
|
664
|
+
* @param preference - `true` forces on, `false` forces off, `undefined` uses size threshold
|
|
665
|
+
*/
|
|
666
|
+
shouldUseChannel(snapshotByteLength: number, preference?: boolean): boolean;
|
|
667
|
+
getThreshold(): number;
|
|
668
|
+
/**
|
|
669
|
+
* Subscribe to incremental updates for a document key.
|
|
670
|
+
*/
|
|
671
|
+
subscribe(collection: string, recordId: string, field: string, listener: RichtextDocListener): () => void;
|
|
672
|
+
/**
|
|
673
|
+
* Publish a local incremental update to peers.
|
|
674
|
+
*/
|
|
675
|
+
send(collection: string, recordId: string, field: string, update: Uint8Array): void;
|
|
676
|
+
/**
|
|
677
|
+
* Deliver a remote incremental update to local subscribers.
|
|
678
|
+
*/
|
|
679
|
+
deliver(message: YjsDocUpdateMessage): void;
|
|
680
|
+
}
|
|
681
|
+
|
|
232
682
|
/**
|
|
233
683
|
* A batch of operations taken from the queue for sending.
|
|
234
684
|
*/
|
|
@@ -299,6 +749,11 @@ declare class OutboundQueue {
|
|
|
299
749
|
* Whether initialize() has been called.
|
|
300
750
|
*/
|
|
301
751
|
get isInitialized(): boolean;
|
|
752
|
+
/**
|
|
753
|
+
* Remove operations by id from queue and persistent storage.
|
|
754
|
+
* Used when ops were already sent during handshake delta exchange.
|
|
755
|
+
*/
|
|
756
|
+
removeByIds(ids: string[]): Promise<void>;
|
|
302
757
|
}
|
|
303
758
|
|
|
304
759
|
/**
|
|
@@ -317,6 +772,34 @@ interface SyncEngineOptions {
|
|
|
317
772
|
emitter?: KoraEventEmitter;
|
|
318
773
|
/** Queue storage for persistent outbound queue. Defaults to in-memory. */
|
|
319
774
|
queueStorage?: QueueStorage;
|
|
775
|
+
/**
|
|
776
|
+
* Optional encryptor for end-to-end encryption.
|
|
777
|
+
* When provided, `data` and `previousData` fields of operations are encrypted
|
|
778
|
+
* before sending and decrypted after receiving. The server never sees plaintext data.
|
|
779
|
+
*/
|
|
780
|
+
encryptor?: SyncEncryptor;
|
|
781
|
+
/** Optional configuration for the metrics collector. */
|
|
782
|
+
metricsConfig?: MetricsCollectorConfig;
|
|
783
|
+
/** Op-log backed sync state (last acked server vector, unsynced counts). */
|
|
784
|
+
syncState?: SyncStatePersistence;
|
|
785
|
+
}
|
|
786
|
+
/**
|
|
787
|
+
* Diagnostics snapshot for debugging and support.
|
|
788
|
+
*/
|
|
789
|
+
interface SyncDiagnostics {
|
|
790
|
+
state: SyncState;
|
|
791
|
+
status: SyncStatusInfo;
|
|
792
|
+
nodeId: string;
|
|
793
|
+
url: string;
|
|
794
|
+
schemaVersion: number;
|
|
795
|
+
lastSyncedAt: number | null;
|
|
796
|
+
lastSuccessfulPush: number | null;
|
|
797
|
+
lastSuccessfulPull: number | null;
|
|
798
|
+
conflicts: number;
|
|
799
|
+
pendingOperations: number;
|
|
800
|
+
hasInFlightBatch: boolean;
|
|
801
|
+
reconnecting: boolean;
|
|
802
|
+
timestamp: number;
|
|
320
803
|
}
|
|
321
804
|
/**
|
|
322
805
|
* Core sync orchestrator. Manages the sync lifecycle:
|
|
@@ -334,13 +817,40 @@ declare class SyncEngine {
|
|
|
334
817
|
private readonly emitter;
|
|
335
818
|
private readonly outboundQueue;
|
|
336
819
|
private readonly batchSize;
|
|
820
|
+
private readonly encryptor;
|
|
821
|
+
private readonly awarenessManager;
|
|
822
|
+
private readonly richtextDocChannel;
|
|
823
|
+
private readonly metricsCollector;
|
|
824
|
+
private readonly syncState;
|
|
337
825
|
private remoteVector;
|
|
826
|
+
private lastAckedServerVector;
|
|
827
|
+
private cachedUnsyncedCount;
|
|
338
828
|
private lastSyncedAt;
|
|
829
|
+
private lastSuccessfulPush;
|
|
830
|
+
private lastSuccessfulPull;
|
|
831
|
+
private conflictCount;
|
|
339
832
|
private currentBatch;
|
|
340
833
|
private reconnecting;
|
|
834
|
+
private schemaBlocked;
|
|
341
835
|
private deltaBatchesReceived;
|
|
342
836
|
private deltaReceiveComplete;
|
|
343
837
|
private deltaSendComplete;
|
|
838
|
+
/** Op ids sent during handshake delta — removed from outbound queue to avoid duplicate send */
|
|
839
|
+
private deltaSentOpIds;
|
|
840
|
+
/** Outbound delta batch message IDs awaiting ACK when strictHandshake is enabled */
|
|
841
|
+
private pendingDeltaBatchAcks;
|
|
842
|
+
/**
|
|
843
|
+
* The effective scope for this sync session.
|
|
844
|
+
* Starts as the configured scopeMap. After handshake, may be replaced
|
|
845
|
+
* with the server-accepted scope (server is authoritative).
|
|
846
|
+
*/
|
|
847
|
+
private activeScope;
|
|
848
|
+
/** Live query subsets registered from reactive subscriptions */
|
|
849
|
+
private querySubsets;
|
|
850
|
+
private querySubsetReconnectTimer;
|
|
851
|
+
/** Resume cursor for paginated initial sync (persisted across reconnects) */
|
|
852
|
+
private resumeDeltaCursor;
|
|
853
|
+
private initialSyncTotalBatches;
|
|
344
854
|
constructor(options: SyncEngineOptions);
|
|
345
855
|
/**
|
|
346
856
|
* Start the sync engine: connect → handshake → delta exchange → streaming.
|
|
@@ -354,6 +864,9 @@ declare class SyncEngine {
|
|
|
354
864
|
/**
|
|
355
865
|
* Push a local operation to the outbound queue.
|
|
356
866
|
* If streaming, flushes immediately.
|
|
867
|
+
*
|
|
868
|
+
* Operations outside the configured sync scope are silently skipped
|
|
869
|
+
* because they should remain local-only and not be sent to the server.
|
|
357
870
|
*/
|
|
358
871
|
pushOperation(op: Operation): Promise<void>;
|
|
359
872
|
/**
|
|
@@ -367,6 +880,36 @@ declare class SyncEngine {
|
|
|
367
880
|
* Get the current developer-facing sync status.
|
|
368
881
|
*/
|
|
369
882
|
getStatus(): SyncStatusInfo;
|
|
883
|
+
/**
|
|
884
|
+
* True when the server rejected the client's schema version at handshake.
|
|
885
|
+
* Sync stays blocked until the app schema is upgraded or sync config changes.
|
|
886
|
+
*/
|
|
887
|
+
isSchemaBlocked(): boolean;
|
|
888
|
+
/**
|
|
889
|
+
* Clears schema-mismatch block after upgrading the local schema / sync config.
|
|
890
|
+
* Moves the engine back to `disconnected` so `start()` can run again.
|
|
891
|
+
*/
|
|
892
|
+
clearSchemaBlock(): void;
|
|
893
|
+
/**
|
|
894
|
+
* Record a merge conflict. Called by the merge-aware sync store
|
|
895
|
+
* to increment the conflict counter for status reporting.
|
|
896
|
+
*/
|
|
897
|
+
recordConflict(): void;
|
|
898
|
+
/**
|
|
899
|
+
* Count of local operations not yet covered by the last acked server vector (op-log source of truth).
|
|
900
|
+
*/
|
|
901
|
+
getUnsyncedOperationCount(): Promise<number>;
|
|
902
|
+
private emitApplyFailure;
|
|
903
|
+
/**
|
|
904
|
+
* Force an immediate reconnection attempt. If the engine is disconnected
|
|
905
|
+
* or in error state, restarts the sync. If already connected, no-op.
|
|
906
|
+
*/
|
|
907
|
+
retryNow(): Promise<void>;
|
|
908
|
+
/**
|
|
909
|
+
* Export a diagnostics snapshot for debugging and support tickets.
|
|
910
|
+
* Contains connection state, timing info, and queue metrics.
|
|
911
|
+
*/
|
|
912
|
+
exportDiagnostics(): SyncDiagnostics;
|
|
370
913
|
/**
|
|
371
914
|
* Get the current internal state (for testing).
|
|
372
915
|
*/
|
|
@@ -375,19 +918,82 @@ declare class SyncEngine {
|
|
|
375
918
|
* Get the outbound queue (for testing).
|
|
376
919
|
*/
|
|
377
920
|
getOutboundQueue(): OutboundQueue;
|
|
378
|
-
|
|
921
|
+
/**
|
|
922
|
+
* Update the sync scope map. Takes effect on the next connection attempt.
|
|
923
|
+
*
|
|
924
|
+
* When the scope changes (e.g., user switches organization), call this method
|
|
925
|
+
* then reconnect. The new scope will be sent in the handshake, and the server
|
|
926
|
+
* will send back data matching the new scope.
|
|
927
|
+
*
|
|
928
|
+
* Data that no longer matches the new scope is NOT deleted locally.
|
|
929
|
+
* It simply stops being synced.
|
|
930
|
+
*
|
|
931
|
+
* @param scopeMap - New per-collection scope filters, or undefined to remove scope
|
|
932
|
+
*/
|
|
933
|
+
updateScope(scopeMap: SyncScopeMap | undefined): void;
|
|
934
|
+
/**
|
|
935
|
+
* Get the currently active scope map. Returns undefined if no scope is configured.
|
|
936
|
+
*/
|
|
937
|
+
getActiveScope(): SyncScopeMap | undefined;
|
|
938
|
+
/**
|
|
939
|
+
* Register a live query subset that narrows synced data for a collection.
|
|
940
|
+
* Takes effect on the next connection; reconnects when already connected.
|
|
941
|
+
*/
|
|
942
|
+
registerQuerySubset(subset: SyncQuerySubset): () => void;
|
|
943
|
+
/**
|
|
944
|
+
* Returns deduplicated active query subsets from registered subscriptions.
|
|
945
|
+
*/
|
|
946
|
+
getActiveQuerySubsets(): SyncQuerySubset[];
|
|
947
|
+
/**
|
|
948
|
+
* Get the awareness manager for collaborative presence.
|
|
949
|
+
* Use this to set local presence, observe remote collaborators,
|
|
950
|
+
* and track cursor positions in richtext fields.
|
|
951
|
+
*/
|
|
952
|
+
getAwarenessManager(): AwarenessManager;
|
|
953
|
+
/**
|
|
954
|
+
* Optional side channel for incremental Yjs updates on large richtext fields.
|
|
955
|
+
*/
|
|
956
|
+
getRichtextDocChannel(): RichtextDocChannel;
|
|
957
|
+
private messageChain;
|
|
958
|
+
private enqueueMessage;
|
|
959
|
+
private handleMessageAsync;
|
|
960
|
+
private handleMessageFailure;
|
|
379
961
|
private handleHandshakeResponse;
|
|
380
962
|
private sendDelta;
|
|
963
|
+
private markDeltaSendCompleteIfReady;
|
|
381
964
|
private collectDelta;
|
|
382
965
|
private handleOperationBatch;
|
|
383
966
|
private handleAcknowledgment;
|
|
384
967
|
private handleError;
|
|
385
968
|
private checkDeltaComplete;
|
|
969
|
+
/**
|
|
970
|
+
* Effective server vector: persisted last-ack merged with live handshake remote vector.
|
|
971
|
+
*/
|
|
972
|
+
private getEffectiveServerVector;
|
|
973
|
+
private persistLastAckedServerVector;
|
|
974
|
+
private advanceLastAckedForLocalNode;
|
|
975
|
+
/**
|
|
976
|
+
* Refresh cached unsynced count from op log vs effective server vector.
|
|
977
|
+
*/
|
|
978
|
+
refreshPendingCount(): Promise<void>;
|
|
979
|
+
/**
|
|
980
|
+
* Returns operations the server has not yet acknowledged (op-log source of truth).
|
|
981
|
+
*/
|
|
982
|
+
getPendingSyncOperations(): Promise<Operation[]>;
|
|
983
|
+
/**
|
|
984
|
+
* Hydrate the outbound queue from unsynced ops in the op log (deduped by operation id).
|
|
985
|
+
*/
|
|
986
|
+
private reconcileOutboundFromOpLog;
|
|
386
987
|
private flushQueue;
|
|
988
|
+
private handleAwarenessUpdate;
|
|
387
989
|
private handleTransportClose;
|
|
388
990
|
private handleTransportError;
|
|
389
991
|
private transitionTo;
|
|
390
992
|
private setSerializerWireFormat;
|
|
993
|
+
private operationAllowedForSync;
|
|
994
|
+
private persistDeltaCursor;
|
|
995
|
+
private scheduleQuerySubsetReconnect;
|
|
996
|
+
private reconnectForQuerySubsets;
|
|
391
997
|
}
|
|
392
998
|
|
|
393
999
|
/**
|
|
@@ -494,6 +1100,11 @@ declare class ReconnectionManager {
|
|
|
494
1100
|
* @returns Promise that resolves when reconnection succeeds or maxAttempts reached.
|
|
495
1101
|
*/
|
|
496
1102
|
start(onReconnect: () => Promise<boolean>): Promise<boolean>;
|
|
1103
|
+
/**
|
|
1104
|
+
* Cancel the current backoff wait and proceed to the next reconnect attempt immediately.
|
|
1105
|
+
* No-op if not waiting.
|
|
1106
|
+
*/
|
|
1107
|
+
wake(): void;
|
|
497
1108
|
/**
|
|
498
1109
|
* Stop any pending reconnection attempt.
|
|
499
1110
|
*/
|
|
@@ -518,4 +1129,70 @@ declare class ReconnectionManager {
|
|
|
518
1129
|
private wait;
|
|
519
1130
|
}
|
|
520
1131
|
|
|
521
|
-
|
|
1132
|
+
/**
|
|
1133
|
+
* Encode a Yjs update for the doc channel wire (base64).
|
|
1134
|
+
*/
|
|
1135
|
+
declare function encodeYjsUpdate(update: Uint8Array): string;
|
|
1136
|
+
/**
|
|
1137
|
+
* Decode a base64 Yjs update from the doc channel wire.
|
|
1138
|
+
*/
|
|
1139
|
+
declare function decodeYjsUpdate(encoded: string): Uint8Array;
|
|
1140
|
+
/**
|
|
1141
|
+
* Build a stable key for a richtext field document.
|
|
1142
|
+
*/
|
|
1143
|
+
declare function richtextDocKey(collection: string, recordId: string, field: string): string;
|
|
1144
|
+
|
|
1145
|
+
/**
|
|
1146
|
+
* Thrown when key derivation fails.
|
|
1147
|
+
*/
|
|
1148
|
+
declare class KeyDerivationError extends SyncError {
|
|
1149
|
+
constructor(message: string, context?: Record<string, unknown>);
|
|
1150
|
+
}
|
|
1151
|
+
/**
|
|
1152
|
+
* Generates a cryptographically random salt for PBKDF2 key derivation.
|
|
1153
|
+
*
|
|
1154
|
+
* @returns A random 32-byte Uint8Array
|
|
1155
|
+
* @throws {KeyDerivationError} If crypto.getRandomValues is unavailable
|
|
1156
|
+
*/
|
|
1157
|
+
declare function generateSalt(): Uint8Array;
|
|
1158
|
+
/**
|
|
1159
|
+
* Derives an AES-256-GCM encryption key from a passphrase using PBKDF2.
|
|
1160
|
+
*
|
|
1161
|
+
* Uses PBKDF2 with SHA-256 and 600,000 iterations (OWASP-recommended minimum)
|
|
1162
|
+
* to derive a 256-bit key. The derived key is deterministic: the same passphrase
|
|
1163
|
+
* and salt always produce the same key.
|
|
1164
|
+
*
|
|
1165
|
+
* @param passphrase - The user's passphrase (must be non-empty)
|
|
1166
|
+
* @param salt - Optional salt bytes. If omitted, a random 32-byte salt is generated.
|
|
1167
|
+
* @returns The derived CryptoKey and the salt used
|
|
1168
|
+
* @throws {KeyDerivationError} If the passphrase is empty, crypto is unavailable, or derivation fails
|
|
1169
|
+
*
|
|
1170
|
+
* @example
|
|
1171
|
+
* ```typescript
|
|
1172
|
+
* // First time: derive key with a new salt
|
|
1173
|
+
* const { key, salt } = await deriveKey('my-secure-passphrase')
|
|
1174
|
+
*
|
|
1175
|
+
* // Later: re-derive the same key using the stored salt
|
|
1176
|
+
* const { key: sameKey } = await deriveKey('my-secure-passphrase', salt)
|
|
1177
|
+
* ```
|
|
1178
|
+
*/
|
|
1179
|
+
declare function deriveKey(passphrase: string, salt?: Uint8Array): Promise<{
|
|
1180
|
+
key: CryptoKey;
|
|
1181
|
+
salt: Uint8Array;
|
|
1182
|
+
}>;
|
|
1183
|
+
/**
|
|
1184
|
+
* Derives a versioned encryption key from a passphrase.
|
|
1185
|
+
*
|
|
1186
|
+
* Wraps {@link deriveKey} with a version number for key rotation support.
|
|
1187
|
+
* When the passphrase changes, create a new versioned key with a higher
|
|
1188
|
+
* version number.
|
|
1189
|
+
*
|
|
1190
|
+
* @param passphrase - The user's passphrase
|
|
1191
|
+
* @param version - Key version number (must be a positive integer)
|
|
1192
|
+
* @param salt - Optional salt bytes. If omitted, a random salt is generated.
|
|
1193
|
+
* @returns A {@link VersionedKey} containing the key, version, and salt
|
|
1194
|
+
* @throws {KeyDerivationError} If parameters are invalid or derivation fails
|
|
1195
|
+
*/
|
|
1196
|
+
declare function deriveVersionedKey(passphrase: string, version: number, salt?: Uint8Array): Promise<VersionedKey>;
|
|
1197
|
+
|
|
1198
|
+
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, 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, SyncStatusInfo, type SyncStore, SyncTransport, TransportCloseHandler, TransportErrorHandler, TransportMessageHandler, TransportOptions, VersionedKey, type WebSocketConstructor, type WebSocketLike, WebSocketTransport, type WebSocketTransportOptions, WireFormat, YjsDocUpdateMessage, createDeltaCursorFromBatch, decodeDeltaCursor, decodeYjsUpdate, deriveKey, deriveVersionedKey, encodeDeltaCursor, encodeYjsUpdate, filterOperationsByScope, generateSalt, isClientSchemaVersionSupported, isEncryptedPayload, isSchemaMismatchReject, operationMatchesScope, richtextDocKey, sliceOperationsAfterCursor, versionVectorToWire, wireToVersionVector };
|