API reference / @evolu/common / Evolu/Internal / ClientStorage

Interface: ClientStorage

Defined in: packages/common/src/Evolu/Sync.ts:421

Common interface for both client and relay SQLite storages.

Evolu uses a Skiplist, which leverages SQLite indexes. The core logic is implemented in SQL, so it doesn't have to make roundtrips to the DB.

While the SQL implementation may look sophisticated, it's conceptually simple and LLMs can explain how it works. The Skiplist data structure is well explained in this Stack Overflow answer. The logic resembles Negentropy's C++ storage, except we use a Skiplist to leverage SQLite indexes, which makes the code simpler.

Note: A paid review by the SQLite team is planned, as they use the same algorithm for their rsync tool.

The ideal storage for a Relay should use an architecture like strfry (a KV storage), but with Skiplist to ensure that insertion order doesn't matter (local-first apps can often write in the past.)

The ideal client implementation should probably use the SQLite extension instead of SQL or even a KV storage, when such a thing for browsers/native will exist and will be faster than SQLite.

Scaling

The load can be distributed by deploying multiple relays, synchronized with each other, if necessary. One relay should handle hundreds of thousands of users, and when it goes down, nothing happens, because it will be synchronized later.

Extends

Properties

PropertyModifierTypeDescriptionInherited fromDefined in
deleteOwnerreadonly(ownerId) => booleanDelete all data for the given Owner. Returns true on success, false on failure.SqliteStorageBase.deleteOwnerpackages/common/src/Evolu/Storage.ts:261
findLowerBoundreadonly(ownerId, begin, end, upperBound) => | number & Brand<"Int"> & Brand<"NonNegative"> | null-SqliteStorageBase.findLowerBoundpackages/common/src/Evolu/Storage.ts:259
fingerprintreadonly(ownerId, begin, end) => | Fingerprint | null-SqliteStorageBase.fingerprintpackages/common/src/Evolu/Storage.ts:257
fingerprintRangesreadonly(ownerId, buckets, upperBound?) => | readonly FingerprintRange[] | nullComputes fingerprints with their upper bounds in one call. This function can be replaced with many fingerprint/findLowerBound calls, but implementations can leverage it for batching and more efficient fingerprint computation.SqliteStorageBase.fingerprintRangespackages/common/src/Evolu/Storage.ts:258
getSizereadonly(ownerId) => | number & Brand<"Int"> & Brand<"NonNegative"> | null-SqliteStorageBase.getSizepackages/common/src/Evolu/Storage.ts:256
insertTimestampreadonly(ownerId, timestamp) => Result<void, SqliteError>Inserts a timestamp for an owner into the skiplist-based storage. Must be idempotent - inserting the same timestamp multiple times has no effect after the first insertion. This is crucial for sync reliability as messages may be received and processed multiple times.SqliteStorageBase.insertTimestamppackages/common/src/Evolu/Storage.ts:251
iteratereadonly(ownerId, begin, end, callback) => void-SqliteStorageBase.iteratepackages/common/src/Evolu/Storage.ts:260
readDbChangereadonly(ownerId, timestamp) => | EncryptedDbChange | nullRead encrypted DbChanges from storage.Storage.readDbChangepackages/common/src/Evolu/Storage.ts:110
setWriteKeyreadonly(ownerId, writeKey) => booleanSets the OwnerWriteKey for the given Owner.Storage.setWriteKeypackages/common/src/Evolu/Storage.ts:91
validateWriteKeyreadonly(ownerId, writeKey) => booleanValidates the OwnerWriteKey for the given Owner. Returns true if the write key is valid, false otherwise.Storage.validateWriteKeypackages/common/src/Evolu/Storage.ts:85
writeMessagesreadonly(ownerId, messages) => Promise<boolean>Write encrypted CrdtMessages to storage. Must use a mutex (per ownerId on Relay) to ensure sequential processing and proper protocol logic handling during sync operations. Returns true on success, false on failure.Storage.writeMessagespackages/common/src/Evolu/Storage.ts:104

Was this page helpful?