Persistence
import {
hydrate,
IDBStorage,
MemoryStorage,
SimpleStorage,
SQLiteStorage,
withPersist,
type PersistOptions,
type StorageEngine,
} from 'amos';
The persistence API is implemented by amos-persist. It adds lazy hydration and automatic
persistence to a store through a store enhancer.
withPersist
function withPersist(options: PersistOptions & { storage: StorageEngine }): StoreEnhancer;
Adds persistence to a store.
const store = createStore(
{},
withPersist({
storage: new SimpleStorage('amos:', localStorage),
}),
);
Persistence is opt-in per box unless includes is provided.
const settingsBox = objectBox('settings', {
theme: 'system',
}).config({
persist: { version: 1 },
});
When a persisted box is selected, Amos schedules hydration if needed. When the store changes, Amos schedules persistence of changed values.
PersistOptions
interface PersistOptions {
storage: StorageEngine;
includes?: (box: Box) => boolean;
excludes?: (box: Box) => boolean;
onError: (error: unknown) => void;
}
storage is the persistence backend.
includes decides whether a box should be persisted. If omitted, Amos persists boxes whose
box.persist option is set.
excludes has priority over includes.
onError receives async hydration/persistence errors. withPersist defaults to logging
[Amos]: failed to persist.
Boxes with persist: false are never persisted. Internal boxes whose keys start with amos. are
also skipped.
BoxPersistOptions
interface BoxPersistOptions<S> {
version: number;
migrate?: ActionFactory<[version: number, row: ID, state: any]>;
}
version is stored with the persisted value. When the stored version differs from the current
version, Amos calls migrate if present.
The migration action should return JSON-like state that can be merged into the box state by
fromJS; it should not return a live state instance directly.
const userBox = recordBox('user', UserRecord).config({
persist: {
version: 2,
migrate: action((dispatch, select, version, row, state) => {
if (version === 1) {
return { ...state, displayName: state.name };
}
return state;
}),
},
});
hydrate
const hydrate: ActionFactory<[keys: readonly PersistKey<any>[]], Promise<void>>;
Ensures that boxes or table rows have been hydrated.
await dispatch(hydrate([settingsBox]));
await dispatch(hydrate([[todoMapBox, 123]]));
await dispatch(hydrate([[todoMapBox, [1, 2, 3]]]));
hydrate requires withPersist; otherwise it throws persist middleware is not enabled.
Persist Keys
type PersistRowKey<T> = readonly [box: Box<T>, rows: ID | ID[]];
type PersistKey<T> = Box<T> | PersistRowKey<T>;
For normal boxes, pass the box. For table boxes, pass [box, rowId] or [box, rowIds].
Table Boxes
Boxes can expose table options so persistence stores rows separately. MapBox and boxes derived
from it provide table options automatically.
When a selector defines loadRow, withPersist can hydrate only that row:
select(todoMapBox.getItem(todoId));
For MapBox, this corresponds to storage keys like:
todos:123
The whole table prefix is represented by a trailing delimiter:
todos:
StorageEngine
interface StorageEngine {
init?(): Promise<void>;
getMulti(items: readonly string[]): Promise<readonly (PersistValue | null)[]>;
getPrefix(prefix: string): Promise<readonly PersistEntry[]>;
setMulti(items: readonly PersistEntry[]): Promise<void>;
deleteMulti(items: readonly string[]): Promise<void>;
deletePrefix(prefix: string): Promise<void>;
}
Storage engines read and write persisted entries.
type PersistValue = readonly [version: number, value: any];
type PersistEntry = readonly [key: string, version: number, value: any];
MemoryStorage
class MemoryStorage implements StorageEngine;
Stores data in an in-memory object. Useful for tests.
const storage = new MemoryStorage();
SimpleStorage
class SimpleStorage implements StorageEngine {
constructor(prefix: string, driver: SimpleStorageDriver);
}
Wraps Web Storage-compatible and React Native AsyncStorage-compatible drivers.
const storage = new SimpleStorage('amos:', localStorage);
SimpleStorageDriver
interface SimpleStorageDriver {
length?: number;
getItem(key: string): ValueOrPromise<string | null>;
setItem(key: string, value: string): ValueOrPromise<void>;
removeItem(key: string): ValueOrPromise<void>;
key?(index: number): string | null;
getAllKeys?(): ValueOrPromise<readonly string[]>;
}
getAllKeys is used when available. Otherwise length and key(index) are used to scan keys by
prefix.
IDBStorage
class IDBStorage implements StorageEngine {
constructor(database: string, table: string);
}
Stores data in IndexedDB. init() opens the database and creates an object store with key path
key.
const storage = new IDBStorage('amos', 'persist');
SQLiteStorage
interface SQLiteDatabase {
runAsync(sql: string, values?: any[]): Promise<void>;
getAllAsync<T>(sql: string, values?: any[]): Promise<T[]>;
}
class SQLiteStorage implements StorageEngine {
constructor(database: string, table: string, open: (db: string) => Promise<SQLiteDatabase>);
}
Stores data in SQLite. init() opens the database, enables WAL mode, and creates the persistence
table if it does not exist.
const storage = new SQLiteStorage('app.db', 'amos_persist', openDatabaseAsync);
The table contains:
key TEXT PRIMARY KEY,
version INTEGER NOT NULL,
value TEXT