zustand-storage
Version:
A universal solution combining @aivron/sync-storage and zust-api for React (web & desktop). It merges local persistence with a Zustand-inspired API to provide core storage operations, bulk actions, JSON support, TTL, and integrated React hooks.
218 lines (200 loc) • 7.09 kB
text/typescript
import { createStore } from "zust-api";
import defaultStorageEngine from "./storageEngine";
import { debounce, deepMerge, shallowMerge } from "./utils";
import { SyncedStore, SyncedStoreConfig } from "./types";
/**
* Creates a synced store that combines zust‑api state management with persistence via @aivron/sync-storage,
* and optional database integration.
*
* @template State - The type of the state. Must extend object.
* @template Actions - The type of the actions.
* @param config - The configuration object for the synced store.
* @returns A synced store containing the React hook and utility methods.
*/
export function createSyncedStore<State extends object, Actions>(
config: SyncedStoreConfig<State, Actions>
): SyncedStore<State, Actions> {
const {
key,
storageEngine = defaultStorageEngine,
initialState,
actions: actionsCreator,
syncOptions = {},
mergeStrategy = "replace",
migrate,
database, // Optional database integration config
} = config;
const {
debounce: debounceDelay = 0,
serialize = JSON.stringify,
deserialize = JSON.parse,
persistOn = "change",
interval = 0,
onError,
} = syncOptions;
// Create the zust‑api store using the initial state.
const store: any = createStore(initialState);
// Build the actions using zust‑api's setDeep and getState.
const actions = actionsCreator(store.setDeep, store.getState);
/**
* Custom setter supporting functional updates.
*/
const customSetState = (partial: any, replace = false) => {
const current = store.getState() as State;
const update = typeof partial === "function" ? partial(current) : partial;
store.setDeep("", update);
};
/**
* Persists the current state to storage.
*/
let triggerPersist = (): void => {
try {
const currentState = store.getState() as State;
const data = serialize(currentState);
storageEngine.setItem(key, data);
} catch (error: any) {
if (onError) onError(error);
else console.error("Persist error:", error);
}
};
if (debounceDelay > 0) {
triggerPersist = debounce(triggerPersist, debounceDelay);
}
// Persist state on change if configured.
if (persistOn === "change") {
store.subscribe((state: State) => {
triggerPersist();
});
}
// Periodic persistence using interval mode.
if (persistOn === "interval" && interval > 0) {
setInterval(() => {
try {
const currentState = store.getState() as State;
const data = serialize(currentState);
storageEngine.setItem(key, data);
} catch (error: any) {
if (onError) onError(error);
else console.error("Persist error:", error);
}
}, interval);
}
/**
* Rehydrates the state from persisted storage.
*/
const rehydrate = (): void => {
try {
const data = storageEngine.getItem(key);
if (data) {
let persistedState: State = deserialize(data);
if (migrate) {
persistedState = migrate(persistedState);
}
let newState: State;
switch (mergeStrategy) {
case "deep":
newState = deepMerge({ ...initialState }, persistedState);
break;
case "shallow":
newState = shallowMerge(initialState, persistedState);
break;
case "replace":
default:
newState = persistedState;
break;
}
customSetState(() => newState, true);
}
} catch (error: any) {
if (onError) onError(error);
else console.error("Rehydrate error:", error);
}
};
// Immediately rehydrate on store creation.
rehydrate();
// Middleware support.
const middlewares: Array<(state: State) => State> = [];
const addMiddleware = (mw: (state: State) => State): void => {
middlewares.push(mw);
const currentState = store.getState() as State;
const newState = mw(currentState);
customSetState(() => newState, true);
};
/**
* Resets the store to its initial state and clears persisted storage.
*/
const resetStore = (): void => {
customSetState(() => initialState, true);
storageEngine.removeItem(key);
};
// Database integration methods (if configured)
let fetchFromDB: ((query: object) => Promise<void>) | undefined;
let saveToDB: ((query: object) => Promise<void>) | undefined;
if (database) {
/**
* Fetches data from the database and merges it into the store state.
* @param query - The query object used to fetch data.
*/
fetchFromDB = async (query: object): Promise<void> => {
try {
const { client, collectionOrTable, fromRecord } = database;
const records = await client.find(collectionOrTable, query);
if (records.length > 0) {
const record = records[0];
const newStatePartial: Partial<State> = fromRecord ? fromRecord(record) : record;
let mergedState: State;
switch (mergeStrategy) {
case "deep":
mergedState = deepMerge(store.getState(), newStatePartial);
break;
case "shallow":
mergedState = shallowMerge(store.getState(), newStatePartial);
break;
case "replace":
default:
mergedState = newStatePartial as State;
break;
}
customSetState(() => mergedState, true);
}
} catch (error: any) {
if (database.onError) database.onError(error);
else console.error("Error fetching from DB:", error);
}
};
/**
* Saves the current store state to the database.
* @param query - The query object to locate an existing record. Updates if found, inserts otherwise.
*/
saveToDB = async (query: object): Promise<void> => {
try {
const { client, collectionOrTable, toRecord } = database;
const state = store.getState();
const record = toRecord ? toRecord(state) : state;
const existingRecords = await client.find(collectionOrTable, query);
if (existingRecords.length > 0) {
await client.update(collectionOrTable, query, record);
} else {
await client.insert(collectionOrTable, record);
}
} catch (error: any) {
if (database.onError) database.onError(error);
else console.error("Error saving to DB:", error);
}
};
}
return {
useStore: store.useStore as () => [State, Actions],
getState: store.getState as () => State,
setState: customSetState,
subscribe: store.subscribe as (listener: (state: State) => void) => () => void,
persist: triggerPersist,
rehydrate,
addMiddleware,
resetStore,
actions,
// Expose DB integration methods when configured
fetchFromDB,
saveToDB,
};
}