@choksheak/react-utils 0.3.1 → 0.3.2
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.
- package/package.json +1 -1
- package/sharedQuery.cjs +67 -21
- package/sharedQuery.min.cjs +1 -1
- package/sharedQuery.min.cjs.map +1 -1
- package/sharedQuery.min.mjs +1 -1
- package/sharedQuery.min.mjs.map +1 -1
- package/sharedQuery.mjs +67 -21
- package/sharedState.cjs +67 -21
- package/sharedState.d.mts +28 -0
- package/sharedState.d.ts +28 -0
- package/sharedState.min.cjs +1 -1
- package/sharedState.min.cjs.map +1 -1
- package/sharedState.min.mjs +1 -1
- package/sharedState.min.mjs.map +1 -1
- package/sharedState.mjs +67 -21
- package/storage.cjs +30 -10
- package/storage.d.mts +7 -0
- package/storage.d.ts +7 -0
- package/storage.min.cjs +1 -1
- package/storage.min.cjs.map +1 -1
- package/storage.min.mjs +1 -1
- package/storage.min.mjs.map +1 -1
- package/storage.mjs +30 -10
package/sharedState.d.mts
CHANGED
|
@@ -77,10 +77,38 @@ declare class SharedState<T> {
|
|
|
77
77
|
initStarted: boolean;
|
|
78
78
|
initDone: boolean;
|
|
79
79
|
constructor(defaultValue: T, options?: SharedStateOptions<T>);
|
|
80
|
+
/** `subscribe` function required by useSyncExternalStore. */
|
|
80
81
|
subscribe(subscriber: () => void): () => void;
|
|
82
|
+
/** Returns true if there is a backing store. */
|
|
83
|
+
hasStore(): boolean;
|
|
84
|
+
/**
|
|
85
|
+
* Set a value directly into the store. Does nothing if there is no
|
|
86
|
+
* backing store.
|
|
87
|
+
*
|
|
88
|
+
* Warning: If you want to set the value properly, you should use
|
|
89
|
+
* `setValue()` instead which also updates the in-memory copy.
|
|
90
|
+
*/
|
|
91
|
+
setInStore(value: T): Promise<void>;
|
|
92
|
+
/**
|
|
93
|
+
* Get a value directly from the store. Returns undefined if there is no
|
|
94
|
+
* backing store.
|
|
95
|
+
*/
|
|
96
|
+
getFromStore(): Promise<T | undefined>;
|
|
97
|
+
/**
|
|
98
|
+
* Get the value of the shared state.
|
|
99
|
+
* `getSnapshot` function — must return same ref if value unchanged.
|
|
100
|
+
* Note that this would return the default value if data init from store is
|
|
101
|
+
* not completed yet.
|
|
102
|
+
*/
|
|
81
103
|
getSnapshot(): T;
|
|
104
|
+
/** Set the value of the shared state. */
|
|
82
105
|
setValue(next: SetStateAction<T>): void;
|
|
106
|
+
/**
|
|
107
|
+
* Remove the value of the shared state. Actually this just sets the value
|
|
108
|
+
* back to the given default value.
|
|
109
|
+
*/
|
|
83
110
|
delete(): void;
|
|
111
|
+
/** Initial default handling. */
|
|
84
112
|
initDefaultValueOnce(): Promise<void>;
|
|
85
113
|
}
|
|
86
114
|
/**
|
package/sharedState.d.ts
CHANGED
|
@@ -74,10 +74,38 @@ export declare class SharedState<T> {
|
|
|
74
74
|
initStarted: boolean;
|
|
75
75
|
initDone: boolean;
|
|
76
76
|
constructor(defaultValue: T, options?: SharedStateOptions<T>);
|
|
77
|
+
/** `subscribe` function required by useSyncExternalStore. */
|
|
77
78
|
subscribe(subscriber: () => void): () => void;
|
|
79
|
+
/** Returns true if there is a backing store. */
|
|
80
|
+
hasStore(): boolean;
|
|
81
|
+
/**
|
|
82
|
+
* Set a value directly into the store. Does nothing if there is no
|
|
83
|
+
* backing store.
|
|
84
|
+
*
|
|
85
|
+
* Warning: If you want to set the value properly, you should use
|
|
86
|
+
* `setValue()` instead which also updates the in-memory copy.
|
|
87
|
+
*/
|
|
88
|
+
setInStore(value: T): Promise<void>;
|
|
89
|
+
/**
|
|
90
|
+
* Get a value directly from the store. Returns undefined if there is no
|
|
91
|
+
* backing store.
|
|
92
|
+
*/
|
|
93
|
+
getFromStore(): Promise<T | undefined>;
|
|
94
|
+
/**
|
|
95
|
+
* Get the value of the shared state.
|
|
96
|
+
* `getSnapshot` function — must return same ref if value unchanged.
|
|
97
|
+
* Note that this would return the default value if data init from store is
|
|
98
|
+
* not completed yet.
|
|
99
|
+
*/
|
|
78
100
|
getSnapshot(): T;
|
|
101
|
+
/** Set the value of the shared state. */
|
|
79
102
|
setValue(next: SetStateAction<T>): void;
|
|
103
|
+
/**
|
|
104
|
+
* Remove the value of the shared state. Actually this just sets the value
|
|
105
|
+
* back to the given default value.
|
|
106
|
+
*/
|
|
80
107
|
delete(): void;
|
|
108
|
+
/** Initial default handling. */
|
|
81
109
|
initDefaultValueOnce(): Promise<void>;
|
|
82
110
|
}
|
|
83
111
|
/**
|
package/sharedState.min.cjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
"use strict";var
|
|
1
|
+
"use strict";var b=Object.defineProperty;var A=Object.getOwnPropertyDescriptor;var K=Object.getOwnPropertyNames;var x=Object.prototype.hasOwnProperty;var O=(r,e)=>{for(var t in e)b(r,t,{get:e[t],enumerable:!0})},V=(r,e,t,i)=>{if(e&&typeof e=="object"||typeof e=="function")for(let n of K(e))!x.call(r,n)&&n!==t&&b(r,n,{get:()=>e[n],enumerable:!(i=A(e,n))||i.enumerable});return r};var B=r=>V(b({},"__esModule",{value:!0}),r);var k={};O(k,{SharedState:()=>S,SharedStateConfig:()=>l,configureSharedState:()=>D,sharedState:()=>P,useSharedState:()=>T,useSharedStateSetter:()=>z,useSharedStateValue:()=>M});module.exports=B(k);var w=require("@choksheak/ts-utils/timeConstants"),m=require("react");var p=require("@choksheak/ts-utils/kvStore"),h=require("@choksheak/ts-utils/localStore"),u=typeof window>"u";function v(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,n=t.isValid;switch(t.persistTo){case"localStorage":{let a=new h.LocalStore(t.key,{defaultExpiryMs:i});return{set:(o,s)=>{u||a.set(o,s)},get:o=>{if(u)return;let s=a.get(o);if(n&&!n(s)){console.warn(`localStorage adapter: Auto-discard invalid value for ${o}: ${JSON.stringify(s)}`),a.delete(o);return}return s},delete:o=>{u||a.delete(o)},clear:()=>{u||a.clear()}}}case"indexedDb":{let a=t.key+":",o=t.isValid;return{set:async(s,c)=>{u||await p.kvStore.set(a+s,c,i)},get:async s=>{if(u)return;let c=a+s,f=await p.kvStore.get(c);if(o&&!o(f)){console.warn(`indexedDb adapter: Auto-discard invalid value for ${s}: ${JSON.stringify(f)}`),await p.kvStore.delete(c);return}return f},delete:async s=>{u||await p.kvStore.delete(a+s)},clear:async()=>{u||await p.kvStore.clear()}}}default:throw t.persistTo,new Error(`Unknown store.adapter ${t.persistTo}`)}}return t}var l={storeExpiryMs:30*w.MS_PER_DAY,lazyLoad:!1};function D(r){Object.assign(l,r)}var g=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 o of a)try{o(t,i)}catch(s){console.error("Error invoking subscriber:",s)}}},d=new g,y="state",E=0,S=class{constructor(e,t){this.defaultValue=e;this.pubSubKey=String(E++),this.storageAdapter=v(t,l.storeExpiryMs),this.setValueBounded=a=>{this.setValue(a)},d.setNoNotify(this.pubSubKey,e);let i=t?.lazyLoad;(i!==void 0?i:l.lazyLoad)||this.initDefaultValueOnce()}pubSubKey;storageAdapter;setValueBounded;initStarted=!1;initDone=!1;subscribe(e){return d.subscribe(this.pubSubKey,()=>e())}hasStore(){return!!this.storageAdapter}async setInStore(e){return await this.storageAdapter?.set(y,e)}async getFromStore(){return await this.storageAdapter?.get(y)}getSnapshot(){return d.get(this.pubSubKey)}setValue(e){if(typeof e=="function"){let t=d.get(this.pubSubKey);e=e(t)}d.set(this.pubSubKey,e),this.setInStore(e)}delete(){d.set(this.pubSubKey,this.defaultValue),this.storageAdapter?.set(y,this.defaultValue)}async initDefaultValueOnce(){if(this.initDone||this.initStarted)return;this.initStarted=!0;let e=await this.getFromStore(),t=e!==void 0?e:this.defaultValue;d.set(this.pubSubKey,t),this.initDone=!0}};function P(r,e){return new S(r,e)}function T(r){return r.initDefaultValueOnce(),[(0,m.useSyncExternalStore)(t=>r.subscribe(t),()=>r.getSnapshot(),()=>r.getSnapshot()),r.setValueBounded]}function M(r){return T(r)[0]}function z(r){return T(r)[1]}0&&(module.exports={SharedState,SharedStateConfig,configureSharedState,sharedState,useSharedState,useSharedStateSetter,useSharedStateValue});
|
|
2
2
|
//# sourceMappingURL=sharedState.min.cjs.map
|
package/sharedState.min.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
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 initStarted = false;\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 || this.initStarted) return;\n this.initStarted = true;\n\n const v = await this.storageAdapter?.get(STORAGE_KEY);\n const value = v !== undefined ? v : this.defaultValue;\n pubSubStore.set(this.pubSubKey, value);\n this.initDone = true;\n\n // Make sure React rerenders with the new initDone=true set.\n pubSubStore.set(this.pubSubKey, value);\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
|
+
{"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 initStarted = false;\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 /** Returns true if there is a backing store. */\n public hasStore(): boolean {\n return Boolean(this.storageAdapter);\n }\n\n /**\n * Set a value directly into the store. Does nothing if there is no\n * backing store.\n *\n * Warning: If you want to set the value properly, you should use\n * `setValue()` instead which also updates the in-memory copy.\n */\n public async setInStore(value: T): Promise<void> {\n return await this.storageAdapter?.set(STORAGE_KEY, value);\n }\n\n /**\n * Get a value directly from the store. Returns undefined if there is no\n * backing store.\n */\n public async getFromStore(): Promise<T | undefined> {\n return await this.storageAdapter?.get(STORAGE_KEY);\n }\n\n /**\n * Get the value of the shared state.\n * `getSnapshot` function — must return same ref if value unchanged.\n * Note that this would return the default value if data init from store is\n * not completed yet.\n */\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.setInStore(next);\n }\n\n /**\n * Remove the value of the shared state. Actually this just sets the value\n * back to the given default value.\n */\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 || this.initStarted) return;\n this.initStarted = true;\n\n const stored = await this.getFromStore();\n const value = stored !== undefined ? stored : this.defaultValue;\n\n pubSubStore.set(this.pubSubKey, value);\n\n this.initDone = true;\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\n/** Options to configure a pre-built store adapter. */\nexport type StoreOptions = {\n /** Choose which pre-built store adapter to use. */\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 /** Returns true if the given value supposed to be of type T is valid. */\n isValid?: (u: unknown) => boolean;\n};\n\n/** Either use a pre-built store, or a custom one you provide. */\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\n// In SSR (Server-Side Rendering) mode, we never want to use our storage\n// adapters for anything.\nconst IS_NOT_BROWSER = typeof window === \"undefined\";\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 const isValid = store.isValid;\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 (IS_NOT_BROWSER) 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 (IS_NOT_BROWSER) return undefined; // handle SSR\n\n const value = adapter.get<T>(key);\n\n if (isValid && !isValid(value)) {\n console.warn(\n `localStorage adapter: Auto-discard invalid value for ${key}: ${JSON.stringify(value)}`,\n );\n adapter.delete(key);\n return undefined;\n }\n\n return value;\n },\n delete: (key: string): void => {\n if (IS_NOT_BROWSER) return; // handle SSR\n\n adapter.delete(key);\n },\n clear: (): void => {\n if (IS_NOT_BROWSER) 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 const isValid = store.isValid;\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 (IS_NOT_BROWSER) 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 (IS_NOT_BROWSER) return undefined; // handle SSR\n\n const fullKey = keyPrefix + key;\n const value = await kvStore.get<T>(fullKey);\n\n if (isValid && !isValid(value)) {\n console.warn(\n `indexedDb adapter: Auto-discard invalid value for ${key}: ${JSON.stringify(value)}`,\n );\n await kvStore.delete(fullKey);\n return undefined;\n }\n\n return value;\n },\n delete: async (key: string): Promise<void> => {\n if (IS_NOT_BROWSER) return; // handle SSR\n\n await kvStore.delete(keyPrefix + key);\n },\n clear: async (): Promise<void> => {\n if (IS_NOT_BROWSER) 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","IS_NOT_BROWSER","getStorageAdapter","options","defaultStoreExpiryMs","store","expiryMs","isValid","adapter","key","value","keyPrefix","fullKey","SharedStateConfig","configureSharedState","config","PubSubStore","key","value","prev","subscriber","subscribers","next","copy","e","pubSubStore","STORAGE_KEY","stateKey","SharedState","defaultValue","options","getStorageAdapter","lazyLoad","stored","sharedState","useSharedState","state","onStoreChange","useSharedStateValue","useSharedStateSetter"]}
|
package/sharedState.min.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{MS_PER_DAY as
|
|
1
|
+
import{MS_PER_DAY as v}from"@choksheak/ts-utils/timeConstants";import{useSyncExternalStore as w}from"react";import{kvStore as p}from"@choksheak/ts-utils/kvStore";import{LocalStore as h}from"@choksheak/ts-utils/localStore";var o=typeof window>"u";function g(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,d=t.isValid;switch(t.persistTo){case"localStorage":{let a=new h(t.key,{defaultExpiryMs:i});return{set:(n,s)=>{o||a.set(n,s)},get:n=>{if(o)return;let s=a.get(n);if(d&&!d(s)){console.warn(`localStorage adapter: Auto-discard invalid value for ${n}: ${JSON.stringify(s)}`),a.delete(n);return}return s},delete:n=>{o||a.delete(n)},clear:()=>{o||a.clear()}}}case"indexedDb":{let a=t.key+":",n=t.isValid;return{set:async(s,c)=>{o||await p.set(a+s,c,i)},get:async s=>{if(o)return;let c=a+s,l=await p.get(c);if(n&&!n(l)){console.warn(`indexedDb adapter: Auto-discard invalid value for ${s}: ${JSON.stringify(l)}`),await p.delete(c);return}return l},delete:async s=>{o||await p.delete(a+s)},clear:async()=>{o||await p.clear()}}}default:throw t.persistTo,new Error(`Unknown store.adapter ${t.persistTo}`)}}return t}var f={storeExpiryMs:30*v,lazyLoad:!1};function M(r){Object.assign(f,r)}var 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 d=this.subscribersByKey.get(e);if(!d)return;let a=Array.from(d.values());for(let n of a)try{n(t,i)}catch(s){console.error("Error invoking subscriber:",s)}}},u=new b,S="state",m=0,y=class{constructor(e,t){this.defaultValue=e;this.pubSubKey=String(m++),this.storageAdapter=g(t,f.storeExpiryMs),this.setValueBounded=a=>{this.setValue(a)},u.setNoNotify(this.pubSubKey,e);let i=t?.lazyLoad;(i!==void 0?i:f.lazyLoad)||this.initDefaultValueOnce()}pubSubKey;storageAdapter;setValueBounded;initStarted=!1;initDone=!1;subscribe(e){return u.subscribe(this.pubSubKey,()=>e())}hasStore(){return!!this.storageAdapter}async setInStore(e){return await this.storageAdapter?.set(S,e)}async getFromStore(){return await this.storageAdapter?.get(S)}getSnapshot(){return u.get(this.pubSubKey)}setValue(e){if(typeof e=="function"){let t=u.get(this.pubSubKey);e=e(t)}u.set(this.pubSubKey,e),this.setInStore(e)}delete(){u.set(this.pubSubKey,this.defaultValue),this.storageAdapter?.set(S,this.defaultValue)}async initDefaultValueOnce(){if(this.initDone||this.initStarted)return;this.initStarted=!0;let e=await this.getFromStore(),t=e!==void 0?e:this.defaultValue;u.set(this.pubSubKey,t),this.initDone=!0}};function z(r,e){return new y(r,e)}function T(r){return r.initDefaultValueOnce(),[w(t=>r.subscribe(t),()=>r.getSnapshot(),()=>r.getSnapshot()),r.setValueBounded]}function k(r){return T(r)[0]}function N(r){return T(r)[1]}export{y as SharedState,f as SharedStateConfig,M as configureSharedState,z as sharedState,T as useSharedState,N as useSharedStateSetter,k as useSharedStateValue};
|
|
2
2
|
//# sourceMappingURL=sharedState.min.mjs.map
|
package/sharedState.min.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
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 initStarted = false;\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 || this.initStarted) return;\n this.initStarted = true;\n\n const v = await this.storageAdapter?.get(STORAGE_KEY);\n const value = v !== undefined ? v : this.defaultValue;\n pubSubStore.set(this.pubSubKey, value);\n this.initDone = true;\n\n // Make sure React rerenders with the new initDone=true set.\n pubSubStore.set(this.pubSubKey, value);\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"]}
|
|
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 initStarted = false;\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 /** Returns true if there is a backing store. */\n public hasStore(): boolean {\n return Boolean(this.storageAdapter);\n }\n\n /**\n * Set a value directly into the store. Does nothing if there is no\n * backing store.\n *\n * Warning: If you want to set the value properly, you should use\n * `setValue()` instead which also updates the in-memory copy.\n */\n public async setInStore(value: T): Promise<void> {\n return await this.storageAdapter?.set(STORAGE_KEY, value);\n }\n\n /**\n * Get a value directly from the store. Returns undefined if there is no\n * backing store.\n */\n public async getFromStore(): Promise<T | undefined> {\n return await this.storageAdapter?.get(STORAGE_KEY);\n }\n\n /**\n * Get the value of the shared state.\n * `getSnapshot` function — must return same ref if value unchanged.\n * Note that this would return the default value if data init from store is\n * not completed yet.\n */\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.setInStore(next);\n }\n\n /**\n * Remove the value of the shared state. Actually this just sets the value\n * back to the given default value.\n */\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 || this.initStarted) return;\n this.initStarted = true;\n\n const stored = await this.getFromStore();\n const value = stored !== undefined ? stored : this.defaultValue;\n\n pubSubStore.set(this.pubSubKey, value);\n\n this.initDone = true;\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\n/** Options to configure a pre-built store adapter. */\nexport type StoreOptions = {\n /** Choose which pre-built store adapter to use. */\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 /** Returns true if the given value supposed to be of type T is valid. */\n isValid?: (u: unknown) => boolean;\n};\n\n/** Either use a pre-built store, or a custom one you provide. */\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\n// In SSR (Server-Side Rendering) mode, we never want to use our storage\n// adapters for anything.\nconst IS_NOT_BROWSER = typeof window === \"undefined\";\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 const isValid = store.isValid;\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 (IS_NOT_BROWSER) 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 (IS_NOT_BROWSER) return undefined; // handle SSR\n\n const value = adapter.get<T>(key);\n\n if (isValid && !isValid(value)) {\n console.warn(\n `localStorage adapter: Auto-discard invalid value for ${key}: ${JSON.stringify(value)}`,\n );\n adapter.delete(key);\n return undefined;\n }\n\n return value;\n },\n delete: (key: string): void => {\n if (IS_NOT_BROWSER) return; // handle SSR\n\n adapter.delete(key);\n },\n clear: (): void => {\n if (IS_NOT_BROWSER) 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 const isValid = store.isValid;\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 (IS_NOT_BROWSER) 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 (IS_NOT_BROWSER) return undefined; // handle SSR\n\n const fullKey = keyPrefix + key;\n const value = await kvStore.get<T>(fullKey);\n\n if (isValid && !isValid(value)) {\n console.warn(\n `indexedDb adapter: Auto-discard invalid value for ${key}: ${JSON.stringify(value)}`,\n );\n await kvStore.delete(fullKey);\n return undefined;\n }\n\n return value;\n },\n delete: async (key: string): Promise<void> => {\n if (IS_NOT_BROWSER) return; // handle SSR\n\n await kvStore.delete(keyPrefix + key);\n },\n clear: async (): Promise<void> => {\n if (IS_NOT_BROWSER) 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","IS_NOT_BROWSER","getStorageAdapter","options","defaultStoreExpiryMs","store","expiryMs","isValid","adapter","key","value","keyPrefix","fullKey","SharedStateConfig","MS_PER_DAY","configureSharedState","config","PubSubStore","key","value","prev","subscriber","subscribers","next","copy","e","pubSubStore","STORAGE_KEY","stateKey","SharedState","defaultValue","options","getStorageAdapter","lazyLoad","stored","sharedState","useSharedState","state","useSyncExternalStore","onStoreChange","useSharedStateValue","useSharedStateSetter"]}
|
package/sharedState.mjs
CHANGED
|
@@ -5,6 +5,7 @@ import { useSyncExternalStore } from "react";
|
|
|
5
5
|
// src/storage.ts
|
|
6
6
|
import { kvStore } from "@choksheak/ts-utils/kvStore";
|
|
7
7
|
import { LocalStore } from "@choksheak/ts-utils/localStore";
|
|
8
|
+
var IS_NOT_BROWSER = typeof window === "undefined";
|
|
8
9
|
function getStorageAdapter(options, defaultStoreExpiryMs) {
|
|
9
10
|
const { store } = options ?? {};
|
|
10
11
|
if (!store) {
|
|
@@ -12,6 +13,7 @@ function getStorageAdapter(options, defaultStoreExpiryMs) {
|
|
|
12
13
|
}
|
|
13
14
|
if ("persistTo" in store && typeof store.persistTo === "string" && "key" in store && typeof store.key === "string") {
|
|
14
15
|
const expiryMs = store.expiryMs || defaultStoreExpiryMs;
|
|
16
|
+
const isValid = store.isValid;
|
|
15
17
|
switch (store.persistTo) {
|
|
16
18
|
case "localStorage": {
|
|
17
19
|
const adapter = new LocalStore(store.key, {
|
|
@@ -19,40 +21,58 @@ function getStorageAdapter(options, defaultStoreExpiryMs) {
|
|
|
19
21
|
});
|
|
20
22
|
return {
|
|
21
23
|
set: (key, value) => {
|
|
22
|
-
if (
|
|
24
|
+
if (IS_NOT_BROWSER) return;
|
|
23
25
|
adapter.set(key, value);
|
|
24
26
|
},
|
|
25
27
|
get: (key) => {
|
|
26
|
-
if (
|
|
27
|
-
|
|
28
|
+
if (IS_NOT_BROWSER) return void 0;
|
|
29
|
+
const value = adapter.get(key);
|
|
30
|
+
if (isValid && !isValid(value)) {
|
|
31
|
+
console.warn(
|
|
32
|
+
`localStorage adapter: Auto-discard invalid value for ${key}: ${JSON.stringify(value)}`
|
|
33
|
+
);
|
|
34
|
+
adapter.delete(key);
|
|
35
|
+
return void 0;
|
|
36
|
+
}
|
|
37
|
+
return value;
|
|
28
38
|
},
|
|
29
39
|
delete: (key) => {
|
|
30
|
-
if (
|
|
40
|
+
if (IS_NOT_BROWSER) return;
|
|
31
41
|
adapter.delete(key);
|
|
32
42
|
},
|
|
33
43
|
clear: () => {
|
|
34
|
-
if (
|
|
44
|
+
if (IS_NOT_BROWSER) return;
|
|
35
45
|
adapter.clear();
|
|
36
46
|
}
|
|
37
47
|
};
|
|
38
48
|
}
|
|
39
49
|
case "indexedDb": {
|
|
40
50
|
const keyPrefix = store.key + ":";
|
|
51
|
+
const isValid2 = store.isValid;
|
|
41
52
|
return {
|
|
42
53
|
set: async (key, value) => {
|
|
43
|
-
if (
|
|
54
|
+
if (IS_NOT_BROWSER) return;
|
|
44
55
|
await kvStore.set(keyPrefix + key, value, expiryMs);
|
|
45
56
|
},
|
|
46
57
|
get: async (key) => {
|
|
47
|
-
if (
|
|
48
|
-
|
|
58
|
+
if (IS_NOT_BROWSER) return void 0;
|
|
59
|
+
const fullKey = keyPrefix + key;
|
|
60
|
+
const value = await kvStore.get(fullKey);
|
|
61
|
+
if (isValid2 && !isValid2(value)) {
|
|
62
|
+
console.warn(
|
|
63
|
+
`indexedDb adapter: Auto-discard invalid value for ${key}: ${JSON.stringify(value)}`
|
|
64
|
+
);
|
|
65
|
+
await kvStore.delete(fullKey);
|
|
66
|
+
return void 0;
|
|
67
|
+
}
|
|
68
|
+
return value;
|
|
49
69
|
},
|
|
50
70
|
delete: async (key) => {
|
|
51
|
-
if (
|
|
71
|
+
if (IS_NOT_BROWSER) return;
|
|
52
72
|
await kvStore.delete(keyPrefix + key);
|
|
53
73
|
},
|
|
54
74
|
clear: async () => {
|
|
55
|
-
if (
|
|
75
|
+
if (IS_NOT_BROWSER) return;
|
|
56
76
|
await kvStore.clear();
|
|
57
77
|
}
|
|
58
78
|
};
|
|
@@ -153,39 +173,65 @@ var SharedState = class {
|
|
|
153
173
|
setValueBounded;
|
|
154
174
|
initStarted = false;
|
|
155
175
|
initDone = false;
|
|
156
|
-
|
|
176
|
+
/** `subscribe` function required by useSyncExternalStore. */
|
|
157
177
|
subscribe(subscriber) {
|
|
158
178
|
return pubSubStore.subscribe(this.pubSubKey, () => subscriber());
|
|
159
179
|
}
|
|
160
|
-
|
|
161
|
-
|
|
180
|
+
/** Returns true if there is a backing store. */
|
|
181
|
+
hasStore() {
|
|
182
|
+
return Boolean(this.storageAdapter);
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Set a value directly into the store. Does nothing if there is no
|
|
186
|
+
* backing store.
|
|
187
|
+
*
|
|
188
|
+
* Warning: If you want to set the value properly, you should use
|
|
189
|
+
* `setValue()` instead which also updates the in-memory copy.
|
|
190
|
+
*/
|
|
191
|
+
async setInStore(value) {
|
|
192
|
+
return await this.storageAdapter?.set(STORAGE_KEY, value);
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Get a value directly from the store. Returns undefined if there is no
|
|
196
|
+
* backing store.
|
|
197
|
+
*/
|
|
198
|
+
async getFromStore() {
|
|
199
|
+
return await this.storageAdapter?.get(STORAGE_KEY);
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Get the value of the shared state.
|
|
203
|
+
* `getSnapshot` function — must return same ref if value unchanged.
|
|
204
|
+
* Note that this would return the default value if data init from store is
|
|
205
|
+
* not completed yet.
|
|
206
|
+
*/
|
|
162
207
|
getSnapshot() {
|
|
163
208
|
return pubSubStore.get(this.pubSubKey);
|
|
164
209
|
}
|
|
165
|
-
|
|
210
|
+
/** Set the value of the shared state. */
|
|
166
211
|
setValue(next) {
|
|
167
212
|
if (typeof next === "function") {
|
|
168
213
|
const prev = pubSubStore.get(this.pubSubKey);
|
|
169
214
|
next = next(prev);
|
|
170
215
|
}
|
|
171
216
|
pubSubStore.set(this.pubSubKey, next);
|
|
172
|
-
void this.
|
|
217
|
+
void this.setInStore(next);
|
|
173
218
|
}
|
|
174
|
-
|
|
175
|
-
|
|
219
|
+
/**
|
|
220
|
+
* Remove the value of the shared state. Actually this just sets the value
|
|
221
|
+
* back to the given default value.
|
|
222
|
+
*/
|
|
176
223
|
delete() {
|
|
177
224
|
pubSubStore.set(this.pubSubKey, this.defaultValue);
|
|
178
225
|
void this.storageAdapter?.set(STORAGE_KEY, this.defaultValue);
|
|
179
226
|
}
|
|
180
|
-
|
|
227
|
+
/** Initial default handling. */
|
|
181
228
|
async initDefaultValueOnce() {
|
|
182
229
|
if (this.initDone || this.initStarted) return;
|
|
183
230
|
this.initStarted = true;
|
|
184
|
-
const
|
|
185
|
-
const value =
|
|
231
|
+
const stored = await this.getFromStore();
|
|
232
|
+
const value = stored !== void 0 ? stored : this.defaultValue;
|
|
186
233
|
pubSubStore.set(this.pubSubKey, value);
|
|
187
234
|
this.initDone = true;
|
|
188
|
-
pubSubStore.set(this.pubSubKey, value);
|
|
189
235
|
}
|
|
190
236
|
};
|
|
191
237
|
function sharedState(defaultValue, options) {
|
package/storage.cjs
CHANGED
|
@@ -25,6 +25,7 @@ __export(storage_exports, {
|
|
|
25
25
|
module.exports = __toCommonJS(storage_exports);
|
|
26
26
|
var import_kvStore = require("@choksheak/ts-utils/kvStore");
|
|
27
27
|
var import_localStore = require("@choksheak/ts-utils/localStore");
|
|
28
|
+
var IS_NOT_BROWSER = typeof window === "undefined";
|
|
28
29
|
function getStorageAdapter(options, defaultStoreExpiryMs) {
|
|
29
30
|
const { store } = options ?? {};
|
|
30
31
|
if (!store) {
|
|
@@ -32,6 +33,7 @@ function getStorageAdapter(options, defaultStoreExpiryMs) {
|
|
|
32
33
|
}
|
|
33
34
|
if ("persistTo" in store && typeof store.persistTo === "string" && "key" in store && typeof store.key === "string") {
|
|
34
35
|
const expiryMs = store.expiryMs || defaultStoreExpiryMs;
|
|
36
|
+
const isValid = store.isValid;
|
|
35
37
|
switch (store.persistTo) {
|
|
36
38
|
case "localStorage": {
|
|
37
39
|
const adapter = new import_localStore.LocalStore(store.key, {
|
|
@@ -39,40 +41,58 @@ function getStorageAdapter(options, defaultStoreExpiryMs) {
|
|
|
39
41
|
});
|
|
40
42
|
return {
|
|
41
43
|
set: (key, value) => {
|
|
42
|
-
if (
|
|
44
|
+
if (IS_NOT_BROWSER) return;
|
|
43
45
|
adapter.set(key, value);
|
|
44
46
|
},
|
|
45
47
|
get: (key) => {
|
|
46
|
-
if (
|
|
47
|
-
|
|
48
|
+
if (IS_NOT_BROWSER) return void 0;
|
|
49
|
+
const value = adapter.get(key);
|
|
50
|
+
if (isValid && !isValid(value)) {
|
|
51
|
+
console.warn(
|
|
52
|
+
`localStorage adapter: Auto-discard invalid value for ${key}: ${JSON.stringify(value)}`
|
|
53
|
+
);
|
|
54
|
+
adapter.delete(key);
|
|
55
|
+
return void 0;
|
|
56
|
+
}
|
|
57
|
+
return value;
|
|
48
58
|
},
|
|
49
59
|
delete: (key) => {
|
|
50
|
-
if (
|
|
60
|
+
if (IS_NOT_BROWSER) return;
|
|
51
61
|
adapter.delete(key);
|
|
52
62
|
},
|
|
53
63
|
clear: () => {
|
|
54
|
-
if (
|
|
64
|
+
if (IS_NOT_BROWSER) return;
|
|
55
65
|
adapter.clear();
|
|
56
66
|
}
|
|
57
67
|
};
|
|
58
68
|
}
|
|
59
69
|
case "indexedDb": {
|
|
60
70
|
const keyPrefix = store.key + ":";
|
|
71
|
+
const isValid2 = store.isValid;
|
|
61
72
|
return {
|
|
62
73
|
set: async (key, value) => {
|
|
63
|
-
if (
|
|
74
|
+
if (IS_NOT_BROWSER) return;
|
|
64
75
|
await import_kvStore.kvStore.set(keyPrefix + key, value, expiryMs);
|
|
65
76
|
},
|
|
66
77
|
get: async (key) => {
|
|
67
|
-
if (
|
|
68
|
-
|
|
78
|
+
if (IS_NOT_BROWSER) return void 0;
|
|
79
|
+
const fullKey = keyPrefix + key;
|
|
80
|
+
const value = await import_kvStore.kvStore.get(fullKey);
|
|
81
|
+
if (isValid2 && !isValid2(value)) {
|
|
82
|
+
console.warn(
|
|
83
|
+
`indexedDb adapter: Auto-discard invalid value for ${key}: ${JSON.stringify(value)}`
|
|
84
|
+
);
|
|
85
|
+
await import_kvStore.kvStore.delete(fullKey);
|
|
86
|
+
return void 0;
|
|
87
|
+
}
|
|
88
|
+
return value;
|
|
69
89
|
},
|
|
70
90
|
delete: async (key) => {
|
|
71
|
-
if (
|
|
91
|
+
if (IS_NOT_BROWSER) return;
|
|
72
92
|
await import_kvStore.kvStore.delete(keyPrefix + key);
|
|
73
93
|
},
|
|
74
94
|
clear: async () => {
|
|
75
|
-
if (
|
|
95
|
+
if (IS_NOT_BROWSER) return;
|
|
76
96
|
await import_kvStore.kvStore.clear();
|
|
77
97
|
}
|
|
78
98
|
};
|
package/storage.d.mts
CHANGED
|
@@ -2,11 +2,18 @@ import { StorageAdapter } from '@choksheak/ts-utils/storageAdapter';
|
|
|
2
2
|
|
|
3
3
|
/** List of all available pre-built persistence options. */
|
|
4
4
|
type PersistTo = "localStorage" | "indexedDb";
|
|
5
|
+
/** Options to configure a pre-built store adapter. */
|
|
5
6
|
type StoreOptions = {
|
|
7
|
+
/** Choose which pre-built store adapter to use. */
|
|
6
8
|
persistTo: PersistTo;
|
|
9
|
+
/** Storage key to act as a namespace to store the data. */
|
|
7
10
|
key: string;
|
|
11
|
+
/** How long to wait before the value is considered expired. */
|
|
8
12
|
expiryMs?: number;
|
|
13
|
+
/** Returns true if the given value supposed to be of type T is valid. */
|
|
14
|
+
isValid?: (u: unknown) => boolean;
|
|
9
15
|
};
|
|
16
|
+
/** Either use a pre-built store, or a custom one you provide. */
|
|
10
17
|
type StorageOptions<T> = {
|
|
11
18
|
/**
|
|
12
19
|
* You can either specify the storage adapter directly to use a custom
|
package/storage.d.ts
CHANGED
|
@@ -1,11 +1,18 @@
|
|
|
1
1
|
import { StorageAdapter } from "@choksheak/ts-utils/storageAdapter";
|
|
2
2
|
/** List of all available pre-built persistence options. */
|
|
3
3
|
export type PersistTo = "localStorage" | "indexedDb";
|
|
4
|
+
/** Options to configure a pre-built store adapter. */
|
|
4
5
|
export type StoreOptions = {
|
|
6
|
+
/** Choose which pre-built store adapter to use. */
|
|
5
7
|
persistTo: PersistTo;
|
|
8
|
+
/** Storage key to act as a namespace to store the data. */
|
|
6
9
|
key: string;
|
|
10
|
+
/** How long to wait before the value is considered expired. */
|
|
7
11
|
expiryMs?: number;
|
|
12
|
+
/** Returns true if the given value supposed to be of type T is valid. */
|
|
13
|
+
isValid?: (u: unknown) => boolean;
|
|
8
14
|
};
|
|
15
|
+
/** Either use a pre-built store, or a custom one you provide. */
|
|
9
16
|
export type StorageOptions<T> = {
|
|
10
17
|
/**
|
|
11
18
|
* You can either specify the storage adapter directly to use a custom
|
package/storage.min.cjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
"use strict";var
|
|
1
|
+
"use strict";var p=Object.defineProperty;var g=Object.getOwnPropertyDescriptor;var T=Object.getOwnPropertyNames;var y=Object.prototype.hasOwnProperty;var S=(n,r)=>{for(var e in r)p(n,e,{get:r[e],enumerable:!0})},w=(n,r,e,u)=>{if(r&&typeof r=="object"||typeof r=="function")for(let s of T(r))!y.call(n,s)&&s!==e&&p(n,s,{get:()=>r[s],enumerable:!(u=g(r,s))||u.enumerable});return n};var v=n=>w(p({},"__esModule",{value:!0}),n);var x={};S(x,{getStorageAdapter:()=>m});module.exports=v(x);var d=require("@choksheak/ts-utils/kvStore"),c=require("@choksheak/ts-utils/localStore"),a=typeof window>"u";function m(n,r){let{store:e}=n??{};if(!e)return null;if("persistTo"in e&&typeof e.persistTo=="string"&&"key"in e&&typeof e.key=="string"){let u=e.expiryMs||r,s=e.isValid;switch(e.persistTo){case"localStorage":{let o=new c.LocalStore(e.key,{defaultExpiryMs:u});return{set:(i,t)=>{a||o.set(i,t)},get:i=>{if(a)return;let t=o.get(i);if(s&&!s(t)){console.warn(`localStorage adapter: Auto-discard invalid value for ${i}: ${JSON.stringify(t)}`),o.delete(i);return}return t},delete:i=>{a||o.delete(i)},clear:()=>{a||o.clear()}}}case"indexedDb":{let o=e.key+":",i=e.isValid;return{set:async(t,l)=>{a||await d.kvStore.set(o+t,l,u)},get:async t=>{if(a)return;let l=o+t,f=await d.kvStore.get(l);if(i&&!i(f)){console.warn(`indexedDb adapter: Auto-discard invalid value for ${t}: ${JSON.stringify(f)}`),await d.kvStore.delete(l);return}return f},delete:async t=>{a||await d.kvStore.delete(o+t)},clear:async()=>{a||await d.kvStore.clear()}}}default:throw e.persistTo,new Error(`Unknown store.adapter ${e.persistTo}`)}}return e}0&&(module.exports={getStorageAdapter});
|
|
2
2
|
//# sourceMappingURL=storage.min.cjs.map
|