API reference@evolu/commonlocal‑first/Storage › BaseSqliteStorage

Defined in: packages/common/src/local-first/Storage.ts:373

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.

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

  • Omit<Storage, "validateWriteKey" | "setWriteKey" | "writeMessages" | "readDbChange">

Properties

deleteOwner

readonly deleteOwner: (ownerId: Uint8Array<ArrayBufferLike> & Brand<"Length16"> & Brand<"IdBytes"> & Brand<"OwnerIdBytes">) => void;

Defined in: packages/common/src/local-first/Storage.ts:191

Delete all data for the given Owner.

Inherited from

Storage.deleteOwner


findLowerBound

readonly findLowerBound: (ownerId: Uint8Array<ArrayBufferLike> & Brand<"Length16"> & Brand<"IdBytes"> & Brand<"OwnerIdBytes">, begin: number & Brand<"NonNaN"> & Brand<"Finite"> & Brand<"Int"> & Brand<"NonNegative">, end: number & Brand<"NonNaN"> & Brand<"Finite"> & Brand<"Int"> & Brand<"NonNegative">, upperBound: RangeUpperBound) => number & Brand<"NonNaN"> & Brand<"Finite"> & Brand<"Int"> & Brand<"NonNegative">;

Defined in: packages/common/src/local-first/Storage.ts:143

Inherited from

Storage.findLowerBound


fingerprint

readonly fingerprint: (ownerId: Uint8Array<ArrayBufferLike> & Brand<"Length16"> & Brand<"IdBytes"> & Brand<"OwnerIdBytes">, begin: number & Brand<"NonNaN"> & Brand<"Finite"> & Brand<"Int"> & Brand<"NonNegative">, end: number & Brand<"NonNaN"> & Brand<"Finite"> & Brand<"Int"> & Brand<"NonNegative">) => Fingerprint;

Defined in: packages/common/src/local-first/Storage.ts:124

Inherited from

Storage.fingerprint


fingerprintRanges

readonly fingerprintRanges: (ownerId: Uint8Array<ArrayBufferLike> & Brand<"Length16"> & Brand<"IdBytes"> & Brand<"OwnerIdBytes">, buckets: readonly number & Brand<"NonNaN"> & Brand<"Finite"> & Brand<"Int"> & Brand<"NonNegative">[], upperBound?: RangeUpperBound) => readonly FingerprintRange[];

Defined in: packages/common/src/local-first/Storage.ts:137

Computes 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.

Inherited from

Storage.fingerprintRanges


getExistingTimestamps

readonly getExistingTimestamps: (ownerIdBytes: Uint8Array<ArrayBufferLike> & Brand<"Length16"> & Brand<"IdBytes"> & Brand<"OwnerIdBytes">, timestampsBytes: readonly [Uint8Array<ArrayBufferLike> & Brand<"Length16"> & Brand<"TimestampBytes">, Uint8Array<ArrayBufferLike> & Brand<"Length16"> & Brand<"TimestampBytes">]) => readonly Uint8Array<ArrayBufferLike> & Brand<"Length16"> & Brand<"TimestampBytes">[];

Defined in: packages/common/src/local-first/Storage.ts:388

Efficiently checks which timestamps already exist in the database using a single CTE query instead of N individual queries.

getSize

readonly getSize: (ownerId: Uint8Array<ArrayBufferLike> & Brand<"Length16"> & Brand<"IdBytes"> & Brand<"OwnerIdBytes">) => number & Brand<"NonNaN"> & Brand<"Finite"> & Brand<"Int"> & Brand<"NonNegative">;

Defined in: packages/common/src/local-first/Storage.ts:122

Inherited from

Storage.getSize


insertTimestamp

readonly insertTimestamp: (ownerId: Uint8Array<ArrayBufferLike> & Brand<"Length16"> & Brand<"IdBytes"> & Brand<"OwnerIdBytes">, timestamp: Uint8Array<ArrayBufferLike> & Brand<"Length16"> & Brand<"TimestampBytes">, strategy: StorageInsertTimestampStrategy) => void;

Defined in: packages/common/src/local-first/Storage.ts:378

Inserts a timestamp for an owner into the skiplist-based storage.

iterate

readonly iterate: (ownerId: Uint8Array<ArrayBufferLike> & Brand<"Length16"> & Brand<"IdBytes"> & Brand<"OwnerIdBytes">, begin: number & Brand<"NonNaN"> & Brand<"Finite"> & Brand<"Int"> & Brand<"NonNegative">, end: number & Brand<"NonNaN"> & Brand<"Finite"> & Brand<"Int"> & Brand<"NonNegative">, callback: (timestamp: Uint8Array<ArrayBufferLike> & Brand<"Length16"> & Brand<"TimestampBytes">, index: number & Brand<"NonNaN"> & Brand<"Finite"> & Brand<"Int"> & Brand<"NonNegative">) => boolean) => void;

Defined in: packages/common/src/local-first/Storage.ts:150

Inherited from

Storage.iterate