@choksheak/react-utils 0.1.9 → 0.2.1
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 +10 -8
- package/docs/classes/sharedState.SharedState.html +2 -2
- package/docs/classes/utils_storage.InMemoryStorageAdapter.html +2 -2
- package/docs/classes/utils_storage.IndexedDbAdapter.html +2 -2
- package/docs/classes/utils_storage.LocalStorageAdapter.html +2 -2
- package/docs/functions/sharedQuery.sharedQuery.html +2 -2
- package/docs/functions/sharedQuery.useSharedQuery.html +4 -0
- package/docs/functions/sharedState.sharedState.html +1 -1
- package/docs/functions/sharedState.useSharedState.html +1 -1
- package/docs/functions/sharedState.useSharedStateSetter.html +1 -1
- package/docs/functions/sharedState.useSharedStateValue.html +1 -1
- package/docs/functions/utils_storage.getStorageAdapter.html +1 -1
- package/docs/functions/utils_stringify.stringifyDeterministicForKeys.html +1 -1
- package/docs/modules/sharedQuery.html +1 -1
- package/docs/modules/sharedState.html +1 -1
- package/docs/types/sharedQuery.FetchFn.html +1 -1
- package/docs/types/sharedQuery.PersistTo.html +1 -0
- package/docs/types/sharedQuery.QueryStateValue.html +5 -3
- package/docs/types/sharedQuery.SharedQueryOptions.html +1 -1
- package/docs/types/sharedQuery.SharedQueryState.html +2 -2
- package/docs/types/sharedQuery.UseQueryResult.html +1 -0
- package/docs/types/sharedState.SharedStateOptions.html +1 -1
- package/docs/types/utils_storage.StorageAdapter.html +2 -2
- package/docs/types/utils_storage.StorageOptions.html +2 -2
- package/docs/variables/fetcher.fetcher.html +1 -1
- package/docs/variables/fetcher.fetcherWithRetries.html +1 -1
- package/docs/variables/sharedQuery.SharedQueryDefaults.html +2 -0
- package/docs/variables/sharedState.SharedStateDefaults.html +6 -0
- package/package.json +8 -1
- package/sharedQuery.d.mts +48 -33
- package/sharedQuery.d.ts +46 -32
- package/sharedQuery.js +120 -95
- package/sharedQuery.mjs +119 -95
- package/sharedState.d.mts +5 -5
- package/sharedState.d.ts +4 -4
- package/sharedState.js +10 -8
- package/sharedState.mjs +9 -7
- package/src/sharedQuery.ts +181 -131
- package/src/sharedState.ts +10 -7
- package/docs/classes/sharedState.SharedStateConfig.html +0 -9
- package/docs/functions/sharedQuery.useQuery.html +0 -4
- package/docs/types/sharedQuery.FetchedData.html +0 -3
package/sharedState.d.mts
CHANGED
|
@@ -15,18 +15,18 @@ import { StorageOptions } from './utils/storage.mjs';
|
|
|
15
15
|
/**
|
|
16
16
|
* These values are configurable by the user at any time.
|
|
17
17
|
*/
|
|
18
|
-
declare
|
|
18
|
+
declare const SharedStateDefaults: {
|
|
19
19
|
/**
|
|
20
20
|
* Default duration for storage expiration in milliseconds. Initial value
|
|
21
21
|
* is 30 days, but can be changed to anything.
|
|
22
22
|
*/
|
|
23
|
-
|
|
23
|
+
storeExpiryMs: number;
|
|
24
24
|
/**
|
|
25
25
|
* True to load from storage only on mount, false (default) to load in
|
|
26
26
|
* the top level scope when the shared state is defined.
|
|
27
27
|
*/
|
|
28
|
-
|
|
29
|
-
}
|
|
28
|
+
lazyLoad: boolean;
|
|
29
|
+
};
|
|
30
30
|
type SharedStateOptions<T> = {
|
|
31
31
|
lazyLoad?: boolean;
|
|
32
32
|
} & StorageOptions<T>;
|
|
@@ -59,4 +59,4 @@ declare function useSharedStateValue<T>(state: SharedState<T>): T;
|
|
|
59
59
|
/** E.g. const setMy = useSharedStateSetter(myState) */
|
|
60
60
|
declare function useSharedStateSetter<T>(state: SharedState<T>): (next: T | ((prev: T) => T)) => void;
|
|
61
61
|
|
|
62
|
-
export { SharedState,
|
|
62
|
+
export { SharedState, SharedStateDefaults, type SharedStateOptions, sharedState, useSharedState, useSharedStateSetter, useSharedStateValue };
|
package/sharedState.d.ts
CHANGED
|
@@ -14,18 +14,18 @@ import { StorageOptions } from "./utils/storage";
|
|
|
14
14
|
/**
|
|
15
15
|
* These values are configurable by the user at any time.
|
|
16
16
|
*/
|
|
17
|
-
export declare
|
|
17
|
+
export declare const SharedStateDefaults: {
|
|
18
18
|
/**
|
|
19
19
|
* Default duration for storage expiration in milliseconds. Initial value
|
|
20
20
|
* is 30 days, but can be changed to anything.
|
|
21
21
|
*/
|
|
22
|
-
|
|
22
|
+
storeExpiryMs: number;
|
|
23
23
|
/**
|
|
24
24
|
* True to load from storage only on mount, false (default) to load in
|
|
25
25
|
* the top level scope when the shared state is defined.
|
|
26
26
|
*/
|
|
27
|
-
|
|
28
|
-
}
|
|
27
|
+
lazyLoad: boolean;
|
|
28
|
+
};
|
|
29
29
|
export type SharedStateOptions<T> = {
|
|
30
30
|
lazyLoad?: boolean;
|
|
31
31
|
} & StorageOptions<T>;
|
package/sharedState.js
CHANGED
|
@@ -21,14 +21,13 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
21
21
|
var sharedState_exports = {};
|
|
22
22
|
__export(sharedState_exports, {
|
|
23
23
|
SharedState: () => SharedState,
|
|
24
|
-
|
|
24
|
+
SharedStateDefaults: () => SharedStateDefaults,
|
|
25
25
|
sharedState: () => sharedState,
|
|
26
26
|
useSharedState: () => useSharedState,
|
|
27
27
|
useSharedStateSetter: () => useSharedStateSetter,
|
|
28
28
|
useSharedStateValue: () => useSharedStateValue
|
|
29
29
|
});
|
|
30
30
|
module.exports = __toCommonJS(sharedState_exports);
|
|
31
|
-
var import_kvStore2 = require("@choksheak/ts-utils/kvStore");
|
|
32
31
|
var import_timeConstants = require("@choksheak/ts-utils/timeConstants");
|
|
33
32
|
var import_react = require("react");
|
|
34
33
|
|
|
@@ -133,17 +132,17 @@ var IndexedDbAdapter = class {
|
|
|
133
132
|
};
|
|
134
133
|
|
|
135
134
|
// src/sharedState.ts
|
|
136
|
-
var
|
|
135
|
+
var SharedStateDefaults = {
|
|
137
136
|
/**
|
|
138
137
|
* Default duration for storage expiration in milliseconds. Initial value
|
|
139
138
|
* is 30 days, but can be changed to anything.
|
|
140
139
|
*/
|
|
141
|
-
|
|
140
|
+
storeExpiryMs: 30 * import_timeConstants.MS_PER_DAY,
|
|
142
141
|
/**
|
|
143
142
|
* True to load from storage only on mount, false (default) to load in
|
|
144
143
|
* the top level scope when the shared state is defined.
|
|
145
144
|
*/
|
|
146
|
-
|
|
145
|
+
lazyLoad: false
|
|
147
146
|
};
|
|
148
147
|
var PubSubStore = class {
|
|
149
148
|
dataByKey = /* @__PURE__ */ new Map();
|
|
@@ -198,11 +197,14 @@ var SharedState = class {
|
|
|
198
197
|
constructor(defaultValue, options) {
|
|
199
198
|
this.defaultValue = defaultValue;
|
|
200
199
|
this.pubSubKey = String(stateKey++);
|
|
201
|
-
this.storageAdapter = getStorageAdapter(
|
|
200
|
+
this.storageAdapter = getStorageAdapter(
|
|
201
|
+
options,
|
|
202
|
+
SharedStateDefaults.storeExpiryMs
|
|
203
|
+
);
|
|
202
204
|
this.setValueBounded = (arg) => this.setValue(arg);
|
|
203
205
|
pubSubStore.setNoNotify(this.pubSubKey, defaultValue);
|
|
204
206
|
const lazyLoad = options?.lazyLoad;
|
|
205
|
-
const lazy = lazyLoad !== void 0 ? lazyLoad :
|
|
207
|
+
const lazy = lazyLoad !== void 0 ? lazyLoad : SharedStateDefaults.lazyLoad;
|
|
206
208
|
if (!lazy) {
|
|
207
209
|
void this.initDefaultValueOnce();
|
|
208
210
|
}
|
|
@@ -265,7 +267,7 @@ function useSharedStateSetter(state) {
|
|
|
265
267
|
// Annotate the CommonJS export names for ESM import in node:
|
|
266
268
|
0 && (module.exports = {
|
|
267
269
|
SharedState,
|
|
268
|
-
|
|
270
|
+
SharedStateDefaults,
|
|
269
271
|
sharedState,
|
|
270
272
|
useSharedState,
|
|
271
273
|
useSharedStateSetter,
|
package/sharedState.mjs
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
// src/sharedState.ts
|
|
2
|
-
import { DEFAULT_EXPIRY_DELTA_MS } from "@choksheak/ts-utils/kvStore";
|
|
3
2
|
import { MS_PER_DAY } from "@choksheak/ts-utils/timeConstants";
|
|
4
3
|
import { useSyncExternalStore } from "react";
|
|
5
4
|
|
|
@@ -104,17 +103,17 @@ var IndexedDbAdapter = class {
|
|
|
104
103
|
};
|
|
105
104
|
|
|
106
105
|
// src/sharedState.ts
|
|
107
|
-
var
|
|
106
|
+
var SharedStateDefaults = {
|
|
108
107
|
/**
|
|
109
108
|
* Default duration for storage expiration in milliseconds. Initial value
|
|
110
109
|
* is 30 days, but can be changed to anything.
|
|
111
110
|
*/
|
|
112
|
-
|
|
111
|
+
storeExpiryMs: 30 * MS_PER_DAY,
|
|
113
112
|
/**
|
|
114
113
|
* True to load from storage only on mount, false (default) to load in
|
|
115
114
|
* the top level scope when the shared state is defined.
|
|
116
115
|
*/
|
|
117
|
-
|
|
116
|
+
lazyLoad: false
|
|
118
117
|
};
|
|
119
118
|
var PubSubStore = class {
|
|
120
119
|
dataByKey = /* @__PURE__ */ new Map();
|
|
@@ -169,11 +168,14 @@ var SharedState = class {
|
|
|
169
168
|
constructor(defaultValue, options) {
|
|
170
169
|
this.defaultValue = defaultValue;
|
|
171
170
|
this.pubSubKey = String(stateKey++);
|
|
172
|
-
this.storageAdapter = getStorageAdapter(
|
|
171
|
+
this.storageAdapter = getStorageAdapter(
|
|
172
|
+
options,
|
|
173
|
+
SharedStateDefaults.storeExpiryMs
|
|
174
|
+
);
|
|
173
175
|
this.setValueBounded = (arg) => this.setValue(arg);
|
|
174
176
|
pubSubStore.setNoNotify(this.pubSubKey, defaultValue);
|
|
175
177
|
const lazyLoad = options?.lazyLoad;
|
|
176
|
-
const lazy = lazyLoad !== void 0 ? lazyLoad :
|
|
178
|
+
const lazy = lazyLoad !== void 0 ? lazyLoad : SharedStateDefaults.lazyLoad;
|
|
177
179
|
if (!lazy) {
|
|
178
180
|
void this.initDefaultValueOnce();
|
|
179
181
|
}
|
|
@@ -235,7 +237,7 @@ function useSharedStateSetter(state) {
|
|
|
235
237
|
}
|
|
236
238
|
export {
|
|
237
239
|
SharedState,
|
|
238
|
-
|
|
240
|
+
SharedStateDefaults,
|
|
239
241
|
sharedState,
|
|
240
242
|
useSharedState,
|
|
241
243
|
useSharedStateSetter,
|
package/src/sharedQuery.ts
CHANGED
|
@@ -1,68 +1,107 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { useCallback, useEffect, useRef } from "react";
|
|
3
|
-
|
|
4
|
-
import {
|
|
5
|
-
|
|
1
|
+
import { MS_PER_DAY } from "@choksheak/ts-utils/timeConstants";
|
|
2
|
+
import { useCallback, useEffect, useMemo, useRef } from "react";
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
SharedState,
|
|
6
|
+
sharedState,
|
|
7
|
+
SharedStateOptions,
|
|
8
|
+
useSharedState,
|
|
9
|
+
} from "./sharedState";
|
|
6
10
|
import { stringifyDeterministicForKeys } from "./utils/stringify";
|
|
7
11
|
|
|
8
|
-
export type FetchFn<TArgs extends unknown[],
|
|
12
|
+
export type FetchFn<TArgs extends unknown[], TData> = (
|
|
9
13
|
...args: TArgs
|
|
10
|
-
) => Promise<
|
|
14
|
+
) => Promise<TData> | TData;
|
|
11
15
|
|
|
12
|
-
export type
|
|
13
|
-
|
|
14
|
-
|
|
16
|
+
export type QueryStateValue<TData> = {
|
|
17
|
+
loading: boolean;
|
|
18
|
+
data?: TData;
|
|
19
|
+
dataUpdatedMs?: number;
|
|
20
|
+
error?: Error;
|
|
21
|
+
errorUpdatedMs?: number;
|
|
15
22
|
};
|
|
16
23
|
|
|
17
|
-
export type
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
24
|
+
export type UseQueryResult<TData> = QueryStateValue<TData> & {
|
|
25
|
+
refetch: () => Promise<TData>;
|
|
26
|
+
setData: (data: TData, dataUpdatedMs?: number) => void;
|
|
27
|
+
deleteData: () => void;
|
|
21
28
|
};
|
|
22
29
|
|
|
30
|
+
export type PersistTo = "localStorage" | "indexedDb";
|
|
31
|
+
|
|
23
32
|
/**
|
|
24
|
-
* The full query state is a record of all the fetch
|
|
33
|
+
* The full query state is a record of all the fetch states mapped by
|
|
25
34
|
* query key to the state entry.
|
|
26
35
|
*/
|
|
27
|
-
export type SharedQueryState<
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
36
|
+
export type SharedQueryState<TData> = Record<string, QueryStateValue<TData>>;
|
|
37
|
+
|
|
38
|
+
export type SharedQueryOptions<TArgs extends unknown[], TData> = {
|
|
39
|
+
// The name could have been auto-generated, but we let the user give us a
|
|
40
|
+
// human-readable name that can be identified quickly in the logs.
|
|
41
|
+
queryName: string;
|
|
42
|
+
|
|
43
|
+
// Function to fetch data.
|
|
44
|
+
queryFn: FetchFn<TArgs, TData>;
|
|
31
45
|
|
|
32
|
-
export type SharedQueryOptions<TResult> = {
|
|
33
46
|
// ms before data considered stale; 0 means never stale.
|
|
34
47
|
staleMs?: number;
|
|
48
|
+
|
|
35
49
|
// ms before data is removed from cache; 0 means never expire.
|
|
36
50
|
expiryMs?: number;
|
|
51
|
+
|
|
37
52
|
// Trigger background re-fetch if stale.
|
|
38
|
-
|
|
39
|
-
|
|
53
|
+
refetchOnStale?: boolean;
|
|
54
|
+
|
|
55
|
+
// Shortcut to set the localStorageKey or indexedDbKey automatically from
|
|
56
|
+
// queryName. This is important because it avoids requiring the user to
|
|
57
|
+
// specify the same queryName twice everytime.
|
|
58
|
+
persistTo?: PersistTo;
|
|
59
|
+
} & SharedStateOptions<SharedQueryState<TData>>;
|
|
60
|
+
|
|
61
|
+
/** Users can override these values globally. */
|
|
62
|
+
export const SharedQueryDefaults = {
|
|
63
|
+
staleMs: 0,
|
|
64
|
+
expiryMs: 30 * MS_PER_DAY,
|
|
65
|
+
persistTo: undefined as PersistTo | undefined,
|
|
66
|
+
};
|
|
40
67
|
|
|
41
|
-
|
|
68
|
+
export class SharedQuery<TArgs extends unknown[], TData> {
|
|
69
|
+
private readonly inflightPromises = new Map<string, Promise<TData>>();
|
|
70
|
+
public readonly queryState: SharedState<SharedQueryState<TData>>;
|
|
42
71
|
|
|
43
|
-
|
|
44
|
-
private readonly
|
|
45
|
-
public readonly queryState: SharedState<SharedQueryState<TResult>>;
|
|
72
|
+
public readonly queryName: string;
|
|
73
|
+
private readonly queryFn: FetchFn<TArgs, TData>;
|
|
46
74
|
public readonly expiryMs: number;
|
|
47
75
|
public readonly staleMs: number;
|
|
48
|
-
public readonly
|
|
76
|
+
public readonly refetchOnStale: boolean;
|
|
77
|
+
|
|
78
|
+
public constructor(options: SharedQueryOptions<TArgs, TData>) {
|
|
79
|
+
this.queryName = options.queryName;
|
|
80
|
+
|
|
81
|
+
this.queryFn = options.queryFn;
|
|
49
82
|
|
|
50
|
-
public constructor(
|
|
51
|
-
public readonly queryName: string,
|
|
52
|
-
private readonly fetchFn: FetchFn<TArgs, TResult>,
|
|
53
|
-
options?: SharedQueryOptions<TResult>,
|
|
54
|
-
) {
|
|
55
83
|
this.expiryMs =
|
|
56
84
|
options?.expiryMs !== undefined
|
|
57
85
|
? options?.expiryMs
|
|
58
|
-
:
|
|
86
|
+
: SharedQueryDefaults.expiryMs;
|
|
59
87
|
|
|
60
88
|
this.staleMs =
|
|
61
|
-
options?.staleMs !== undefined
|
|
89
|
+
options?.staleMs !== undefined
|
|
90
|
+
? options?.staleMs
|
|
91
|
+
: SharedQueryDefaults.staleMs;
|
|
92
|
+
|
|
93
|
+
this.refetchOnStale = Boolean(options?.refetchOnStale);
|
|
62
94
|
|
|
63
|
-
|
|
95
|
+
// Apply shortcut when using `persistTo`.
|
|
96
|
+
const persistTo = options.persistTo ?? SharedQueryDefaults.persistTo;
|
|
97
|
+
|
|
98
|
+
if (persistTo === "localStorage") {
|
|
99
|
+
options.localStorageKey = options.queryName;
|
|
100
|
+
} else if (persistTo === "indexedDb") {
|
|
101
|
+
options.indexedDbKey = options.queryName;
|
|
102
|
+
}
|
|
64
103
|
|
|
65
|
-
this.queryState = sharedState<SharedQueryState<
|
|
104
|
+
this.queryState = sharedState<SharedQueryState<TData>>(
|
|
66
105
|
{},
|
|
67
106
|
{
|
|
68
107
|
...options,
|
|
@@ -76,21 +115,48 @@ export class SharedQuery<TArgs extends unknown[], TResult> {
|
|
|
76
115
|
return this.queryName + ":" + stringifyDeterministicForKeys(args);
|
|
77
116
|
}
|
|
78
117
|
|
|
79
|
-
private isStale(
|
|
118
|
+
private isStale(dataUpdatedMs: number): boolean {
|
|
80
119
|
if (this.staleMs === 0) return true; // always stale
|
|
81
120
|
|
|
82
|
-
const ageMs = Date.now() -
|
|
121
|
+
const ageMs = Date.now() - dataUpdatedMs;
|
|
83
122
|
return ageMs > this.staleMs;
|
|
84
123
|
}
|
|
85
124
|
|
|
86
|
-
private isExpired(
|
|
125
|
+
private isExpired(dataUpdatedMs: number): boolean {
|
|
87
126
|
if (this.expiryMs === 0) return false; // never expire
|
|
88
127
|
|
|
89
|
-
const ageMs = Date.now() -
|
|
128
|
+
const ageMs = Date.now() - dataUpdatedMs;
|
|
90
129
|
return ageMs > this.expiryMs;
|
|
91
130
|
}
|
|
92
131
|
|
|
93
|
-
|
|
132
|
+
public async getCachedOrFetch(...args: TArgs): Promise<TData> {
|
|
133
|
+
const queryKey = this.getQueryKey(args);
|
|
134
|
+
const cached = this.queryState.getSnapshot()?.[queryKey];
|
|
135
|
+
|
|
136
|
+
if (cached?.data && !this.isExpired(cached.dataUpdatedMs ?? 0)) {
|
|
137
|
+
// Return cached value if not stale.
|
|
138
|
+
if (!this.isStale(cached.dataUpdatedMs ?? 0)) {
|
|
139
|
+
console.log(
|
|
140
|
+
`Return fresh data ${queryKey} from cache without fetching`,
|
|
141
|
+
);
|
|
142
|
+
return cached.data;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// If stale, optionally update the data in the background.
|
|
146
|
+
if (this.refetchOnStale) {
|
|
147
|
+
console.log(`refetchOnStale for ${queryKey}`);
|
|
148
|
+
void this.dedupedFetch(queryKey, ...args);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Still return stale data immediately.
|
|
152
|
+
console.log(`Returning stale data for ${queryKey}`);
|
|
153
|
+
return cached.data;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return await this.dedupedFetch(queryKey, ...args);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
private dedupedFetch(queryKey: string, ...args: TArgs): Promise<TData> {
|
|
94
160
|
const inflightPromise = this.inflightPromises.get(queryKey);
|
|
95
161
|
|
|
96
162
|
// De-duplicate in-flight requests
|
|
@@ -100,87 +166,64 @@ export class SharedQuery<TArgs extends unknown[], TResult> {
|
|
|
100
166
|
}
|
|
101
167
|
|
|
102
168
|
// Fetch new data
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
const promise = this.fetchFn(...args)
|
|
106
|
-
.then((result) => {
|
|
107
|
-
console.log(`Successfully fetched ${queryKey}`);
|
|
108
|
-
this.setCache(queryKey, result);
|
|
109
|
-
return result;
|
|
110
|
-
})
|
|
111
|
-
.catch((e) => {
|
|
112
|
-
console.error(`Failed to fetch ${queryKey}: ${e}`);
|
|
113
|
-
throw e;
|
|
114
|
-
})
|
|
115
|
-
.finally(() => {
|
|
116
|
-
this.inflightPromises.delete(queryKey);
|
|
117
|
-
});
|
|
169
|
+
const promise = this.startFetching(queryKey, args);
|
|
118
170
|
|
|
119
171
|
this.inflightPromises.set(queryKey, promise);
|
|
120
172
|
return promise;
|
|
121
173
|
}
|
|
122
174
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
const cached = this.queryState.getSnapshot()?.[queryKey];
|
|
126
|
-
|
|
127
|
-
if (cached?.data && !this.isExpired(cached.data)) {
|
|
128
|
-
// Return cached value if not stale.
|
|
129
|
-
if (!this.isStale(cached.data)) {
|
|
130
|
-
console.log(
|
|
131
|
-
`Return fresh data ${queryKey} from cache without fetching`,
|
|
132
|
-
);
|
|
133
|
-
return cached.data.value;
|
|
134
|
-
}
|
|
175
|
+
private async startFetching(queryKey: string, args: TArgs) {
|
|
176
|
+
console.log(`Start fetching ${queryKey}`);
|
|
135
177
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
console.log(`revalidateOnStale for ${queryKey}`);
|
|
139
|
-
void this.fetchNoCaching(queryKey, ...args);
|
|
140
|
-
}
|
|
178
|
+
try {
|
|
179
|
+
const data = await this.queryFn(...args);
|
|
141
180
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
return
|
|
181
|
+
console.log(`Successfully fetched ${queryKey}`);
|
|
182
|
+
this.setData(queryKey, data, Date.now());
|
|
183
|
+
return data;
|
|
184
|
+
} catch (e) {
|
|
185
|
+
console.error(`Failed to fetch ${queryKey}: ${e}`);
|
|
186
|
+
throw e;
|
|
187
|
+
} finally {
|
|
188
|
+
this.inflightPromises.delete(queryKey);
|
|
145
189
|
}
|
|
146
|
-
|
|
147
|
-
return await this.fetchNoCaching(queryKey, ...args);
|
|
148
190
|
}
|
|
149
191
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
192
|
+
/** Set the data directly if the user obtained it from somewhere else. */
|
|
193
|
+
public async setData(
|
|
194
|
+
queryKey: string,
|
|
195
|
+
data: TData,
|
|
196
|
+
dataUpdatedMs = Date.now(),
|
|
197
|
+
): Promise<void> {
|
|
198
|
+
const entry: QueryStateValue<TData> = {
|
|
153
199
|
loading: false,
|
|
154
|
-
|
|
200
|
+
data,
|
|
201
|
+
dataUpdatedMs,
|
|
155
202
|
};
|
|
156
203
|
|
|
157
204
|
this.queryState.setValue((prev) => ({ ...prev, [queryKey]: entry }));
|
|
158
205
|
}
|
|
159
206
|
|
|
160
207
|
/** Do a new fetch even when the data is already cached. */
|
|
161
|
-
public async updateFromSource(...args: TArgs): Promise<
|
|
208
|
+
public async updateFromSource(...args: TArgs): Promise<TData> {
|
|
162
209
|
const key = this.getQueryKey(args);
|
|
163
|
-
return await this.
|
|
210
|
+
return await this.dedupedFetch(key, ...args);
|
|
164
211
|
}
|
|
165
212
|
|
|
166
|
-
/** Delete the
|
|
167
|
-
public
|
|
168
|
-
|
|
169
|
-
const queryKey = this.getQueryKey(args);
|
|
213
|
+
/** Delete the cached data for one set of arguments. */
|
|
214
|
+
public deleteData(args: TArgs): void {
|
|
215
|
+
const queryKey = this.getQueryKey(args);
|
|
170
216
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
217
|
+
const record = this.queryState.getSnapshot();
|
|
218
|
+
const clone = { ...record };
|
|
219
|
+
delete clone[queryKey];
|
|
220
|
+
this.queryState.setValue(clone);
|
|
175
221
|
|
|
176
|
-
|
|
177
|
-
} else {
|
|
178
|
-
this.clearAll();
|
|
179
|
-
}
|
|
222
|
+
this.inflightPromises.delete(queryKey);
|
|
180
223
|
}
|
|
181
224
|
|
|
182
|
-
/** Delete
|
|
183
|
-
public
|
|
225
|
+
/** Delete all currently cached data & all inflight promises. */
|
|
226
|
+
public clear(): void {
|
|
184
227
|
this.queryState.delete();
|
|
185
228
|
this.inflightPromises.clear();
|
|
186
229
|
}
|
|
@@ -195,36 +238,31 @@ const seenQueryNames = new Set<string>();
|
|
|
195
238
|
* Each `queryName` must be unique, and can only be declared once, most often
|
|
196
239
|
* in the top level scope. The queryName is used for logging only.
|
|
197
240
|
*/
|
|
198
|
-
export function sharedQuery<TArgs extends unknown[],
|
|
241
|
+
export function sharedQuery<TArgs extends unknown[], TData>(
|
|
199
242
|
// The name could have been auto-generated, but we let the user give us a
|
|
200
243
|
// human-readable name that can be identified quickly in the logs.
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
options
|
|
204
|
-
|
|
205
|
-
if (seenQueryNames.has(queryName)) {
|
|
206
|
-
throw new Error(`Duplicate shared query "${queryName}"`);
|
|
244
|
+
options: SharedQueryOptions<TArgs, TData>,
|
|
245
|
+
): SharedQuery<TArgs, TData> {
|
|
246
|
+
if (seenQueryNames.has(options.queryName)) {
|
|
247
|
+
throw new Error(`Duplicate shared query "${options.queryName}"`);
|
|
207
248
|
}
|
|
208
|
-
seenQueryNames.add(queryName);
|
|
249
|
+
seenQueryNames.add(options.queryName);
|
|
209
250
|
|
|
210
|
-
return new SharedQuery(
|
|
251
|
+
return new SharedQuery(options);
|
|
211
252
|
}
|
|
212
253
|
|
|
213
|
-
const DEFAULT_QUERY_STATE_ENTRY: QueryStateValue<unknown> = {
|
|
214
|
-
data: null,
|
|
215
|
-
loading: true,
|
|
216
|
-
error: null,
|
|
217
|
-
};
|
|
254
|
+
const DEFAULT_QUERY_STATE_ENTRY: QueryStateValue<unknown> = { loading: true };
|
|
218
255
|
|
|
219
256
|
/**
|
|
220
257
|
* React hook to make use of a shared query inside any React component.
|
|
221
258
|
*
|
|
222
|
-
* const users =
|
|
259
|
+
* const users = useSharedQuery(usersQuery, ["123"]);
|
|
223
260
|
*/
|
|
224
|
-
export function
|
|
225
|
-
query: SharedQuery<TArgs,
|
|
226
|
-
|
|
227
|
-
|
|
261
|
+
export function useSharedQuery<TArgs extends unknown[], TData>(
|
|
262
|
+
query: SharedQuery<TArgs, TData>,
|
|
263
|
+
// Default to use no arguments.
|
|
264
|
+
args: TArgs = [] as unknown as TArgs,
|
|
265
|
+
): UseQueryResult<TData> {
|
|
228
266
|
const [queryState, setQueryState] = useSharedState(query.queryState);
|
|
229
267
|
|
|
230
268
|
const isMounted = useRef(true);
|
|
@@ -241,27 +279,23 @@ export function useQuery<TArgs extends unknown[], TResult>(
|
|
|
241
279
|
setQueryState((prev) => {
|
|
242
280
|
const clone = { ...prev };
|
|
243
281
|
clone[queryKey] = {
|
|
244
|
-
|
|
282
|
+
...(clone[queryKey] ?? {}),
|
|
245
283
|
loading: true,
|
|
246
|
-
error: null,
|
|
247
284
|
};
|
|
248
285
|
return clone;
|
|
249
286
|
});
|
|
250
287
|
|
|
251
288
|
try {
|
|
252
|
-
const
|
|
289
|
+
const data = await query.getCachedOrFetch(...args);
|
|
253
290
|
|
|
254
291
|
if (isMounted.current) {
|
|
255
292
|
setQueryState((prev) => {
|
|
256
293
|
const clone = { ...prev };
|
|
257
|
-
const now = Date.now();
|
|
258
294
|
clone[queryKey] = {
|
|
259
|
-
|
|
260
|
-
value,
|
|
261
|
-
updatedMs: now,
|
|
262
|
-
},
|
|
295
|
+
// Don't keep old error.
|
|
263
296
|
loading: false,
|
|
264
|
-
|
|
297
|
+
data,
|
|
298
|
+
dataUpdatedMs: Date.now(),
|
|
265
299
|
};
|
|
266
300
|
return clone;
|
|
267
301
|
});
|
|
@@ -276,9 +310,11 @@ export function useQuery<TArgs extends unknown[], TResult>(
|
|
|
276
310
|
setQueryState((prev) => {
|
|
277
311
|
const clone = { ...prev };
|
|
278
312
|
clone[queryKey] = {
|
|
279
|
-
|
|
313
|
+
// Keep old data.
|
|
314
|
+
...(clone[queryKey] ?? {}),
|
|
280
315
|
loading: false,
|
|
281
316
|
error,
|
|
317
|
+
errorUpdatedMs: Date.now(),
|
|
282
318
|
};
|
|
283
319
|
return clone;
|
|
284
320
|
});
|
|
@@ -296,8 +332,22 @@ export function useQuery<TArgs extends unknown[], TResult>(
|
|
|
296
332
|
};
|
|
297
333
|
}, [execute]);
|
|
298
334
|
|
|
299
|
-
return (
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
335
|
+
return useMemo(() => {
|
|
336
|
+
const state =
|
|
337
|
+
queryState[queryKey] ??
|
|
338
|
+
(DEFAULT_QUERY_STATE_ENTRY as QueryStateValue<TData>);
|
|
339
|
+
|
|
340
|
+
return {
|
|
341
|
+
...state,
|
|
342
|
+
refetch: () => {
|
|
343
|
+
return query.updateFromSource(...args);
|
|
344
|
+
},
|
|
345
|
+
setData: (data: TData, dataUpdatedMs?: number) => {
|
|
346
|
+
query.setData(queryKey, data, dataUpdatedMs ?? Date.now());
|
|
347
|
+
},
|
|
348
|
+
deleteData: () => {
|
|
349
|
+
query.deleteData(args);
|
|
350
|
+
},
|
|
351
|
+
};
|
|
352
|
+
}, [query, queryKey, queryState]);
|
|
303
353
|
}
|
package/src/sharedState.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { DEFAULT_EXPIRY_DELTA_MS } from "@choksheak/ts-utils/kvStore";
|
|
2
1
|
import { MS_PER_DAY } from "@choksheak/ts-utils/timeConstants";
|
|
3
2
|
import { useSyncExternalStore } from "react";
|
|
4
3
|
|
|
@@ -26,19 +25,19 @@ import {
|
|
|
26
25
|
/**
|
|
27
26
|
* These values are configurable by the user at any time.
|
|
28
27
|
*/
|
|
29
|
-
export
|
|
28
|
+
export const SharedStateDefaults = {
|
|
30
29
|
/**
|
|
31
30
|
* Default duration for storage expiration in milliseconds. Initial value
|
|
32
31
|
* is 30 days, but can be changed to anything.
|
|
33
32
|
*/
|
|
34
|
-
|
|
33
|
+
storeExpiryMs: 30 * MS_PER_DAY,
|
|
35
34
|
|
|
36
35
|
/**
|
|
37
36
|
* True to load from storage only on mount, false (default) to load in
|
|
38
37
|
* the top level scope when the shared state is defined.
|
|
39
38
|
*/
|
|
40
|
-
|
|
41
|
-
}
|
|
39
|
+
lazyLoad: false,
|
|
40
|
+
};
|
|
42
41
|
|
|
43
42
|
/************************************************************************/
|
|
44
43
|
/* Shared state implementation */
|
|
@@ -131,7 +130,10 @@ export class SharedState<T> {
|
|
|
131
130
|
this.pubSubKey = String(stateKey++);
|
|
132
131
|
|
|
133
132
|
// Use max of 1 storage adapter per shared state.
|
|
134
|
-
this.storageAdapter = getStorageAdapter(
|
|
133
|
+
this.storageAdapter = getStorageAdapter(
|
|
134
|
+
options,
|
|
135
|
+
SharedStateDefaults.storeExpiryMs,
|
|
136
|
+
);
|
|
135
137
|
|
|
136
138
|
// Same as this.setValue, but binding the `this` reference.
|
|
137
139
|
this.setValueBounded = (arg) => this.setValue(arg);
|
|
@@ -141,7 +143,8 @@ export class SharedState<T> {
|
|
|
141
143
|
pubSubStore.setNoNotify(this.pubSubKey, defaultValue);
|
|
142
144
|
|
|
143
145
|
const lazyLoad = options?.lazyLoad;
|
|
144
|
-
const lazy =
|
|
146
|
+
const lazy =
|
|
147
|
+
lazyLoad !== undefined ? lazyLoad : SharedStateDefaults.lazyLoad;
|
|
145
148
|
|
|
146
149
|
if (!lazy) {
|
|
147
150
|
void this.initDefaultValueOnce();
|