// This is an override of the default zustand-persist implementation, it avoids "Cannot use 'import.meta' outside a module" error. // Source: https://github.com/pedroafsouza/expo-zustand-persist/blob/main/src/Persist.ts /* eslint-disable consistent-return */ /* eslint-disable no-console */ /* eslint-disable @typescript-eslint/no-explicit-any */ // Basic Imports for Types import type { StateCreator, StoreApi, StoreMutatorIdentifier } from "zustand"; // Define the StateStorage interface export interface StateStorage { getItem: (name: string) => string | null | Promise; setItem: (name: string, value: string) => unknown | Promise; removeItem: (name: string) => unknown | Promise; } // Define the StorageValue type export type StorageValue = { state: S; version?: number; }; // Define the PersistStorage interface export interface PersistStorage { getItem: ( name: string ) => StorageValue | null | Promise | null>; setItem: (name: string, value: StorageValue) => unknown | Promise; removeItem: (name: string) => unknown | Promise; } // Define JsonStorageOptions type export type JsonStorageOptions = { reviver?: (key: string, value: unknown) => unknown; replacer?: (key: string, value: unknown) => unknown; }; // Create JSON Storage with fallback support export function createJSONStorage( getStorage: () => StateStorage, options?: JsonStorageOptions ): PersistStorage | undefined { let storage: StateStorage | undefined; try { storage = getStorage(); } catch { return undefined; } return { getItem: (name) => { const parse = (str: string | null) => str === null ? null : (JSON.parse(str, options?.reviver) as StorageValue); const str = storage.getItem(name) ?? null; return str instanceof Promise ? str.then(parse) : parse(str); }, setItem: (name, newValue) => storage.setItem(name, JSON.stringify(newValue, options?.replacer)), removeItem: (name) => storage.removeItem(name), }; } // Define the PersistOptions interface export interface PersistOptions { name: string; storage?: PersistStorage; partialize?: (state: S) => PersistedState; onRehydrateStorage?: ( state: S ) => ((state?: S, error?: unknown) => void) | void; version?: number; migrate?: ( persistedState: unknown, version: number ) => PersistedState | Promise; merge?: (persistedState: unknown, currentState: S) => S; skipHydration?: boolean; } // Define utility types type PersistListener = (state: S) => void; type StorePersist = { persist: { setOptions: (options: Partial>) => void; clearStorage: () => void; rehydrate: () => Promise | void; hasHydrated: () => boolean; onHydrate: (fn: PersistListener) => () => void; onFinishHydration: (fn: PersistListener) => () => void; getOptions: () => Partial>; }; }; // A utility function to wrap a function as Thenable for Promise handling type Thenable = { then( onFulfilled: (value: Value) => V | Promise | Thenable ): Thenable; catch( onRejected: (reason: Error) => V | Promise | Thenable ): Thenable; }; const toThenable = ( fn: (input: Input) => Result | Promise | Thenable ) => (input: Input): Thenable => { try { const result = fn(input); return result instanceof Promise ? (result as Thenable) : { then: (onFulfilled) => toThenable(onFulfilled)(result as Result), catch: (_onRejected) => this as unknown as Thenable, }; } catch (e: any) { return { then: (_onFulfilled) => this as unknown as Thenable, catch: (onRejected) => toThenable(onRejected)(e), }; } }; // persistImpl function to handle persistent state storage type PersistImpl = < T, Mps extends [StoreMutatorIdentifier, unknown][] = [], Mcs extends [StoreMutatorIdentifier, unknown][] = [] >( storeInitializer: StateCreator, options: PersistOptions ) => StateCreator; const persistImpl = (( config: StateCreator, [], []>, baseOptions: PersistOptions< Record, Record > ) => ( set: StoreApi>["setState"], get: StoreApi>["getState"], api: StoreApi> ) => { type S = Record; let options = { storage: createJSONStorage(() => localStorage), partialize: (state: S) => state, version: 0, merge: (persistedState: unknown, currentState: S) => ({ ...currentState, ...(persistedState as object), }), ...baseOptions, }; let hasHydrated = false; const hydrationListeners = new Set>(); const finishHydrationListeners = new Set>(); let storage = options.storage; if (!storage) { return config( (...args) => { console.warn( `[zustand persist middleware] Unable to update item '${options.name}', the given storage is currently unavailable.` ); set(...(args as Parameters)); }, get, api ); } const setItem = () => { const state = options.partialize({ ...get() }); return (storage as PersistStorage).setItem(options.name, { state, version: options.version, }); }; const savedSetState = api.setState; api.setState = (state, replace) => { // eslint-disable-next-line @typescript-eslint/no-explicit-any savedSetState(state, replace as any); void setItem(); }; const configResult = config( (...args) => { set(...(args as Parameters)); void setItem(); }, get, api ); api.getInitialState = () => configResult; let stateFromStorage: S | undefined; const hydrate = () => { if (!storage) return; hasHydrated = false; hydrationListeners.forEach((cb) => cb(get() ?? configResult)); const postRehydrationCallback = options.onRehydrateStorage?.(get() ?? configResult) || undefined; return toThenable(storage.getItem.bind(storage))(options.name) .then((deserializedStorageValue) => { if (deserializedStorageValue) { const newDeserializedObj: any = deserializedStorageValue; if ( typeof newDeserializedObj.version === "number" && newDeserializedObj.version !== options.version ) { if (options.migrate) { return [ true, options.migrate( newDeserializedObj.state, newDeserializedObj.version ), ] as const; } console.error( `State loaded from storage couldn't be migrated since no migrate function was provided` ); } else { return [false, newDeserializedObj.state] as const; } } return [false, undefined] as const; }) .then((migrationResult) => { const [migrated, migratedState] = migrationResult; stateFromStorage = options.merge( migratedState as S, get() ?? configResult ); set(stateFromStorage as S, true); if (migrated) { return setItem(); } else { return null; } }) .then(() => { postRehydrationCallback?.(stateFromStorage, undefined); stateFromStorage = get(); hasHydrated = true; finishHydrationListeners.forEach((cb) => cb(stateFromStorage as S)); }) .catch((e: Error) => { postRehydrationCallback?.(undefined, e); }); }; (api as StoreApi & StorePersist).persist = { setOptions: (newOptions) => { options = { ...options, ...newOptions, }; if (newOptions.storage) { storage = newOptions.storage; } }, clearStorage: () => { storage?.removeItem(options.name); }, getOptions: () => options, rehydrate: () => hydrate() as Promise, hasHydrated: () => hasHydrated, onHydrate: (cb) => { hydrationListeners.add(cb); return () => { hydrationListeners.delete(cb); }; }, onFinishHydration: (cb) => { finishHydrationListeners.add(cb); return () => { finishHydrationListeners.delete(cb); }; }, }; if (!options.skipHydration) { hydrate(); } return stateFromStorage || configResult; }) as PersistImpl; export const persist = persistImpl as PersistImpl;