@choksheak/react-utils 0.2.8 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/package.json +50 -2
  2. package/sharedQuery.cjs +119 -130
  3. package/sharedQuery.d.mts +14 -8
  4. package/sharedQuery.d.ts +12 -6
  5. package/sharedQuery.min.cjs +1 -1
  6. package/sharedQuery.min.cjs.map +1 -1
  7. package/sharedQuery.min.mjs +1 -1
  8. package/sharedQuery.min.mjs.map +1 -1
  9. package/sharedQuery.mjs +117 -129
  10. package/sharedState.cjs +69 -101
  11. package/sharedState.d.mts +7 -3
  12. package/sharedState.d.ts +5 -2
  13. package/sharedState.min.cjs +1 -1
  14. package/sharedState.min.cjs.map +1 -1
  15. package/sharedState.min.mjs +1 -1
  16. package/sharedState.min.mjs.map +1 -1
  17. package/sharedState.mjs +67 -100
  18. package/storage.cjs +90 -0
  19. package/storage.d.mts +19 -0
  20. package/storage.d.ts +16 -0
  21. package/storage.min.cjs +2 -0
  22. package/storage.min.cjs.map +1 -0
  23. package/storage.min.mjs +2 -0
  24. package/storage.min.mjs.map +1 -0
  25. package/storage.mjs +65 -0
  26. package/{utils/stringify.cjs → stringify.cjs} +1 -1
  27. package/stringify.min.cjs.map +1 -0
  28. package/stringify.min.mjs.map +1 -0
  29. package/{utils/stringify.mjs → stringify.mjs} +1 -1
  30. package/{utils/useDeepMemo.cjs → useDeepMemo.cjs} +1 -1
  31. package/useDeepMemo.d.mts +7 -0
  32. package/useDeepMemo.d.ts +5 -0
  33. package/useDeepMemo.min.cjs.map +1 -0
  34. package/useDeepMemo.min.mjs.map +1 -0
  35. package/{utils/useDeepMemo.mjs → useDeepMemo.mjs} +1 -1
  36. package/utils/storage.cjs +0 -162
  37. package/utils/storage.d.mts +0 -46
  38. package/utils/storage.d.ts +0 -44
  39. package/utils/storage.min.cjs +0 -2
  40. package/utils/storage.min.cjs.map +0 -1
  41. package/utils/storage.min.mjs +0 -2
  42. package/utils/storage.min.mjs.map +0 -1
  43. package/utils/storage.mjs +0 -134
  44. package/utils/stringify.min.cjs.map +0 -1
  45. package/utils/stringify.min.mjs.map +0 -1
  46. package/utils/useDeepMemo.d.mts +0 -4
  47. package/utils/useDeepMemo.d.ts +0 -2
  48. package/utils/useDeepMemo.min.cjs.map +0 -1
  49. package/utils/useDeepMemo.min.mjs.map +0 -1
  50. /package/{utils/stringify.d.mts → stringify.d.mts} +0 -0
  51. /package/{utils/stringify.d.ts → stringify.d.ts} +0 -0
  52. /package/{utils/stringify.min.cjs → stringify.min.cjs} +0 -0
  53. /package/{utils/stringify.min.mjs → stringify.min.mjs} +0 -0
  54. /package/{utils/useDeepMemo.min.cjs → useDeepMemo.min.cjs} +0 -0
  55. /package/{utils/useDeepMemo.min.mjs → useDeepMemo.min.mjs} +0 -0
@@ -1,2 +1,2 @@
1
- "use strict";var l=Object.defineProperty;var w=Object.getOwnPropertyDescriptor;var x=Object.getOwnPropertyNames;var m=Object.prototype.hasOwnProperty;var K=(r,e)=>{for(var t in e)l(r,t,{get:e[t],enumerable:!0})},A=(r,e,t,i)=>{if(e&&typeof e=="object"||typeof e=="function")for(let n of x(e))!m.call(r,n)&&n!==t&&l(r,n,{get:()=>e[n],enumerable:!(i=w(e,n))||i.enumerable});return r};var P=r=>A(l({},"__esModule",{value:!0}),r);var M={};K(M,{SharedState:()=>d,SharedStateDefaults:()=>f,sharedState:()=>D,useSharedState:()=>h,useSharedStateSetter:()=>V,useSharedStateValue:()=>O});module.exports=P(M);var g=require("@choksheak/ts-utils/timeConstants"),T=require("react");var s=require("@choksheak/ts-utils/kvStore");function S(r,e){let{localStorageKey:t,indexedDbKey:i,storeExpiryMs:n,store:a}=r??{};if(a)return a;let u=n||e;return t?new c(t,u):i?new p(i,u):null}var c=class{constructor(e,t){this.keyPrefix=e;this.expiryMs=t}save(e,t){if(typeof window>"u")return;let i=this.keyPrefix+":"+e;window.localStorage.setItem(i,JSON.stringify({value:t,expiresAt:this.expiryMs?Date.now()+this.expiryMs:0}))}load(e){if(typeof window>"u")return;let t=this.keyPrefix+":"+e,i=window.localStorage.getItem(t);if(i)try{let n=JSON.parse(i);if(!n||n.expiresAt&&Date.now()>=n.expiresAt){window.localStorage.removeItem(t);return}return n.value}catch{window.localStorage.removeItem(t);return}}delete(e){if(typeof window>"u")return;let t=this.keyPrefix+":"+e;window.localStorage.removeItem(t)}clear(){if(!(typeof window>"u"))for(let e of Object.keys(window.localStorage))e.startsWith(this.keyPrefix)&&window.localStorage.removeItem(e)}},p=class{constructor(e,t){this.keyPrefix=e;this.expiryMs=t}async save(e,t){if(typeof window>"u")return;let i=this.keyPrefix+":"+e;await s.kvStore.set(i,JSON.stringify(t),this.expiryMs)}async load(e){if(typeof window>"u")return;let t=this.keyPrefix+":"+e,i=await s.kvStore.get(t);if(i!==void 0)try{return JSON.parse(i)}catch{await s.kvStore.delete(t);return}}async delete(e){if(typeof window>"u")return;let t=this.keyPrefix+":"+e;await s.kvStore.delete(t)}async clear(){typeof window>"u"||await s.kvStore.forEach(async e=>{e.startsWith(this.keyPrefix)&&await s.kvStore.delete(e)})}};var f={storeExpiryMs:30*g.MS_PER_DAY,lazyLoad:!1},b=class{dataByKey=new Map;subscribersByKey=new Map;get(e){return this.dataByKey.get(e)}set(e,t){let i=this.dataByKey.get(e);i!==t&&(this.dataByKey.set(e,t),this.notify(e,t,i))}delete(e){this.dataByKey.delete(e)}setNoNotify(e,t){this.dataByKey.set(e,t)}subscribe(e,t){let i=this.subscribersByKey.get(e);return i||(i=new Set,this.subscribersByKey.set(e,i)),i.add(t),()=>{i.delete(t),i.size===0&&this.subscribersByKey.delete(e)}}notify(e,t,i){let n=this.subscribersByKey.get(e);if(!n)return;let a=Array.from(n.values());for(let u of a)try{u(t,i)}catch(v){console.error("Error invoking subscriber:",v)}}},o=new b,y="state",k=0,d=class{constructor(e,t){this.defaultValue=e;this.pubSubKey=String(k++),this.storageAdapter=S(t,f.storeExpiryMs),this.setValueBounded=a=>{this.setValue(a)},o.setNoNotify(this.pubSubKey,e);let i=t?.lazyLoad;(i!==void 0?i:f.lazyLoad)||this.initDefaultValueOnce()}pubSubKey;storageAdapter;setValueBounded;initDone=!1;subscribe(e){return o.subscribe(this.pubSubKey,()=>e())}getSnapshot(){return o.get(this.pubSubKey)}setValue(e){if(typeof e=="function"){let t=o.get(this.pubSubKey);e=e(t)}o.set(this.pubSubKey,e),this.storageAdapter?.save(y,e)}delete(){o.set(this.pubSubKey,this.defaultValue),this.storageAdapter?.save(y,this.defaultValue)}async initDefaultValueOnce(){if(this.initDone)return;this.initDone=!0;let e=await this.storageAdapter?.load(y);o.set(this.pubSubKey,e!==void 0?e:this.defaultValue)}};function D(r,e){return new d(r,e)}function h(r){return r.initDefaultValueOnce(),[(0,T.useSyncExternalStore)(t=>r.subscribe(t),()=>r.getSnapshot(),()=>r.getSnapshot()),r.setValueBounded]}function O(r){return h(r)[0]}function V(r){return h(r)[1]}0&&(module.exports={SharedState,SharedStateDefaults,sharedState,useSharedState,useSharedStateSetter,useSharedStateValue});
1
+ "use strict";var c=Object.defineProperty;var w=Object.getOwnPropertyDescriptor;var v=Object.getOwnPropertyNames;var K=Object.prototype.hasOwnProperty;var m=(r,e)=>{for(var t in e)c(r,t,{get:e[t],enumerable:!0})},A=(r,e,t,i)=>{if(e&&typeof e=="object"||typeof e=="function")for(let s of v(e))!K.call(r,s)&&s!==t&&c(r,s,{get:()=>e[s],enumerable:!(i=w(e,s))||i.enumerable});return r};var x=r=>A(c({},"__esModule",{value:!0}),r);var E={};m(E,{SharedState:()=>p,SharedStateConfig:()=>d,configureSharedState:()=>B,sharedState:()=>D,useSharedState:()=>S,useSharedStateSetter:()=>k,useSharedStateValue:()=>V});module.exports=x(E);var g=require("@choksheak/ts-utils/timeConstants"),T=require("react");var u=require("@choksheak/ts-utils/kvStore"),l=require("@choksheak/ts-utils/localStore");function b(r,e){let{store:t}=r??{};if(!t)return null;if("persistTo"in t&&typeof t.persistTo=="string"&&"key"in t&&typeof t.key=="string"){let i=t.expiryMs||e;switch(t.persistTo){case"localStorage":{let s=new l.LocalStore(t.key,{defaultExpiryMs:i});return{set:(n,a)=>{typeof window>"u"||s.set(n,a)},get:n=>{if(!(typeof window>"u"))return s.get(n)},delete:n=>{typeof window>"u"||s.delete(n)},clear:()=>{typeof window>"u"||s.clear()}}}case"indexedDb":{let s=t.key+":";return{set:async(n,a)=>{typeof window>"u"||await u.kvStore.set(s+n,a,i)},get:async n=>{if(!(typeof window>"u"))return await u.kvStore.get(s+n)},delete:async n=>{typeof window>"u"||await u.kvStore.delete(s+n)},clear:async()=>{typeof window>"u"||await u.kvStore.clear()}}}default:throw t.persistTo,new Error(`Unknown store.adapter ${t.persistTo}`)}}return t}var d={storeExpiryMs:30*g.MS_PER_DAY,lazyLoad:!1};function B(r){Object.assign(d,r)}var y=class{dataByKey=new Map;subscribersByKey=new Map;get(e){return this.dataByKey.get(e)}set(e,t){let i=this.dataByKey.get(e);i!==t&&(this.dataByKey.set(e,t),this.notify(e,t,i))}delete(e){this.dataByKey.delete(e)}setNoNotify(e,t){this.dataByKey.set(e,t)}subscribe(e,t){let i=this.subscribersByKey.get(e);return i||(i=new Set,this.subscribersByKey.set(e,i)),i.add(t),()=>{i.delete(t),i.size===0&&this.subscribersByKey.delete(e)}}notify(e,t,i){let s=this.subscribersByKey.get(e);if(!s)return;let n=Array.from(s.values());for(let a of n)try{a(t,i)}catch(h){console.error("Error invoking subscriber:",h)}}},o=new y,f="state",O=0,p=class{constructor(e,t){this.defaultValue=e;this.pubSubKey=String(O++),this.storageAdapter=b(t,d.storeExpiryMs),this.setValueBounded=n=>{this.setValue(n)},o.setNoNotify(this.pubSubKey,e);let i=t?.lazyLoad;(i!==void 0?i:d.lazyLoad)||this.initDefaultValueOnce()}pubSubKey;storageAdapter;setValueBounded;initDone=!1;subscribe(e){return o.subscribe(this.pubSubKey,()=>e())}getSnapshot(){return o.get(this.pubSubKey)}setValue(e){if(typeof e=="function"){let t=o.get(this.pubSubKey);e=e(t)}o.set(this.pubSubKey,e),this.storageAdapter?.set(f,e)}delete(){o.set(this.pubSubKey,this.defaultValue),this.storageAdapter?.set(f,this.defaultValue)}async initDefaultValueOnce(){if(this.initDone)return;this.initDone=!0;let e=await this.storageAdapter?.get(f);o.set(this.pubSubKey,e!==void 0?e:this.defaultValue)}};function D(r,e){return new p(r,e)}function S(r){return r.initDefaultValueOnce(),[(0,T.useSyncExternalStore)(t=>r.subscribe(t),()=>r.getSnapshot(),()=>r.getSnapshot()),r.setValueBounded]}function V(r){return S(r)[0]}function k(r){return S(r)[1]}0&&(module.exports={SharedState,SharedStateConfig,configureSharedState,sharedState,useSharedState,useSharedStateSetter,useSharedStateValue});
2
2
  //# sourceMappingURL=sharedState.min.cjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/sharedState.ts","../src/utils/storage.ts"],"sourcesContent":["/**\n * @packageDocumentation\n *\n * The \"shared state\" is a React state that shares the same value across the\n * entire app. It is largely similar to the jotai library, but is much smaller\n * and much easier to work with when used for persisting in local storage or\n * indexed db, with an automatic expiration for persisted values.\n *\n * This library aims to be the shortest, simplest, easiest to use, and most\n * efficient implementation of shared global React states with seamless support\n * for client persistence.\n *\n * Example:\n * ```\n * import { sharedState, useSharedState } from \"@choksheak/react-utils/sharedState\";\n *\n * // Create a new shared state in the top level scope.\n * // The first argument is the default value, which is required.\n * // The second argument is an options object, and is optional (defaults\n * // to storing in memory only without any persistence).\n * const usersState = sharedState([], { indexedDbKey: \"users\" });\n *\n * export const UsersComponent: React.FC = () => {\n * // Get both the value and the setter.\n * const [users, setUsers] = useSharedState(usersState);\n * ...\n * };\n *\n * export const ReadOnlyComponent: React.FC = () => {\n * // Get the value only.\n * const users = useSharedStateValue(usersState);\n * ...\n * };\n *\n * export const WriteOnlyComponent: React.FC = () => {\n * // Get the setter only.\n * const setUsers = useSharedStateSetter(usersState);\n * ...\n * };\n * ```\n */\n\nimport { MS_PER_DAY } from \"@choksheak/ts-utils/timeConstants\";\nimport { Dispatch, SetStateAction, useSyncExternalStore } from \"react\";\n\nimport {\n getStorageAdapter,\n StorageAdapter,\n StorageOptions,\n} from \"./utils/storage\";\n\n/************************************************************************/\n/* Global config */\n/************************************************************************/\n\n/**\n * These values are configurable by the user at any time.\n */\nexport const SharedStateDefaults = {\n /**\n * Default duration for storage expiration in milliseconds. Initial value\n * is 30 days, but can be changed to anything.\n */\n storeExpiryMs: 30 * MS_PER_DAY,\n\n /**\n * True to load from storage only on mount, false (default) to load in\n * the top level scope when the shared state is defined.\n */\n lazyLoad: false,\n};\n\n/************************************************************************/\n/* Shared state implementation */\n/************************************************************************/\n\ntype Subscriber<T> = (next: T, prev: T) => void;\n\nclass PubSubStore {\n private dataByKey = new Map<string, unknown>();\n private subscribersByKey = new Map<string, Set<Subscriber<unknown>>>();\n\n // This could be undefined if T includes undefined, else it should never\n // return undefined because the user has to always specify the default value.\n public get<T>(key: string): T {\n return this.dataByKey.get(key) as T;\n }\n\n public set<T>(key: string, value: T): void {\n const prev = this.dataByKey.get(key) as T;\n\n // Do nothing if no change.\n if (prev === value) return;\n\n this.dataByKey.set(key, value);\n this.notify(key, value, prev);\n }\n\n public delete(key: string): void {\n this.dataByKey.delete(key);\n }\n\n public setNoNotify<T>(key: string, value: T): void {\n this.dataByKey.set(key, value);\n }\n\n // Subscribe to changes of a specific key\n public subscribe<T>(key: string, subscriber: Subscriber<T>): () => void {\n let subscribers = this.subscribersByKey.get(key);\n if (!subscribers) {\n subscribers = new Set();\n this.subscribersByKey.set(key, subscribers);\n }\n\n // Type coercion: we store Subscriber<unknown>\n subscribers.add(subscriber as Subscriber<unknown>);\n\n // Return unsubscribe function\n return () => {\n subscribers.delete(subscriber as Subscriber<unknown>);\n if (subscribers.size === 0) this.subscribersByKey.delete(key);\n };\n }\n\n private notify<T>(key: string, next: T, prev: T): void {\n const subscribers = this.subscribersByKey.get(key);\n if (!subscribers) return;\n\n // Make copy to protect against mutation during iteration.\n const copy = Array.from(subscribers.values());\n for (const subscriber of copy) {\n try {\n subscriber(next, prev);\n } catch (e) {\n console.error(`Error invoking subscriber:`, e);\n }\n }\n }\n}\n\nconst pubSubStore = new PubSubStore();\n\nconst STORAGE_KEY = \"state\";\n\n/** Options to configure a shared state. */\nexport type SharedStateOptions<T> = {\n lazyLoad?: boolean;\n} & StorageOptions<T>;\n\nlet stateKey = 0;\n\n/** Please use sharedState() instead. */\nexport class SharedState<T> {\n private readonly pubSubKey: string;\n private readonly storageAdapter: StorageAdapter<T> | null;\n public readonly setValueBounded: Dispatch<SetStateAction<T>>;\n public initDone = false;\n\n public constructor(\n private readonly defaultValue: T,\n options?: SharedStateOptions<T>,\n ) {\n this.pubSubKey = String(stateKey++);\n\n // Use max of 1 storage adapter per shared state.\n this.storageAdapter = getStorageAdapter(\n options,\n SharedStateDefaults.storeExpiryMs,\n );\n\n // Same as this.setValue, but binding the `this` reference.\n this.setValueBounded = (next: SetStateAction<T>) => {\n this.setValue(next);\n };\n\n // Always set the default value first to avoid returning undefineds if that\n // is not part of T.\n pubSubStore.setNoNotify(this.pubSubKey, defaultValue);\n\n const lazyLoad = options?.lazyLoad;\n const lazy =\n lazyLoad !== undefined ? lazyLoad : SharedStateDefaults.lazyLoad;\n\n if (!lazy) {\n void this.initDefaultValueOnce();\n }\n }\n\n // `subscribe` function required by useSyncExternalStore\n public subscribe(subscriber: () => void): () => void {\n return pubSubStore.subscribe(this.pubSubKey, () => subscriber());\n }\n\n // Get the value of the shared state.\n // `getSnapshot` function — must return same ref if value unchanged\n public getSnapshot(): T {\n return pubSubStore.get(this.pubSubKey);\n }\n\n // Set the value of the shared state.\n public setValue(next: SetStateAction<T>): void {\n if (typeof next === \"function\") {\n const prev = pubSubStore.get<T>(this.pubSubKey);\n next = (next as (p: T) => T)(prev);\n }\n\n pubSubStore.set(this.pubSubKey, next);\n void this.storageAdapter?.save(STORAGE_KEY, next);\n }\n\n // Remove the value of the shared state. Actually this just sets the value\n // back to the given default value.\n public delete() {\n pubSubStore.set(this.pubSubKey, this.defaultValue);\n void this.storageAdapter?.save(STORAGE_KEY, this.defaultValue);\n }\n\n // Initial default handling\n public async initDefaultValueOnce() {\n if (this.initDone) return;\n this.initDone = true;\n\n const v = await this.storageAdapter?.load(STORAGE_KEY);\n pubSubStore.set(this.pubSubKey, v !== undefined ? v : this.defaultValue);\n }\n}\n\n/**\n * Create a new shared state object. Put this code at the top level scope:\n *\n * Example:\n * ```\n * const counterState = sharedState<number>(0);\n * ```\n */\nexport function sharedState<T>(\n defaultValue: T,\n options?: SharedStateOptions<T>,\n): SharedState<T> {\n return new SharedState(defaultValue, options);\n}\n\n/**\n * React hook for subscribing to a key in the global store.\n *\n * Example:\n * ```\n * const [counter, setCounter] = useSharedState(counterState);\n * ```\n */\nexport function useSharedState<T>(\n state: SharedState<T>,\n): [T, Dispatch<SetStateAction<T>>] {\n state.initDefaultValueOnce();\n\n const value = useSyncExternalStore(\n (onStoreChange) => state.subscribe(onStoreChange),\n () => state.getSnapshot(),\n // getServerSnapshot is needed to prevent SSR dev errors in nextjs.\n () => state.getSnapshot(),\n );\n\n return [value, state.setValueBounded];\n}\n\n/**\n * Same as useSharedState(), but returns the value only.\n *\n * Example:\n * ```\n * const counter = useSharedStateValue(counterState);\n * ```\n */\nexport function useSharedStateValue<T>(state: SharedState<T>) {\n return useSharedState(state)[0];\n}\n\n/**\n * Same as useSharedState(), but returns the setter only.\n *\n * Example:\n * ```\n * const setCounter = useSharedStateSetter(counterState);\n * ```\n */\nexport function useSharedStateSetter<T>(state: SharedState<T>) {\n return useSharedState(state)[1];\n}\n","import { kvStore } from \"@choksheak/ts-utils/kvStore\";\n\nexport type StorageAdapter<T> = {\n save: (key: string, value: T) => Promise<void> | void;\n load: (key: string) => Promise<T | undefined> | T | undefined;\n delete: (key: string) => Promise<void> | void;\n clear: () => Promise<void> | void;\n};\n\nexport type StorageOptions<T> = {\n // If specified, this will be used and all other fields will be ignored.\n store?: StorageAdapter<T>;\n\n // Storage key in local storage for this data.\n localStorageKey?: string;\n\n // Storage key in indexed db for this data.\n indexedDbKey?: string;\n\n // How long to wait before the value is considered expired.\n storeExpiryMs?: number;\n};\n\nexport function getStorageAdapter<T>(\n options: StorageOptions<T> | undefined,\n defaultStoreExpiryMs: number,\n): StorageAdapter<T> | null {\n const { localStorageKey, indexedDbKey, storeExpiryMs, store } = options ?? {};\n\n if (store) {\n return store;\n }\n\n const expiryMs = storeExpiryMs || defaultStoreExpiryMs;\n\n if (localStorageKey) {\n return new LocalStorageAdapter<T>(localStorageKey, expiryMs);\n }\n\n if (indexedDbKey) {\n return new IndexedDbAdapter<T>(indexedDbKey, expiryMs);\n }\n\n return null;\n}\n\n/************************************************************************/\n/* Storage adapters */\n/************************************************************************/\n\ntype ValueWithExpiration<T> = { value: T; expiresAt: number };\n\n/** Save state to memory only - not persisted. */\nexport class InMemoryStorageAdapter<T> implements StorageAdapter<T> {\n private readonly cache = new Map<string, ValueWithExpiration<T>>();\n\n public constructor(\n public readonly keyPrefix: string,\n public readonly expiryMs: number,\n ) {}\n\n public save(key: string, value: T): void {\n const fullKey = this.keyPrefix + \":\" + key;\n this.cache.set(fullKey, {\n value,\n expiresAt: Date.now() + this.expiryMs,\n });\n }\n\n public load(key: string): T | undefined {\n const fullKey = this.keyPrefix + \":\" + key;\n const cached = this.cache.get(fullKey);\n\n if (cached && Date.now() < cached.expiresAt) {\n return cached.value;\n }\n\n return undefined;\n }\n\n public delete(key: string): void {\n const fullKey = this.keyPrefix + \":\" + key;\n this.cache.delete(fullKey);\n }\n\n public clear(): void {\n this.cache.clear();\n }\n}\n\n/** Save state to local storage. */\nexport class LocalStorageAdapter<T> implements StorageAdapter<T> {\n public constructor(\n public readonly keyPrefix: string,\n public readonly expiryMs: number,\n ) {}\n\n public save(key: string, value: T): void {\n if (typeof window === \"undefined\") return;\n\n const fullKey = this.keyPrefix + \":\" + key;\n window.localStorage.setItem(\n fullKey,\n JSON.stringify({\n value,\n expiresAt: this.expiryMs ? Date.now() + this.expiryMs : 0,\n } satisfies ValueWithExpiration<T>),\n );\n }\n\n public load(key: string): T | undefined {\n if (typeof window === \"undefined\") return undefined;\n\n const fullKey = this.keyPrefix + \":\" + key;\n const raw = window.localStorage.getItem(fullKey);\n if (!raw) return undefined;\n\n try {\n const box = JSON.parse(raw) as ValueWithExpiration<T>;\n if (!box || (box.expiresAt && Date.now() >= box.expiresAt)) {\n window.localStorage.removeItem(fullKey);\n return undefined;\n }\n\n return box.value;\n } catch {\n // Ignore error.\n window.localStorage.removeItem(fullKey);\n return undefined;\n }\n }\n\n public delete(key: string): void {\n if (typeof window === \"undefined\") return;\n\n const fullKey = this.keyPrefix + \":\" + key;\n window.localStorage.removeItem(fullKey);\n }\n\n public clear(): void {\n if (typeof window === \"undefined\") return;\n\n for (const key of Object.keys(window.localStorage)) {\n if (key.startsWith(this.keyPrefix)) {\n window.localStorage.removeItem(key);\n }\n }\n }\n}\n\n/** Save state to indexed db. */\nexport class IndexedDbAdapter<T> implements StorageAdapter<T> {\n public constructor(\n public readonly keyPrefix: string,\n public readonly expiryMs: number,\n ) {}\n\n public async save(key: string, value: T): Promise<void> {\n if (typeof window === \"undefined\") return;\n\n const fullKey = this.keyPrefix + \":\" + key;\n await kvStore.set(fullKey, JSON.stringify(value), this.expiryMs);\n }\n\n public async load(key: string): Promise<T | undefined> {\n if (typeof window === \"undefined\") return undefined;\n\n const fullKey = this.keyPrefix + \":\" + key;\n const raw = await kvStore.get<string>(fullKey);\n if (raw === undefined) return undefined;\n\n try {\n return JSON.parse(raw);\n } catch {\n // Ignore error.\n await kvStore.delete(fullKey);\n return undefined;\n }\n }\n\n public async delete(key: string): Promise<void> {\n if (typeof window === \"undefined\") return;\n\n const fullKey = this.keyPrefix + \":\" + key;\n await kvStore.delete(fullKey);\n }\n\n public async clear(): Promise<void> {\n if (typeof window === \"undefined\") return;\n\n await kvStore.forEach(async (k) => {\n if (k.startsWith(this.keyPrefix)) {\n await kvStore.delete(k);\n }\n });\n }\n}\n"],"mappings":"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","names":["sharedState_exports","__export","SharedState","SharedStateDefaults","sharedState","useSharedState","useSharedStateSetter","useSharedStateValue","__toCommonJS","import_timeConstants","import_react","import_kvStore","getStorageAdapter","options","defaultStoreExpiryMs","localStorageKey","indexedDbKey","storeExpiryMs","store","expiryMs","LocalStorageAdapter","IndexedDbAdapter","LocalStorageAdapter","keyPrefix","expiryMs","key","value","fullKey","raw","box","IndexedDbAdapter","k","SharedStateDefaults","PubSubStore","key","value","prev","subscriber","subscribers","next","copy","e","pubSubStore","STORAGE_KEY","stateKey","SharedState","defaultValue","options","getStorageAdapter","lazyLoad","v","sharedState","useSharedState","state","onStoreChange","useSharedStateValue","useSharedStateSetter"]}
1
+ {"version":3,"sources":["../src/sharedState.ts","../src/storage.ts"],"sourcesContent":["/**\n * @packageDocumentation\n *\n * The \"shared state\" is a React state that shares the same value across the\n * entire app. It is largely similar to the jotai library, but is much smaller\n * and much easier to work with when used for persisting in local storage or\n * indexed db, with an automatic expiration for persisted values.\n *\n * This library aims to be the shortest, simplest, easiest to use, and most\n * efficient implementation of shared global React states with seamless support\n * for client persistence.\n *\n * Example:\n * ```\n * import { sharedState, useSharedState } from \"@choksheak/react-utils/sharedState\";\n *\n * // Create a new shared state in the top level scope.\n * // The first argument is the default value, which is required.\n * // The second argument is an options object, and is optional (defaults\n * // to storing in memory only without any persistence).\n * const usersState = sharedState([], { indexedDbKey: \"users\" });\n *\n * export const UsersComponent: React.FC = () => {\n * // Get both the value and the setter.\n * const [users, setUsers] = useSharedState(usersState);\n * ...\n * };\n *\n * export const ReadOnlyComponent: React.FC = () => {\n * // Get the value only.\n * const users = useSharedStateValue(usersState);\n * ...\n * };\n *\n * export const WriteOnlyComponent: React.FC = () => {\n * // Get the setter only.\n * const setUsers = useSharedStateSetter(usersState);\n * ...\n * };\n * ```\n */\n\nimport { StorageAdapter } from \"@choksheak/ts-utils/storageAdapter\";\nimport { MS_PER_DAY } from \"@choksheak/ts-utils/timeConstants\";\nimport { Dispatch, SetStateAction, useSyncExternalStore } from \"react\";\n\nimport { getStorageAdapter, StorageOptions } from \"./storage\";\n\n/************************************************************************/\n/* Global config */\n/************************************************************************/\n\n/**\n * These values are configurable by the user at any time.\n */\nexport const SharedStateConfig = {\n /**\n * Default duration for storage expiration in milliseconds. Initial value\n * is 30 days, but can be changed to anything.\n */\n storeExpiryMs: 30 * MS_PER_DAY,\n\n /**\n * True to load from storage only on mount, false (default) to load in\n * the top level scope when the shared state is defined.\n */\n lazyLoad: false,\n};\n\nexport type SharedStateConfig = typeof SharedStateConfig;\n\n/** Convenience function to update global defaults. */\nexport function configureSharedState(config: Partial<SharedStateConfig>) {\n Object.assign(SharedStateConfig, config);\n}\n\n/************************************************************************/\n/* Shared state implementation */\n/************************************************************************/\n\ntype Subscriber<T> = (next: T, prev: T) => void;\n\nclass PubSubStore {\n private dataByKey = new Map<string, unknown>();\n private subscribersByKey = new Map<string, Set<Subscriber<unknown>>>();\n\n // This could be undefined if T includes undefined, else it should never\n // return undefined because the user has to always specify the default value.\n public get<T>(key: string): T {\n return this.dataByKey.get(key) as T;\n }\n\n public set<T>(key: string, value: T): void {\n const prev = this.dataByKey.get(key) as T;\n\n // Do nothing if no change.\n if (prev === value) return;\n\n this.dataByKey.set(key, value);\n this.notify(key, value, prev);\n }\n\n public delete(key: string): void {\n this.dataByKey.delete(key);\n }\n\n public setNoNotify<T>(key: string, value: T): void {\n this.dataByKey.set(key, value);\n }\n\n // Subscribe to changes of a specific key\n public subscribe<T>(key: string, subscriber: Subscriber<T>): () => void {\n let subscribers = this.subscribersByKey.get(key);\n if (!subscribers) {\n subscribers = new Set();\n this.subscribersByKey.set(key, subscribers);\n }\n\n // Type coercion: we store Subscriber<unknown>\n subscribers.add(subscriber as Subscriber<unknown>);\n\n // Return unsubscribe function\n return () => {\n subscribers.delete(subscriber as Subscriber<unknown>);\n if (subscribers.size === 0) this.subscribersByKey.delete(key);\n };\n }\n\n private notify<T>(key: string, next: T, prev: T): void {\n const subscribers = this.subscribersByKey.get(key);\n if (!subscribers) return;\n\n // Make copy to protect against mutation during iteration.\n const copy = Array.from(subscribers.values());\n for (const subscriber of copy) {\n try {\n subscriber(next, prev);\n } catch (e) {\n console.error(`Error invoking subscriber:`, e);\n }\n }\n }\n}\n\nconst pubSubStore = new PubSubStore();\n\nconst STORAGE_KEY = \"state\";\n\n/** Options to configure a shared state. */\nexport type SharedStateOptions<T> = {\n lazyLoad?: boolean;\n} & StorageOptions<T>;\n\nlet stateKey = 0;\n\n/** Please use sharedState() instead. */\nexport class SharedState<T> {\n private readonly pubSubKey: string;\n private readonly storageAdapter: StorageAdapter<T> | null;\n public readonly setValueBounded: Dispatch<SetStateAction<T>>;\n public initDone = false;\n\n public constructor(\n private readonly defaultValue: T,\n options?: SharedStateOptions<T>,\n ) {\n this.pubSubKey = String(stateKey++);\n\n // Use max of 1 storage adapter per shared state.\n this.storageAdapter = getStorageAdapter(\n options,\n SharedStateConfig.storeExpiryMs,\n );\n\n // Same as this.setValue, but binding the `this` reference.\n this.setValueBounded = (next: SetStateAction<T>) => {\n this.setValue(next);\n };\n\n // Always set the default value first to avoid returning undefineds if that\n // is not part of T.\n pubSubStore.setNoNotify(this.pubSubKey, defaultValue);\n\n const lazyLoad = options?.lazyLoad;\n const lazy = lazyLoad !== undefined ? lazyLoad : SharedStateConfig.lazyLoad;\n\n if (!lazy) {\n void this.initDefaultValueOnce();\n }\n }\n\n // `subscribe` function required by useSyncExternalStore\n public subscribe(subscriber: () => void): () => void {\n return pubSubStore.subscribe(this.pubSubKey, () => subscriber());\n }\n\n // Get the value of the shared state.\n // `getSnapshot` function — must return same ref if value unchanged\n public getSnapshot(): T {\n return pubSubStore.get(this.pubSubKey);\n }\n\n // Set the value of the shared state.\n public setValue(next: SetStateAction<T>): void {\n if (typeof next === \"function\") {\n const prev = pubSubStore.get<T>(this.pubSubKey);\n next = (next as (p: T) => T)(prev);\n }\n\n pubSubStore.set(this.pubSubKey, next);\n void this.storageAdapter?.set(STORAGE_KEY, next);\n }\n\n // Remove the value of the shared state. Actually this just sets the value\n // back to the given default value.\n public delete() {\n pubSubStore.set(this.pubSubKey, this.defaultValue);\n void this.storageAdapter?.set(STORAGE_KEY, this.defaultValue);\n }\n\n // Initial default handling\n public async initDefaultValueOnce() {\n if (this.initDone) return;\n this.initDone = true;\n\n const v = await this.storageAdapter?.get(STORAGE_KEY);\n pubSubStore.set(this.pubSubKey, v !== undefined ? v : this.defaultValue);\n }\n}\n\n/**\n * Create a new shared state object. Put this code at the top level scope:\n *\n * Example:\n * ```\n * const counterState = sharedState<number>(0);\n * ```\n */\nexport function sharedState<T>(\n defaultValue: T,\n options?: SharedStateOptions<T>,\n): SharedState<T> {\n return new SharedState(defaultValue, options);\n}\n\n/**\n * React hook for subscribing to a key in the global store.\n *\n * Example:\n * ```\n * const [counter, setCounter] = useSharedState(counterState);\n * ```\n */\nexport function useSharedState<T>(\n state: SharedState<T>,\n): [T, Dispatch<SetStateAction<T>>] {\n state.initDefaultValueOnce();\n\n const value = useSyncExternalStore(\n (onStoreChange) => state.subscribe(onStoreChange),\n () => state.getSnapshot(),\n // getServerSnapshot is needed to prevent SSR dev errors in nextjs.\n () => state.getSnapshot(),\n );\n\n return [value, state.setValueBounded];\n}\n\n/**\n * Same as useSharedState(), but returns the value only.\n *\n * Example:\n * ```\n * const counter = useSharedStateValue(counterState);\n * ```\n */\nexport function useSharedStateValue<T>(state: SharedState<T>) {\n return useSharedState(state)[0];\n}\n\n/**\n * Same as useSharedState(), but returns the setter only.\n *\n * Example:\n * ```\n * const setCounter = useSharedStateSetter(counterState);\n * ```\n */\nexport function useSharedStateSetter<T>(state: SharedState<T>) {\n return useSharedState(state)[1];\n}\n","import { kvStore } from \"@choksheak/ts-utils/kvStore\";\nimport { LocalStore } from \"@choksheak/ts-utils/localStore\";\nimport { StorageAdapter } from \"@choksheak/ts-utils/storageAdapter\";\n\n/** List of all available pre-built persistence options. */\nexport type PersistTo = \"localStorage\" | \"indexedDb\";\n\nexport type StoreOptions = {\n persistTo: PersistTo;\n\n // Storage key to act as a namespace to store the data.\n key: string;\n\n // How long to wait before the value is considered expired.\n expiryMs?: number;\n};\n\nexport type StorageOptions<T> = {\n /**\n * You can either specify the storage adapter directly to use a custom\n * store, or specify the properties to use the pre-built ones.\n */\n store?: StorageAdapter<T> | StoreOptions;\n};\n\nexport function getStorageAdapter<T>(\n options: StorageOptions<T> | undefined,\n defaultStoreExpiryMs: number,\n): StorageAdapter<T> | null {\n const { store } = options ?? {};\n\n if (!store) {\n return null;\n }\n\n if (\n \"persistTo\" in store &&\n typeof store.persistTo === \"string\" &&\n \"key\" in store &&\n typeof store.key === \"string\"\n ) {\n const expiryMs = store.expiryMs || defaultStoreExpiryMs;\n\n switch (store.persistTo) {\n case \"localStorage\": {\n const adapter = new LocalStore(store.key, {\n defaultExpiryMs: expiryMs,\n });\n\n // Handle SSR.\n return {\n set: (key: string, value: T): void => {\n if (typeof window === \"undefined\") return; // handle SSR\n\n // Also apply the custom expiryMs on every item in this store.\n adapter.set(key, value);\n },\n get: (key: string): T | undefined => {\n if (typeof window === \"undefined\") return undefined; // handle SSR\n\n return adapter.get(key);\n },\n delete: (key: string): void => {\n if (typeof window === \"undefined\") return; // handle SSR\n\n adapter.delete(key);\n },\n clear: (): void => {\n if (typeof window === \"undefined\") return; // handle SSR\n\n adapter.clear();\n },\n };\n }\n\n case \"indexedDb\": {\n // Use the key prefix as a unique namespace for this store.\n const keyPrefix = store.key + \":\";\n\n // Prefix all keys using `keyPrefix` so that we can keep all data in\n // the same store in the same DB. If we keep the data in different\n // DB stores, then GC will not be able to clean up abandoned stores.\n return {\n set: async (key: string, value: T): Promise<void> => {\n if (typeof window === \"undefined\") return; // handle SSR\n\n // Also apply the custom expiryMs on every item in this store.\n await kvStore.set(keyPrefix + key, value, expiryMs);\n },\n get: async (key: string): Promise<T | undefined> => {\n if (typeof window === \"undefined\") return undefined; // handle SSR\n\n return await kvStore.get(keyPrefix + key);\n },\n delete: async (key: string): Promise<void> => {\n if (typeof window === \"undefined\") return; // handle SSR\n\n await kvStore.delete(keyPrefix + key);\n },\n clear: async (): Promise<void> => {\n if (typeof window === \"undefined\") return; // handle SSR\n\n await kvStore.clear();\n },\n };\n }\n\n default:\n store.persistTo satisfies never;\n throw new Error(`Unknown store.adapter ${store.persistTo}`);\n }\n }\n\n return store as StorageAdapter<T>;\n}\n"],"mappings":"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","names":["sharedState_exports","__export","SharedState","SharedStateConfig","configureSharedState","sharedState","useSharedState","useSharedStateSetter","useSharedStateValue","__toCommonJS","import_timeConstants","import_react","import_kvStore","import_localStore","getStorageAdapter","options","defaultStoreExpiryMs","store","expiryMs","adapter","key","value","keyPrefix","SharedStateConfig","configureSharedState","config","PubSubStore","key","value","prev","subscriber","subscribers","next","copy","e","pubSubStore","STORAGE_KEY","stateKey","SharedState","defaultValue","options","getStorageAdapter","lazyLoad","v","sharedState","useSharedState","state","onStoreChange","useSharedStateValue","useSharedStateSetter"]}
@@ -1,2 +1,2 @@
1
- import{MS_PER_DAY as g}from"@choksheak/ts-utils/timeConstants";import{useSyncExternalStore as T}from"react";import{kvStore as a}from"@choksheak/ts-utils/kvStore";function f(r,e){let{localStorageKey:t,indexedDbKey:i,storeExpiryMs:n,store:o}=r??{};if(o)return o;let u=n||e;return t?new d(t,u):i?new l(i,u):null}var d=class{constructor(e,t){this.keyPrefix=e;this.expiryMs=t}save(e,t){if(typeof window>"u")return;let i=this.keyPrefix+":"+e;window.localStorage.setItem(i,JSON.stringify({value:t,expiresAt:this.expiryMs?Date.now()+this.expiryMs:0}))}load(e){if(typeof window>"u")return;let t=this.keyPrefix+":"+e,i=window.localStorage.getItem(t);if(i)try{let n=JSON.parse(i);if(!n||n.expiresAt&&Date.now()>=n.expiresAt){window.localStorage.removeItem(t);return}return n.value}catch{window.localStorage.removeItem(t);return}}delete(e){if(typeof window>"u")return;let t=this.keyPrefix+":"+e;window.localStorage.removeItem(t)}clear(){if(!(typeof window>"u"))for(let e of Object.keys(window.localStorage))e.startsWith(this.keyPrefix)&&window.localStorage.removeItem(e)}},l=class{constructor(e,t){this.keyPrefix=e;this.expiryMs=t}async save(e,t){if(typeof window>"u")return;let i=this.keyPrefix+":"+e;await a.set(i,JSON.stringify(t),this.expiryMs)}async load(e){if(typeof window>"u")return;let t=this.keyPrefix+":"+e,i=await a.get(t);if(i!==void 0)try{return JSON.parse(i)}catch{await a.delete(t);return}}async delete(e){if(typeof window>"u")return;let t=this.keyPrefix+":"+e;await a.delete(t)}async clear(){typeof window>"u"||await a.forEach(async e=>{e.startsWith(this.keyPrefix)&&await a.delete(e)})}};var b={storeExpiryMs:30*g,lazyLoad:!1},p=class{dataByKey=new Map;subscribersByKey=new Map;get(e){return this.dataByKey.get(e)}set(e,t){let i=this.dataByKey.get(e);i!==t&&(this.dataByKey.set(e,t),this.notify(e,t,i))}delete(e){this.dataByKey.delete(e)}setNoNotify(e,t){this.dataByKey.set(e,t)}subscribe(e,t){let i=this.subscribersByKey.get(e);return i||(i=new Set,this.subscribersByKey.set(e,i)),i.add(t),()=>{i.delete(t),i.size===0&&this.subscribersByKey.delete(e)}}notify(e,t,i){let n=this.subscribersByKey.get(e);if(!n)return;let o=Array.from(n.values());for(let u of o)try{u(t,i)}catch(S){console.error("Error invoking subscriber:",S)}}},s=new p,c="state",v=0,y=class{constructor(e,t){this.defaultValue=e;this.pubSubKey=String(v++),this.storageAdapter=f(t,b.storeExpiryMs),this.setValueBounded=o=>{this.setValue(o)},s.setNoNotify(this.pubSubKey,e);let i=t?.lazyLoad;(i!==void 0?i:b.lazyLoad)||this.initDefaultValueOnce()}pubSubKey;storageAdapter;setValueBounded;initDone=!1;subscribe(e){return s.subscribe(this.pubSubKey,()=>e())}getSnapshot(){return s.get(this.pubSubKey)}setValue(e){if(typeof e=="function"){let t=s.get(this.pubSubKey);e=e(t)}s.set(this.pubSubKey,e),this.storageAdapter?.save(c,e)}delete(){s.set(this.pubSubKey,this.defaultValue),this.storageAdapter?.save(c,this.defaultValue)}async initDefaultValueOnce(){if(this.initDone)return;this.initDone=!0;let e=await this.storageAdapter?.load(c);s.set(this.pubSubKey,e!==void 0?e:this.defaultValue)}};function V(r,e){return new y(r,e)}function h(r){return r.initDefaultValueOnce(),[T(t=>r.subscribe(t),()=>r.getSnapshot(),()=>r.getSnapshot()),r.setValueBounded]}function M(r){return h(r)[0]}function B(r){return h(r)[1]}export{y as SharedState,b as SharedStateDefaults,V as sharedState,h as useSharedState,B as useSharedStateSetter,M as useSharedStateValue};
1
+ import{MS_PER_DAY as g}from"@choksheak/ts-utils/timeConstants";import{useSyncExternalStore as T}from"react";import{kvStore as u}from"@choksheak/ts-utils/kvStore";import{LocalStore as b}from"@choksheak/ts-utils/localStore";function y(r,e){let{store:t}=r??{};if(!t)return null;if("persistTo"in t&&typeof t.persistTo=="string"&&"key"in t&&typeof t.key=="string"){let i=t.expiryMs||e;switch(t.persistTo){case"localStorage":{let n=new b(t.key,{defaultExpiryMs:i});return{set:(s,a)=>{typeof window>"u"||n.set(s,a)},get:s=>{if(!(typeof window>"u"))return n.get(s)},delete:s=>{typeof window>"u"||n.delete(s)},clear:()=>{typeof window>"u"||n.clear()}}}case"indexedDb":{let n=t.key+":";return{set:async(s,a)=>{typeof window>"u"||await u.set(n+s,a,i)},get:async s=>{if(!(typeof window>"u"))return await u.get(n+s)},delete:async s=>{typeof window>"u"||await u.delete(n+s)},clear:async()=>{typeof window>"u"||await u.clear()}}}default:throw t.persistTo,new Error(`Unknown store.adapter ${t.persistTo}`)}}return t}var p={storeExpiryMs:30*g,lazyLoad:!1};function V(r){Object.assign(p,r)}var c=class{dataByKey=new Map;subscribersByKey=new Map;get(e){return this.dataByKey.get(e)}set(e,t){let i=this.dataByKey.get(e);i!==t&&(this.dataByKey.set(e,t),this.notify(e,t,i))}delete(e){this.dataByKey.delete(e)}setNoNotify(e,t){this.dataByKey.set(e,t)}subscribe(e,t){let i=this.subscribersByKey.get(e);return i||(i=new Set,this.subscribersByKey.set(e,i)),i.add(t),()=>{i.delete(t),i.size===0&&this.subscribersByKey.delete(e)}}notify(e,t,i){let n=this.subscribersByKey.get(e);if(!n)return;let s=Array.from(n.values());for(let a of s)try{a(t,i)}catch(l){console.error("Error invoking subscriber:",l)}}},o=new c,d="state",h=0,f=class{constructor(e,t){this.defaultValue=e;this.pubSubKey=String(h++),this.storageAdapter=y(t,p.storeExpiryMs),this.setValueBounded=s=>{this.setValue(s)},o.setNoNotify(this.pubSubKey,e);let i=t?.lazyLoad;(i!==void 0?i:p.lazyLoad)||this.initDefaultValueOnce()}pubSubKey;storageAdapter;setValueBounded;initDone=!1;subscribe(e){return o.subscribe(this.pubSubKey,()=>e())}getSnapshot(){return o.get(this.pubSubKey)}setValue(e){if(typeof e=="function"){let t=o.get(this.pubSubKey);e=e(t)}o.set(this.pubSubKey,e),this.storageAdapter?.set(d,e)}delete(){o.set(this.pubSubKey,this.defaultValue),this.storageAdapter?.set(d,this.defaultValue)}async initDefaultValueOnce(){if(this.initDone)return;this.initDone=!0;let e=await this.storageAdapter?.get(d);o.set(this.pubSubKey,e!==void 0?e:this.defaultValue)}};function k(r,e){return new f(r,e)}function S(r){return r.initDefaultValueOnce(),[T(t=>r.subscribe(t),()=>r.getSnapshot(),()=>r.getSnapshot()),r.setValueBounded]}function E(r){return S(r)[0]}function M(r){return S(r)[1]}export{f as SharedState,p as SharedStateConfig,V as configureSharedState,k as sharedState,S as useSharedState,M as useSharedStateSetter,E as useSharedStateValue};
2
2
  //# sourceMappingURL=sharedState.min.mjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/sharedState.ts","../src/utils/storage.ts"],"sourcesContent":["/**\n * @packageDocumentation\n *\n * The \"shared state\" is a React state that shares the same value across the\n * entire app. It is largely similar to the jotai library, but is much smaller\n * and much easier to work with when used for persisting in local storage or\n * indexed db, with an automatic expiration for persisted values.\n *\n * This library aims to be the shortest, simplest, easiest to use, and most\n * efficient implementation of shared global React states with seamless support\n * for client persistence.\n *\n * Example:\n * ```\n * import { sharedState, useSharedState } from \"@choksheak/react-utils/sharedState\";\n *\n * // Create a new shared state in the top level scope.\n * // The first argument is the default value, which is required.\n * // The second argument is an options object, and is optional (defaults\n * // to storing in memory only without any persistence).\n * const usersState = sharedState([], { indexedDbKey: \"users\" });\n *\n * export const UsersComponent: React.FC = () => {\n * // Get both the value and the setter.\n * const [users, setUsers] = useSharedState(usersState);\n * ...\n * };\n *\n * export const ReadOnlyComponent: React.FC = () => {\n * // Get the value only.\n * const users = useSharedStateValue(usersState);\n * ...\n * };\n *\n * export const WriteOnlyComponent: React.FC = () => {\n * // Get the setter only.\n * const setUsers = useSharedStateSetter(usersState);\n * ...\n * };\n * ```\n */\n\nimport { MS_PER_DAY } from \"@choksheak/ts-utils/timeConstants\";\nimport { Dispatch, SetStateAction, useSyncExternalStore } from \"react\";\n\nimport {\n getStorageAdapter,\n StorageAdapter,\n StorageOptions,\n} from \"./utils/storage\";\n\n/************************************************************************/\n/* Global config */\n/************************************************************************/\n\n/**\n * These values are configurable by the user at any time.\n */\nexport const SharedStateDefaults = {\n /**\n * Default duration for storage expiration in milliseconds. Initial value\n * is 30 days, but can be changed to anything.\n */\n storeExpiryMs: 30 * MS_PER_DAY,\n\n /**\n * True to load from storage only on mount, false (default) to load in\n * the top level scope when the shared state is defined.\n */\n lazyLoad: false,\n};\n\n/************************************************************************/\n/* Shared state implementation */\n/************************************************************************/\n\ntype Subscriber<T> = (next: T, prev: T) => void;\n\nclass PubSubStore {\n private dataByKey = new Map<string, unknown>();\n private subscribersByKey = new Map<string, Set<Subscriber<unknown>>>();\n\n // This could be undefined if T includes undefined, else it should never\n // return undefined because the user has to always specify the default value.\n public get<T>(key: string): T {\n return this.dataByKey.get(key) as T;\n }\n\n public set<T>(key: string, value: T): void {\n const prev = this.dataByKey.get(key) as T;\n\n // Do nothing if no change.\n if (prev === value) return;\n\n this.dataByKey.set(key, value);\n this.notify(key, value, prev);\n }\n\n public delete(key: string): void {\n this.dataByKey.delete(key);\n }\n\n public setNoNotify<T>(key: string, value: T): void {\n this.dataByKey.set(key, value);\n }\n\n // Subscribe to changes of a specific key\n public subscribe<T>(key: string, subscriber: Subscriber<T>): () => void {\n let subscribers = this.subscribersByKey.get(key);\n if (!subscribers) {\n subscribers = new Set();\n this.subscribersByKey.set(key, subscribers);\n }\n\n // Type coercion: we store Subscriber<unknown>\n subscribers.add(subscriber as Subscriber<unknown>);\n\n // Return unsubscribe function\n return () => {\n subscribers.delete(subscriber as Subscriber<unknown>);\n if (subscribers.size === 0) this.subscribersByKey.delete(key);\n };\n }\n\n private notify<T>(key: string, next: T, prev: T): void {\n const subscribers = this.subscribersByKey.get(key);\n if (!subscribers) return;\n\n // Make copy to protect against mutation during iteration.\n const copy = Array.from(subscribers.values());\n for (const subscriber of copy) {\n try {\n subscriber(next, prev);\n } catch (e) {\n console.error(`Error invoking subscriber:`, e);\n }\n }\n }\n}\n\nconst pubSubStore = new PubSubStore();\n\nconst STORAGE_KEY = \"state\";\n\n/** Options to configure a shared state. */\nexport type SharedStateOptions<T> = {\n lazyLoad?: boolean;\n} & StorageOptions<T>;\n\nlet stateKey = 0;\n\n/** Please use sharedState() instead. */\nexport class SharedState<T> {\n private readonly pubSubKey: string;\n private readonly storageAdapter: StorageAdapter<T> | null;\n public readonly setValueBounded: Dispatch<SetStateAction<T>>;\n public initDone = false;\n\n public constructor(\n private readonly defaultValue: T,\n options?: SharedStateOptions<T>,\n ) {\n this.pubSubKey = String(stateKey++);\n\n // Use max of 1 storage adapter per shared state.\n this.storageAdapter = getStorageAdapter(\n options,\n SharedStateDefaults.storeExpiryMs,\n );\n\n // Same as this.setValue, but binding the `this` reference.\n this.setValueBounded = (next: SetStateAction<T>) => {\n this.setValue(next);\n };\n\n // Always set the default value first to avoid returning undefineds if that\n // is not part of T.\n pubSubStore.setNoNotify(this.pubSubKey, defaultValue);\n\n const lazyLoad = options?.lazyLoad;\n const lazy =\n lazyLoad !== undefined ? lazyLoad : SharedStateDefaults.lazyLoad;\n\n if (!lazy) {\n void this.initDefaultValueOnce();\n }\n }\n\n // `subscribe` function required by useSyncExternalStore\n public subscribe(subscriber: () => void): () => void {\n return pubSubStore.subscribe(this.pubSubKey, () => subscriber());\n }\n\n // Get the value of the shared state.\n // `getSnapshot` function — must return same ref if value unchanged\n public getSnapshot(): T {\n return pubSubStore.get(this.pubSubKey);\n }\n\n // Set the value of the shared state.\n public setValue(next: SetStateAction<T>): void {\n if (typeof next === \"function\") {\n const prev = pubSubStore.get<T>(this.pubSubKey);\n next = (next as (p: T) => T)(prev);\n }\n\n pubSubStore.set(this.pubSubKey, next);\n void this.storageAdapter?.save(STORAGE_KEY, next);\n }\n\n // Remove the value of the shared state. Actually this just sets the value\n // back to the given default value.\n public delete() {\n pubSubStore.set(this.pubSubKey, this.defaultValue);\n void this.storageAdapter?.save(STORAGE_KEY, this.defaultValue);\n }\n\n // Initial default handling\n public async initDefaultValueOnce() {\n if (this.initDone) return;\n this.initDone = true;\n\n const v = await this.storageAdapter?.load(STORAGE_KEY);\n pubSubStore.set(this.pubSubKey, v !== undefined ? v : this.defaultValue);\n }\n}\n\n/**\n * Create a new shared state object. Put this code at the top level scope:\n *\n * Example:\n * ```\n * const counterState = sharedState<number>(0);\n * ```\n */\nexport function sharedState<T>(\n defaultValue: T,\n options?: SharedStateOptions<T>,\n): SharedState<T> {\n return new SharedState(defaultValue, options);\n}\n\n/**\n * React hook for subscribing to a key in the global store.\n *\n * Example:\n * ```\n * const [counter, setCounter] = useSharedState(counterState);\n * ```\n */\nexport function useSharedState<T>(\n state: SharedState<T>,\n): [T, Dispatch<SetStateAction<T>>] {\n state.initDefaultValueOnce();\n\n const value = useSyncExternalStore(\n (onStoreChange) => state.subscribe(onStoreChange),\n () => state.getSnapshot(),\n // getServerSnapshot is needed to prevent SSR dev errors in nextjs.\n () => state.getSnapshot(),\n );\n\n return [value, state.setValueBounded];\n}\n\n/**\n * Same as useSharedState(), but returns the value only.\n *\n * Example:\n * ```\n * const counter = useSharedStateValue(counterState);\n * ```\n */\nexport function useSharedStateValue<T>(state: SharedState<T>) {\n return useSharedState(state)[0];\n}\n\n/**\n * Same as useSharedState(), but returns the setter only.\n *\n * Example:\n * ```\n * const setCounter = useSharedStateSetter(counterState);\n * ```\n */\nexport function useSharedStateSetter<T>(state: SharedState<T>) {\n return useSharedState(state)[1];\n}\n","import { kvStore } from \"@choksheak/ts-utils/kvStore\";\n\nexport type StorageAdapter<T> = {\n save: (key: string, value: T) => Promise<void> | void;\n load: (key: string) => Promise<T | undefined> | T | undefined;\n delete: (key: string) => Promise<void> | void;\n clear: () => Promise<void> | void;\n};\n\nexport type StorageOptions<T> = {\n // If specified, this will be used and all other fields will be ignored.\n store?: StorageAdapter<T>;\n\n // Storage key in local storage for this data.\n localStorageKey?: string;\n\n // Storage key in indexed db for this data.\n indexedDbKey?: string;\n\n // How long to wait before the value is considered expired.\n storeExpiryMs?: number;\n};\n\nexport function getStorageAdapter<T>(\n options: StorageOptions<T> | undefined,\n defaultStoreExpiryMs: number,\n): StorageAdapter<T> | null {\n const { localStorageKey, indexedDbKey, storeExpiryMs, store } = options ?? {};\n\n if (store) {\n return store;\n }\n\n const expiryMs = storeExpiryMs || defaultStoreExpiryMs;\n\n if (localStorageKey) {\n return new LocalStorageAdapter<T>(localStorageKey, expiryMs);\n }\n\n if (indexedDbKey) {\n return new IndexedDbAdapter<T>(indexedDbKey, expiryMs);\n }\n\n return null;\n}\n\n/************************************************************************/\n/* Storage adapters */\n/************************************************************************/\n\ntype ValueWithExpiration<T> = { value: T; expiresAt: number };\n\n/** Save state to memory only - not persisted. */\nexport class InMemoryStorageAdapter<T> implements StorageAdapter<T> {\n private readonly cache = new Map<string, ValueWithExpiration<T>>();\n\n public constructor(\n public readonly keyPrefix: string,\n public readonly expiryMs: number,\n ) {}\n\n public save(key: string, value: T): void {\n const fullKey = this.keyPrefix + \":\" + key;\n this.cache.set(fullKey, {\n value,\n expiresAt: Date.now() + this.expiryMs,\n });\n }\n\n public load(key: string): T | undefined {\n const fullKey = this.keyPrefix + \":\" + key;\n const cached = this.cache.get(fullKey);\n\n if (cached && Date.now() < cached.expiresAt) {\n return cached.value;\n }\n\n return undefined;\n }\n\n public delete(key: string): void {\n const fullKey = this.keyPrefix + \":\" + key;\n this.cache.delete(fullKey);\n }\n\n public clear(): void {\n this.cache.clear();\n }\n}\n\n/** Save state to local storage. */\nexport class LocalStorageAdapter<T> implements StorageAdapter<T> {\n public constructor(\n public readonly keyPrefix: string,\n public readonly expiryMs: number,\n ) {}\n\n public save(key: string, value: T): void {\n if (typeof window === \"undefined\") return;\n\n const fullKey = this.keyPrefix + \":\" + key;\n window.localStorage.setItem(\n fullKey,\n JSON.stringify({\n value,\n expiresAt: this.expiryMs ? Date.now() + this.expiryMs : 0,\n } satisfies ValueWithExpiration<T>),\n );\n }\n\n public load(key: string): T | undefined {\n if (typeof window === \"undefined\") return undefined;\n\n const fullKey = this.keyPrefix + \":\" + key;\n const raw = window.localStorage.getItem(fullKey);\n if (!raw) return undefined;\n\n try {\n const box = JSON.parse(raw) as ValueWithExpiration<T>;\n if (!box || (box.expiresAt && Date.now() >= box.expiresAt)) {\n window.localStorage.removeItem(fullKey);\n return undefined;\n }\n\n return box.value;\n } catch {\n // Ignore error.\n window.localStorage.removeItem(fullKey);\n return undefined;\n }\n }\n\n public delete(key: string): void {\n if (typeof window === \"undefined\") return;\n\n const fullKey = this.keyPrefix + \":\" + key;\n window.localStorage.removeItem(fullKey);\n }\n\n public clear(): void {\n if (typeof window === \"undefined\") return;\n\n for (const key of Object.keys(window.localStorage)) {\n if (key.startsWith(this.keyPrefix)) {\n window.localStorage.removeItem(key);\n }\n }\n }\n}\n\n/** Save state to indexed db. */\nexport class IndexedDbAdapter<T> implements StorageAdapter<T> {\n public constructor(\n public readonly keyPrefix: string,\n public readonly expiryMs: number,\n ) {}\n\n public async save(key: string, value: T): Promise<void> {\n if (typeof window === \"undefined\") return;\n\n const fullKey = this.keyPrefix + \":\" + key;\n await kvStore.set(fullKey, JSON.stringify(value), this.expiryMs);\n }\n\n public async load(key: string): Promise<T | undefined> {\n if (typeof window === \"undefined\") return undefined;\n\n const fullKey = this.keyPrefix + \":\" + key;\n const raw = await kvStore.get<string>(fullKey);\n if (raw === undefined) return undefined;\n\n try {\n return JSON.parse(raw);\n } catch {\n // Ignore error.\n await kvStore.delete(fullKey);\n return undefined;\n }\n }\n\n public async delete(key: string): Promise<void> {\n if (typeof window === \"undefined\") return;\n\n const fullKey = this.keyPrefix + \":\" + key;\n await kvStore.delete(fullKey);\n }\n\n public async clear(): Promise<void> {\n if (typeof window === \"undefined\") return;\n\n await kvStore.forEach(async (k) => {\n if (k.startsWith(this.keyPrefix)) {\n await kvStore.delete(k);\n }\n });\n }\n}\n"],"mappings":"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","names":["MS_PER_DAY","useSyncExternalStore","kvStore","getStorageAdapter","options","defaultStoreExpiryMs","localStorageKey","indexedDbKey","storeExpiryMs","store","expiryMs","LocalStorageAdapter","IndexedDbAdapter","LocalStorageAdapter","keyPrefix","expiryMs","key","value","fullKey","raw","box","IndexedDbAdapter","kvStore","k","SharedStateDefaults","MS_PER_DAY","PubSubStore","key","value","prev","subscriber","subscribers","next","copy","e","pubSubStore","STORAGE_KEY","stateKey","SharedState","defaultValue","options","getStorageAdapter","lazyLoad","v","sharedState","useSharedState","state","useSyncExternalStore","onStoreChange","useSharedStateValue","useSharedStateSetter"]}
1
+ {"version":3,"sources":["../src/sharedState.ts","../src/storage.ts"],"sourcesContent":["/**\n * @packageDocumentation\n *\n * The \"shared state\" is a React state that shares the same value across the\n * entire app. It is largely similar to the jotai library, but is much smaller\n * and much easier to work with when used for persisting in local storage or\n * indexed db, with an automatic expiration for persisted values.\n *\n * This library aims to be the shortest, simplest, easiest to use, and most\n * efficient implementation of shared global React states with seamless support\n * for client persistence.\n *\n * Example:\n * ```\n * import { sharedState, useSharedState } from \"@choksheak/react-utils/sharedState\";\n *\n * // Create a new shared state in the top level scope.\n * // The first argument is the default value, which is required.\n * // The second argument is an options object, and is optional (defaults\n * // to storing in memory only without any persistence).\n * const usersState = sharedState([], { indexedDbKey: \"users\" });\n *\n * export const UsersComponent: React.FC = () => {\n * // Get both the value and the setter.\n * const [users, setUsers] = useSharedState(usersState);\n * ...\n * };\n *\n * export const ReadOnlyComponent: React.FC = () => {\n * // Get the value only.\n * const users = useSharedStateValue(usersState);\n * ...\n * };\n *\n * export const WriteOnlyComponent: React.FC = () => {\n * // Get the setter only.\n * const setUsers = useSharedStateSetter(usersState);\n * ...\n * };\n * ```\n */\n\nimport { StorageAdapter } from \"@choksheak/ts-utils/storageAdapter\";\nimport { MS_PER_DAY } from \"@choksheak/ts-utils/timeConstants\";\nimport { Dispatch, SetStateAction, useSyncExternalStore } from \"react\";\n\nimport { getStorageAdapter, StorageOptions } from \"./storage\";\n\n/************************************************************************/\n/* Global config */\n/************************************************************************/\n\n/**\n * These values are configurable by the user at any time.\n */\nexport const SharedStateConfig = {\n /**\n * Default duration for storage expiration in milliseconds. Initial value\n * is 30 days, but can be changed to anything.\n */\n storeExpiryMs: 30 * MS_PER_DAY,\n\n /**\n * True to load from storage only on mount, false (default) to load in\n * the top level scope when the shared state is defined.\n */\n lazyLoad: false,\n};\n\nexport type SharedStateConfig = typeof SharedStateConfig;\n\n/** Convenience function to update global defaults. */\nexport function configureSharedState(config: Partial<SharedStateConfig>) {\n Object.assign(SharedStateConfig, config);\n}\n\n/************************************************************************/\n/* Shared state implementation */\n/************************************************************************/\n\ntype Subscriber<T> = (next: T, prev: T) => void;\n\nclass PubSubStore {\n private dataByKey = new Map<string, unknown>();\n private subscribersByKey = new Map<string, Set<Subscriber<unknown>>>();\n\n // This could be undefined if T includes undefined, else it should never\n // return undefined because the user has to always specify the default value.\n public get<T>(key: string): T {\n return this.dataByKey.get(key) as T;\n }\n\n public set<T>(key: string, value: T): void {\n const prev = this.dataByKey.get(key) as T;\n\n // Do nothing if no change.\n if (prev === value) return;\n\n this.dataByKey.set(key, value);\n this.notify(key, value, prev);\n }\n\n public delete(key: string): void {\n this.dataByKey.delete(key);\n }\n\n public setNoNotify<T>(key: string, value: T): void {\n this.dataByKey.set(key, value);\n }\n\n // Subscribe to changes of a specific key\n public subscribe<T>(key: string, subscriber: Subscriber<T>): () => void {\n let subscribers = this.subscribersByKey.get(key);\n if (!subscribers) {\n subscribers = new Set();\n this.subscribersByKey.set(key, subscribers);\n }\n\n // Type coercion: we store Subscriber<unknown>\n subscribers.add(subscriber as Subscriber<unknown>);\n\n // Return unsubscribe function\n return () => {\n subscribers.delete(subscriber as Subscriber<unknown>);\n if (subscribers.size === 0) this.subscribersByKey.delete(key);\n };\n }\n\n private notify<T>(key: string, next: T, prev: T): void {\n const subscribers = this.subscribersByKey.get(key);\n if (!subscribers) return;\n\n // Make copy to protect against mutation during iteration.\n const copy = Array.from(subscribers.values());\n for (const subscriber of copy) {\n try {\n subscriber(next, prev);\n } catch (e) {\n console.error(`Error invoking subscriber:`, e);\n }\n }\n }\n}\n\nconst pubSubStore = new PubSubStore();\n\nconst STORAGE_KEY = \"state\";\n\n/** Options to configure a shared state. */\nexport type SharedStateOptions<T> = {\n lazyLoad?: boolean;\n} & StorageOptions<T>;\n\nlet stateKey = 0;\n\n/** Please use sharedState() instead. */\nexport class SharedState<T> {\n private readonly pubSubKey: string;\n private readonly storageAdapter: StorageAdapter<T> | null;\n public readonly setValueBounded: Dispatch<SetStateAction<T>>;\n public initDone = false;\n\n public constructor(\n private readonly defaultValue: T,\n options?: SharedStateOptions<T>,\n ) {\n this.pubSubKey = String(stateKey++);\n\n // Use max of 1 storage adapter per shared state.\n this.storageAdapter = getStorageAdapter(\n options,\n SharedStateConfig.storeExpiryMs,\n );\n\n // Same as this.setValue, but binding the `this` reference.\n this.setValueBounded = (next: SetStateAction<T>) => {\n this.setValue(next);\n };\n\n // Always set the default value first to avoid returning undefineds if that\n // is not part of T.\n pubSubStore.setNoNotify(this.pubSubKey, defaultValue);\n\n const lazyLoad = options?.lazyLoad;\n const lazy = lazyLoad !== undefined ? lazyLoad : SharedStateConfig.lazyLoad;\n\n if (!lazy) {\n void this.initDefaultValueOnce();\n }\n }\n\n // `subscribe` function required by useSyncExternalStore\n public subscribe(subscriber: () => void): () => void {\n return pubSubStore.subscribe(this.pubSubKey, () => subscriber());\n }\n\n // Get the value of the shared state.\n // `getSnapshot` function — must return same ref if value unchanged\n public getSnapshot(): T {\n return pubSubStore.get(this.pubSubKey);\n }\n\n // Set the value of the shared state.\n public setValue(next: SetStateAction<T>): void {\n if (typeof next === \"function\") {\n const prev = pubSubStore.get<T>(this.pubSubKey);\n next = (next as (p: T) => T)(prev);\n }\n\n pubSubStore.set(this.pubSubKey, next);\n void this.storageAdapter?.set(STORAGE_KEY, next);\n }\n\n // Remove the value of the shared state. Actually this just sets the value\n // back to the given default value.\n public delete() {\n pubSubStore.set(this.pubSubKey, this.defaultValue);\n void this.storageAdapter?.set(STORAGE_KEY, this.defaultValue);\n }\n\n // Initial default handling\n public async initDefaultValueOnce() {\n if (this.initDone) return;\n this.initDone = true;\n\n const v = await this.storageAdapter?.get(STORAGE_KEY);\n pubSubStore.set(this.pubSubKey, v !== undefined ? v : this.defaultValue);\n }\n}\n\n/**\n * Create a new shared state object. Put this code at the top level scope:\n *\n * Example:\n * ```\n * const counterState = sharedState<number>(0);\n * ```\n */\nexport function sharedState<T>(\n defaultValue: T,\n options?: SharedStateOptions<T>,\n): SharedState<T> {\n return new SharedState(defaultValue, options);\n}\n\n/**\n * React hook for subscribing to a key in the global store.\n *\n * Example:\n * ```\n * const [counter, setCounter] = useSharedState(counterState);\n * ```\n */\nexport function useSharedState<T>(\n state: SharedState<T>,\n): [T, Dispatch<SetStateAction<T>>] {\n state.initDefaultValueOnce();\n\n const value = useSyncExternalStore(\n (onStoreChange) => state.subscribe(onStoreChange),\n () => state.getSnapshot(),\n // getServerSnapshot is needed to prevent SSR dev errors in nextjs.\n () => state.getSnapshot(),\n );\n\n return [value, state.setValueBounded];\n}\n\n/**\n * Same as useSharedState(), but returns the value only.\n *\n * Example:\n * ```\n * const counter = useSharedStateValue(counterState);\n * ```\n */\nexport function useSharedStateValue<T>(state: SharedState<T>) {\n return useSharedState(state)[0];\n}\n\n/**\n * Same as useSharedState(), but returns the setter only.\n *\n * Example:\n * ```\n * const setCounter = useSharedStateSetter(counterState);\n * ```\n */\nexport function useSharedStateSetter<T>(state: SharedState<T>) {\n return useSharedState(state)[1];\n}\n","import { kvStore } from \"@choksheak/ts-utils/kvStore\";\nimport { LocalStore } from \"@choksheak/ts-utils/localStore\";\nimport { StorageAdapter } from \"@choksheak/ts-utils/storageAdapter\";\n\n/** List of all available pre-built persistence options. */\nexport type PersistTo = \"localStorage\" | \"indexedDb\";\n\nexport type StoreOptions = {\n persistTo: PersistTo;\n\n // Storage key to act as a namespace to store the data.\n key: string;\n\n // How long to wait before the value is considered expired.\n expiryMs?: number;\n};\n\nexport type StorageOptions<T> = {\n /**\n * You can either specify the storage adapter directly to use a custom\n * store, or specify the properties to use the pre-built ones.\n */\n store?: StorageAdapter<T> | StoreOptions;\n};\n\nexport function getStorageAdapter<T>(\n options: StorageOptions<T> | undefined,\n defaultStoreExpiryMs: number,\n): StorageAdapter<T> | null {\n const { store } = options ?? {};\n\n if (!store) {\n return null;\n }\n\n if (\n \"persistTo\" in store &&\n typeof store.persistTo === \"string\" &&\n \"key\" in store &&\n typeof store.key === \"string\"\n ) {\n const expiryMs = store.expiryMs || defaultStoreExpiryMs;\n\n switch (store.persistTo) {\n case \"localStorage\": {\n const adapter = new LocalStore(store.key, {\n defaultExpiryMs: expiryMs,\n });\n\n // Handle SSR.\n return {\n set: (key: string, value: T): void => {\n if (typeof window === \"undefined\") return; // handle SSR\n\n // Also apply the custom expiryMs on every item in this store.\n adapter.set(key, value);\n },\n get: (key: string): T | undefined => {\n if (typeof window === \"undefined\") return undefined; // handle SSR\n\n return adapter.get(key);\n },\n delete: (key: string): void => {\n if (typeof window === \"undefined\") return; // handle SSR\n\n adapter.delete(key);\n },\n clear: (): void => {\n if (typeof window === \"undefined\") return; // handle SSR\n\n adapter.clear();\n },\n };\n }\n\n case \"indexedDb\": {\n // Use the key prefix as a unique namespace for this store.\n const keyPrefix = store.key + \":\";\n\n // Prefix all keys using `keyPrefix` so that we can keep all data in\n // the same store in the same DB. If we keep the data in different\n // DB stores, then GC will not be able to clean up abandoned stores.\n return {\n set: async (key: string, value: T): Promise<void> => {\n if (typeof window === \"undefined\") return; // handle SSR\n\n // Also apply the custom expiryMs on every item in this store.\n await kvStore.set(keyPrefix + key, value, expiryMs);\n },\n get: async (key: string): Promise<T | undefined> => {\n if (typeof window === \"undefined\") return undefined; // handle SSR\n\n return await kvStore.get(keyPrefix + key);\n },\n delete: async (key: string): Promise<void> => {\n if (typeof window === \"undefined\") return; // handle SSR\n\n await kvStore.delete(keyPrefix + key);\n },\n clear: async (): Promise<void> => {\n if (typeof window === \"undefined\") return; // handle SSR\n\n await kvStore.clear();\n },\n };\n }\n\n default:\n store.persistTo satisfies never;\n throw new Error(`Unknown store.adapter ${store.persistTo}`);\n }\n }\n\n return store as StorageAdapter<T>;\n}\n"],"mappings":"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","names":["MS_PER_DAY","useSyncExternalStore","kvStore","LocalStore","getStorageAdapter","options","defaultStoreExpiryMs","store","expiryMs","adapter","key","value","keyPrefix","SharedStateConfig","MS_PER_DAY","configureSharedState","config","PubSubStore","key","value","prev","subscriber","subscribers","next","copy","e","pubSubStore","STORAGE_KEY","stateKey","SharedState","defaultValue","options","getStorageAdapter","lazyLoad","v","sharedState","useSharedState","state","useSyncExternalStore","onStoreChange","useSharedStateValue","useSharedStateSetter"]}
package/sharedState.mjs CHANGED
@@ -2,108 +2,71 @@
2
2
  import { MS_PER_DAY } from "@choksheak/ts-utils/timeConstants";
3
3
  import { useSyncExternalStore } from "react";
4
4
 
5
- // src/utils/storage.ts
5
+ // src/storage.ts
6
6
  import { kvStore } from "@choksheak/ts-utils/kvStore";
7
+ import { LocalStore } from "@choksheak/ts-utils/localStore";
7
8
  function getStorageAdapter(options, defaultStoreExpiryMs) {
8
- const { localStorageKey, indexedDbKey, storeExpiryMs, store } = options ?? {};
9
- if (store) {
10
- return store;
11
- }
12
- const expiryMs = storeExpiryMs || defaultStoreExpiryMs;
13
- if (localStorageKey) {
14
- return new LocalStorageAdapter(localStorageKey, expiryMs);
15
- }
16
- if (indexedDbKey) {
17
- return new IndexedDbAdapter(indexedDbKey, expiryMs);
18
- }
19
- return null;
20
- }
21
- var LocalStorageAdapter = class {
22
- constructor(keyPrefix, expiryMs) {
23
- this.keyPrefix = keyPrefix;
24
- this.expiryMs = expiryMs;
25
- }
26
- save(key, value) {
27
- if (typeof window === "undefined") return;
28
- const fullKey = this.keyPrefix + ":" + key;
29
- window.localStorage.setItem(
30
- fullKey,
31
- JSON.stringify({
32
- value,
33
- expiresAt: this.expiryMs ? Date.now() + this.expiryMs : 0
34
- })
35
- );
36
- }
37
- load(key) {
38
- if (typeof window === "undefined") return void 0;
39
- const fullKey = this.keyPrefix + ":" + key;
40
- const raw = window.localStorage.getItem(fullKey);
41
- if (!raw) return void 0;
42
- try {
43
- const box = JSON.parse(raw);
44
- if (!box || box.expiresAt && Date.now() >= box.expiresAt) {
45
- window.localStorage.removeItem(fullKey);
46
- return void 0;
9
+ const { store } = options ?? {};
10
+ if (!store) {
11
+ return null;
12
+ }
13
+ if ("persistTo" in store && typeof store.persistTo === "string" && "key" in store && typeof store.key === "string") {
14
+ const expiryMs = store.expiryMs || defaultStoreExpiryMs;
15
+ switch (store.persistTo) {
16
+ case "localStorage": {
17
+ const adapter = new LocalStore(store.key, {
18
+ defaultExpiryMs: expiryMs
19
+ });
20
+ return {
21
+ set: (key, value) => {
22
+ if (typeof window === "undefined") return;
23
+ adapter.set(key, value);
24
+ },
25
+ get: (key) => {
26
+ if (typeof window === "undefined") return void 0;
27
+ return adapter.get(key);
28
+ },
29
+ delete: (key) => {
30
+ if (typeof window === "undefined") return;
31
+ adapter.delete(key);
32
+ },
33
+ clear: () => {
34
+ if (typeof window === "undefined") return;
35
+ adapter.clear();
36
+ }
37
+ };
47
38
  }
48
- return box.value;
49
- } catch {
50
- window.localStorage.removeItem(fullKey);
51
- return void 0;
52
- }
53
- }
54
- delete(key) {
55
- if (typeof window === "undefined") return;
56
- const fullKey = this.keyPrefix + ":" + key;
57
- window.localStorage.removeItem(fullKey);
58
- }
59
- clear() {
60
- if (typeof window === "undefined") return;
61
- for (const key of Object.keys(window.localStorage)) {
62
- if (key.startsWith(this.keyPrefix)) {
63
- window.localStorage.removeItem(key);
39
+ case "indexedDb": {
40
+ const keyPrefix = store.key + ":";
41
+ return {
42
+ set: async (key, value) => {
43
+ if (typeof window === "undefined") return;
44
+ await kvStore.set(keyPrefix + key, value, expiryMs);
45
+ },
46
+ get: async (key) => {
47
+ if (typeof window === "undefined") return void 0;
48
+ return await kvStore.get(keyPrefix + key);
49
+ },
50
+ delete: async (key) => {
51
+ if (typeof window === "undefined") return;
52
+ await kvStore.delete(keyPrefix + key);
53
+ },
54
+ clear: async () => {
55
+ if (typeof window === "undefined") return;
56
+ await kvStore.clear();
57
+ }
58
+ };
64
59
  }
60
+ default:
61
+ store.persistTo;
62
+ throw new Error(`Unknown store.adapter ${store.persistTo}`);
65
63
  }
66
64
  }
67
- };
68
- var IndexedDbAdapter = class {
69
- constructor(keyPrefix, expiryMs) {
70
- this.keyPrefix = keyPrefix;
71
- this.expiryMs = expiryMs;
72
- }
73
- async save(key, value) {
74
- if (typeof window === "undefined") return;
75
- const fullKey = this.keyPrefix + ":" + key;
76
- await kvStore.set(fullKey, JSON.stringify(value), this.expiryMs);
77
- }
78
- async load(key) {
79
- if (typeof window === "undefined") return void 0;
80
- const fullKey = this.keyPrefix + ":" + key;
81
- const raw = await kvStore.get(fullKey);
82
- if (raw === void 0) return void 0;
83
- try {
84
- return JSON.parse(raw);
85
- } catch {
86
- await kvStore.delete(fullKey);
87
- return void 0;
88
- }
89
- }
90
- async delete(key) {
91
- if (typeof window === "undefined") return;
92
- const fullKey = this.keyPrefix + ":" + key;
93
- await kvStore.delete(fullKey);
94
- }
95
- async clear() {
96
- if (typeof window === "undefined") return;
97
- await kvStore.forEach(async (k) => {
98
- if (k.startsWith(this.keyPrefix)) {
99
- await kvStore.delete(k);
100
- }
101
- });
102
- }
103
- };
65
+ return store;
66
+ }
104
67
 
105
68
  // src/sharedState.ts
106
- var SharedStateDefaults = {
69
+ var SharedStateConfig = {
107
70
  /**
108
71
  * Default duration for storage expiration in milliseconds. Initial value
109
72
  * is 30 days, but can be changed to anything.
@@ -115,6 +78,9 @@ var SharedStateDefaults = {
115
78
  */
116
79
  lazyLoad: false
117
80
  };
81
+ function configureSharedState(config) {
82
+ Object.assign(SharedStateConfig, config);
83
+ }
118
84
  var PubSubStore = class {
119
85
  dataByKey = /* @__PURE__ */ new Map();
120
86
  subscribersByKey = /* @__PURE__ */ new Map();
@@ -170,14 +136,14 @@ var SharedState = class {
170
136
  this.pubSubKey = String(stateKey++);
171
137
  this.storageAdapter = getStorageAdapter(
172
138
  options,
173
- SharedStateDefaults.storeExpiryMs
139
+ SharedStateConfig.storeExpiryMs
174
140
  );
175
141
  this.setValueBounded = (next) => {
176
142
  this.setValue(next);
177
143
  };
178
144
  pubSubStore.setNoNotify(this.pubSubKey, defaultValue);
179
145
  const lazyLoad = options?.lazyLoad;
180
- const lazy = lazyLoad !== void 0 ? lazyLoad : SharedStateDefaults.lazyLoad;
146
+ const lazy = lazyLoad !== void 0 ? lazyLoad : SharedStateConfig.lazyLoad;
181
147
  if (!lazy) {
182
148
  void this.initDefaultValueOnce();
183
149
  }
@@ -202,19 +168,19 @@ var SharedState = class {
202
168
  next = next(prev);
203
169
  }
204
170
  pubSubStore.set(this.pubSubKey, next);
205
- void this.storageAdapter?.save(STORAGE_KEY, next);
171
+ void this.storageAdapter?.set(STORAGE_KEY, next);
206
172
  }
207
173
  // Remove the value of the shared state. Actually this just sets the value
208
174
  // back to the given default value.
209
175
  delete() {
210
176
  pubSubStore.set(this.pubSubKey, this.defaultValue);
211
- void this.storageAdapter?.save(STORAGE_KEY, this.defaultValue);
177
+ void this.storageAdapter?.set(STORAGE_KEY, this.defaultValue);
212
178
  }
213
179
  // Initial default handling
214
180
  async initDefaultValueOnce() {
215
181
  if (this.initDone) return;
216
182
  this.initDone = true;
217
- const v = await this.storageAdapter?.load(STORAGE_KEY);
183
+ const v = await this.storageAdapter?.get(STORAGE_KEY);
218
184
  pubSubStore.set(this.pubSubKey, v !== void 0 ? v : this.defaultValue);
219
185
  }
220
186
  };
@@ -239,7 +205,8 @@ function useSharedStateSetter(state) {
239
205
  }
240
206
  export {
241
207
  SharedState,
242
- SharedStateDefaults,
208
+ SharedStateConfig,
209
+ configureSharedState,
243
210
  sharedState,
244
211
  useSharedState,
245
212
  useSharedStateSetter,
package/storage.cjs ADDED
@@ -0,0 +1,90 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/storage.ts
21
+ var storage_exports = {};
22
+ __export(storage_exports, {
23
+ getStorageAdapter: () => getStorageAdapter
24
+ });
25
+ module.exports = __toCommonJS(storage_exports);
26
+ var import_kvStore = require("@choksheak/ts-utils/kvStore");
27
+ var import_localStore = require("@choksheak/ts-utils/localStore");
28
+ function getStorageAdapter(options, defaultStoreExpiryMs) {
29
+ const { store } = options ?? {};
30
+ if (!store) {
31
+ return null;
32
+ }
33
+ if ("persistTo" in store && typeof store.persistTo === "string" && "key" in store && typeof store.key === "string") {
34
+ const expiryMs = store.expiryMs || defaultStoreExpiryMs;
35
+ switch (store.persistTo) {
36
+ case "localStorage": {
37
+ const adapter = new import_localStore.LocalStore(store.key, {
38
+ defaultExpiryMs: expiryMs
39
+ });
40
+ return {
41
+ set: (key, value) => {
42
+ if (typeof window === "undefined") return;
43
+ adapter.set(key, value);
44
+ },
45
+ get: (key) => {
46
+ if (typeof window === "undefined") return void 0;
47
+ return adapter.get(key);
48
+ },
49
+ delete: (key) => {
50
+ if (typeof window === "undefined") return;
51
+ adapter.delete(key);
52
+ },
53
+ clear: () => {
54
+ if (typeof window === "undefined") return;
55
+ adapter.clear();
56
+ }
57
+ };
58
+ }
59
+ case "indexedDb": {
60
+ const keyPrefix = store.key + ":";
61
+ return {
62
+ set: async (key, value) => {
63
+ if (typeof window === "undefined") return;
64
+ await import_kvStore.kvStore.set(keyPrefix + key, value, expiryMs);
65
+ },
66
+ get: async (key) => {
67
+ if (typeof window === "undefined") return void 0;
68
+ return await import_kvStore.kvStore.get(keyPrefix + key);
69
+ },
70
+ delete: async (key) => {
71
+ if (typeof window === "undefined") return;
72
+ await import_kvStore.kvStore.delete(keyPrefix + key);
73
+ },
74
+ clear: async () => {
75
+ if (typeof window === "undefined") return;
76
+ await import_kvStore.kvStore.clear();
77
+ }
78
+ };
79
+ }
80
+ default:
81
+ store.persistTo;
82
+ throw new Error(`Unknown store.adapter ${store.persistTo}`);
83
+ }
84
+ }
85
+ return store;
86
+ }
87
+ // Annotate the CommonJS export names for ESM import in node:
88
+ 0 && (module.exports = {
89
+ getStorageAdapter
90
+ });
package/storage.d.mts ADDED
@@ -0,0 +1,19 @@
1
+ import { StorageAdapter } from '@choksheak/ts-utils/storageAdapter';
2
+
3
+ /** List of all available pre-built persistence options. */
4
+ type PersistTo = "localStorage" | "indexedDb";
5
+ type StoreOptions = {
6
+ persistTo: PersistTo;
7
+ key: string;
8
+ expiryMs?: number;
9
+ };
10
+ type StorageOptions<T> = {
11
+ /**
12
+ * You can either specify the storage adapter directly to use a custom
13
+ * store, or specify the properties to use the pre-built ones.
14
+ */
15
+ store?: StorageAdapter<T> | StoreOptions;
16
+ };
17
+ declare function getStorageAdapter<T>(options: StorageOptions<T> | undefined, defaultStoreExpiryMs: number): StorageAdapter<T> | null;
18
+
19
+ export { type PersistTo, type StorageOptions, type StoreOptions, getStorageAdapter };
package/storage.d.ts ADDED
@@ -0,0 +1,16 @@
1
+ import { StorageAdapter } from "@choksheak/ts-utils/storageAdapter";
2
+ /** List of all available pre-built persistence options. */
3
+ export type PersistTo = "localStorage" | "indexedDb";
4
+ export type StoreOptions = {
5
+ persistTo: PersistTo;
6
+ key: string;
7
+ expiryMs?: number;
8
+ };
9
+ export type StorageOptions<T> = {
10
+ /**
11
+ * You can either specify the storage adapter directly to use a custom
12
+ * store, or specify the properties to use the pre-built ones.
13
+ */
14
+ store?: StorageAdapter<T> | StoreOptions;
15
+ };
16
+ export declare function getStorageAdapter<T>(options: StorageOptions<T> | undefined, defaultStoreExpiryMs: number): StorageAdapter<T> | null;
@@ -0,0 +1,2 @@
1
+ "use strict";var a=Object.defineProperty;var p=Object.getOwnPropertyDescriptor;var u=Object.getOwnPropertyNames;var y=Object.prototype.hasOwnProperty;var w=(i,r)=>{for(var e in r)a(i,e,{get:r[e],enumerable:!0})},g=(i,r,e,o)=>{if(r&&typeof r=="object"||typeof r=="function")for(let t of u(r))!y.call(i,t)&&t!==e&&a(i,t,{get:()=>r[t],enumerable:!(o=p(r,t))||o.enumerable});return i};var c=i=>g(a({},"__esModule",{value:!0}),i);var T={};w(T,{getStorageAdapter:()=>l});module.exports=c(T);var s=require("@choksheak/ts-utils/kvStore"),f=require("@choksheak/ts-utils/localStore");function l(i,r){let{store:e}=i??{};if(!e)return null;if("persistTo"in e&&typeof e.persistTo=="string"&&"key"in e&&typeof e.key=="string"){let o=e.expiryMs||r;switch(e.persistTo){case"localStorage":{let t=new f.LocalStore(e.key,{defaultExpiryMs:o});return{set:(n,d)=>{typeof window>"u"||t.set(n,d)},get:n=>{if(!(typeof window>"u"))return t.get(n)},delete:n=>{typeof window>"u"||t.delete(n)},clear:()=>{typeof window>"u"||t.clear()}}}case"indexedDb":{let t=e.key+":";return{set:async(n,d)=>{typeof window>"u"||await s.kvStore.set(t+n,d,o)},get:async n=>{if(!(typeof window>"u"))return await s.kvStore.get(t+n)},delete:async n=>{typeof window>"u"||await s.kvStore.delete(t+n)},clear:async()=>{typeof window>"u"||await s.kvStore.clear()}}}default:throw e.persistTo,new Error(`Unknown store.adapter ${e.persistTo}`)}}return e}0&&(module.exports={getStorageAdapter});
2
+ //# sourceMappingURL=storage.min.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/storage.ts"],"sourcesContent":["import { kvStore } from \"@choksheak/ts-utils/kvStore\";\nimport { LocalStore } from \"@choksheak/ts-utils/localStore\";\nimport { StorageAdapter } from \"@choksheak/ts-utils/storageAdapter\";\n\n/** List of all available pre-built persistence options. */\nexport type PersistTo = \"localStorage\" | \"indexedDb\";\n\nexport type StoreOptions = {\n persistTo: PersistTo;\n\n // Storage key to act as a namespace to store the data.\n key: string;\n\n // How long to wait before the value is considered expired.\n expiryMs?: number;\n};\n\nexport type StorageOptions<T> = {\n /**\n * You can either specify the storage adapter directly to use a custom\n * store, or specify the properties to use the pre-built ones.\n */\n store?: StorageAdapter<T> | StoreOptions;\n};\n\nexport function getStorageAdapter<T>(\n options: StorageOptions<T> | undefined,\n defaultStoreExpiryMs: number,\n): StorageAdapter<T> | null {\n const { store } = options ?? {};\n\n if (!store) {\n return null;\n }\n\n if (\n \"persistTo\" in store &&\n typeof store.persistTo === \"string\" &&\n \"key\" in store &&\n typeof store.key === \"string\"\n ) {\n const expiryMs = store.expiryMs || defaultStoreExpiryMs;\n\n switch (store.persistTo) {\n case \"localStorage\": {\n const adapter = new LocalStore(store.key, {\n defaultExpiryMs: expiryMs,\n });\n\n // Handle SSR.\n return {\n set: (key: string, value: T): void => {\n if (typeof window === \"undefined\") return; // handle SSR\n\n // Also apply the custom expiryMs on every item in this store.\n adapter.set(key, value);\n },\n get: (key: string): T | undefined => {\n if (typeof window === \"undefined\") return undefined; // handle SSR\n\n return adapter.get(key);\n },\n delete: (key: string): void => {\n if (typeof window === \"undefined\") return; // handle SSR\n\n adapter.delete(key);\n },\n clear: (): void => {\n if (typeof window === \"undefined\") return; // handle SSR\n\n adapter.clear();\n },\n };\n }\n\n case \"indexedDb\": {\n // Use the key prefix as a unique namespace for this store.\n const keyPrefix = store.key + \":\";\n\n // Prefix all keys using `keyPrefix` so that we can keep all data in\n // the same store in the same DB. If we keep the data in different\n // DB stores, then GC will not be able to clean up abandoned stores.\n return {\n set: async (key: string, value: T): Promise<void> => {\n if (typeof window === \"undefined\") return; // handle SSR\n\n // Also apply the custom expiryMs on every item in this store.\n await kvStore.set(keyPrefix + key, value, expiryMs);\n },\n get: async (key: string): Promise<T | undefined> => {\n if (typeof window === \"undefined\") return undefined; // handle SSR\n\n return await kvStore.get(keyPrefix + key);\n },\n delete: async (key: string): Promise<void> => {\n if (typeof window === \"undefined\") return; // handle SSR\n\n await kvStore.delete(keyPrefix + key);\n },\n clear: async (): Promise<void> => {\n if (typeof window === \"undefined\") return; // handle SSR\n\n await kvStore.clear();\n },\n };\n }\n\n default:\n store.persistTo satisfies never;\n throw new Error(`Unknown store.adapter ${store.persistTo}`);\n }\n }\n\n return store as StorageAdapter<T>;\n}\n"],"mappings":"yaAAA,IAAAA,EAAA,GAAAC,EAAAD,EAAA,uBAAAE,IAAA,eAAAC,EAAAH,GAAA,IAAAI,EAAwB,uCACxBC,EAA2B,0CAwBpB,SAASH,EACdI,EACAC,EAC0B,CAC1B,GAAM,CAAE,MAAAC,CAAM,EAAIF,GAAW,CAAC,EAE9B,GAAI,CAACE,EACH,OAAO,KAGT,GACE,cAAeA,GACf,OAAOA,EAAM,WAAc,UAC3B,QAASA,GACT,OAAOA,EAAM,KAAQ,SACrB,CACA,IAAMC,EAAWD,EAAM,UAAYD,EAEnC,OAAQC,EAAM,UAAW,CACvB,IAAK,eAAgB,CACnB,IAAME,EAAU,IAAI,aAAWF,EAAM,IAAK,CACxC,gBAAiBC,CACnB,CAAC,EAGD,MAAO,CACL,IAAK,CAACE,EAAaC,IAAmB,CAChC,OAAO,OAAW,KAGtBF,EAAQ,IAAIC,EAAKC,CAAK,CACxB,EACA,IAAMD,GAA+B,CACnC,GAAI,SAAO,OAAW,KAEtB,OAAOD,EAAQ,IAAIC,CAAG,CACxB,EACA,OAASA,GAAsB,CACzB,OAAO,OAAW,KAEtBD,EAAQ,OAAOC,CAAG,CACpB,EACA,MAAO,IAAY,CACb,OAAO,OAAW,KAEtBD,EAAQ,MAAM,CAChB,CACF,CACF,CAEA,IAAK,YAAa,CAEhB,IAAMG,EAAYL,EAAM,IAAM,IAK9B,MAAO,CACL,IAAK,MAAOG,EAAaC,IAA4B,CAC/C,OAAO,OAAW,KAGtB,MAAM,UAAQ,IAAIC,EAAYF,EAAKC,EAAOH,CAAQ,CACpD,EACA,IAAK,MAAOE,GAAwC,CAClD,GAAI,SAAO,OAAW,KAEtB,OAAO,MAAM,UAAQ,IAAIE,EAAYF,CAAG,CAC1C,EACA,OAAQ,MAAOA,GAA+B,CACxC,OAAO,OAAW,KAEtB,MAAM,UAAQ,OAAOE,EAAYF,CAAG,CACtC,EACA,MAAO,SAA2B,CAC5B,OAAO,OAAW,KAEtB,MAAM,UAAQ,MAAM,CACtB,CACF,CACF,CAEA,QACE,MAAAH,EAAM,UACA,IAAI,MAAM,yBAAyBA,EAAM,SAAS,EAAE,CAC9D,CACF,CAEA,OAAOA,CACT","names":["storage_exports","__export","getStorageAdapter","__toCommonJS","import_kvStore","import_localStore","options","defaultStoreExpiryMs","store","expiryMs","adapter","key","value","keyPrefix"]}
@@ -0,0 +1,2 @@
1
+ import{kvStore as n}from"@choksheak/ts-utils/kvStore";import{LocalStore as a}from"@choksheak/ts-utils/localStore";function u(s,d){let{store:e}=s??{};if(!e)return null;if("persistTo"in e&&typeof e.persistTo=="string"&&"key"in e&&typeof e.key=="string"){let o=e.expiryMs||d;switch(e.persistTo){case"localStorage":{let r=new a(e.key,{defaultExpiryMs:o});return{set:(t,i)=>{typeof window>"u"||r.set(t,i)},get:t=>{if(!(typeof window>"u"))return r.get(t)},delete:t=>{typeof window>"u"||r.delete(t)},clear:()=>{typeof window>"u"||r.clear()}}}case"indexedDb":{let r=e.key+":";return{set:async(t,i)=>{typeof window>"u"||await n.set(r+t,i,o)},get:async t=>{if(!(typeof window>"u"))return await n.get(r+t)},delete:async t=>{typeof window>"u"||await n.delete(r+t)},clear:async()=>{typeof window>"u"||await n.clear()}}}default:throw e.persistTo,new Error(`Unknown store.adapter ${e.persistTo}`)}}return e}export{u as getStorageAdapter};
2
+ //# sourceMappingURL=storage.min.mjs.map