@choksheak/react-utils 0.2.8 → 0.2.9

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 +97 -131
  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 +95 -130
  10. package/sharedState.cjs +47 -102
  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 +45 -101
  18. package/storage.cjs +67 -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 +42 -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
package/sharedState.cjs CHANGED
@@ -21,7 +21,8 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
21
21
  var sharedState_exports = {};
22
22
  __export(sharedState_exports, {
23
23
  SharedState: () => SharedState,
24
- SharedStateDefaults: () => SharedStateDefaults,
24
+ SharedStateConfig: () => SharedStateConfig,
25
+ configureSharedState: () => configureSharedState,
25
26
  sharedState: () => sharedState,
26
27
  useSharedState: () => useSharedState,
27
28
  useSharedStateSetter: () => useSharedStateSetter,
@@ -31,108 +32,48 @@ module.exports = __toCommonJS(sharedState_exports);
31
32
  var import_timeConstants = require("@choksheak/ts-utils/timeConstants");
32
33
  var import_react = require("react");
33
34
 
34
- // src/utils/storage.ts
35
+ // src/storage.ts
35
36
  var import_kvStore = require("@choksheak/ts-utils/kvStore");
37
+ var import_localStore = require("@choksheak/ts-utils/localStore");
36
38
  function getStorageAdapter(options, defaultStoreExpiryMs) {
37
- const { localStorageKey, indexedDbKey, storeExpiryMs, store } = options ?? {};
38
- if (store) {
39
- return store;
40
- }
41
- const expiryMs = storeExpiryMs || defaultStoreExpiryMs;
42
- if (localStorageKey) {
43
- return new LocalStorageAdapter(localStorageKey, expiryMs);
44
- }
45
- if (indexedDbKey) {
46
- return new IndexedDbAdapter(indexedDbKey, expiryMs);
47
- }
48
- return null;
49
- }
50
- var LocalStorageAdapter = class {
51
- constructor(keyPrefix, expiryMs) {
52
- this.keyPrefix = keyPrefix;
53
- this.expiryMs = expiryMs;
54
- }
55
- save(key, value) {
56
- if (typeof window === "undefined") return;
57
- const fullKey = this.keyPrefix + ":" + key;
58
- window.localStorage.setItem(
59
- fullKey,
60
- JSON.stringify({
61
- value,
62
- expiresAt: this.expiryMs ? Date.now() + this.expiryMs : 0
63
- })
64
- );
65
- }
66
- load(key) {
67
- if (typeof window === "undefined") return void 0;
68
- const fullKey = this.keyPrefix + ":" + key;
69
- const raw = window.localStorage.getItem(fullKey);
70
- if (!raw) return void 0;
71
- try {
72
- const box = JSON.parse(raw);
73
- if (!box || box.expiresAt && Date.now() >= box.expiresAt) {
74
- window.localStorage.removeItem(fullKey);
75
- return void 0;
76
- }
77
- return box.value;
78
- } catch {
79
- window.localStorage.removeItem(fullKey);
80
- return void 0;
81
- }
82
- }
83
- delete(key) {
84
- if (typeof window === "undefined") return;
85
- const fullKey = this.keyPrefix + ":" + key;
86
- window.localStorage.removeItem(fullKey);
87
- }
88
- clear() {
89
- if (typeof window === "undefined") return;
90
- for (const key of Object.keys(window.localStorage)) {
91
- if (key.startsWith(this.keyPrefix)) {
92
- window.localStorage.removeItem(key);
39
+ const { store } = options ?? {};
40
+ if (!store) {
41
+ return null;
42
+ }
43
+ if ("persistTo" in store && typeof store.persistTo === "string" && "key" in store && typeof store.key === "string") {
44
+ const expiryMs = store.expiryMs || defaultStoreExpiryMs;
45
+ switch (store.persistTo) {
46
+ case "localStorage":
47
+ return new import_localStore.LocalStore(store.key, {
48
+ defaultExpiryMs: expiryMs
49
+ }).asStorageAdapter();
50
+ case "indexedDb": {
51
+ const keyPrefix = store.key + ":";
52
+ return {
53
+ set: async (key, value) => {
54
+ await import_kvStore.kvStore.set(keyPrefix + key, value, expiryMs);
55
+ },
56
+ get: async (key) => {
57
+ return await import_kvStore.kvStore.get(keyPrefix + key);
58
+ },
59
+ delete: async (key) => {
60
+ await import_kvStore.kvStore.delete(keyPrefix + key);
61
+ },
62
+ clear: async () => {
63
+ await import_kvStore.kvStore.clear();
64
+ }
65
+ };
93
66
  }
67
+ default:
68
+ store.persistTo;
69
+ throw new Error(`Unknown store.adapter ${store.persistTo}`);
94
70
  }
95
71
  }
96
- };
97
- var IndexedDbAdapter = class {
98
- constructor(keyPrefix, expiryMs) {
99
- this.keyPrefix = keyPrefix;
100
- this.expiryMs = expiryMs;
101
- }
102
- async save(key, value) {
103
- if (typeof window === "undefined") return;
104
- const fullKey = this.keyPrefix + ":" + key;
105
- await import_kvStore.kvStore.set(fullKey, JSON.stringify(value), this.expiryMs);
106
- }
107
- async load(key) {
108
- if (typeof window === "undefined") return void 0;
109
- const fullKey = this.keyPrefix + ":" + key;
110
- const raw = await import_kvStore.kvStore.get(fullKey);
111
- if (raw === void 0) return void 0;
112
- try {
113
- return JSON.parse(raw);
114
- } catch {
115
- await import_kvStore.kvStore.delete(fullKey);
116
- return void 0;
117
- }
118
- }
119
- async delete(key) {
120
- if (typeof window === "undefined") return;
121
- const fullKey = this.keyPrefix + ":" + key;
122
- await import_kvStore.kvStore.delete(fullKey);
123
- }
124
- async clear() {
125
- if (typeof window === "undefined") return;
126
- await import_kvStore.kvStore.forEach(async (k) => {
127
- if (k.startsWith(this.keyPrefix)) {
128
- await import_kvStore.kvStore.delete(k);
129
- }
130
- });
131
- }
132
- };
72
+ return store;
73
+ }
133
74
 
134
75
  // src/sharedState.ts
135
- var SharedStateDefaults = {
76
+ var SharedStateConfig = {
136
77
  /**
137
78
  * Default duration for storage expiration in milliseconds. Initial value
138
79
  * is 30 days, but can be changed to anything.
@@ -144,6 +85,9 @@ var SharedStateDefaults = {
144
85
  */
145
86
  lazyLoad: false
146
87
  };
88
+ function configureSharedState(config) {
89
+ Object.assign(SharedStateConfig, config);
90
+ }
147
91
  var PubSubStore = class {
148
92
  dataByKey = /* @__PURE__ */ new Map();
149
93
  subscribersByKey = /* @__PURE__ */ new Map();
@@ -199,14 +143,14 @@ var SharedState = class {
199
143
  this.pubSubKey = String(stateKey++);
200
144
  this.storageAdapter = getStorageAdapter(
201
145
  options,
202
- SharedStateDefaults.storeExpiryMs
146
+ SharedStateConfig.storeExpiryMs
203
147
  );
204
148
  this.setValueBounded = (next) => {
205
149
  this.setValue(next);
206
150
  };
207
151
  pubSubStore.setNoNotify(this.pubSubKey, defaultValue);
208
152
  const lazyLoad = options?.lazyLoad;
209
- const lazy = lazyLoad !== void 0 ? lazyLoad : SharedStateDefaults.lazyLoad;
153
+ const lazy = lazyLoad !== void 0 ? lazyLoad : SharedStateConfig.lazyLoad;
210
154
  if (!lazy) {
211
155
  void this.initDefaultValueOnce();
212
156
  }
@@ -231,19 +175,19 @@ var SharedState = class {
231
175
  next = next(prev);
232
176
  }
233
177
  pubSubStore.set(this.pubSubKey, next);
234
- void this.storageAdapter?.save(STORAGE_KEY, next);
178
+ void this.storageAdapter?.set(STORAGE_KEY, next);
235
179
  }
236
180
  // Remove the value of the shared state. Actually this just sets the value
237
181
  // back to the given default value.
238
182
  delete() {
239
183
  pubSubStore.set(this.pubSubKey, this.defaultValue);
240
- void this.storageAdapter?.save(STORAGE_KEY, this.defaultValue);
184
+ void this.storageAdapter?.set(STORAGE_KEY, this.defaultValue);
241
185
  }
242
186
  // Initial default handling
243
187
  async initDefaultValueOnce() {
244
188
  if (this.initDone) return;
245
189
  this.initDone = true;
246
- const v = await this.storageAdapter?.load(STORAGE_KEY);
190
+ const v = await this.storageAdapter?.get(STORAGE_KEY);
247
191
  pubSubStore.set(this.pubSubKey, v !== void 0 ? v : this.defaultValue);
248
192
  }
249
193
  };
@@ -269,7 +213,8 @@ function useSharedStateSetter(state) {
269
213
  // Annotate the CommonJS export names for ESM import in node:
270
214
  0 && (module.exports = {
271
215
  SharedState,
272
- SharedStateDefaults,
216
+ SharedStateConfig,
217
+ configureSharedState,
273
218
  sharedState,
274
219
  useSharedState,
275
220
  useSharedStateSetter,
package/sharedState.d.mts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { Dispatch, SetStateAction } from 'react';
2
- import { StorageOptions } from './utils/storage.mjs';
2
+ import { StorageOptions } from './storage.mjs';
3
+ import '@choksheak/ts-utils/storageAdapter';
3
4
 
4
5
  /**
5
6
  * @packageDocumentation
@@ -48,7 +49,7 @@ import { StorageOptions } from './utils/storage.mjs';
48
49
  /**
49
50
  * These values are configurable by the user at any time.
50
51
  */
51
- declare const SharedStateDefaults: {
52
+ declare const SharedStateConfig: {
52
53
  /**
53
54
  * Default duration for storage expiration in milliseconds. Initial value
54
55
  * is 30 days, but can be changed to anything.
@@ -60,6 +61,9 @@ declare const SharedStateDefaults: {
60
61
  */
61
62
  lazyLoad: boolean;
62
63
  };
64
+ type SharedStateConfig = typeof SharedStateConfig;
65
+ /** Convenience function to update global defaults. */
66
+ declare function configureSharedState(config: Partial<SharedStateConfig>): void;
63
67
  /** Options to configure a shared state. */
64
68
  type SharedStateOptions<T> = {
65
69
  lazyLoad?: boolean;
@@ -115,4 +119,4 @@ declare function useSharedStateValue<T>(state: SharedState<T>): T;
115
119
  */
116
120
  declare function useSharedStateSetter<T>(state: SharedState<T>): Dispatch<SetStateAction<T>>;
117
121
 
118
- export { SharedState, SharedStateDefaults, type SharedStateOptions, sharedState, useSharedState, useSharedStateSetter, useSharedStateValue };
122
+ export { SharedState, SharedStateConfig, type SharedStateOptions, configureSharedState, sharedState, useSharedState, useSharedStateSetter, useSharedStateValue };
package/sharedState.d.ts CHANGED
@@ -40,13 +40,13 @@
40
40
  * ```
41
41
  */
42
42
  import { Dispatch, SetStateAction } from "react";
43
- import { StorageOptions } from "./utils/storage";
43
+ import { StorageOptions } from "./storage";
44
44
  /************************************************************************/
45
45
  /************************************************************************/
46
46
  /**
47
47
  * These values are configurable by the user at any time.
48
48
  */
49
- export declare const SharedStateDefaults: {
49
+ export declare const SharedStateConfig: {
50
50
  /**
51
51
  * Default duration for storage expiration in milliseconds. Initial value
52
52
  * is 30 days, but can be changed to anything.
@@ -58,6 +58,9 @@ export declare const SharedStateDefaults: {
58
58
  */
59
59
  lazyLoad: boolean;
60
60
  };
61
+ export type SharedStateConfig = typeof SharedStateConfig;
62
+ /** Convenience function to update global defaults. */
63
+ export declare function configureSharedState(config: Partial<SharedStateConfig>): void;
61
64
  /** Options to configure a shared state. */
62
65
  export type SharedStateOptions<T> = {
63
66
  lazyLoad?: boolean;
@@ -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 v=Object.getOwnPropertyDescriptor;var A=Object.getOwnPropertyNames;var K=Object.prototype.hasOwnProperty;var m=(r,e)=>{for(var t in e)c(r,t,{get:e[t],enumerable:!0})},x=(r,e,t,i)=>{if(e&&typeof e=="object"||typeof e=="function")for(let s of A(e))!K.call(r,s)&&s!==t&&c(r,s,{get:()=>e[s],enumerable:!(i=v(e,s))||i.enumerable});return r};var w=r=>x(c({},"__esModule",{value:!0}),r);var E={};m(E,{SharedState:()=>p,SharedStateConfig:()=>u,configureSharedState:()=>B,sharedState:()=>D,useSharedState:()=>y,useSharedStateSetter:()=>k,useSharedStateValue:()=>V});module.exports=w(E);var g=require("@choksheak/ts-utils/timeConstants"),f=require("react");var n=require("@choksheak/ts-utils/kvStore"),b=require("@choksheak/ts-utils/localStore");function T(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":return new b.LocalStore(t.key,{defaultExpiryMs:i}).asStorageAdapter();case"indexedDb":{let s=t.key+":";return{set:async(a,d)=>{await n.kvStore.set(s+a,d,i)},get:async a=>await n.kvStore.get(s+a),delete:async a=>{await n.kvStore.delete(s+a)},clear:async()=>{await n.kvStore.clear()}}}default:throw t.persistTo,new Error(`Unknown store.adapter ${t.persistTo}`)}}return t}var u={storeExpiryMs:30*g.MS_PER_DAY,lazyLoad:!1};function B(r){Object.assign(u,r)}var l=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 a=Array.from(s.values());for(let d of a)try{d(t,i)}catch(h){console.error("Error invoking subscriber:",h)}}},o=new l,S="state",O=0,p=class{constructor(e,t){this.defaultValue=e;this.pubSubKey=String(O++),this.storageAdapter=T(t,u.storeExpiryMs),this.setValueBounded=a=>{this.setValue(a)},o.setNoNotify(this.pubSubKey,e);let i=t?.lazyLoad;(i!==void 0?i:u.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(S,e)}delete(){o.set(this.pubSubKey,this.defaultValue),this.storageAdapter?.set(S,this.defaultValue)}async initDefaultValueOnce(){if(this.initDone)return;this.initDone=!0;let e=await this.storageAdapter?.get(S);o.set(this.pubSubKey,e!==void 0?e:this.defaultValue)}};function D(r,e){return new p(r,e)}function y(r){return r.initDefaultValueOnce(),[(0,f.useSyncExternalStore)(t=>r.subscribe(t),()=>r.getSnapshot(),()=>r.getSnapshot()),r.setValueBounded]}function V(r){return y(r)[0]}function k(r){return y(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 return new LocalStore(store.key, {\n defaultExpiryMs: expiryMs,\n }).asStorageAdapter<T>();\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 // 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 return await kvStore.get(keyPrefix + key);\n },\n delete: async (key: string): Promise<void> => {\n await kvStore.delete(keyPrefix + key);\n },\n clear: async (): Promise<void> => {\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","keyPrefix","key","value","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 f}from"react";import{kvStore as n}from"@choksheak/ts-utils/kvStore";import{LocalStore as T}from"@choksheak/ts-utils/localStore";function l(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":return new T(t.key,{defaultExpiryMs:i}).asStorageAdapter();case"indexedDb":{let a=t.key+":";return{set:async(s,u)=>{await n.set(a+s,u,i)},get:async s=>await n.get(a+s),delete:async s=>{await n.delete(a+s)},clear:async()=>{await n.clear()}}}default:throw t.persistTo,new Error(`Unknown store.adapter ${t.persistTo}`)}}return t}var d={storeExpiryMs:30*g,lazyLoad:!1};function V(r){Object.assign(d,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 a=this.subscribersByKey.get(e);if(!a)return;let s=Array.from(a.values());for(let u of s)try{u(t,i)}catch(b){console.error("Error invoking subscriber:",b)}}},o=new c,p="state",h=0,S=class{constructor(e,t){this.defaultValue=e;this.pubSubKey=String(h++),this.storageAdapter=l(t,d.storeExpiryMs),this.setValueBounded=s=>{this.setValue(s)},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(p,e)}delete(){o.set(this.pubSubKey,this.defaultValue),this.storageAdapter?.set(p,this.defaultValue)}async initDefaultValueOnce(){if(this.initDone)return;this.initDone=!0;let e=await this.storageAdapter?.get(p);o.set(this.pubSubKey,e!==void 0?e:this.defaultValue)}};function k(r,e){return new S(r,e)}function y(r){return r.initDefaultValueOnce(),[f(t=>r.subscribe(t),()=>r.getSnapshot(),()=>r.getSnapshot()),r.setValueBounded]}function E(r){return y(r)[0]}function M(r){return y(r)[1]}export{S as SharedState,d as SharedStateConfig,V as configureSharedState,k as sharedState,y 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 return new LocalStore(store.key, {\n defaultExpiryMs: expiryMs,\n }).asStorageAdapter<T>();\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 // 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 return await kvStore.get(keyPrefix + key);\n },\n delete: async (key: string): Promise<void> => {\n await kvStore.delete(keyPrefix + key);\n },\n clear: async (): Promise<void> => {\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","keyPrefix","key","value","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"]}