import { ref, shallowRef, watch, toValue, type Ref, type ShallowRef, type MaybeRefOrGetter, type UnwrapRef } from 'vue'; import type { ConfigurableFlush } from '@/types'; import { tryOnScopeDispose } from '@/composables/lifecycle/tryOnScopeDispose'; import { guessSerializer, shallowMerge } from '../useStorage'; export interface StorageSerializerAsync { read: (raw: string) => T | Promise; write: (value: T) => string | Promise; } export interface StorageLikeAsync { getItem: (key: string) => string | null | Promise; setItem: (key: string, value: string) => void | Promise; removeItem: (key: string) => void | Promise; } export interface UseStorageAsyncOptions extends ConfigurableFlush { /** * Use shallowRef instead of ref for the internal state * @default true */ shallow?: Shallow; /** * Watch for deep changes * @default true */ deep?: boolean; /** * Write the default value to the storage when it does not exist * @default true */ writeDefaults?: boolean; /** * Custom serializer for reading/writing storage values */ serializer?: StorageSerializerAsync; /** * Merge the default value with the stored value * @default false */ mergeDefaults?: boolean | ((stored: T, defaults: T) => T); /** * Called once when the initial value has been loaded from storage */ onReady?: (value: T) => void; /** * Error handler for read/write failures */ onError?: (error: unknown) => void; } export interface UseStorageAsyncReturnBase { state: Shallow extends true ? ShallowRef : Ref>; isReady: Ref; } export type UseStorageAsyncReturn = & UseStorageAsyncReturnBase & PromiseLike>; /** * @name useStorageAsync * @category Storage * @description Reactive Storage binding with async support — creates a ref synced with an async storage backend * * @param {string} key The storage key * @param {MaybeRefOrGetter} initialValue The initial/default value * @param {StorageLikeAsync} storage The async storage backend * @param {UseStorageAsyncOptions} [options={}] Options * @returns {UseStorageAsyncReturn} An object with state ref and isReady flag, also awaitable * * @example * const { state } = useStorageAsync('access-token', '', asyncStorage); * * @example * const { state, isReady } = await useStorageAsync('settings', { theme: 'dark' }, asyncStorage); * * @example * const { state } = useStorageAsync('key', 'default', asyncStorage, { * onReady: (value) => console.log('Loaded:', value), * }); * * @since 0.0.12 */ export function useStorageAsync(key: string, initialValue: MaybeRefOrGetter, storage: StorageLikeAsync, options?: UseStorageAsyncOptions): UseStorageAsyncReturn; export function useStorageAsync(key: string, initialValue: MaybeRefOrGetter, storage: StorageLikeAsync, options?: UseStorageAsyncOptions): UseStorageAsyncReturn; export function useStorageAsync(key: string, initialValue: MaybeRefOrGetter, storage: StorageLikeAsync, options?: UseStorageAsyncOptions): UseStorageAsyncReturn; export function useStorageAsync(key: string, initialValue: MaybeRefOrGetter, storage: StorageLikeAsync, options?: UseStorageAsyncOptions): UseStorageAsyncReturn; export function useStorageAsync(key: string, initialValue: MaybeRefOrGetter, storage: StorageLikeAsync, options?: UseStorageAsyncOptions): UseStorageAsyncReturn; export function useStorageAsync( key: string, initialValue: MaybeRefOrGetter, storage: StorageLikeAsync, options: UseStorageAsyncOptions = {}, ): UseStorageAsyncReturn { const { shallow = true, deep = true, flush = 'pre', writeDefaults = true, mergeDefaults = false, onReady, onError = console.error, } = options; const defaults = toValue(initialValue); const serializer = options.serializer ?? guessSerializer(defaults); const state = (shallow ? shallowRef : ref)(defaults) as Shallow extends true ? ShallowRef : Ref>; const isReady = ref(false); async function read(): Promise { try { const raw = await storage.getItem(key); if (raw == null) { if (writeDefaults && defaults != null) { try { await storage.setItem(key, await serializer.write(defaults)); } catch (e) { onError(e); } } return defaults; } let value: T = await serializer.read(raw) as T; if (mergeDefaults) { value = typeof mergeDefaults === 'function' ? mergeDefaults(value, defaults) : shallowMerge(value, defaults); } return value; } catch (e) { onError(e); return defaults; } } async function write(value: T) { try { if (value == null) { await storage.removeItem(key); } else { const raw = await serializer.write(value); await storage.setItem(key, raw); } } catch (e) { onError(e); } } let stopWatch: (() => void) | null = null; tryOnScopeDispose(() => stopWatch?.()); const shell: UseStorageAsyncReturnBase = { state, isReady, }; const readyPromise: Promise> = read().then((value) => { (state as Ref).value = value; isReady.value = true; onReady?.(value); // Set up watcher AFTER initial state is set — avoids write-back on init const stop = watch(state, (newValue) => { write(newValue as T); }, { flush, deep }); stopWatch = stop; return shell; }); return { ...shell, then(onFulfilled, onRejected) { return readyPromise.then(onFulfilled, onRejected); }, }; }