@choksheak/react-utils 0.1.6 → 0.1.8
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/docs/assets/navigation.js +1 -1
- package/docs/assets/search.js +1 -1
- package/docs/classes/sharedQuery.SharedQuery.html +15 -0
- package/docs/classes/sharedState.SharedState.html +8 -0
- package/docs/classes/sharedState.SharedStateConfig.html +5 -5
- package/docs/classes/utils_storage.InMemoryStorageAdapter.html +9 -0
- package/docs/classes/utils_storage.IndexedDbAdapter.html +9 -0
- package/docs/classes/utils_storage.LocalStorageAdapter.html +9 -0
- package/docs/functions/sharedQuery.sharedQuery.html +5 -0
- package/docs/functions/sharedQuery.useQuery.html +4 -0
- package/docs/functions/sharedState.sharedState.html +2 -2
- package/docs/functions/sharedState.useSharedState.html +2 -2
- package/docs/functions/sharedState.useSharedStateSetter.html +2 -2
- package/docs/functions/sharedState.useSharedStateValue.html +2 -2
- package/docs/functions/utils_storage.getStorageAdapter.html +1 -0
- package/docs/functions/utils_stringify.stringifyDeterministicForKeys.html +7 -0
- package/docs/modules/sharedQuery.html +1 -0
- package/docs/modules/sharedState.html +1 -1
- package/docs/modules/utils_storage.html +1 -0
- package/docs/modules/utils_stringify.html +1 -0
- package/docs/modules.html +1 -1
- package/docs/types/sharedQuery.FetchFn.html +1 -0
- package/docs/types/sharedQuery.FetchedData.html +3 -0
- package/docs/types/sharedQuery.QueryStateValue.html +4 -0
- package/docs/types/sharedQuery.SharedQueryOptions.html +1 -0
- package/docs/types/sharedQuery.SharedQueryState.html +3 -0
- package/docs/types/sharedState.SharedStateOptions.html +1 -0
- package/docs/types/utils_storage.StorageAdapter.html +5 -0
- package/docs/types/utils_storage.StorageOptions.html +5 -0
- package/docs/variables/fetcher.fetcher.html +1 -1
- package/docs/variables/fetcher.fetcherWithRetries.html +1 -1
- package/package.json +9 -6
- package/sharedQuery.d.mts +61 -0
- package/sharedQuery.d.ts +58 -0
- package/sharedQuery.js +471 -0
- package/sharedQuery.mjs +444 -0
- package/sharedState.d.mts +21 -16
- package/sharedState.d.ts +19 -15
- package/sharedState.js +159 -88
- package/sharedState.mjs +158 -88
- package/src/sharedQuery.ts +303 -0
- package/src/sharedState.ts +77 -140
- package/src/utils/storage.ts +197 -0
- package/src/utils/stringify.test.ts +38 -0
- package/src/utils/stringify.ts +49 -0
- package/utils/storage.d.mts +46 -0
- package/utils/storage.d.ts +44 -0
- package/utils/storage.js +162 -0
- package/utils/storage.mjs +134 -0
- package/utils/stringify.d.mts +12 -0
- package/utils/stringify.d.ts +10 -0
- package/utils/stringify.js +58 -0
- package/utils/stringify.mjs +33 -0
- package/utils/stringify.test.d.mts +2 -0
- package/utils/stringify.test.d.ts +2 -0
- package/utils/stringify.test.js +64 -0
- package/utils/stringify.test.mjs +81 -0
- package/docs/types/sharedState.SharedState.html +0 -6
package/sharedQuery.mjs
ADDED
|
@@ -0,0 +1,444 @@
|
|
|
1
|
+
// src/sharedQuery.ts
|
|
2
|
+
import { DEFAULT_EXPIRY_DELTA_MS as DEFAULT_EXPIRY_DELTA_MS2 } from "@choksheak/ts-utils/kvStore";
|
|
3
|
+
import { useCallback, useEffect, useRef } from "react";
|
|
4
|
+
|
|
5
|
+
// src/sharedState.ts
|
|
6
|
+
import { DEFAULT_EXPIRY_DELTA_MS } from "@choksheak/ts-utils/kvStore";
|
|
7
|
+
import { MS_PER_DAY } from "@choksheak/ts-utils/timeConstants";
|
|
8
|
+
import { useSyncExternalStore } from "react";
|
|
9
|
+
|
|
10
|
+
// src/utils/storage.ts
|
|
11
|
+
import { kvStore } from "@choksheak/ts-utils/kvStore";
|
|
12
|
+
function getStorageAdapter(options, defaultStoreExpiryMs) {
|
|
13
|
+
const { localStorageKey, indexedDbKey, storeExpiryMs, store } = options ?? {};
|
|
14
|
+
if (store) {
|
|
15
|
+
return store;
|
|
16
|
+
}
|
|
17
|
+
const expiryMs = storeExpiryMs || defaultStoreExpiryMs;
|
|
18
|
+
if (localStorageKey) {
|
|
19
|
+
return new LocalStorageAdapter(localStorageKey, expiryMs);
|
|
20
|
+
}
|
|
21
|
+
if (indexedDbKey) {
|
|
22
|
+
return new IndexedDbAdapter(indexedDbKey, expiryMs);
|
|
23
|
+
}
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
var LocalStorageAdapter = class {
|
|
27
|
+
constructor(keyPrefix, expiryMs) {
|
|
28
|
+
this.keyPrefix = keyPrefix;
|
|
29
|
+
this.expiryMs = expiryMs;
|
|
30
|
+
}
|
|
31
|
+
save(key, value) {
|
|
32
|
+
if (typeof window === "undefined") return;
|
|
33
|
+
const fullKey = this.keyPrefix + ":" + key;
|
|
34
|
+
window.localStorage.setItem(
|
|
35
|
+
fullKey,
|
|
36
|
+
JSON.stringify({
|
|
37
|
+
value,
|
|
38
|
+
expiresAt: this.expiryMs ? Date.now() + this.expiryMs : 0
|
|
39
|
+
})
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
load(key) {
|
|
43
|
+
if (typeof window === "undefined") return void 0;
|
|
44
|
+
const fullKey = this.keyPrefix + ":" + key;
|
|
45
|
+
const raw = window.localStorage.getItem(fullKey);
|
|
46
|
+
if (!raw) return void 0;
|
|
47
|
+
try {
|
|
48
|
+
const box = JSON.parse(raw);
|
|
49
|
+
if (!box || box.expiresAt && Date.now() >= box.expiresAt) {
|
|
50
|
+
window.localStorage.removeItem(fullKey);
|
|
51
|
+
return void 0;
|
|
52
|
+
}
|
|
53
|
+
return box.value;
|
|
54
|
+
} catch {
|
|
55
|
+
window.localStorage.removeItem(fullKey);
|
|
56
|
+
return void 0;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
delete(key) {
|
|
60
|
+
if (typeof window === "undefined") return;
|
|
61
|
+
const fullKey = this.keyPrefix + ":" + key;
|
|
62
|
+
window.localStorage.removeItem(fullKey);
|
|
63
|
+
}
|
|
64
|
+
clear() {
|
|
65
|
+
if (typeof window === "undefined") return;
|
|
66
|
+
for (const key of Object.keys(window.localStorage)) {
|
|
67
|
+
if (key.startsWith(this.keyPrefix)) {
|
|
68
|
+
window.localStorage.removeItem(key);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
var IndexedDbAdapter = class {
|
|
74
|
+
constructor(keyPrefix, expiryMs) {
|
|
75
|
+
this.keyPrefix = keyPrefix;
|
|
76
|
+
this.expiryMs = expiryMs;
|
|
77
|
+
}
|
|
78
|
+
async save(key, value) {
|
|
79
|
+
if (typeof window === "undefined") return;
|
|
80
|
+
const fullKey = this.keyPrefix + ":" + key;
|
|
81
|
+
await kvStore.set(fullKey, JSON.stringify(value), this.expiryMs);
|
|
82
|
+
}
|
|
83
|
+
async load(key) {
|
|
84
|
+
if (typeof window === "undefined") return void 0;
|
|
85
|
+
const fullKey = this.keyPrefix + ":" + key;
|
|
86
|
+
const raw = await kvStore.get(fullKey);
|
|
87
|
+
if (raw === void 0) return void 0;
|
|
88
|
+
try {
|
|
89
|
+
return JSON.parse(raw);
|
|
90
|
+
} catch {
|
|
91
|
+
await kvStore.delete(fullKey);
|
|
92
|
+
return void 0;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
async delete(key) {
|
|
96
|
+
if (typeof window === "undefined") return;
|
|
97
|
+
const fullKey = this.keyPrefix + ":" + key;
|
|
98
|
+
await kvStore.delete(fullKey);
|
|
99
|
+
}
|
|
100
|
+
async clear() {
|
|
101
|
+
if (typeof window === "undefined") return;
|
|
102
|
+
await kvStore.forEach(async (k) => {
|
|
103
|
+
if (k.startsWith(this.keyPrefix)) {
|
|
104
|
+
await kvStore.delete(k);
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
// src/sharedState.ts
|
|
111
|
+
var SharedStateConfig = class {
|
|
112
|
+
/**
|
|
113
|
+
* Default duration for storage expiration in milliseconds. Initial value
|
|
114
|
+
* is 30 days, but can be changed to anything.
|
|
115
|
+
*/
|
|
116
|
+
static storeExpiryMs = MS_PER_DAY * 30;
|
|
117
|
+
/**
|
|
118
|
+
* True to load from storage only on mount, false (default) to load in
|
|
119
|
+
* the top level scope when the shared state is defined.
|
|
120
|
+
*/
|
|
121
|
+
static lazyLoad = false;
|
|
122
|
+
};
|
|
123
|
+
var PubSubStore = class {
|
|
124
|
+
dataByKey = /* @__PURE__ */ new Map();
|
|
125
|
+
subscribersByKey = /* @__PURE__ */ new Map();
|
|
126
|
+
// This could be undefined if T includes undefined, else it should never
|
|
127
|
+
// return undefined because the user has to always specify the default value.
|
|
128
|
+
get(key) {
|
|
129
|
+
return this.dataByKey.get(key);
|
|
130
|
+
}
|
|
131
|
+
set(key, value) {
|
|
132
|
+
const prev = this.dataByKey.get(key);
|
|
133
|
+
if (prev === value) return;
|
|
134
|
+
this.dataByKey.set(key, value);
|
|
135
|
+
this.notify(key, value, prev);
|
|
136
|
+
}
|
|
137
|
+
delete(key) {
|
|
138
|
+
this.dataByKey.delete(key);
|
|
139
|
+
}
|
|
140
|
+
setNoNotify(key, value) {
|
|
141
|
+
this.dataByKey.set(key, value);
|
|
142
|
+
}
|
|
143
|
+
// Subscribe to changes of a specific key
|
|
144
|
+
subscribe(key, subscriber) {
|
|
145
|
+
let subscribers = this.subscribersByKey.get(key);
|
|
146
|
+
if (!subscribers) {
|
|
147
|
+
subscribers = /* @__PURE__ */ new Set();
|
|
148
|
+
this.subscribersByKey.set(key, subscribers);
|
|
149
|
+
}
|
|
150
|
+
subscribers.add(subscriber);
|
|
151
|
+
return () => {
|
|
152
|
+
subscribers.delete(subscriber);
|
|
153
|
+
if (subscribers.size === 0) this.subscribersByKey.delete(key);
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
notify(key, next, prev) {
|
|
157
|
+
const subscribers = this.subscribersByKey.get(key);
|
|
158
|
+
if (!subscribers) return;
|
|
159
|
+
const copy = Array.from(subscribers.values());
|
|
160
|
+
for (const subscriber of copy) {
|
|
161
|
+
try {
|
|
162
|
+
subscriber(next, prev);
|
|
163
|
+
} catch (e) {
|
|
164
|
+
console.error(`Error invoking subscriber:`, e);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
var pubSubStore = new PubSubStore();
|
|
170
|
+
var STORAGE_KEY = "state";
|
|
171
|
+
var stateKey = 0;
|
|
172
|
+
var SharedState = class {
|
|
173
|
+
constructor(defaultValue, options) {
|
|
174
|
+
this.defaultValue = defaultValue;
|
|
175
|
+
this.pubSubKey = String(stateKey++);
|
|
176
|
+
this.storageAdapter = getStorageAdapter(options, DEFAULT_EXPIRY_DELTA_MS);
|
|
177
|
+
pubSubStore.setNoNotify(this.pubSubKey, defaultValue);
|
|
178
|
+
const lazyLoad = options?.lazyLoad;
|
|
179
|
+
const lazy = lazyLoad !== void 0 ? lazyLoad : SharedStateConfig.lazyLoad;
|
|
180
|
+
if (!lazy) {
|
|
181
|
+
void this.initDefaultValueOnce();
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
pubSubKey;
|
|
185
|
+
storageAdapter;
|
|
186
|
+
initDone = false;
|
|
187
|
+
// `subscribe` function required by useSyncExternalStore
|
|
188
|
+
subscribe(subscriber) {
|
|
189
|
+
return pubSubStore.subscribe(this.pubSubKey, () => subscriber());
|
|
190
|
+
}
|
|
191
|
+
// Get the value of the shared state.
|
|
192
|
+
// `getSnapshot` function — must return same ref if value unchanged
|
|
193
|
+
getSnapshot() {
|
|
194
|
+
return pubSubStore.get(this.pubSubKey);
|
|
195
|
+
}
|
|
196
|
+
// Set the value of the shared state.
|
|
197
|
+
setValue(next) {
|
|
198
|
+
if (typeof next === "function") {
|
|
199
|
+
const prev = pubSubStore.get(this.pubSubKey);
|
|
200
|
+
next = next(prev);
|
|
201
|
+
}
|
|
202
|
+
pubSubStore.set(this.pubSubKey, next);
|
|
203
|
+
void this.storageAdapter?.save(STORAGE_KEY, next);
|
|
204
|
+
}
|
|
205
|
+
// Remove the value of the shared state. Actually this just sets the value
|
|
206
|
+
// back to the given default value.
|
|
207
|
+
delete() {
|
|
208
|
+
pubSubStore.set(this.pubSubKey, this.defaultValue);
|
|
209
|
+
void this.storageAdapter?.save(STORAGE_KEY, this.defaultValue);
|
|
210
|
+
}
|
|
211
|
+
// Initial default handling
|
|
212
|
+
async initDefaultValueOnce() {
|
|
213
|
+
if (this.initDone) return;
|
|
214
|
+
this.initDone = true;
|
|
215
|
+
const v = await this.storageAdapter?.load(STORAGE_KEY);
|
|
216
|
+
pubSubStore.set(this.pubSubKey, v !== void 0 ? v : this.defaultValue);
|
|
217
|
+
}
|
|
218
|
+
};
|
|
219
|
+
function sharedState(defaultValue, options) {
|
|
220
|
+
return new SharedState(defaultValue, options);
|
|
221
|
+
}
|
|
222
|
+
function useSharedState(state) {
|
|
223
|
+
state.initDefaultValueOnce();
|
|
224
|
+
const value = useSyncExternalStore(
|
|
225
|
+
(onStoreChange) => state.subscribe(onStoreChange),
|
|
226
|
+
() => state.getSnapshot(),
|
|
227
|
+
// getServerSnapshot is needed to prevent SSR dev errors in nextjs.
|
|
228
|
+
() => state.getSnapshot()
|
|
229
|
+
);
|
|
230
|
+
return [value, state.setValue];
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// src/utils/stringify.ts
|
|
234
|
+
function stringifyDeterministicForKeys(params) {
|
|
235
|
+
const cache = /* @__PURE__ */ new Set();
|
|
236
|
+
return JSON.stringify(params, (_, value) => {
|
|
237
|
+
if (typeof value === "function") {
|
|
238
|
+
throw new Error(`Functions should not be passed into query parameters`);
|
|
239
|
+
}
|
|
240
|
+
if (!value || typeof value !== "object") {
|
|
241
|
+
return value;
|
|
242
|
+
}
|
|
243
|
+
if (cache.has(value)) {
|
|
244
|
+
return "[Circular]";
|
|
245
|
+
}
|
|
246
|
+
cache.add(value);
|
|
247
|
+
if (value !== params) {
|
|
248
|
+
try {
|
|
249
|
+
JSON.stringify(value);
|
|
250
|
+
} catch {
|
|
251
|
+
return "[Unserializable]";
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
return Object.keys(value).sort().reduce(
|
|
255
|
+
(result, key) => {
|
|
256
|
+
result[key] = value[key];
|
|
257
|
+
return result;
|
|
258
|
+
},
|
|
259
|
+
{}
|
|
260
|
+
);
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// src/sharedQuery.ts
|
|
265
|
+
var DEFAULT_STALE_MS = 0;
|
|
266
|
+
var SharedQuery = class {
|
|
267
|
+
constructor(queryName, fetchFn, options) {
|
|
268
|
+
this.queryName = queryName;
|
|
269
|
+
this.fetchFn = fetchFn;
|
|
270
|
+
this.expiryMs = options?.expiryMs !== void 0 ? options?.expiryMs : DEFAULT_EXPIRY_DELTA_MS2;
|
|
271
|
+
this.staleMs = options?.staleMs !== void 0 ? options?.staleMs : DEFAULT_STALE_MS;
|
|
272
|
+
this.revalidateOnStale = Boolean(options?.revalidateOnStale);
|
|
273
|
+
this.queryState = sharedState(
|
|
274
|
+
{},
|
|
275
|
+
{
|
|
276
|
+
...options,
|
|
277
|
+
// Fallback to use expiryMs for the storeExpiryMs.
|
|
278
|
+
storeExpiryMs: options?.storeExpiryMs || this.expiryMs
|
|
279
|
+
}
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
inflightPromises = /* @__PURE__ */ new Map();
|
|
283
|
+
queryState;
|
|
284
|
+
expiryMs;
|
|
285
|
+
staleMs;
|
|
286
|
+
revalidateOnStale;
|
|
287
|
+
getQueryKey(args) {
|
|
288
|
+
return this.queryName + ":" + stringifyDeterministicForKeys(args);
|
|
289
|
+
}
|
|
290
|
+
isStale(entry) {
|
|
291
|
+
if (this.staleMs === 0) return true;
|
|
292
|
+
const ageMs = Date.now() - entry.updatedMs;
|
|
293
|
+
return ageMs > this.staleMs;
|
|
294
|
+
}
|
|
295
|
+
isExpired(entry) {
|
|
296
|
+
if (this.expiryMs === 0) return false;
|
|
297
|
+
const ageMs = Date.now() - entry.updatedMs;
|
|
298
|
+
return ageMs > this.expiryMs;
|
|
299
|
+
}
|
|
300
|
+
fetchNoCaching(queryKey, ...args) {
|
|
301
|
+
const inflightPromise = this.inflightPromises.get(queryKey);
|
|
302
|
+
if (inflightPromise) {
|
|
303
|
+
console.log(`Deduplicating inflight fetch for ${queryKey}`);
|
|
304
|
+
return inflightPromise;
|
|
305
|
+
}
|
|
306
|
+
console.log(`Start fetching ${queryKey}`);
|
|
307
|
+
const promise = this.fetchFn(...args).then((result) => {
|
|
308
|
+
console.log(`Successfully fetched ${queryKey}`);
|
|
309
|
+
this.setCache(queryKey, result);
|
|
310
|
+
return result;
|
|
311
|
+
}).catch((e) => {
|
|
312
|
+
console.error(`Failed to fetch ${queryKey}: ${e}`);
|
|
313
|
+
throw e;
|
|
314
|
+
}).finally(() => {
|
|
315
|
+
this.inflightPromises.delete(queryKey);
|
|
316
|
+
});
|
|
317
|
+
this.inflightPromises.set(queryKey, promise);
|
|
318
|
+
return promise;
|
|
319
|
+
}
|
|
320
|
+
async getCachedOrFetch(...args) {
|
|
321
|
+
const queryKey = this.getQueryKey(args);
|
|
322
|
+
const cached = this.queryState.getSnapshot()?.[queryKey];
|
|
323
|
+
if (cached?.data && !this.isExpired(cached.data)) {
|
|
324
|
+
if (!this.isStale(cached.data)) {
|
|
325
|
+
console.log(
|
|
326
|
+
`Return fresh data ${queryKey} from cache without fetching`
|
|
327
|
+
);
|
|
328
|
+
return cached.data.value;
|
|
329
|
+
}
|
|
330
|
+
if (this.revalidateOnStale) {
|
|
331
|
+
console.log(`revalidateOnStale for ${queryKey}`);
|
|
332
|
+
void this.fetchNoCaching(queryKey, ...args);
|
|
333
|
+
}
|
|
334
|
+
console.log(`Returning stale data for ${queryKey}`);
|
|
335
|
+
return cached.data.value;
|
|
336
|
+
}
|
|
337
|
+
return await this.fetchNoCaching(queryKey, ...args);
|
|
338
|
+
}
|
|
339
|
+
async setCache(queryKey, value) {
|
|
340
|
+
const entry = {
|
|
341
|
+
data: { value, updatedMs: Date.now() },
|
|
342
|
+
loading: false,
|
|
343
|
+
error: null
|
|
344
|
+
};
|
|
345
|
+
this.queryState.setValue((prev) => ({ ...prev, [queryKey]: entry }));
|
|
346
|
+
}
|
|
347
|
+
/** Do a new fetch even when the data is already cached. */
|
|
348
|
+
async updateFromSource(...args) {
|
|
349
|
+
const key = this.getQueryKey(args);
|
|
350
|
+
return await this.fetchNoCaching(key, ...args);
|
|
351
|
+
}
|
|
352
|
+
/** Delete the currently cached data. */
|
|
353
|
+
invalidate(args) {
|
|
354
|
+
if (args) {
|
|
355
|
+
const queryKey = this.getQueryKey(args);
|
|
356
|
+
const record = this.queryState.getSnapshot();
|
|
357
|
+
const clone = { ...record };
|
|
358
|
+
delete clone[queryKey];
|
|
359
|
+
this.queryState.setValue(clone);
|
|
360
|
+
this.inflightPromises.delete(queryKey);
|
|
361
|
+
} else {
|
|
362
|
+
this.clearAll();
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
/** Delete the currently cached data & all inflight promises. */
|
|
366
|
+
clearAll() {
|
|
367
|
+
this.queryState.delete();
|
|
368
|
+
this.inflightPromises.clear();
|
|
369
|
+
}
|
|
370
|
+
};
|
|
371
|
+
var seenQueryNames = /* @__PURE__ */ new Set();
|
|
372
|
+
function sharedQuery(queryName, fetchFn, options) {
|
|
373
|
+
if (seenQueryNames.has(queryName)) {
|
|
374
|
+
throw new Error(`Duplicate shared query "${queryName}"`);
|
|
375
|
+
}
|
|
376
|
+
seenQueryNames.add(queryName);
|
|
377
|
+
return new SharedQuery(queryName, fetchFn, options);
|
|
378
|
+
}
|
|
379
|
+
var DEFAULT_QUERY_STATE_ENTRY = {
|
|
380
|
+
data: null,
|
|
381
|
+
loading: true,
|
|
382
|
+
error: null
|
|
383
|
+
};
|
|
384
|
+
function useQuery(query, args) {
|
|
385
|
+
const [queryState, setQueryState] = useSharedState(query.queryState);
|
|
386
|
+
const isMounted = useRef(true);
|
|
387
|
+
const queryKey = query.getQueryKey(args);
|
|
388
|
+
const execute = useCallback(async () => {
|
|
389
|
+
console.log(`Begin executing shared query ${queryKey}`);
|
|
390
|
+
setQueryState((prev) => {
|
|
391
|
+
const clone = { ...prev };
|
|
392
|
+
clone[queryKey] = {
|
|
393
|
+
data: clone[queryKey]?.data ?? null,
|
|
394
|
+
loading: true,
|
|
395
|
+
error: null
|
|
396
|
+
};
|
|
397
|
+
return clone;
|
|
398
|
+
});
|
|
399
|
+
try {
|
|
400
|
+
const value = await query.getCachedOrFetch(...args);
|
|
401
|
+
if (isMounted.current) {
|
|
402
|
+
setQueryState((prev) => {
|
|
403
|
+
const clone = { ...prev };
|
|
404
|
+
const now = Date.now();
|
|
405
|
+
clone[queryKey] = {
|
|
406
|
+
data: {
|
|
407
|
+
value,
|
|
408
|
+
updatedMs: now
|
|
409
|
+
},
|
|
410
|
+
loading: false,
|
|
411
|
+
error: null
|
|
412
|
+
};
|
|
413
|
+
return clone;
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
} catch (e) {
|
|
417
|
+
const error = e instanceof Error ? e : new Error(`An unknown error occurred during fetch: ${e}`);
|
|
418
|
+
if (isMounted.current) {
|
|
419
|
+
setQueryState((prev) => {
|
|
420
|
+
const clone = { ...prev };
|
|
421
|
+
clone[queryKey] = {
|
|
422
|
+
data: clone[queryKey]?.data ?? null,
|
|
423
|
+
loading: false,
|
|
424
|
+
error
|
|
425
|
+
};
|
|
426
|
+
return clone;
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
}, [queryKey]);
|
|
431
|
+
useEffect(() => {
|
|
432
|
+
isMounted.current = true;
|
|
433
|
+
void execute();
|
|
434
|
+
return () => {
|
|
435
|
+
isMounted.current = false;
|
|
436
|
+
};
|
|
437
|
+
}, [execute]);
|
|
438
|
+
return queryState[queryKey] ?? DEFAULT_QUERY_STATE_ENTRY;
|
|
439
|
+
}
|
|
440
|
+
export {
|
|
441
|
+
SharedQuery,
|
|
442
|
+
sharedQuery,
|
|
443
|
+
useQuery
|
|
444
|
+
};
|
package/sharedState.d.mts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { StorageOptions } from './utils/storage.mjs';
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* The "shared state" is a React state that shares the same value across the
|
|
3
5
|
* entire app. It is largely similar to the jotai library, but is much smaller
|
|
@@ -15,34 +17,37 @@
|
|
|
15
17
|
*/
|
|
16
18
|
declare class SharedStateConfig {
|
|
17
19
|
/**
|
|
18
|
-
* Default duration for
|
|
20
|
+
* Default duration for storage expiration in milliseconds. Initial value
|
|
19
21
|
* is 30 days, but can be changed to anything.
|
|
20
22
|
*/
|
|
21
|
-
static
|
|
23
|
+
static storeExpiryMs: number;
|
|
22
24
|
/**
|
|
23
|
-
* True to load from
|
|
25
|
+
* True to load from storage only on mount, false (default) to load in
|
|
24
26
|
* the top level scope when the shared state is defined.
|
|
25
27
|
*/
|
|
26
28
|
static lazyLoad: boolean;
|
|
27
29
|
}
|
|
28
|
-
type
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
30
|
+
type SharedStateOptions<T> = {
|
|
31
|
+
lazyLoad?: boolean;
|
|
32
|
+
} & StorageOptions<T>;
|
|
33
|
+
declare class SharedState<T> {
|
|
34
|
+
private readonly defaultValue;
|
|
35
|
+
private readonly pubSubKey;
|
|
36
|
+
private readonly storageAdapter;
|
|
32
37
|
initDone: boolean;
|
|
33
|
-
|
|
34
|
-
|
|
38
|
+
constructor(defaultValue: T, options?: SharedStateOptions<T>);
|
|
39
|
+
subscribe(subscriber: () => void): () => void;
|
|
40
|
+
getSnapshot(): T;
|
|
41
|
+
setValue(next: T | ((prev: T) => T)): void;
|
|
42
|
+
delete(): void;
|
|
43
|
+
initDefaultValueOnce(): Promise<void>;
|
|
44
|
+
}
|
|
35
45
|
/**
|
|
36
46
|
* Create a new shared state object. Put this code at the top level scope:
|
|
37
47
|
*
|
|
38
48
|
* const myState = sharedState<number>(0);
|
|
39
49
|
*/
|
|
40
|
-
declare function sharedState<T>(defaultValue: T, options?:
|
|
41
|
-
localStorageKey?: string;
|
|
42
|
-
indexedDbKey?: string;
|
|
43
|
-
persistExpiryMs?: number;
|
|
44
|
-
lazyLoad?: boolean;
|
|
45
|
-
}): SharedState<T>;
|
|
50
|
+
declare function sharedState<T>(defaultValue: T, options?: SharedStateOptions<T>): SharedState<T>;
|
|
46
51
|
/**
|
|
47
52
|
* React hook for subscribing to a key in the global store.
|
|
48
53
|
* const [my, setMy] = useSharedState(myState);
|
|
@@ -53,4 +58,4 @@ declare function useSharedStateValue<T>(state: SharedState<T>): T;
|
|
|
53
58
|
/** E.g. const setMy = useSharedStateSetter(myState) */
|
|
54
59
|
declare function useSharedStateSetter<T>(state: SharedState<T>): (next: T | ((prev: T) => T)) => void;
|
|
55
60
|
|
|
56
|
-
export {
|
|
61
|
+
export { SharedState, SharedStateConfig, type SharedStateOptions, sharedState, useSharedState, useSharedStateSetter, useSharedStateValue };
|
package/sharedState.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { StorageOptions } from "./utils/storage";
|
|
1
2
|
/**
|
|
2
3
|
* The "shared state" is a React state that shares the same value across the
|
|
3
4
|
* entire app. It is largely similar to the jotai library, but is much smaller
|
|
@@ -15,34 +16,37 @@
|
|
|
15
16
|
*/
|
|
16
17
|
export declare class SharedStateConfig {
|
|
17
18
|
/**
|
|
18
|
-
* Default duration for
|
|
19
|
+
* Default duration for storage expiration in milliseconds. Initial value
|
|
19
20
|
* is 30 days, but can be changed to anything.
|
|
20
21
|
*/
|
|
21
|
-
static
|
|
22
|
+
static storeExpiryMs: number;
|
|
22
23
|
/**
|
|
23
|
-
* True to load from
|
|
24
|
+
* True to load from storage only on mount, false (default) to load in
|
|
24
25
|
* the top level scope when the shared state is defined.
|
|
25
26
|
*/
|
|
26
27
|
static lazyLoad: boolean;
|
|
27
28
|
}
|
|
28
|
-
export type
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
29
|
+
export type SharedStateOptions<T> = {
|
|
30
|
+
lazyLoad?: boolean;
|
|
31
|
+
} & StorageOptions<T>;
|
|
32
|
+
export declare class SharedState<T> {
|
|
33
|
+
private readonly defaultValue;
|
|
34
|
+
private readonly pubSubKey;
|
|
35
|
+
private readonly storageAdapter;
|
|
32
36
|
initDone: boolean;
|
|
33
|
-
|
|
34
|
-
|
|
37
|
+
constructor(defaultValue: T, options?: SharedStateOptions<T>);
|
|
38
|
+
subscribe(subscriber: () => void): () => void;
|
|
39
|
+
getSnapshot(): T;
|
|
40
|
+
setValue(next: T | ((prev: T) => T)): void;
|
|
41
|
+
delete(): void;
|
|
42
|
+
initDefaultValueOnce(): Promise<void>;
|
|
43
|
+
}
|
|
35
44
|
/**
|
|
36
45
|
* Create a new shared state object. Put this code at the top level scope:
|
|
37
46
|
*
|
|
38
47
|
* const myState = sharedState<number>(0);
|
|
39
48
|
*/
|
|
40
|
-
export declare function sharedState<T>(defaultValue: T, options?:
|
|
41
|
-
localStorageKey?: string;
|
|
42
|
-
indexedDbKey?: string;
|
|
43
|
-
persistExpiryMs?: number;
|
|
44
|
-
lazyLoad?: boolean;
|
|
45
|
-
}): SharedState<T>;
|
|
49
|
+
export declare function sharedState<T>(defaultValue: T, options?: SharedStateOptions<T>): SharedState<T>;
|
|
46
50
|
/**
|
|
47
51
|
* React hook for subscribing to a key in the global store.
|
|
48
52
|
* const [my, setMy] = useSharedState(myState);
|