tinybase
Version:
A reactive data store and sync engine.
1,342 lines (1,320 loc) • 77.4 kB
text/typescript
/**
* The persisters module of the TinyBase project provides a simple framework for
* saving and loading Store and MergeableStore data, to and from different
* destinations, or underlying storage types.
*
* Many entry points are provided (in separately installed modules), each of
* which returns different types of Persister that can load and save a Store.
* Between them, these allow you to store your TinyBase data locally, remotely,
* to SQLite and PostgreSQL databases, and across synchronization boundaries
* with CRDT frameworks.
*
* |Persister|Storage|Store|MergeableStore
* |-|-|-|-|
* |SessionPersister|Browser session storage|Yes|Yes
* |LocalPersister|Browser local storage|Yes|Yes
* |FilePersister|Local file (where possible)|Yes|Yes
* |IndexedDbPersister|Browser IndexedDB|Yes|No
* |RemotePersister|Remote server|Yes|No
* |Sqlite3Persister|SQLite in Node, via [sqlite3](https://github.com/TryGhost/node-sqlite3)|Yes|Yes*
* |SqliteWasmPersister|SQLite in a browser, via [sqlite-wasm](https://github.com/tomayac/sqlite-wasm)|Yes|Yes*
* |ExpoSqlitePersister|SQLite in React Native, via [expo-sqlite](https://github.com/expo/expo/tree/main/packages/expo-sqlite)|Yes|Yes*
* |PostgresPersister|PostgreSQL, via [postgres](https://github.com/porsager/postgres)|Yes|Yes*
* |PglitePersister|PostgreSQL, via [PGlite](https://github.com/electric-sql/pglite)|Yes|Yes*
* |CrSqliteWasmPersister|SQLite CRDTs, via [cr-sqlite-wasm](https://github.com/vlcn-io/cr-sqlite)|Yes|No
* |ElectricSqlPersister|Electric SQL, via [electric-sql](https://github.com/electric-sql/electric)|Yes|No
* |LibSqlPersister|LibSQL for Turso, via [libsql-client](https://github.com/tursodatabase/libsql-client-ts)|Yes|No
* |PowerSyncPersister|PowerSync, via [powersync-sdk](https://github.com/powersync-ja/powersync-js)|Yes|No
* |YjsPersister|Yjs CRDTs, via [yjs](https://github.com/yjs/yjs)|Yes|No
* |AutomergePersister|Automerge CRDTs, via [automerge-repo](https://github.com/automerge/automerge-repo)|Yes|No
* |PartyKitPersister|[PartyKit](https://www.partykit.io/), via the persister-partykit-server module|Yes|No|
*
* (*) Note that SQLite- and PostgreSQL-based Persisters can currently only
* persist MergeableStore data when used with the JSON-based DpcJson mode, and
* not when using the DpcTabular mode.
*
* Since persistence requirements can be different for every app, the
* createCustomPersister function in this module can also be used to easily
* create a fully customized way to save and load Store data.
*
* Similarly, the createCustomSqlitePersister function and
* createCustomPostgreSqlPersister function can be used to build Persister objects
* against SQLite and PostgreSQL SDKs (or forks) that are not already included
* with TinyBase.
* @see Persistence guides
* @see Countries demo
* @see Todo App demos
* @see Drawing demo
* @packageDocumentation
* @module persisters
* @since v1.0.0
*/
import type {TableIdFromSchema} from '../../_internal/store/with-schemas/index.d.cts';
import type {
MergeableChanges,
MergeableContent,
MergeableStore,
} from '../../mergeable-store/with-schemas/index.d.cts';
import type {
Changes,
Content,
OptionalSchemas,
OptionalTablesSchema,
Store,
} from '../../store/with-schemas/index.d.cts';
import type {Id} from '../../with-schemas/index.d.cts';
/**
* The Status enum is used to indicate whether a Persister is idle, or loading or
* saving data.
*
* The enum is intended to be used to understand the status of the Persister in
* conjunction with the getStatus and addStatusListener methods.
*
* Note that a Persister cannot be loading and saving data at the same time.
* @category Lifecycle
* @since v5.3.0
*/
export const enum Status {
/**
* Indicates that the Persister is neither loading or saving data.
* @category Enum
* @since v5.3.0
*/
Idle = 0,
/**
* Indicates that the Persister is loading data.
* @category Enum
* @since v5.3.0
*/
Loading = 1,
/**
* Indicates that the Persister is saving data.
* @category Enum
* @since v5.3.0
*/
Saving = 2,
}
/**
* The Persists enum is used to indicate whether a Persister can support a
* regular Store, a MergeableStore, or both.
*
* The enum is intended to be used by the author of a Persister to indicate
* which types of store can be persisted. If you discover type errors when
* trying to instantiate a Persister, it is most likely that you are passing in
* an unsupported type of store.
*
* See the createCustomPersister method for an example of this enum being used.
* @category Mergeable
* @since v5.0.0
*/
export const enum Persists {
/**
* Indicates that only a regular Store can be supported by a Persister.
* @category Enum
* @since v5.0.0
*/
StoreOnly = 1,
/**
* Indicates that only a MergeableStore can be supported by a Persister.
* @category Enum
* @since v5.0.0
*/
MergeableStoreOnly = 2,
/**
* Indicates that either a regular Store or a MergeableStore can be supported
* by a Persister.
* @category Enum
* @since v5.0.0
*/
StoreOrMergeableStore = 3,
}
/**
* The PersistedStore type is a generic representation of the type of store
* being handled by a Persister.
*
* This has schema-based typing. The following is a simplified representation:
*
* ```ts override
* export type PersistedStore<Persist extends Persists = Persists.StoreOnly> =
* Persist extends Persists.StoreOrMergeableStore
* ? Store | MergeableStore
* : Persist extends Persists.MergeableStoreOnly
* ? MergeableStore
* : Store;
* ```
*
* Using the values of the Persists enum, the generic parameter indicates
* whether the Persister is handling a regular Store, a MergeableStore, or
* either.
*
* If the generic parameter is unspecified, the StoreOnly enum value is used,
* meaning that PersistedStore is equivalent to a regular Store.
* @category Mergeable
* @since v5.0.0
*/
export type PersistedStore<
Schemas extends OptionalSchemas,
Persist extends Persists = Persists.StoreOnly,
> = Persist extends Persists.StoreOrMergeableStore
? Store<Schemas> | MergeableStore<Schemas>
: Persist extends Persists.MergeableStoreOnly
? MergeableStore<Schemas>
: Store<Schemas>;
/**
* The PersistedContent type is a generic representation of the content in the
* type of store being handled by a Persister.
*
* This has schema-based typing. The following is a simplified representation:
*
* ```ts override
* export type PersistedContent<Persist extends Persists = Persists.StoreOnly> =
* Persist extends Persists.StoreOrMergeableStore
* ? Content | MergeableContent
* : Persist extends Persists.MergeableStoreOnly
* ? MergeableContent
* : Content;
* ```
*
* Using the values of the Persists enum, the generic parameter indicates
* whether the Persister is handling content from a regular Store (the Content
* type), a MergeableStore (the MergeableContent type), or either (the union of
* the two).
*
* If the generic parameter is unspecified, the StoreOnly enum value is used,
* meaning that PersistedContent is equivalent to the Content type.
* @category Mergeable
* @since v5.0.0
*/
export type PersistedContent<
Schemas extends OptionalSchemas,
Persist extends Persists = Persists.StoreOnly,
> = Persist extends Persists.StoreOrMergeableStore
? Content<Schemas> | MergeableContent<Schemas>
: Persist extends Persists.MergeableStoreOnly
? MergeableContent<Schemas>
: Content<Schemas>;
/**
* The PersistedChanges type is a generic representation of changes made to the
* type of store being handled by a Persister.
*
* This has schema-based typing. The following is a simplified representation:
*
* ```ts override
* export type PersistedChanges<
* Persist extends Persists = Persists.StoreOnly,
* Hashed extends boolean = false,
* > = Persist extends Persists.StoreOrMergeableStore
* ? Changes | MergeableChanges<Hashed>
* : Persist extends Persists.MergeableStoreOnly
* ? MergeableChanges<Hashed>
* : Changes;
* ```
*
* Using the values of the Persists enum, the generic parameter indicates
* whether the Persister is handling changes for a regular Store (the Changes
* type), a MergeableStore (the MergeableChanges type), or either (the union of
* the two).
* @category Mergeable
* @since v5.0.0
*/
export type PersistedChanges<
Schemas extends OptionalSchemas,
Persist extends Persists = Persists.StoreOnly,
> = Persist extends Persists.StoreOrMergeableStore
? Changes<Schemas> | MergeableChanges<Schemas>
: Persist extends Persists.MergeableStoreOnly
? MergeableChanges<Schemas>
: Changes<Schemas>;
/**
* A PersisterListener is a generic representation of the callback that lets a
* Persister inform the store that a change has happened to the underlying data.
*
* This has schema-based typing. The following is a simplified representation:
*
* ```ts override
* export type PersisterListener<Persist extends Persists = Persists.StoreOnly> = (
* content?: PersistedContent<Persist>,
* changes?: PersistedChanges<Persist>,
* ) => void;
* ```
*
* Using the values of the Persists enum, the generic parameter indicates
* whether the Persister is handling content and changes from a regular Store, a
* MergeableStore, or either.
*
* If the listener is called with the `changes` parameter, it will be used to
* make an incremental change to the Store. If not, but the `content` parameter
* is available, that will be used to make a wholesale change to the Store. If
* neither are present, the content will be loaded using the Persister's load
* method. Prior to v5.0, these parameters were callbacks and the overall type
* was non-generic.
* @param content If provided, this is a Content object from the the Persister
* that will be used to immediately wholesale update the Store.
* @param changes If provided, this is a Changes object from the the Persister
* that will be used to immediately incrementally update the Store. This takes
* priority over the content argument above if present.
* @category Creation
* @since v4.0.0
*/
export type PersisterListener<
Schemas extends OptionalSchemas,
Persist extends Persists = Persists.StoreOnly,
> = (
content?: PersistedContent<Schemas, Persist>,
changes?: PersistedChanges<Schemas, Persist>,
) => void;
/**
* The StatusListener type describes a function that is used to listen to
* changes to the loading and saving status of the Persister.
*
* This has schema-based typing. The following is a simplified representation:
*
* ```ts override
* export type StatusListener<Persist extends Persists = Persists.StoreOnly> = (
* persister: Persister<Persist>,
* status: Status,
* ) => void;
* ```
*
* A StatusListener is provided when using the addStatusListener method. See
* that method for specific examples.
*
* When called, a StatusListener is given a reference to the Persister and the
* new Status: 0 means now idle, 1 means now loading, and 2 means now saving.
* @param persister A reference to the Persister that changed.
* @param status The new loading or saving Status.
* @category Listener
* @since v5.3.0
*/
export type StatusListener<
Schemas extends OptionalSchemas,
Persist extends Persists = Persists.StoreOnly,
> = (persister: Persister<Schemas, Persist>, status: Status) => void;
/**
* The PersisterStats type describes the number of times a Persister object has
* loaded or saved data.
*
* A PersisterStats object is returned from the getStats method.
* @category Development
* @since v1.0.0
*/
export type PersisterStats = {
/**
* The number of times data has been loaded.
* @category Stat
* @since v1.0.0
*/
loads: number;
/**
* The number of times data has been saved.
* @category Stat
* @since v1.0.0
*/
saves: number;
};
/**
* The DatabasePersisterConfig type describes the configuration of a
* database-oriented Persister, such as those for SQLite and PostgreSQL.
*
* This has schema-based typing. The following is a simplified representation:
*
* ```ts override
* DpcJson | DpcTabular;
* ```
*
* There are two modes for persisting a Store with a database:
*
* - A JSON serialization of the whole Store, which is stored in a single row of
* a table (normally called `tinybase`) within the database. This is
* configured by providing a DpcJson object.
* - A tabular mapping of Table Ids to database table names (and vice-versa).
* Values are stored in a separate special table (normally called
* `tinybase_values`). This is configured by providing a DpcTabular object.
*
* Please see the DpcJson and DpcTabular type documentation for more detail on
* each. If not specified otherwise, JSON serialization will be used for
* persistence.
*
* Changes made to the database (outside of this Persister) are picked up
* immediately if they are made via the same connection or library that it is
* using. If the database is being changed by another client, the Persister
* needs to poll for changes. Hence both configuration types also contain an
* `autoLoadIntervalSeconds` property which indicates how often it should do
* that. This defaults to 1 second.
*
* Note that all the nested types within this type have a 'Dpc' prefix, short
* for 'DatabasePersisterConfig'.
* @example
* When applied to a database Persister, this DatabasePersisterConfig will load
* and save a JSON serialization from and to a table called `my_tinybase`,
* polling the database every 2 seconds. See DpcJson for more details on these
* settings.
*
* ```js
* import type {DatabasePersisterConfig} from 'tinybase';
*
* export const databasePersisterConfig: DatabasePersisterConfig = {
* mode: 'json',
* storeTableName: 'my_tinybase',
* autoLoadIntervalSeconds: 2,
* };
* ```
* @example
* When applied to a database Persister, this DatabasePersisterConfig will load
* and save tabular data from and to tables specified in the `load` and `save`
* mappings. See DpcTabular for more details on these settings.
*
* ```js
* import type {DatabasePersisterConfig} from 'tinybase';
*
* export const databasePersisterConfig: DatabasePersisterConfig = {
* mode: 'tabular',
* tables: {
* load: {petsInDb: 'pets', speciesInDb: 'species'},
* save: {pets: 'petsInDb', species: 'speciesInDb'},
* },
* };
* ```
* @category Configuration
* @since v4.0.0
*/
export type DatabasePersisterConfig<Schemas extends OptionalSchemas> =
| DpcJson
| DpcTabular<Schemas[0]>;
/**
* The DpcJson type describes the configuration of a database-oriented Persister
* operating in serialized JSON mode.
*
* One setting is the `storeTableName` property, which indicates the name of a
* table in the database which will be used to serialize the Store content into.
* It defaults to `tinybase`.
*
* That table in the database will be given two columns: a primary key column
* called `_id`, and one called `store`. (These column names can be changed
* using the `rowIdColumnName` and `storeColumnName` settings). The Persister
* will place a single row in this table with `_` in the `_id` column, and the
* JSON serialization in the `store` column, something like the following.
*
* ```
* > SELECT * FROM tinybase;
* +-----+-----------------------------------------------------+
* | _id | store |
* +-----+-----------------------------------------------------+
* | _ | [{"pets":{"fido":{"species":"dog"}}},{"open":true}] |
* +-----+-----------------------------------------------------+
* ```
*
* The 'Dpc' prefix indicates that this type is used within the
* DatabasePersisterConfig type.
* @example
* When applied to a database Persister, this DatabasePersisterConfig will load
* and save a JSON serialization from and to a table called `tinybase_json`.
*
* ```js
* import type {DatabasePersisterConfig} from 'tinybase';
*
* export const databasePersisterConfig: DatabasePersisterConfig = {
* mode: 'json',
* storeTableName: 'tinybase_json',
* };
* ```
* @category Configuration
* @since v4.0.0
*/
export type DpcJson = {
/**
* The mode to be used for persisting the Store to the database, in this case
* JSON serialization. See the DpcTabular type for the alternative tabular
* mapping mode.
* @category Configuration
* @since v4.0.0
*/
mode: 'json';
/**
* An optional string which indicates the name of a table in the database
* which will be used to serialize the Store content into. It defaults to
* `tinybase`.
* @category Configuration
* @since v4.0.0
*/
storeTableName?: string;
/**
* The optional name of the column in the database table that will be used as
* the Id for the Store, defaulting to '_id', since v5.0.
* @category Configuration
* @since v4.0.0
*/
storeIdColumnName?: string;
/**
* The optional name of the column in the database table that will be used for
* the JSON of the Store, defaulting to 'store', since v5.0.
* @category Configuration
* @since v4.0.0
*/
storeColumnName?: string;
/**
* How often the Persister should poll the database for any changes made to it
* by other clients, defaulting to 1 second.
* @category Configuration
* @since v4.0.0
*/
autoLoadIntervalSeconds?: number;
};
/**
* The DpcTabular type describes the configuration of a database-oriented
* Persister that is operating in tabular mapping mode.
*
* This configuration can only be used when the Persister is persisting a
* regular Store. For those database-oriented Persister types that support
* MergeableStore data, you will need to use JSON-serialization, es described in
* the DpcJson section.
*
* It is important to note that both the tabular mapping in ('save') and out
* ('load') of an underlying database are disabled by default. This is to ensure
* that if you pass in an existing populated database you don't run the
* immediate risk of corrupting or losing all your data.
*
* This configuration therefore takes a `tables` property object (with child
* `load` and `save` property objects) and a `values` property object. These
* indicate how you want to load and save Tables and Values respectively. At
* least one of these two properties are required for the Persister to do
* anything!
*
* Note that if you are planning to both load from and save to a database, it is
* important to make sure that the load and save table mappings are symmetrical.
* For example, consider the following.
*
* ```js
* import type {DatabasePersisterConfig} from 'tinybase';
*
* export const databasePersisterConfig: DatabasePersisterConfig = {
* mode: 'tabular',
* tables: {
* load: {petsInDb: 'pets', speciesInDb: 'species'},
* save: {pets: 'petsInDb', species: 'speciesInDb'},
* },
* };
* ```
*
* See the documentation for the DpcTabularLoad, DpcTabularSave, and
* DpcTabularValues types for more details on how to configure the tabular
* mapping mode.
*
* Columns in SQLite database have no type, and so in this mode, the table can
* contain strings and numbers for Cells and Values, just as TinyBase does.
* Booleans, unfortunately, are stored as 0 or 1 in SQLite, and cannot be
* distinguished from numbers.
*
* In PostgreSQL databases, all Cell and Value columns are expected to be typed
* as `text`, and the strings, booleans, and numbers are all JSON-encoded by the
* Persister.
*
* The 'Dpc' prefix indicates that this type is used within the
* DatabasePersisterConfig type.
* @example
* When applied to a database Persister, this DatabasePersisterConfig will load
* and save Tables data from and to tables specified in the `load` and `save`
* mappings, and Values data from and to a table called `my_tinybase_values`.
*
* ```js
* import type {DatabasePersisterConfig} from 'tinybase';
*
* export const databasePersisterConfig: DatabasePersisterConfig = {
* mode: 'tabular',
* tables: {
* load: {petsInDb: 'pets', speciesInDb: 'species'},
* save: {pets: 'petsInDb', species: 'speciesInDb'},
* },
* values: {
* load: true,
* save: true,
* tableName: 'my_tinybase_values',
* },
* };
* ```
* @category Configuration
* @since v4.0.0
*/
export type DpcTabular<Schema extends OptionalTablesSchema> = {
/**
* The mode to be used for persisting the Store to the database, in this case
* tabular mapping. See the DpcJson type for the alternative JSON
* serialization mode.
* @category Configuration
* @since v4.0.0
*/
mode: 'tabular';
/**
* The settings for how the Store Tables are mapped to and from the database.
* @category Configuration
* @since v4.0.0
*/
tables?: {
/**
* The settings for how the database tables are mapped into the Store Tables
* when loading.
* @category Configuration
* @since v4.0.0
*/
load?: DpcTabularLoad<Schema>;
/**
* The settings for how the Store Tables are mapped out to the database
* tables when saving.
* @category Configuration
* @since v4.0.0
*/
save?: DpcTabularSave<Schema>;
};
/**
* The settings for how the Store Values are mapped to and from the database.
* @category Configuration
* @since v4.0.0
*/
values?: DpcTabularValues;
/**
* How often the Persister should poll the database for any changes made to it
* by other clients, defaulting to 1 second.
* @category Configuration
* @since v4.0.0
*/
autoLoadIntervalSeconds?: number;
};
/**
* The DpcTabularLoad type describes the configuration for loading Tables in a
* database-oriented Persister that is operating in tabular mode.
*
* It is an object where each key is a name of a database table, and the value
* is a child configuration object for how that table should be loaded into the
* Store. The properties of the child configuration object are:
*
* ||Type|Description|
* |-|-|-|
* |`tableId`|Id|The Id of the Store Table into which data from this database table should be loaded.|
* |`rowIdColumnName?`|string|The optional name of the column in the database table that will be used as the Row Ids in the Store Table, defaulting to '_id'.|
*
* As a shortcut, if you do not need to specify a custom `rowIdColumnName`, you
* can simply provide the Id of the Store Table instead of the whole object.
*
* The 'Dpc' prefix indicates that this type is used within the
* DatabasePersisterConfig type.
* @example
* When applied to a database Persister, this DatabasePersisterConfig will load
* the data of two database tables (called 'petsInDb' and 'speciesInDb') into
* two Store Tables (called 'pets' and 'species'). One has a column for the Row
* Id called 'id' and the other defaults it to '_id'.
*
* ```js
* import type {DatabasePersisterConfig} from 'tinybase';
*
* export const databasePersisterConfig: DatabasePersisterConfig = {
* mode: 'tabular',
* tables: {
* load: {
* petsInDb: {tableId: 'pets', rowIdColumnName: 'id'},
* speciesInDb: 'species',
* },
* },
* };
* ```
*
* Imagine database tables that look like this:
*
* ```
* > SELECT * FROM petsInDb;
* +-------+---------+-------+
* | id | species | color |
* +-------+---------+-------+
* | fido | dog | brown |
* | felix | cat | black |
* +-------+---------+-------+
*
* > SELECT * FROM speciesInDb;
* +------+-------+
* | _id | price |
* +------+-------+
* | dog | 5 |
* | cat | 4 |
* +------+-------+
* ```
*
* With the configuration above, this will load into a Store with Tables that
* look like this:
*
* ```json
* {
* "pets": {
* "fido": {"species": "dog", "color": "brown"},
* "felix": {"species": "cat", "color": "black"},
* },
* "species": {
* "dog": {"price": 5},
* "cat": {"price": 4},
* },
* }
* ```
*
* The example above represents what happens with a SQLite Persister. In
* PostgreSQL databases, all Cell and Value columns are expected to be
* typed as `text`, and the strings, booleans, and numbers would be JSON-encoded
* if you queried them.
* @category Configuration
* @since v4.0.0
*/
export type DpcTabularLoad<Schema extends OptionalTablesSchema> = {
[tableName: string]:
| {
/**
* The Id of the Store Table into which data from this database table
* should be loaded.
* @category Configuration
* @since v4.0.0
*/
tableId: TableIdFromSchema<Schema>;
/**
* The optional name of the column in the database table that will be
* used as the Row Ids in the Store Table, defaulting to '_id'.
* @category Configuration
* @since v4.0.0
*/
rowIdColumnName?: string;
}
| TableIdFromSchema<Schema>;
};
/**
* The DpcTabularSave type describes the configuration for saving Tables in a
* database-oriented Persister that is operating in tabular mode.
*
* It is an object where each key is an Id of a Store Table, and the value is a
* child configuration object for how that Table should be saved out to the
* database. The properties of the child configuration object are:
*
* ||Type|Description|
* |-|-|-|
* |`tableName`|string|The name of the database table out to which the Store Table should be saved.|
* |`rowIdColumnName?`|string|The optional name of the column in the database table that will be used to save the Row Ids from the Store Table, defaulting to '_id'.|
* |`deleteEmptyColumns?`|boolean|Whether columns in the database table will be removed if they are empty in the Store Table, defaulting to false.|
* |`deleteEmptyTable?`|boolean|Whether tables in the database will be removed if the Store Table is empty, defaulting to false.|
*
* As a shortcut, if you do not need to specify a custom `rowIdColumnName`, or
* enable the `deleteEmptyColumns` or `deleteEmptyTable` settings, you can
* simply provide the name of the database table instead of the whole object.
*
* `deleteEmptyColumns` and `deleteEmptyTable` only have a guaranteed effect
* when an explicit call is made to the Persister's save method. Columns and
* tables will not necessarily be removed when the Persister is incrementally
* 'autoSaving', due to performance reasons. If you want to be sure that your
* database table matches a TinyBase Table without any extraneous columns,
* simply call the save method at an idle moment.
*
* The 'Dpc' prefix indicates that this type is used within the
* DatabasePersisterConfig type.
* @example
* When applied to a database Persister, this DatabasePersisterConfig will save
* the data of two Store Tables (called 'pets' and 'species') into two database
* tables (called 'petsInDb' and 'speciesInDb'). One has a column for the Row
* Id called 'id' and will delete columns and the whole table if empty, the
* other defaults to '_id' and will not delete columns or the whole table if
* empty.
*
* ```js
* import type {DatabasePersisterConfig} from 'tinybase';
*
* export const databasePersisterConfig: DatabasePersisterConfig = {
* mode: 'tabular',
* tables: {
* save: {
* pets: {
* tableName: 'petsInDb',
* deleteEmptyColumns: true,
* deleteEmptyTable: true,
* },
* species: 'speciesInDb',
* },
* },
* };
* ```
*
* Imagine a Store with Tables that look like this:
*
* ```json
* {
* "pets": {
* "fido": {"species": "dog", "color": "brown"},
* "felix": {"species": "cat", "color": "black"},
* },
* "species": {
* "dog": {"price": 5},
* "cat": {"price": 4},
* },
* }
* ```
*
* With the configuration above, this will save out to a database with tables
* that look like this:
*
* ```
* > SELECT * FROM petsInDb;
* +-------+---------+-------+
* | id | species | color |
* +-------+---------+-------+
* | fido | dog | brown |
* | felix | cat | black |
* +-------+---------+-------+
*
* > SELECT * FROM speciesInDb;
* +------+-------+
* | _id | price |
* +------+-------+
* | dog | 5 |
* | cat | 4 |
* +------+-------+
* ```
* The example above represents what happens with a SQLite Persister. In
* PostgreSQL databases, all Cell and Value columns are expected to be
* typed as `text`, and the strings, booleans, and numbers would be JSON-encoded
* if you queried them.
* @category Configuration
* @since v4.0.0
*/
export type DpcTabularSave<Schema extends OptionalTablesSchema> = {
[TableId in TableIdFromSchema<Schema>]:
| {
/**
* The name of the database table out to which the Store Table should be
* saved.
* @category Configuration
* @since v4.0.0
*/
tableName: string;
/**
* The optional name of the column in the database table that will be
* used to save the Row Ids from the Store Table, defaulting to '_id'.
* @category Configuration
* @since v4.0.0
*/
rowIdColumnName?: string;
/**
* Whether columns in the database table will be removed if they are
* empty in the Store Table, defaulting to false.
* @category Configuration
* @since v4.0.0
*/
deleteEmptyColumns?: boolean;
/**
* Whether tables in the database will be removed if the Store Table is
* empty, defaulting to false.
* @category Configuration
* @since v4.0.0
*/
deleteEmptyTable?: boolean;
}
| string;
};
/**
* The DpcTabularValues type describes the configuration for handling Values in
* a database-oriented Persister that is operating in tabular mode.
*
* Note that both loading and saving of Values from and to the database are
* disabled by default.
*
* The 'Dpc' prefix indicates that this type is used within the
* DatabasePersisterConfig type.
* @example
* When applied to a database Persister, this DatabasePersisterConfig will load
* and save the data of a Store's Values into a database
* table called 'my_tinybase_values'.
*
* ```js
* import type {DatabasePersisterConfig} from 'tinybase';
*
* export const databasePersisterConfig: DatabasePersisterConfig = {
* mode: 'tabular',
* values: {
* load: true,
* save: true,
* tableName: 'my_tinybase_values',
* },
* };
* ```
* @category Configuration
* @since v4.0.0
*/
export type DpcTabularValues = {
/**
* Whether Store Values will be loaded from a database table.
* @category Configuration
* @since v4.0.0
*/
load?: boolean;
/**
* Whether Store Values will be saved to a database table.
* @category Configuration
* @since v4.0.0
*/
save?: boolean;
/**
* The optional name of the database table from and to which the Store Values
* should be loaded or saved, defaulting to `tinybase_values`.
* @category Configuration
* @since v4.0.0
*/
tableName?: string;
};
/**
* A Persister object lets you save and load Store data to and from different
* locations, or underlying storage types.
*
* This is useful for preserving Store or MergeableStore data between browser
* sessions or reloads, saving or loading browser state to or from a server, or
* saving Store data to disk in a environment with filesystem access.
*
* Creating a Persister depends on the choice of underlying storage where the
* data is to be stored. Options include the createSessionPersister function,
* the createLocalPersister function, the createRemotePersister function, and
* the createFilePersister function, as just simple examples. The
* createCustomPersister function can also be used to easily create a fully
* customized way to save and load Store data.
*
* Using the values of the Persists enum, the generic parameter to the Persister
* indicates whether it can handle a regular Store, a MergeableStore, or either.
* Consult the table in the overall persisters module documentation to see
* current support for each. The different levels of support are also described
* for each of the types of Persister themselves.
*
* A Persister lets you explicit save or load data, with the save method and the
* load method respectively. These methods are both asynchronous (since the
* underlying data storage may also be) and return promises. As a result you
* should use the `await` keyword to call them in a way that guarantees
* subsequent execution order.
*
* When you don't want to deal with explicit persistence operations, a Persister
* object also provides automatic saving and loading. Automatic saving listens
* for changes to the Store and persists the data immediately. Automatic loading
* listens (or polls) for changes to the persisted data and reflects those
* changes in the Store.
*
* You can start automatic saving or loading with the startAutoSave method and
* startAutoLoad method. Both are asynchronous since they will do an immediate
* save and load before starting to listen for subsequent changes. You can stop
* the behavior with the stopAutoSave method and stopAutoLoad method (which are
* synchronous).
*
* You may often want to have both automatic saving and loading of a Store so
* that changes are constantly synchronized (allowing basic state preservation
* between browser tabs, for example). The framework has some basic provisions
* to prevent race conditions - for example it will not attempt to save data if
* it is currently loading it and vice-versa - and will sequentially schedule
* methods that could cause race conditions.
*
* That said, be aware that you should always comprehensively test your
* persistence strategy to understand the opportunity for data loss (in the case
* of trying to save data to a server under poor network conditions, for
* example).
*
* To help debug such issues, since v4.0.4, the create methods for all Persister
* objects take an optional `onIgnoredError` argument. This is a handler for the
* errors that the Persister would otherwise ignore when trying to save or load
* data (such as when handling corrupted stored data). It's recommended you use
* this for debugging persistence issues, but only in a development environment.
* Database-based Persister objects also take an optional `onSqlCommand`
* argument for logging commands and queries made to the underlying database.
* @example
* This example creates a Store, persists it to the browser's session storage as
* a JSON string, changes the persisted data, updates the Store from it, and
* finally destroys the Persister again.
*
* ```js
* import {createStore} from 'tinybase';
* import {createSessionPersister} from 'tinybase/persisters/persister-browser';
*
* const store = createStore().setTables({pets: {fido: {species: 'dog'}}});
* const persister = createSessionPersister(store, 'pets');
*
* await persister.save();
* console.log(sessionStorage.getItem('pets'));
* // -> '[{"pets":{"fido":{"species":"dog"}}},{}]'
*
* sessionStorage.setItem('pets', '[{"pets":{"toto":{"species":"dog"}}},{}]');
* await persister.load();
* console.log(store.getTables());
* // -> {pets: {toto: {species: 'dog'}}}
*
* persister.destroy();
* sessionStorage.clear();
* ```
* @example
* This example creates a Store, and automatically saves and loads it to the
* browser's session storage as a JSON string. Changes to the Store data, or the
* persisted data (implicitly firing a StorageEvent), are reflected accordingly.
*
* ```js
* import {createStore} from 'tinybase';
* import {createSessionPersister} from 'tinybase/persisters/persister-browser';
*
* const store = createStore();
* const persister = createSessionPersister(store, 'pets');
*
* await persister.startAutoLoad([{pets: {fido: {species: 'dog'}}}, {}]);
* await persister.startAutoSave();
*
* store.setTables({pets: {felix: {species: 'cat'}}});
* // ...
* console.log(sessionStorage.getItem('pets'));
* // -> '[{"pets":{"felix":{"species":"cat"}}},{}]'
*
* // In another browser tab:
* sessionStorage.setItem('pets', '[{"pets":{"toto":{"species":"dog"}}},{}]');
* // -> StorageEvent('storage', {storageArea: sessionStorage, key: 'pets'})
*
* // ...
* console.log(store.getTables());
* // -> {pets: {toto: {species: 'dog'}}}
*
* persister.destroy();
* sessionStorage.clear();
* ```
* @category Persister
* @since v1.0.0
*/
export interface Persister<
in out Schemas extends OptionalSchemas,
Persist extends Persists = Persists.StoreOnly,
> {
/**
* The load method gets persisted data from storage, and loads it into the
* Store with which the Persister is associated, once.
*
* This has schema-based typing. The following is a simplified representation:
*
* ```ts override
* load(initialContent?: Content | (() => Content)): Promise<this>;
* ```
*
* The optional parameter allows you to specify what the initial content for
* the Store will be if there is nothing currently persisted or if the load
* fails (for example when the Persister is remote and the environment is
* offline). This allows you to fallback or instantiate a Store whether it's
* loading from previously persisted storage or being run for the first time.
* Since v5.4.2, this parameter can also be a function that returns the
* content.
*
* This method is asynchronous because the persisted data may be on a remote
* machine or a filesystem. Even for those storage types that are synchronous
* (like browser storage) it is still recommended that you `await` calls to
* this method or handle the return type natively as a Promise.
* @param initialContent An optional Content object used when the underlying
* storage has not previously been populated.
* @returns A Promise containing a reference to the Persister object.
* @example
* This example creates an empty Store, and loads data into it from the
* browser's session storage, which for the purposes of this example has been
* previously populated.
*
* ```js
* import {createStore} from 'tinybase';
* import {createSessionPersister} from 'tinybase/persisters/persister-browser';
*
* sessionStorage.setItem('pets', '[{"pets":{"fido":{"species":"dog"}}},{}]');
*
* const store = createStore();
* const persister = createSessionPersister(store, 'pets');
*
* await persister.load();
* console.log(store.getTables());
* // -> {pets: {fido: {species: 'dog'}}}
*
* sessionStorage.clear();
* ```
* @example
* This example creates an empty Store, and loads data into it from the
* browser's session storage, which is at first empty, so the optional
* parameter is used. The second time the load method is called, data has
* previously been persisted and instead, that is loaded.
*
* ```js
* import {createStore} from 'tinybase';
* import {createSessionPersister} from 'tinybase/persisters/persister-browser';
*
* const store = createStore();
* const persister = createSessionPersister(store, 'pets');
*
* await persister.load([{pets: {fido: {species: 'dog'}}}, {}]);
* console.log(store.getTables());
* // -> {pets: {fido: {species: 'dog'}}}
*
* sessionStorage.setItem('pets', '[{"pets":{"toto":{"species":"dog"}}},{}]');
* await persister.load({pets: {fido: {species: 'dog'}}});
* console.log(store.getTables());
* // -> {pets: {toto: {species: 'dog'}}}
*
* sessionStorage.clear();
* ```
* @category Load
* @since v1.0.0
*/
load(
initialContent?: Content<Schemas, true> | (() => Content<Schemas, true>),
): Promise<this>;
/**
* The startAutoLoad method gets persisted data from storage, and loads it
* into the Store with which the Persister is associated, once, and then
* continuously.
*
* This has schema-based typing. The following is a simplified representation:
*
* ```ts override
* startAutoLoad(initialContent?: Content | (() => Content)): Promise<this>;
* ```
*
* The optional parameter allows you to specify what the initial content for
* the Store will be if there is nothing currently persisted or if the load
* fails (for example when the Persister is remote and the environment is
* offline). This allows you to fallback or instantiate a Store whether it's
* loading from previously persisted storage or being run for the first time.
* Since v5.4.2, this parameter can also be a function that returns the
* content.
*
* This method first runs a single call to the load method to ensure the data
* is in sync with the persisted storage. It then continues to watch for
* changes to the underlying data (either through events or polling, depending
* on the storage type), automatically loading the data into the Store.
*
* This method is asynchronous because it starts by making a single call to
* the asynchronous load method. Even for those storage types that are
* synchronous (like browser storage) it is still recommended that you `await`
* calls to this method or handle the return type natively as a Promise.
* @param initialContent An optional Content object used when the underlying
* storage has not previously been populated.
* @returns A Promise containing a reference to the Persister object.
* @example
* This example creates an empty Store, and loads data into it from the
* browser's session storage, which at first is empty (so the `initialTables`
* parameter is used). Subsequent changes to the underlying storage are then
* reflected in the Store (in this case through detection of StorageEvents
* from session storage changes made in another browser tab).
*
* ```js
* import {createStore} from 'tinybase';
* import {createSessionPersister} from 'tinybase/persisters/persister-browser';
*
* const store = createStore();
* const persister = createSessionPersister(store, 'pets');
*
* await persister.startAutoLoad([{pets: {fido: {species: 'dog'}}}, {}]);
* console.log(store.getTables());
* // -> {pets: {fido: {species: 'dog'}}}
*
* // In another browser tab:
* sessionStorage.setItem('pets', '[{"pets":{"toto":{"species":"dog"}}},{}]');
* // -> StorageEvent('storage', {storageArea: sessionStorage, key: 'pets'})
*
* // ...
* console.log(store.getTables());
* // -> {pets: {toto: {species: 'dog'}}}
*
* persister.destroy();
* sessionStorage.clear();
* ```
* @category Load
* @since v1.0.0
*/
startAutoLoad(
initialContent?: Content<Schemas, true> | (() => Content<Schemas, true>),
): Promise<this>;
/**
* The stopAutoLoad method stops the automatic loading of data from storage
* previously started with the startAutoLoad method.
*
* If the Persister is not currently set to automatically load, this method
* has no effect.
* @returns A reference to the Persister object.
* @example
* This example creates an empty Store, and starts automatically loading data
* into it from the browser's session storage. Once the automatic loading is
* stopped, subsequent changes are not reflected in the Store.
*
* ```js
* import {createStore} from 'tinybase';
* import {createSessionPersister} from 'tinybase/persisters/persister-browser';
*
* const store = createStore();
* const persister = createSessionPersister(store, 'pets');
* await persister.startAutoLoad();
*
* // In another browser tab:
* sessionStorage.setItem('pets', '[{"pets":{"toto":{"species":"dog"}}},{}]');
* // -> StorageEvent('storage', {storageArea: sessionStorage, key: 'pets'})
* // ...
* console.log(store.getTables());
* // -> {pets: {toto: {species: 'dog'}}}
*
* persister.stopAutoLoad();
*
* // In another browser tab:
* sessionStorage.setItem(
* 'pets',
* '[{"pets":{"felix":{"species":"cat"}}},{}]',
* );
* // -> StorageEvent('storage', {storageArea: sessionStorage, key: 'pets'})
* // ...
* console.log(store.getTables());
* // -> {pets: {toto: {species: 'dog'}}}
* // Storage change has not been automatically loaded.
*
* persister.destroy();
* sessionStorage.clear();
* ```
* @category Load
* @since v1.0.0
*/
stopAutoLoad(): this;
/**
* The isAutoLoading method lets you find out if the Persister is currently
* automatically loading its content.
* @returns A boolean indicating whether the Persister is currently
* autoLoading.
* @example
* This example creates a Persister and queries whether it is autoLoading.
*
* ```js
* import {createStore} from 'tinybase';
* import {createSessionPersister} from 'tinybase/persisters/persister-browser';
*
* const persister = createSessionPersister(createStore(), 'pets');
*
* console.log(persister.isAutoLoading());
* // -> false
*
* await persister.startAutoLoad();
* console.log(persister.isAutoLoading());
* // -> true
*
* await persister.stopAutoLoad();
* console.log(persister.isAutoLoading());
* // -> false
* ```
* @category Load
* @since v5.0.0
*/
isAutoLoading(): boolean;
/**
* The save method takes data from the Store with which the Persister is
* associated and persists it into storage, once.
*
* This method is asynchronous because the persisted data may be on a remote
* machine or a filesystem. Even for those storage types that are synchronous
* (like browser storage) it is still recommended that you `await` calls to
* this method or handle the return type natively as a Promise.
* @returns A Promise containing a reference to the Persister object.
* @example
* This example creates a Store with some data, and saves into the browser's
* session storage.
*
* ```js
* import {createStore} from 'tinybase';
* import {createSessionPersister} from 'tinybase/persisters/persister-browser';
*
* const store = createStore().setTables({pets: {fido: {species: 'dog'}}});
* const persister = createSessionPersister(store, 'pets');
*
* await persister.save();
* console.log(sessionStorage.getItem('pets'));
* // -> '[{"pets":{"fido":{"species":"dog"}}},{}]'
*
* persister.destroy();
* sessionStorage.clear();
* ```
* @category Save
* @since v1.0.0
*/
save(): Promise<this>;
/**
* The save method takes data from the Store with which the Persister is
* associated and persists it into storage, once, and then continuously.
*
* This method first runs a single call to the save method to ensure the data
* is in sync with the persisted storage. It then continues to watch for
* changes to the Store, automatically saving the data to storage.
*
* This method is asynchronous because it starts by making a single call to
* the asynchronous save method. Even for those storage types that are
* synchronous (like browser storage) it is still recommended that you `await`
* calls to this method or handle the return type natively as a Promise.
* @returns A Promise containing a reference to the Persister object.
* @example
* This example creates a Store with some data, and saves into the browser's
* session storage. Subsequent changes to the Store are then automatically
* saved to the underlying storage.
*
* ```js
* import {createStore} from 'tinybase';
* import {createSessionPersister} from 'tinybase/persisters/persister-browser';
*
* const store = createStore().setTables({pets: {fido: {species: 'dog'}}});
* const persister = createSessionPersister(store, 'pets');
*
* await persister.startAutoSave();
* console.log(sessionStorage.getItem('pets'));
* // -> '[{"pets":{"fido":{"species":"dog"}}},{}]'
*
* store.setTables({pets: {toto: {species: 'dog'}}});
* // ...
* console.log(sessionStorage.getItem('pets'));
* // -> '[{"pets":{"toto":{"species":"dog"}}},{}]'
*
* sessionStorage.clear();
* ```
* @category Save
* @since v1.0.0
*/
startAutoSave(): Promise<this>;
/**
* The stopAutoSave method stops the automatic save of data to storage
* previously started with the startAutoSave method.
*
* If the Persister is not currently set to automatically save, this method
* has no effect.
* @returns A reference to the Persister object.
* @example
* This example creates a Store with some data, and saves into the browser's
* session storage. Subsequent changes to the Store are then automatically
* saved to the underlying storage. Once the automatic saving is
* stopped, subsequent changes are not reflected.
*
* ```js
* import {createStore} from 'tinybase';
* import {createSessionPersister} from 'tinybase/persisters/persister-browser';
*
* const store = createStore().setTables({pets: {fido: {species: 'dog'}}});
* const persister = createSessionPersister(store, 'pets');
* await persister.startAutoSave();
*
* store.setTables({pets: {toto: {species: 'dog'}}});
* // ...
* console.log(sessionStorage.getItem('pets'));
* // -> '[{"pets":{"toto":{"species":"dog"}}},{}]'
*
* persister.stopAutoSave();
*
* store.setTables({pets: {felix: {species: 'cat'}}});
* // ...
* console.log(sessionStorage.getItem('pets'));
* // -> '[{"pets":{"toto":{"species":"dog"}}},{}]'
* // Store change has not been automatically saved.
*
* sessionStorage.clear();
* ```
* @category Save
* @since v1.0.0
*/
stopAutoSave(): this;
/**
* The isAutoSaving method lets you find out if the Persister is currently
* automatically saving its content.
* @returns A boolean indicating whether the Persister is currently
* autoSaving.
* @example
* This example creates a Persister and queries whether it is autoSaving.
*
* ```js
* import {createSto