@korajs/store 1.0.0-beta.13 → 1.0.0-beta.14

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.
Files changed (53) hide show
  1. package/dist/adapters/better-sqlite3.cjs.map +1 -1
  2. package/dist/adapters/better-sqlite3.js +1 -1
  3. package/dist/adapters/indexeddb.cjs.map +1 -1
  4. package/dist/adapters/indexeddb.js +4 -4
  5. package/dist/adapters/sqlite-wasm-worker.cjs +67 -15
  6. package/dist/adapters/sqlite-wasm-worker.cjs.map +1 -1
  7. package/dist/adapters/sqlite-wasm-worker.js +65 -17
  8. package/dist/adapters/sqlite-wasm-worker.js.map +1 -1
  9. package/dist/adapters/sqlite-wasm.cjs.map +1 -1
  10. package/dist/adapters/sqlite-wasm.js +5 -5
  11. package/dist/{backup-4NUPDEMB.js → backup-IHKVMUAM.js} +5 -5
  12. package/dist/{chunk-Y5LRH7KI.js → chunk-5AQSGMJV.js} +151 -21
  13. package/dist/chunk-5AQSGMJV.js.map +1 -0
  14. package/dist/{chunk-W5F2LWG6.js → chunk-5JMGMPUE.js} +2 -2
  15. package/dist/{chunk-HZTDBLDB.js → chunk-BILSA6XZ.js} +4 -3
  16. package/dist/{chunk-HZTDBLDB.js.map → chunk-BILSA6XZ.js.map} +1 -1
  17. package/dist/{chunk-QDUSJMFV.js → chunk-BQSIL6TT.js} +5 -5
  18. package/dist/{chunk-MCQKEHOD.js → chunk-KK3GBONL.js} +2 -2
  19. package/dist/{chunk-32EDKWMI.js → chunk-KTVNANUH.js} +2 -2
  20. package/dist/{chunk-SJP2HTYZ.js → chunk-QW4UU7NG.js} +2 -2
  21. package/dist/{chunk-DUKTWVZW.js → chunk-SRHDJF5B.js} +13 -2
  22. package/dist/{chunk-DUKTWVZW.js.map → chunk-SRHDJF5B.js.map} +1 -1
  23. package/dist/{chunk-P4V375FY.js → chunk-UC2LGFPV.js} +4 -4
  24. package/dist/index.cjs +203 -47
  25. package/dist/index.cjs.map +1 -1
  26. package/dist/index.d.cts +96 -6
  27. package/dist/index.d.ts +96 -6
  28. package/dist/index.js +55 -39
  29. package/dist/index.js.map +1 -1
  30. package/dist/internal.cjs +1 -0
  31. package/dist/internal.cjs.map +1 -1
  32. package/dist/internal.d.cts +2 -2
  33. package/dist/internal.d.ts +2 -2
  34. package/dist/internal.js +3 -3
  35. package/dist/{restore-FR7XIKT2.js → restore-MNDPR557.js} +4 -4
  36. package/dist/{sql-builder-C4u55jKW.d.cts → sql-builder-D0tC3sv9.d.cts} +8 -1
  37. package/dist/{sql-builder-CGckdBDL.d.ts → sql-builder-D5UMzHEo.d.ts} +8 -1
  38. package/dist/sqlite-wasm-channel-QCCNBYCU.js +12 -0
  39. package/dist/store-HNGX4KTT.js +16 -0
  40. package/package.json +2 -2
  41. package/dist/chunk-Y5LRH7KI.js.map +0 -1
  42. package/dist/sqlite-wasm-channel-MLHLKEED.js +0 -12
  43. package/dist/store-IMWKZLWB.js +0 -16
  44. /package/dist/{backup-4NUPDEMB.js.map → backup-IHKVMUAM.js.map} +0 -0
  45. /package/dist/{chunk-W5F2LWG6.js.map → chunk-5JMGMPUE.js.map} +0 -0
  46. /package/dist/{chunk-QDUSJMFV.js.map → chunk-BQSIL6TT.js.map} +0 -0
  47. /package/dist/{chunk-MCQKEHOD.js.map → chunk-KK3GBONL.js.map} +0 -0
  48. /package/dist/{chunk-32EDKWMI.js.map → chunk-KTVNANUH.js.map} +0 -0
  49. /package/dist/{chunk-SJP2HTYZ.js.map → chunk-QW4UU7NG.js.map} +0 -0
  50. /package/dist/{chunk-P4V375FY.js.map → chunk-UC2LGFPV.js.map} +0 -0
  51. /package/dist/{restore-FR7XIKT2.js.map → restore-MNDPR557.js.map} +0 -0
  52. /package/dist/{sqlite-wasm-channel-MLHLKEED.js.map → sqlite-wasm-channel-QCCNBYCU.js.map} +0 -0
  53. /package/dist/{store-IMWKZLWB.js.map → store-HNGX4KTT.js.map} +0 -0
package/dist/index.d.cts CHANGED
@@ -2,8 +2,8 @@ import { C as CollectionRecord, S as StorageAdapter, Q as QueryDescriptor, d as
2
2
  export { M as MigrationPlan, k as OrderByClause, l as QueryErrorPhase, m as StorageFallbackReason, a as StorageOpenState, n as StoreIsolation, T as Transaction, o as WhereOperators } from './types-Dh-xyUjP.cjs';
3
3
  import { Operation, KoraError, VersionVector, SchemaDefinition, HLCTimestamp, MergeTrace, CollectionDefinition, SequenceConfig, HybridLogicalClock, CausalTracker, SecretKeyProvider, OperationLog, OperationTransform, ApplyResult, FoldState, KoraEventEmitter, StateMachineDefinition, KoraBytesValue, KoraEvent, BlobRefMetadata, BlobRef } from '@korajs/core';
4
4
  export { ApplyResult } from '@korajs/core';
5
- import { R as RelationEnforcer, j as LocalDeleteHook, k as RecordFolder, L as LocalMutationContext, A as AdoptionSchedule, T as TerminalRejection, l as LocalNodeRecord } from './sql-builder-C4u55jKW.cjs';
6
- export { C as Collection, P as ParkedAdoption, V as VIRTUAL_TIMESTAMP_FIELDS, m as VIRTUAL_TIMESTAMP_FIELD_NAMES, n as VirtualTimestampField, o as mergeYjsUpdates } from './sql-builder-C4u55jKW.cjs';
5
+ import { R as RelationEnforcer, j as LocalDeleteHook, k as RecordFolder, L as LocalMutationContext, A as AdoptionSchedule, T as TerminalRejection, l as LocalNodeRecord } from './sql-builder-D0tC3sv9.cjs';
6
+ export { C as Collection, P as ParkedAdoption, V as VIRTUAL_TIMESTAMP_FIELDS, m as VIRTUAL_TIMESTAMP_FIELD_NAMES, n as VirtualTimestampField, o as mergeYjsUpdates } from './sql-builder-D0tC3sv9.cjs';
7
7
  import * as yjs from 'yjs';
8
8
  import { C as ContentAddressedBlobStore, M as MemoryBlobStore } from './content-addressed-blob-store-Dcj3SB9r.cjs';
9
9
  export { B as BlobIntegrityError } from './content-addressed-blob-store-Dcj3SB9r.cjs';
@@ -157,6 +157,14 @@ declare class StorageBackendMismatchError extends KoraError {
157
157
  declare class PersistenceError extends KoraError {
158
158
  constructor(message: string, context?: Record<string, unknown>);
159
159
  }
160
+ /**
161
+ * Thrown by a local write while the store's pinned node id (an auth device id) belongs
162
+ * to another user than the one signed in (F9): a browser two users shared. The write
163
+ * could only be stored under the owner's node and would later upload as the owner's.
164
+ */
165
+ declare class NodeOwnedByAnotherUserError extends KoraError {
166
+ constructor(nodeId: string, principal: string | null);
167
+ }
160
168
 
161
169
  /**
162
170
  * How the local operation log may be compacted.
@@ -396,8 +404,13 @@ interface RestoreResult {
396
404
  success: boolean;
397
405
  /** Error message if failed */
398
406
  error?: string;
399
- /** Error code if failed (`BACKUP_FORMAT_OUTDATED`, `BACKUP_CHECKSUM_MISMATCH`, ...) */
407
+ /** Error code if failed (`BACKUP_CHECKSUM_MISMATCH`, `BACKUP_OPERATION_INVALID`, ...) */
400
408
  errorCode?: string;
409
+ /**
410
+ * Set when the file was a version-1 backup (Kora 1.0.0-beta.12 and earlier) that the
411
+ * import converted first, as `convertBackupV1` does.
412
+ */
413
+ convertedFromVersion?: 1;
401
414
  /**
402
415
  * Replace mode: this device's own writes the sync server had not acknowledged,
403
416
  * re-applied on top of the restored data (see `RestoreOptions.keepUnsyncedWrites`).
@@ -924,6 +937,10 @@ interface TransactionContextConfig {
924
937
  onStorageError?: (error: unknown) => void;
925
938
  /** The W7 record fold (see `WriteEnv.fold`). */
926
939
  fold?: RecordFolder;
940
+ /** See `WriteEnv.maxOperationBytes`. */
941
+ maxOperationBytes?: number;
942
+ /** See `WriteEnv.assertLocalWriteAllowed`. */
943
+ assertLocalWriteAllowed?: () => void;
927
944
  }
928
945
  /**
929
946
  * A collection accessor within a transaction.
@@ -1038,6 +1055,12 @@ declare class Store implements OperationLog {
1038
1055
  private readonly schema;
1039
1056
  private readonly adapter;
1040
1057
  private readonly configNodeId;
1058
+ /** The owner of the pinned node while it belongs to another user than the signed-in one (F9). */
1059
+ private pinnedNodeOwnedBy;
1060
+ /** The sync server refused the pinned node as another user's (F9). */
1061
+ private pinnedNodeRefusedByServer;
1062
+ /** The user of the last {@link bindPrincipal}. */
1063
+ private signedInPrincipal;
1041
1064
  private readonly dbName;
1042
1065
  private readonly isolation;
1043
1066
  private readonly emitter;
@@ -1485,6 +1508,23 @@ declare class Store implements OperationLog {
1485
1508
  * @throws {KoraError} When the node id is pinned (`StoreConfig.nodeId`, or per-tab isolation)
1486
1509
  */
1487
1510
  rotateNodeId(unsyncedOpIds: string[]): Promise<NodeRotationResult>;
1511
+ /**
1512
+ * Re-author another local node's never-acknowledged operations under a fresh node id
1513
+ * bound to `principal` (the user they belong to). Used when the sync server refuses an
1514
+ * adopted node whose writes belong to the signed-in user (the app assigned them, or
1515
+ * the node was created for them) but which no claims-aware server ever accepted: a
1516
+ * beta.12 database's node with history the server recorded no owner for. The store's
1517
+ * own node id, clock and counters are untouched; the fresh node is registered, so the
1518
+ * sync engine adopts it like any other local node.
1519
+ *
1520
+ * @param fromNodeId - The refused local node (never the store's own)
1521
+ * @param unsyncedOpIds - Ids of its operations the server never acknowledged
1522
+ * @param principal - The user the writes belong to (the fresh node's binding)
1523
+ * @returns The fresh node id and the rewritten operations (for the outbound queue)
1524
+ * @throws {KoraError} `NODE_ID_UNKNOWN` when `fromNodeId` is the store's own node or
1525
+ * not a local node
1526
+ */
1527
+ reauthorLocalNode(fromNodeId: string, unsyncedOpIds: string[], principal: string): Promise<NodeRotationResult>;
1488
1528
  /**
1489
1529
  * Move this database back to a node id it authored under before (RT-38): after the
1490
1530
  * sync server refused the current node, the device tries a node a returning user
@@ -1525,6 +1565,19 @@ declare class Store implements OperationLog {
1525
1565
  * @returns The node now in use and whether it changed
1526
1566
  */
1527
1567
  bindPrincipal(principal: string): Promise<PrincipalBinding>;
1568
+ /**
1569
+ * The sync server refused this store's pinned node id because another user owns it
1570
+ * (`owned: true`), or later accepted it (`false`). A pinned node cannot move to a fresh
1571
+ * id, so writes made under it could only ever upload as its owner: while the server
1572
+ * says so, local writes are refused (F9). A no-op for a store whose node is not pinned
1573
+ * (it moves to a fresh node instead).
1574
+ *
1575
+ * @param owned - Whether the server reported the node owned by another user
1576
+ */
1577
+ setPinnedNodeOwnedElsewhere(owned: boolean): void;
1578
+ /** Refuse a local write while the pinned node belongs to another user (F9). */
1579
+ private assertLocalWriteAllowed;
1580
+ private bindPrincipalToNode;
1528
1581
  /**
1529
1582
  * A sync handshake as `nodeId` was accepted for `principal` (RT-50): bind an unbound
1530
1583
  * node (or replace a guessed binding) from the server's answer.
@@ -1684,8 +1737,9 @@ declare class Store implements OperationLog {
1684
1737
  * move backwards, and unsynced own writes are kept (see
1685
1738
  * `RestoreOptions.keepUnsyncedWrites`). Then {@link reloadFromDisk} runs.
1686
1739
  *
1687
- * A version-1 file is refused (`errorCode: 'BACKUP_FORMAT_OUTDATED'`); convert it with
1688
- * `convertBackupV1`.
1740
+ * A version-1 file (Kora 1.0.0-beta.12 and earlier) is converted first, exactly as
1741
+ * `convertBackupV1` does (`result.convertedFromVersion: 1`); one holding an operation
1742
+ * whose timestamp cannot be recovered is refused (`BACKUP_OPERATION_INVALID`).
1689
1743
  *
1690
1744
  * @param data - The backup data
1691
1745
  * @param options - Restore options (merge, collections, keepUnsyncedWrites, onProgress)
@@ -1738,6 +1792,19 @@ declare class Store implements OperationLog {
1738
1792
  * write path so they sync, unless declared `localOnly`.
1739
1793
  */
1740
1794
  private runMigrationsIfNeeded;
1795
+ /** The node id the database persisted before this open, if any. */
1796
+ private readPersistedNodeId;
1797
+ /**
1798
+ * Register node ids this database authored under that the registry does not know
1799
+ * yet: the node id a pinned `nodeId` (the auth device id) just replaced, and the
1800
+ * authors of queued operations. A beta.12 database had no registry, and a template
1801
+ * app created before its user signed in authored under a random node; once the
1802
+ * store is pinned to the device id, that node's queued writes would otherwise sit in
1803
+ * the queue forever, neither uploaded nor reported. Registered, they are adopted or
1804
+ * held (`status.heldNodes`) like any other local node's writes. Only the outbound
1805
+ * queue is read: received operations never enter it.
1806
+ */
1807
+ private registerUnregisteredAuthors;
1741
1808
  private loadOrGenerateNodeId;
1742
1809
  private loadSequenceNumber;
1743
1810
  private loadVersionVector;
@@ -2167,6 +2234,12 @@ interface RichTextControllerSnapshot {
2167
2234
  canUndo: boolean;
2168
2235
  canRedo: boolean;
2169
2236
  cursors: readonly RichTextCursorInfo[];
2237
+ /**
2238
+ * True while local edits are not in a successful save: a save is pending, in
2239
+ * flight, or was refused (see `error`). The edits stay in the live document and
2240
+ * are saved by the next edit or `retrySave()`.
2241
+ */
2242
+ hasUnsavedChanges: boolean;
2170
2243
  }
2171
2244
  interface RichTextController {
2172
2245
  readonly doc: yjs.Doc;
@@ -2178,12 +2251,29 @@ interface RichTextController {
2178
2251
  setCursor(anchor: number, head: number): void;
2179
2252
  clearCursor(): void;
2180
2253
  setUser(user: RichTextAwarenessUser | undefined): void;
2254
+ /**
2255
+ * Save the live document now (for example after a refused save, once the cause
2256
+ * is fixed). Resolves when the save settled; its outcome is in the snapshot.
2257
+ */
2258
+ retrySave(): Promise<void>;
2259
+ /**
2260
+ * The full document state (a Yjs update) while local edits are unsaved, else
2261
+ * null. Keep it as a recovery copy when saves keep failing.
2262
+ */
2263
+ getUnsavedState(): Uint8Array | null;
2264
+ /** Stop syncing; edits still waiting for the debounced save are saved first. */
2181
2265
  destroy(): void;
2182
2266
  }
2183
2267
 
2184
2268
  /**
2185
2269
  * Framework-agnostic richtext binding: Yjs document lifecycle, persistence,
2186
2270
  * incremental doc channel, and awareness cursors.
2271
+ *
2272
+ * Every save writes the full state of the live Y.Doc, which holds every local edit
2273
+ * and every merged remote change, so a stored change arriving while a save waits
2274
+ * cannot drop local typing. A refused save keeps the edits unsaved
2275
+ * (`hasUnsavedChanges`, `getUnsavedState()`) and the next edit or `retrySave()`
2276
+ * saves them again; the first successful save clears `error`.
2187
2277
  */
2188
2278
  declare function createRichTextController(options: CreateRichTextControllerOptions): RichTextController;
2189
2279
 
@@ -2750,4 +2840,4 @@ declare function createChunkPortPair(): {
2750
2840
  b: ChunkMessagePort;
2751
2841
  };
2752
2842
 
2753
- export { AdapterError, AdoptionSchedule, ApplyRemoteOptions, type AuditExportManifest, type AuditExportOptions, type AuditExportPayload, type AuditExportProgress, type AuditTraceQuery, BACKUP_VERSION, BackupFormatError, type BackupManifest, type BackupOptions, type BackupProgress, type BlobGcOptions, type BlobGcResult, type BlobManifest, type BlobManifestMetadata, BridgeTerminatedError, COMPACTION_BASELINE_META_KEY, type ChunkMessage, type ChunkMessagePort, type ChunkProvider, type ChunkRequestMessage, type ChunkResponseMessage, type ClockRebaseResult, type CollectionAccessor, CollectionRecord, type CompactionResult, type CompactionStrategy, ContentAddressedBlobStore, type ConvertBackupV1Options, type CreateRichTextControllerOptions, DEFAULT_CHUNK_SIZE, InvalidStateTransitionError, LAST_ACKED_SERVER_VECTOR_META_KEY, LOG_QUARANTINE_TABLE, LeaderUnresponsiveError, LocalMutationHandler, LocalNodeRecord, type LogIntegrityReport, type LogIntegrityRow, type LogRowProblem, type LogSequenceGap, MaterializedRowSnapshot, MemoryBlobStore, NoLeaderError, type NodeRotationResult, type OpfsBlobDirectory, OpfsBlobStore, OptimisticLockError, OrderByDirection, type PersistedAuditTrace, PersistenceError, type PersistenceRequestReason, type PersistenceStorageManager, type PrincipalBinding, QueryBuilder, QueryDescriptor, QueryError, QueryStore, QueryStoreCache, QuerySubscriptionError, type ReceiveBlobResult, type ReceiveBlobStores, RecordNotFoundError, type RecordsChange, type RecordsChangeListener, type RegisterOptions, type RematerializationMode, type RemoteChunkProvider, type RemoteChunkProviderOptions, type ReplaySnapshot, RequestAbortedError, type ResequenceResult, type RestoreOptions, type RestoreResult, type ResultsEqual, type RichTextAwarenessUser, type RichTextController, type RichTextControllerSnapshot, type RichTextCursorInfo, type RichTextSyncEngine, RowVersionState, SchemaVersionAheadError, SequenceManager, type ServerBlobCallbacks, StorageAdapter, StorageBackendMismatchError, StorageDurabilityError, StorageInUseError, StoragePersistence, type StoragePersistenceOptions, type StoragePersistenceState, type StoragePersistenceStatus, Store, StoreConfig, StoreNotOpenError, SubscribeOptions, SubscriptionBloomFilter, SubscriptionCallback, SubscriptionManager, type SubscriptionManagerOptions, type SubscriptionStats, TerminalRejection, type TransactionCollectionAccessor, TransactionContext, type TransactionContextConfig, type UnappliedOperation, UnsyncedDataError, WhereClause, WorkerInitError, WorkerTimeoutError, asRichTextSyncEngine, assertQueryReady, chunkBlob, collectBlobGarbage, collectOperationsAheadOfServer, compactOperationLog, computeAckCompactionWatermark, convertBackupV1, createChunkPortPair, createMemoryServerBlobStore, createOpfsBlobDirectory, createOpfsBlobStore, createRemoteChunkProvider, createRichTextController, decodeAuditExport, decodeRichtext, deserializeVersionVectorFromMeta, encodeRichtext, exportBackup, extractBlobRefs, fetchBlobManifest, getSharedQueryStoreCache, mergeVersionVectors, normalizeWhere, parseBlobManifest, persistedAuditTraceFromEvent, pluralize, prepareBlobForSend, putBlobForTransfer, queryKey, readAuditExportManifest, readBackupManifest, reassembleBlob, receiveBlob, resolveBlobManifest, restoreBackup, richtextStatesEqual, richtextToPlainText, serializeBlobManifest, serializeVersionVectorToMeta, serveBlobChunks, singularize, toServerBlobCallbacks, validateStateTransition, validateUpdateStateMachine, verifyAuditExportChecksum, verifyBackupChecksum };
2843
+ export { AdapterError, AdoptionSchedule, ApplyRemoteOptions, type AuditExportManifest, type AuditExportOptions, type AuditExportPayload, type AuditExportProgress, type AuditTraceQuery, BACKUP_VERSION, BackupFormatError, type BackupManifest, type BackupOptions, type BackupProgress, type BlobGcOptions, type BlobGcResult, type BlobManifest, type BlobManifestMetadata, BridgeTerminatedError, COMPACTION_BASELINE_META_KEY, type ChunkMessage, type ChunkMessagePort, type ChunkProvider, type ChunkRequestMessage, type ChunkResponseMessage, type ClockRebaseResult, type CollectionAccessor, CollectionRecord, type CompactionResult, type CompactionStrategy, ContentAddressedBlobStore, type ConvertBackupV1Options, type CreateRichTextControllerOptions, DEFAULT_CHUNK_SIZE, InvalidStateTransitionError, LAST_ACKED_SERVER_VECTOR_META_KEY, LOG_QUARANTINE_TABLE, LeaderUnresponsiveError, LocalMutationHandler, LocalNodeRecord, type LogIntegrityReport, type LogIntegrityRow, type LogRowProblem, type LogSequenceGap, MaterializedRowSnapshot, MemoryBlobStore, NoLeaderError, NodeOwnedByAnotherUserError, type NodeRotationResult, type OpfsBlobDirectory, OpfsBlobStore, OptimisticLockError, OrderByDirection, type PersistedAuditTrace, PersistenceError, type PersistenceRequestReason, type PersistenceStorageManager, type PrincipalBinding, QueryBuilder, QueryDescriptor, QueryError, QueryStore, QueryStoreCache, QuerySubscriptionError, type ReceiveBlobResult, type ReceiveBlobStores, RecordNotFoundError, type RecordsChange, type RecordsChangeListener, type RegisterOptions, type RematerializationMode, type RemoteChunkProvider, type RemoteChunkProviderOptions, type ReplaySnapshot, RequestAbortedError, type ResequenceResult, type RestoreOptions, type RestoreResult, type ResultsEqual, type RichTextAwarenessUser, type RichTextController, type RichTextControllerSnapshot, type RichTextCursorInfo, type RichTextSyncEngine, RowVersionState, SchemaVersionAheadError, SequenceManager, type ServerBlobCallbacks, StorageAdapter, StorageBackendMismatchError, StorageDurabilityError, StorageInUseError, StoragePersistence, type StoragePersistenceOptions, type StoragePersistenceState, type StoragePersistenceStatus, Store, StoreConfig, StoreNotOpenError, SubscribeOptions, SubscriptionBloomFilter, SubscriptionCallback, SubscriptionManager, type SubscriptionManagerOptions, type SubscriptionStats, TerminalRejection, type TransactionCollectionAccessor, TransactionContext, type TransactionContextConfig, type UnappliedOperation, UnsyncedDataError, WhereClause, WorkerInitError, WorkerTimeoutError, asRichTextSyncEngine, assertQueryReady, chunkBlob, collectBlobGarbage, collectOperationsAheadOfServer, compactOperationLog, computeAckCompactionWatermark, convertBackupV1, createChunkPortPair, createMemoryServerBlobStore, createOpfsBlobDirectory, createOpfsBlobStore, createRemoteChunkProvider, createRichTextController, decodeAuditExport, decodeRichtext, deserializeVersionVectorFromMeta, encodeRichtext, exportBackup, extractBlobRefs, fetchBlobManifest, getSharedQueryStoreCache, mergeVersionVectors, normalizeWhere, parseBlobManifest, persistedAuditTraceFromEvent, pluralize, prepareBlobForSend, putBlobForTransfer, queryKey, readAuditExportManifest, readBackupManifest, reassembleBlob, receiveBlob, resolveBlobManifest, restoreBackup, richtextStatesEqual, richtextToPlainText, serializeBlobManifest, serializeVersionVectorToMeta, serveBlobChunks, singularize, toServerBlobCallbacks, validateStateTransition, validateUpdateStateMachine, verifyAuditExportChecksum, verifyBackupChecksum };
package/dist/index.d.ts CHANGED
@@ -2,8 +2,8 @@ import { C as CollectionRecord, S as StorageAdapter, Q as QueryDescriptor, d as
2
2
  export { M as MigrationPlan, k as OrderByClause, l as QueryErrorPhase, m as StorageFallbackReason, a as StorageOpenState, n as StoreIsolation, T as Transaction, o as WhereOperators } from './types-Dh-xyUjP.js';
3
3
  import { Operation, KoraError, VersionVector, SchemaDefinition, HLCTimestamp, MergeTrace, CollectionDefinition, SequenceConfig, HybridLogicalClock, CausalTracker, SecretKeyProvider, OperationLog, OperationTransform, ApplyResult, FoldState, KoraEventEmitter, StateMachineDefinition, KoraBytesValue, KoraEvent, BlobRefMetadata, BlobRef } from '@korajs/core';
4
4
  export { ApplyResult } from '@korajs/core';
5
- import { R as RelationEnforcer, j as LocalDeleteHook, k as RecordFolder, L as LocalMutationContext, A as AdoptionSchedule, T as TerminalRejection, l as LocalNodeRecord } from './sql-builder-CGckdBDL.js';
6
- export { C as Collection, P as ParkedAdoption, V as VIRTUAL_TIMESTAMP_FIELDS, m as VIRTUAL_TIMESTAMP_FIELD_NAMES, n as VirtualTimestampField, o as mergeYjsUpdates } from './sql-builder-CGckdBDL.js';
5
+ import { R as RelationEnforcer, j as LocalDeleteHook, k as RecordFolder, L as LocalMutationContext, A as AdoptionSchedule, T as TerminalRejection, l as LocalNodeRecord } from './sql-builder-D5UMzHEo.js';
6
+ export { C as Collection, P as ParkedAdoption, V as VIRTUAL_TIMESTAMP_FIELDS, m as VIRTUAL_TIMESTAMP_FIELD_NAMES, n as VirtualTimestampField, o as mergeYjsUpdates } from './sql-builder-D5UMzHEo.js';
7
7
  import * as yjs from 'yjs';
8
8
  import { C as ContentAddressedBlobStore, M as MemoryBlobStore } from './content-addressed-blob-store-Dcj3SB9r.js';
9
9
  export { B as BlobIntegrityError } from './content-addressed-blob-store-Dcj3SB9r.js';
@@ -157,6 +157,14 @@ declare class StorageBackendMismatchError extends KoraError {
157
157
  declare class PersistenceError extends KoraError {
158
158
  constructor(message: string, context?: Record<string, unknown>);
159
159
  }
160
+ /**
161
+ * Thrown by a local write while the store's pinned node id (an auth device id) belongs
162
+ * to another user than the one signed in (F9): a browser two users shared. The write
163
+ * could only be stored under the owner's node and would later upload as the owner's.
164
+ */
165
+ declare class NodeOwnedByAnotherUserError extends KoraError {
166
+ constructor(nodeId: string, principal: string | null);
167
+ }
160
168
 
161
169
  /**
162
170
  * How the local operation log may be compacted.
@@ -396,8 +404,13 @@ interface RestoreResult {
396
404
  success: boolean;
397
405
  /** Error message if failed */
398
406
  error?: string;
399
- /** Error code if failed (`BACKUP_FORMAT_OUTDATED`, `BACKUP_CHECKSUM_MISMATCH`, ...) */
407
+ /** Error code if failed (`BACKUP_CHECKSUM_MISMATCH`, `BACKUP_OPERATION_INVALID`, ...) */
400
408
  errorCode?: string;
409
+ /**
410
+ * Set when the file was a version-1 backup (Kora 1.0.0-beta.12 and earlier) that the
411
+ * import converted first, as `convertBackupV1` does.
412
+ */
413
+ convertedFromVersion?: 1;
401
414
  /**
402
415
  * Replace mode: this device's own writes the sync server had not acknowledged,
403
416
  * re-applied on top of the restored data (see `RestoreOptions.keepUnsyncedWrites`).
@@ -924,6 +937,10 @@ interface TransactionContextConfig {
924
937
  onStorageError?: (error: unknown) => void;
925
938
  /** The W7 record fold (see `WriteEnv.fold`). */
926
939
  fold?: RecordFolder;
940
+ /** See `WriteEnv.maxOperationBytes`. */
941
+ maxOperationBytes?: number;
942
+ /** See `WriteEnv.assertLocalWriteAllowed`. */
943
+ assertLocalWriteAllowed?: () => void;
927
944
  }
928
945
  /**
929
946
  * A collection accessor within a transaction.
@@ -1038,6 +1055,12 @@ declare class Store implements OperationLog {
1038
1055
  private readonly schema;
1039
1056
  private readonly adapter;
1040
1057
  private readonly configNodeId;
1058
+ /** The owner of the pinned node while it belongs to another user than the signed-in one (F9). */
1059
+ private pinnedNodeOwnedBy;
1060
+ /** The sync server refused the pinned node as another user's (F9). */
1061
+ private pinnedNodeRefusedByServer;
1062
+ /** The user of the last {@link bindPrincipal}. */
1063
+ private signedInPrincipal;
1041
1064
  private readonly dbName;
1042
1065
  private readonly isolation;
1043
1066
  private readonly emitter;
@@ -1485,6 +1508,23 @@ declare class Store implements OperationLog {
1485
1508
  * @throws {KoraError} When the node id is pinned (`StoreConfig.nodeId`, or per-tab isolation)
1486
1509
  */
1487
1510
  rotateNodeId(unsyncedOpIds: string[]): Promise<NodeRotationResult>;
1511
+ /**
1512
+ * Re-author another local node's never-acknowledged operations under a fresh node id
1513
+ * bound to `principal` (the user they belong to). Used when the sync server refuses an
1514
+ * adopted node whose writes belong to the signed-in user (the app assigned them, or
1515
+ * the node was created for them) but which no claims-aware server ever accepted: a
1516
+ * beta.12 database's node with history the server recorded no owner for. The store's
1517
+ * own node id, clock and counters are untouched; the fresh node is registered, so the
1518
+ * sync engine adopts it like any other local node.
1519
+ *
1520
+ * @param fromNodeId - The refused local node (never the store's own)
1521
+ * @param unsyncedOpIds - Ids of its operations the server never acknowledged
1522
+ * @param principal - The user the writes belong to (the fresh node's binding)
1523
+ * @returns The fresh node id and the rewritten operations (for the outbound queue)
1524
+ * @throws {KoraError} `NODE_ID_UNKNOWN` when `fromNodeId` is the store's own node or
1525
+ * not a local node
1526
+ */
1527
+ reauthorLocalNode(fromNodeId: string, unsyncedOpIds: string[], principal: string): Promise<NodeRotationResult>;
1488
1528
  /**
1489
1529
  * Move this database back to a node id it authored under before (RT-38): after the
1490
1530
  * sync server refused the current node, the device tries a node a returning user
@@ -1525,6 +1565,19 @@ declare class Store implements OperationLog {
1525
1565
  * @returns The node now in use and whether it changed
1526
1566
  */
1527
1567
  bindPrincipal(principal: string): Promise<PrincipalBinding>;
1568
+ /**
1569
+ * The sync server refused this store's pinned node id because another user owns it
1570
+ * (`owned: true`), or later accepted it (`false`). A pinned node cannot move to a fresh
1571
+ * id, so writes made under it could only ever upload as its owner: while the server
1572
+ * says so, local writes are refused (F9). A no-op for a store whose node is not pinned
1573
+ * (it moves to a fresh node instead).
1574
+ *
1575
+ * @param owned - Whether the server reported the node owned by another user
1576
+ */
1577
+ setPinnedNodeOwnedElsewhere(owned: boolean): void;
1578
+ /** Refuse a local write while the pinned node belongs to another user (F9). */
1579
+ private assertLocalWriteAllowed;
1580
+ private bindPrincipalToNode;
1528
1581
  /**
1529
1582
  * A sync handshake as `nodeId` was accepted for `principal` (RT-50): bind an unbound
1530
1583
  * node (or replace a guessed binding) from the server's answer.
@@ -1684,8 +1737,9 @@ declare class Store implements OperationLog {
1684
1737
  * move backwards, and unsynced own writes are kept (see
1685
1738
  * `RestoreOptions.keepUnsyncedWrites`). Then {@link reloadFromDisk} runs.
1686
1739
  *
1687
- * A version-1 file is refused (`errorCode: 'BACKUP_FORMAT_OUTDATED'`); convert it with
1688
- * `convertBackupV1`.
1740
+ * A version-1 file (Kora 1.0.0-beta.12 and earlier) is converted first, exactly as
1741
+ * `convertBackupV1` does (`result.convertedFromVersion: 1`); one holding an operation
1742
+ * whose timestamp cannot be recovered is refused (`BACKUP_OPERATION_INVALID`).
1689
1743
  *
1690
1744
  * @param data - The backup data
1691
1745
  * @param options - Restore options (merge, collections, keepUnsyncedWrites, onProgress)
@@ -1738,6 +1792,19 @@ declare class Store implements OperationLog {
1738
1792
  * write path so they sync, unless declared `localOnly`.
1739
1793
  */
1740
1794
  private runMigrationsIfNeeded;
1795
+ /** The node id the database persisted before this open, if any. */
1796
+ private readPersistedNodeId;
1797
+ /**
1798
+ * Register node ids this database authored under that the registry does not know
1799
+ * yet: the node id a pinned `nodeId` (the auth device id) just replaced, and the
1800
+ * authors of queued operations. A beta.12 database had no registry, and a template
1801
+ * app created before its user signed in authored under a random node; once the
1802
+ * store is pinned to the device id, that node's queued writes would otherwise sit in
1803
+ * the queue forever, neither uploaded nor reported. Registered, they are adopted or
1804
+ * held (`status.heldNodes`) like any other local node's writes. Only the outbound
1805
+ * queue is read: received operations never enter it.
1806
+ */
1807
+ private registerUnregisteredAuthors;
1741
1808
  private loadOrGenerateNodeId;
1742
1809
  private loadSequenceNumber;
1743
1810
  private loadVersionVector;
@@ -2167,6 +2234,12 @@ interface RichTextControllerSnapshot {
2167
2234
  canUndo: boolean;
2168
2235
  canRedo: boolean;
2169
2236
  cursors: readonly RichTextCursorInfo[];
2237
+ /**
2238
+ * True while local edits are not in a successful save: a save is pending, in
2239
+ * flight, or was refused (see `error`). The edits stay in the live document and
2240
+ * are saved by the next edit or `retrySave()`.
2241
+ */
2242
+ hasUnsavedChanges: boolean;
2170
2243
  }
2171
2244
  interface RichTextController {
2172
2245
  readonly doc: yjs.Doc;
@@ -2178,12 +2251,29 @@ interface RichTextController {
2178
2251
  setCursor(anchor: number, head: number): void;
2179
2252
  clearCursor(): void;
2180
2253
  setUser(user: RichTextAwarenessUser | undefined): void;
2254
+ /**
2255
+ * Save the live document now (for example after a refused save, once the cause
2256
+ * is fixed). Resolves when the save settled; its outcome is in the snapshot.
2257
+ */
2258
+ retrySave(): Promise<void>;
2259
+ /**
2260
+ * The full document state (a Yjs update) while local edits are unsaved, else
2261
+ * null. Keep it as a recovery copy when saves keep failing.
2262
+ */
2263
+ getUnsavedState(): Uint8Array | null;
2264
+ /** Stop syncing; edits still waiting for the debounced save are saved first. */
2181
2265
  destroy(): void;
2182
2266
  }
2183
2267
 
2184
2268
  /**
2185
2269
  * Framework-agnostic richtext binding: Yjs document lifecycle, persistence,
2186
2270
  * incremental doc channel, and awareness cursors.
2271
+ *
2272
+ * Every save writes the full state of the live Y.Doc, which holds every local edit
2273
+ * and every merged remote change, so a stored change arriving while a save waits
2274
+ * cannot drop local typing. A refused save keeps the edits unsaved
2275
+ * (`hasUnsavedChanges`, `getUnsavedState()`) and the next edit or `retrySave()`
2276
+ * saves them again; the first successful save clears `error`.
2187
2277
  */
2188
2278
  declare function createRichTextController(options: CreateRichTextControllerOptions): RichTextController;
2189
2279
 
@@ -2750,4 +2840,4 @@ declare function createChunkPortPair(): {
2750
2840
  b: ChunkMessagePort;
2751
2841
  };
2752
2842
 
2753
- export { AdapterError, AdoptionSchedule, ApplyRemoteOptions, type AuditExportManifest, type AuditExportOptions, type AuditExportPayload, type AuditExportProgress, type AuditTraceQuery, BACKUP_VERSION, BackupFormatError, type BackupManifest, type BackupOptions, type BackupProgress, type BlobGcOptions, type BlobGcResult, type BlobManifest, type BlobManifestMetadata, BridgeTerminatedError, COMPACTION_BASELINE_META_KEY, type ChunkMessage, type ChunkMessagePort, type ChunkProvider, type ChunkRequestMessage, type ChunkResponseMessage, type ClockRebaseResult, type CollectionAccessor, CollectionRecord, type CompactionResult, type CompactionStrategy, ContentAddressedBlobStore, type ConvertBackupV1Options, type CreateRichTextControllerOptions, DEFAULT_CHUNK_SIZE, InvalidStateTransitionError, LAST_ACKED_SERVER_VECTOR_META_KEY, LOG_QUARANTINE_TABLE, LeaderUnresponsiveError, LocalMutationHandler, LocalNodeRecord, type LogIntegrityReport, type LogIntegrityRow, type LogRowProblem, type LogSequenceGap, MaterializedRowSnapshot, MemoryBlobStore, NoLeaderError, type NodeRotationResult, type OpfsBlobDirectory, OpfsBlobStore, OptimisticLockError, OrderByDirection, type PersistedAuditTrace, PersistenceError, type PersistenceRequestReason, type PersistenceStorageManager, type PrincipalBinding, QueryBuilder, QueryDescriptor, QueryError, QueryStore, QueryStoreCache, QuerySubscriptionError, type ReceiveBlobResult, type ReceiveBlobStores, RecordNotFoundError, type RecordsChange, type RecordsChangeListener, type RegisterOptions, type RematerializationMode, type RemoteChunkProvider, type RemoteChunkProviderOptions, type ReplaySnapshot, RequestAbortedError, type ResequenceResult, type RestoreOptions, type RestoreResult, type ResultsEqual, type RichTextAwarenessUser, type RichTextController, type RichTextControllerSnapshot, type RichTextCursorInfo, type RichTextSyncEngine, RowVersionState, SchemaVersionAheadError, SequenceManager, type ServerBlobCallbacks, StorageAdapter, StorageBackendMismatchError, StorageDurabilityError, StorageInUseError, StoragePersistence, type StoragePersistenceOptions, type StoragePersistenceState, type StoragePersistenceStatus, Store, StoreConfig, StoreNotOpenError, SubscribeOptions, SubscriptionBloomFilter, SubscriptionCallback, SubscriptionManager, type SubscriptionManagerOptions, type SubscriptionStats, TerminalRejection, type TransactionCollectionAccessor, TransactionContext, type TransactionContextConfig, type UnappliedOperation, UnsyncedDataError, WhereClause, WorkerInitError, WorkerTimeoutError, asRichTextSyncEngine, assertQueryReady, chunkBlob, collectBlobGarbage, collectOperationsAheadOfServer, compactOperationLog, computeAckCompactionWatermark, convertBackupV1, createChunkPortPair, createMemoryServerBlobStore, createOpfsBlobDirectory, createOpfsBlobStore, createRemoteChunkProvider, createRichTextController, decodeAuditExport, decodeRichtext, deserializeVersionVectorFromMeta, encodeRichtext, exportBackup, extractBlobRefs, fetchBlobManifest, getSharedQueryStoreCache, mergeVersionVectors, normalizeWhere, parseBlobManifest, persistedAuditTraceFromEvent, pluralize, prepareBlobForSend, putBlobForTransfer, queryKey, readAuditExportManifest, readBackupManifest, reassembleBlob, receiveBlob, resolveBlobManifest, restoreBackup, richtextStatesEqual, richtextToPlainText, serializeBlobManifest, serializeVersionVectorToMeta, serveBlobChunks, singularize, toServerBlobCallbacks, validateStateTransition, validateUpdateStateMachine, verifyAuditExportChecksum, verifyBackupChecksum };
2843
+ export { AdapterError, AdoptionSchedule, ApplyRemoteOptions, type AuditExportManifest, type AuditExportOptions, type AuditExportPayload, type AuditExportProgress, type AuditTraceQuery, BACKUP_VERSION, BackupFormatError, type BackupManifest, type BackupOptions, type BackupProgress, type BlobGcOptions, type BlobGcResult, type BlobManifest, type BlobManifestMetadata, BridgeTerminatedError, COMPACTION_BASELINE_META_KEY, type ChunkMessage, type ChunkMessagePort, type ChunkProvider, type ChunkRequestMessage, type ChunkResponseMessage, type ClockRebaseResult, type CollectionAccessor, CollectionRecord, type CompactionResult, type CompactionStrategy, ContentAddressedBlobStore, type ConvertBackupV1Options, type CreateRichTextControllerOptions, DEFAULT_CHUNK_SIZE, InvalidStateTransitionError, LAST_ACKED_SERVER_VECTOR_META_KEY, LOG_QUARANTINE_TABLE, LeaderUnresponsiveError, LocalMutationHandler, LocalNodeRecord, type LogIntegrityReport, type LogIntegrityRow, type LogRowProblem, type LogSequenceGap, MaterializedRowSnapshot, MemoryBlobStore, NoLeaderError, NodeOwnedByAnotherUserError, type NodeRotationResult, type OpfsBlobDirectory, OpfsBlobStore, OptimisticLockError, OrderByDirection, type PersistedAuditTrace, PersistenceError, type PersistenceRequestReason, type PersistenceStorageManager, type PrincipalBinding, QueryBuilder, QueryDescriptor, QueryError, QueryStore, QueryStoreCache, QuerySubscriptionError, type ReceiveBlobResult, type ReceiveBlobStores, RecordNotFoundError, type RecordsChange, type RecordsChangeListener, type RegisterOptions, type RematerializationMode, type RemoteChunkProvider, type RemoteChunkProviderOptions, type ReplaySnapshot, RequestAbortedError, type ResequenceResult, type RestoreOptions, type RestoreResult, type ResultsEqual, type RichTextAwarenessUser, type RichTextController, type RichTextControllerSnapshot, type RichTextCursorInfo, type RichTextSyncEngine, RowVersionState, SchemaVersionAheadError, SequenceManager, type ServerBlobCallbacks, StorageAdapter, StorageBackendMismatchError, StorageDurabilityError, StorageInUseError, StoragePersistence, type StoragePersistenceOptions, type StoragePersistenceState, type StoragePersistenceStatus, Store, StoreConfig, StoreNotOpenError, SubscribeOptions, SubscriptionBloomFilter, SubscriptionCallback, SubscriptionManager, type SubscriptionManagerOptions, type SubscriptionStats, TerminalRejection, type TransactionCollectionAccessor, TransactionContext, type TransactionContextConfig, type UnappliedOperation, UnsyncedDataError, WhereClause, WorkerInitError, WorkerTimeoutError, asRichTextSyncEngine, assertQueryReady, chunkBlob, collectBlobGarbage, collectOperationsAheadOfServer, compactOperationLog, computeAckCompactionWatermark, convertBackupV1, createChunkPortPair, createMemoryServerBlobStore, createOpfsBlobDirectory, createOpfsBlobStore, createRemoteChunkProvider, createRichTextController, decodeAuditExport, decodeRichtext, deserializeVersionVectorFromMeta, encodeRichtext, exportBackup, extractBlobRefs, fetchBlobManifest, getSharedQueryStoreCache, mergeVersionVectors, normalizeWhere, parseBlobManifest, persistedAuditTraceFromEvent, pluralize, prepareBlobForSend, putBlobForTransfer, queryKey, readAuditExportManifest, readBackupManifest, reassembleBlob, receiveBlob, resolveBlobManifest, restoreBackup, richtextStatesEqual, richtextToPlainText, serializeBlobManifest, serializeVersionVectorToMeta, serveBlobChunks, singularize, toServerBlobCallbacks, validateStateTransition, validateUpdateStateMachine, verifyAuditExportChecksum, verifyBackupChecksum };
package/dist/index.js CHANGED
@@ -20,7 +20,7 @@ import {
20
20
  pluralize,
21
21
  queryKey,
22
22
  singularize
23
- } from "./chunk-Y5LRH7KI.js";
23
+ } from "./chunk-5AQSGMJV.js";
24
24
  import {
25
25
  BlobIntegrityError,
26
26
  MemoryBlobStore
@@ -29,7 +29,7 @@ import {
29
29
  InvalidStateTransitionError,
30
30
  validateStateTransition,
31
31
  validateUpdateStateMachine
32
- } from "./chunk-HZTDBLDB.js";
32
+ } from "./chunk-BILSA6XZ.js";
33
33
  import "./chunk-A6GFH5IG.js";
34
34
  import {
35
35
  BACKUP_VERSION,
@@ -41,10 +41,10 @@ import {
41
41
  readBackupManifest,
42
42
  restoreBackup,
43
43
  verifyBackupChecksum
44
- } from "./chunk-P4V375FY.js";
44
+ } from "./chunk-UC2LGFPV.js";
45
45
  import {
46
46
  mergeYjsUpdates
47
- } from "./chunk-32EDKWMI.js";
47
+ } from "./chunk-KTVNANUH.js";
48
48
  import {
49
49
  LAST_ACKED_SERVER_VECTOR_META_KEY,
50
50
  VIRTUAL_TIMESTAMP_FIELDS,
@@ -53,7 +53,7 @@ import {
53
53
  deserializeVersionVectorFromMeta,
54
54
  mergeVersionVectors,
55
55
  serializeVersionVectorToMeta
56
- } from "./chunk-SJP2HTYZ.js";
56
+ } from "./chunk-QW4UU7NG.js";
57
57
  import {
58
58
  decodeRichtext,
59
59
  encodeRichtext,
@@ -65,6 +65,7 @@ import {
65
65
  BridgeTerminatedError,
66
66
  LeaderUnresponsiveError,
67
67
  NoLeaderError,
68
+ NodeOwnedByAnotherUserError,
68
69
  OptimisticLockError,
69
70
  PersistenceError,
70
71
  QueryError,
@@ -78,7 +79,7 @@ import {
78
79
  UnsyncedDataError,
79
80
  WorkerInitError,
80
81
  WorkerTimeoutError
81
- } from "./chunk-DUKTWVZW.js";
82
+ } from "./chunk-SRHDJF5B.js";
82
83
 
83
84
  // src/persistence/storage-persistence.ts
84
85
  function defaultStorageManager() {
@@ -355,7 +356,6 @@ var LOAD_ORIGIN = "kora-load";
355
356
  var REMOTE_ORIGIN = "kora-remote";
356
357
  var DOC_CHANNEL_ORIGIN = "kora-doc-channel";
357
358
  var TEXT_KEY = "content";
358
- var COMPACT_AFTER_DELTAS = 20;
359
359
  var PERSIST_DEBOUNCE_MS = 400;
360
360
  function createRichTextController(options) {
361
361
  const {
@@ -377,16 +377,19 @@ function createRichTextController(options) {
377
377
  let canUndo = false;
378
378
  let canRedo = false;
379
379
  let cursors = [];
380
+ let editGeneration = 0;
381
+ let savedGeneration = 0;
382
+ let saving = null;
383
+ let saveAgain = false;
380
384
  const listeners = /* @__PURE__ */ new Set();
381
385
  let snapshot = {
382
386
  ready: false,
383
387
  error: null,
384
388
  canUndo: false,
385
389
  canRedo: false,
386
- cursors: []
390
+ cursors: [],
391
+ hasUnsavedChanges: false
387
392
  };
388
- const baseUpdateRef = { current: null };
389
- const pendingDeltasRef = { current: [] };
390
393
  const docChannelActiveRef = { current: false };
391
394
  let persistTimer = null;
392
395
  let recordUnsubscribe = null;
@@ -398,7 +401,8 @@ function createRichTextController(options) {
398
401
  error,
399
402
  canUndo,
400
403
  canRedo,
401
- cursors: [...cursors]
404
+ cursors: [...cursors],
405
+ hasUnsavedChanges: editGeneration > savedGeneration
402
406
  };
403
407
  };
404
408
  const notify = () => {
@@ -469,26 +473,38 @@ function createRichTextController(options) {
469
473
  return;
470
474
  }
471
475
  Y.applyUpdate(doc, encoded, REMOTE_ORIGIN);
472
- baseUpdateRef.current = encoded;
473
- pendingDeltasRef.current = [];
474
476
  syncHistoryState();
475
477
  };
476
- const flushPersist = async () => {
477
- const snapshot2 = composeRichtextSnapshot(baseUpdateRef.current, pendingDeltasRef.current);
478
- if (pendingDeltasRef.current.length >= COMPACT_AFTER_DELTAS) {
479
- baseUpdateRef.current = snapshot2;
480
- pendingDeltasRef.current = [];
481
- }
478
+ const persistOnce = async () => {
479
+ const generation = editGeneration;
480
+ const state = Y.encodeStateAsUpdate(doc);
482
481
  try {
483
482
  await collection.update(recordId, {
484
- [fieldName]: snapshot2
483
+ [fieldName]: state
485
484
  });
485
+ if (generation > savedGeneration) savedGeneration = generation;
486
+ error = null;
486
487
  } catch (cause) {
487
- if (!disposed) {
488
- error = cause instanceof Error ? cause : new Error(String(cause));
489
- notify();
490
- }
488
+ error = cause instanceof Error ? cause : new Error(String(cause));
491
489
  }
490
+ if (!disposed) notify();
491
+ };
492
+ const flushPersist = () => {
493
+ if (saving) {
494
+ saveAgain = true;
495
+ return saving;
496
+ }
497
+ const run = (async () => {
498
+ do {
499
+ saveAgain = false;
500
+ await persistOnce();
501
+ } while (saveAgain);
502
+ })();
503
+ saving = run;
504
+ void run.finally(() => {
505
+ if (saving === run) saving = null;
506
+ });
507
+ return run;
492
508
  };
493
509
  const schedulePersist = () => {
494
510
  if (persistTimer) {
@@ -504,7 +520,7 @@ function createRichTextController(options) {
504
520
  if (origin === LOAD_ORIGIN || origin === REMOTE_ORIGIN || origin === DOC_CHANNEL_ORIGIN) {
505
521
  return;
506
522
  }
507
- pendingDeltasRef.current.push(update);
523
+ editGeneration += 1;
508
524
  if (docChannelActiveRef.current && syncEngine) {
509
525
  syncEngine.getRichtextDocChannel?.().send(collectionName, recordId, fieldName, update);
510
526
  schedulePersist();
@@ -588,8 +604,6 @@ function createRichTextController(options) {
588
604
  target.delete(0, target.length);
589
605
  }, LOAD_ORIGIN);
590
606
  const encoded = encodeRichtextInput(record?.[fieldName]);
591
- baseUpdateRef.current = encoded;
592
- pendingDeltasRef.current = [];
593
607
  if (encoded) {
594
608
  Y.applyUpdate(doc, encoded, LOAD_ORIGIN);
595
609
  }
@@ -632,6 +646,16 @@ function createRichTextController(options) {
632
646
  setUser(nextUser) {
633
647
  user = nextUser;
634
648
  },
649
+ retrySave() {
650
+ if (persistTimer) {
651
+ clearTimeout(persistTimer);
652
+ persistTimer = null;
653
+ }
654
+ return flushPersist();
655
+ },
656
+ getUnsavedState() {
657
+ return editGeneration > savedGeneration ? Y.encodeStateAsUpdate(doc) : null;
658
+ },
635
659
  destroy() {
636
660
  if (disposed) {
637
661
  return;
@@ -641,14 +665,15 @@ function createRichTextController(options) {
641
665
  clearTimeout(persistTimer);
642
666
  persistTimer = null;
643
667
  }
668
+ if (editGeneration > savedGeneration) {
669
+ void flushPersist();
670
+ }
644
671
  doc.off("update", onDocUpdate);
645
672
  recordUnsubscribe?.();
646
673
  docChannelUnsubscribe?.();
647
674
  awarenessUnsubscribe?.();
648
675
  clearCursor();
649
676
  undoManager.destroy();
650
- baseUpdateRef.current = null;
651
- pendingDeltasRef.current = [];
652
677
  docChannelActiveRef.current = false;
653
678
  listeners.clear();
654
679
  }
@@ -669,16 +694,6 @@ function encodeRichtextInput(value) {
669
694
  throw new Error("Richtext record value must be a string, Uint8Array, ArrayBuffer, or null.");
670
695
  }
671
696
  }
672
- function composeRichtextSnapshot(base, deltas) {
673
- const mergedDoc = new Y.Doc();
674
- if (base) {
675
- Y.applyUpdate(mergedDoc, base);
676
- }
677
- for (const delta of deltas) {
678
- Y.applyUpdate(mergedDoc, delta);
679
- }
680
- return Y.encodeStateAsUpdate(mergedDoc);
681
- }
682
697
  function updatesEqual(left, right) {
683
698
  if (left.length !== right.length) {
684
699
  return false;
@@ -1241,6 +1256,7 @@ export {
1241
1256
  LeaderUnresponsiveError,
1242
1257
  MemoryBlobStore,
1243
1258
  NoLeaderError,
1259
+ NodeOwnedByAnotherUserError,
1244
1260
  OpfsBlobStore,
1245
1261
  OptimisticLockError,
1246
1262
  PersistenceError,