@choksheak/react-utils 0.3.6 → 0.3.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/NoSSR.cjs +37 -0
- package/NoSSR.d.mts +24 -0
- package/NoSSR.d.ts +20 -0
- package/NoSSR.min.cjs +2 -0
- package/NoSSR.min.cjs.map +1 -0
- package/NoSSR.min.mjs +2 -0
- package/NoSSR.min.mjs.map +1 -0
- package/NoSSR.mjs +12 -0
- package/README.md +10 -1
- package/fetcher.cjs +3 -3
- package/fetcher.d.mts +5 -4
- package/fetcher.d.ts +4 -3
- package/fetcher.min.cjs +1 -1
- package/fetcher.min.cjs.map +1 -1
- package/fetcher.min.mjs +1 -1
- package/fetcher.min.mjs.map +1 -1
- package/fetcher.mjs +2 -2
- package/package.json +21 -3
- package/sharedQuery.cjs +486 -486
- package/sharedQuery.d.mts +64 -61
- package/sharedQuery.d.ts +62 -59
- package/sharedQuery.min.cjs +1 -1
- package/sharedQuery.min.cjs.map +1 -1
- package/sharedQuery.min.mjs +1 -1
- package/sharedQuery.min.mjs.map +1 -1
- package/sharedQuery.mjs +487 -486
- package/sharedState.cjs +201 -199
- package/sharedState.d.mts +48 -38
- package/sharedState.d.ts +45 -36
- package/sharedState.min.cjs +1 -1
- package/sharedState.min.cjs.map +1 -1
- package/sharedState.min.mjs +1 -1
- package/sharedState.min.mjs.map +1 -1
- package/sharedState.mjs +202 -199
- package/storage.cjs +79 -65
- package/storage.d.mts +39 -2
- package/storage.d.ts +37 -1
- package/storage.min.cjs +1 -1
- package/storage.min.cjs.map +1 -1
- package/storage.min.mjs +1 -1
- package/storage.min.mjs.map +1 -1
- package/storage.mjs +77 -65
- package/stringify.d.mts +9 -0
- package/stringify.d.ts +9 -0
- package/stringify.min.cjs.map +1 -1
- package/stringify.min.mjs.map +1 -1
- package/useDeepMemo.cjs +1 -1
- package/useDeepMemo.d.mts +11 -0
- package/useDeepMemo.d.ts +11 -0
- package/useDeepMemo.min.cjs +1 -1
- package/useDeepMemo.min.cjs.map +1 -1
- package/useDeepMemo.min.mjs +1 -1
- package/useDeepMemo.min.mjs.map +1 -1
- package/useDeepMemo.mjs +1 -1
package/sharedQuery.mjs
CHANGED
|
@@ -15,7 +15,7 @@ import { useSyncExternalStore } from "react";
|
|
|
15
15
|
|
|
16
16
|
// src/storage.ts
|
|
17
17
|
import { kvStore } from "@choksheak/ts-utils/kvStore";
|
|
18
|
-
import {
|
|
18
|
+
import { createLocalStore } from "@choksheak/ts-utils/localStore";
|
|
19
19
|
var IS_NOT_BROWSER = typeof window === "undefined";
|
|
20
20
|
function getStorageAdapter(options, defaultStoreExpiryMs) {
|
|
21
21
|
const { store } = options ?? {};
|
|
@@ -24,70 +24,12 @@ function getStorageAdapter(options, defaultStoreExpiryMs) {
|
|
|
24
24
|
}
|
|
25
25
|
if ("persistTo" in store && typeof store.persistTo === "string" && "key" in store && typeof store.key === "string") {
|
|
26
26
|
const expiryMs = store.expiryMs || defaultStoreExpiryMs;
|
|
27
|
-
const
|
|
27
|
+
const config = { ...store, expiryMs };
|
|
28
28
|
switch (store.persistTo) {
|
|
29
|
-
case "localStorage":
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
return {
|
|
34
|
-
set: (key, value) => {
|
|
35
|
-
if (IS_NOT_BROWSER) return;
|
|
36
|
-
adapter.set(key, value);
|
|
37
|
-
},
|
|
38
|
-
get: (key) => {
|
|
39
|
-
if (IS_NOT_BROWSER) return void 0;
|
|
40
|
-
const value = adapter.get(key);
|
|
41
|
-
if (isValid && !isValid(value)) {
|
|
42
|
-
console.warn(
|
|
43
|
-
`localStorage adapter: Auto-discard invalid value for ${key}: ${JSON.stringify(value)}`
|
|
44
|
-
);
|
|
45
|
-
adapter.delete(key);
|
|
46
|
-
return void 0;
|
|
47
|
-
}
|
|
48
|
-
return value;
|
|
49
|
-
},
|
|
50
|
-
delete: (key) => {
|
|
51
|
-
if (IS_NOT_BROWSER) return;
|
|
52
|
-
adapter.delete(key);
|
|
53
|
-
},
|
|
54
|
-
clear: () => {
|
|
55
|
-
if (IS_NOT_BROWSER) return;
|
|
56
|
-
adapter.clear();
|
|
57
|
-
}
|
|
58
|
-
};
|
|
59
|
-
}
|
|
60
|
-
case "indexedDb": {
|
|
61
|
-
const keyPrefix = store.key + ":";
|
|
62
|
-
const isValid2 = store.isValid;
|
|
63
|
-
return {
|
|
64
|
-
set: async (key, value) => {
|
|
65
|
-
if (IS_NOT_BROWSER) return;
|
|
66
|
-
await kvStore.set(keyPrefix + key, value, expiryMs);
|
|
67
|
-
},
|
|
68
|
-
get: async (key) => {
|
|
69
|
-
if (IS_NOT_BROWSER) return void 0;
|
|
70
|
-
const fullKey = keyPrefix + key;
|
|
71
|
-
const value = await kvStore.get(fullKey);
|
|
72
|
-
if (isValid2 && !isValid2(value)) {
|
|
73
|
-
console.warn(
|
|
74
|
-
`indexedDb adapter: Auto-discard invalid value for ${key}: ${JSON.stringify(value)}`
|
|
75
|
-
);
|
|
76
|
-
await kvStore.delete(fullKey);
|
|
77
|
-
return void 0;
|
|
78
|
-
}
|
|
79
|
-
return value;
|
|
80
|
-
},
|
|
81
|
-
delete: async (key) => {
|
|
82
|
-
if (IS_NOT_BROWSER) return;
|
|
83
|
-
await kvStore.delete(keyPrefix + key);
|
|
84
|
-
},
|
|
85
|
-
clear: async () => {
|
|
86
|
-
if (IS_NOT_BROWSER) return;
|
|
87
|
-
await kvStore.clear();
|
|
88
|
-
}
|
|
89
|
-
};
|
|
90
|
-
}
|
|
29
|
+
case "localStorage":
|
|
30
|
+
return localStoreAdapter(config);
|
|
31
|
+
case "indexedDb":
|
|
32
|
+
return kvStoreAdapter(config);
|
|
91
33
|
default:
|
|
92
34
|
store.persistTo;
|
|
93
35
|
throw new Error(`Unknown store.adapter ${store.persistTo}`);
|
|
@@ -95,6 +37,74 @@ function getStorageAdapter(options, defaultStoreExpiryMs) {
|
|
|
95
37
|
}
|
|
96
38
|
return store;
|
|
97
39
|
}
|
|
40
|
+
function localStoreAdapter({
|
|
41
|
+
key,
|
|
42
|
+
expiryMs,
|
|
43
|
+
isValid
|
|
44
|
+
}) {
|
|
45
|
+
const adapter = createLocalStore(key, { defaultExpiryMs: expiryMs });
|
|
46
|
+
return {
|
|
47
|
+
set: (key2, value) => {
|
|
48
|
+
if (IS_NOT_BROWSER) return;
|
|
49
|
+
adapter.set(key2, value);
|
|
50
|
+
},
|
|
51
|
+
get: (key2) => {
|
|
52
|
+
if (IS_NOT_BROWSER) return void 0;
|
|
53
|
+
const value = adapter.get(key2);
|
|
54
|
+
if (isValid && !isValid(value)) {
|
|
55
|
+
console.warn(
|
|
56
|
+
`localStorage adapter: Auto-discard invalid value for ${key2}: ${JSON.stringify(value)}`
|
|
57
|
+
);
|
|
58
|
+
adapter.delete(key2);
|
|
59
|
+
return void 0;
|
|
60
|
+
}
|
|
61
|
+
return value;
|
|
62
|
+
},
|
|
63
|
+
delete: (key2) => {
|
|
64
|
+
if (IS_NOT_BROWSER) return;
|
|
65
|
+
adapter.delete(key2);
|
|
66
|
+
},
|
|
67
|
+
clear: () => {
|
|
68
|
+
if (IS_NOT_BROWSER) return;
|
|
69
|
+
adapter.clear();
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
function kvStoreAdapter({
|
|
74
|
+
key,
|
|
75
|
+
expiryMs,
|
|
76
|
+
isValid
|
|
77
|
+
}) {
|
|
78
|
+
const keyPrefix = key + ":";
|
|
79
|
+
return {
|
|
80
|
+
set: async (key2, value) => {
|
|
81
|
+
if (IS_NOT_BROWSER) return;
|
|
82
|
+
await kvStore.set(keyPrefix + key2, value, expiryMs);
|
|
83
|
+
},
|
|
84
|
+
get: async (key2) => {
|
|
85
|
+
if (IS_NOT_BROWSER) return void 0;
|
|
86
|
+
const fullKey = keyPrefix + key2;
|
|
87
|
+
const value = await kvStore.get(fullKey);
|
|
88
|
+
if (value !== void 0 && isValid && !isValid(value)) {
|
|
89
|
+
console.warn(
|
|
90
|
+
`indexedDb adapter: Auto-discard invalid value for ${key2}:`,
|
|
91
|
+
value
|
|
92
|
+
);
|
|
93
|
+
await kvStore.delete(fullKey);
|
|
94
|
+
return void 0;
|
|
95
|
+
}
|
|
96
|
+
return value;
|
|
97
|
+
},
|
|
98
|
+
delete: async (key2) => {
|
|
99
|
+
if (IS_NOT_BROWSER) return;
|
|
100
|
+
await kvStore.delete(keyPrefix + key2);
|
|
101
|
+
},
|
|
102
|
+
clear: async () => {
|
|
103
|
+
if (IS_NOT_BROWSER) return;
|
|
104
|
+
await kvStore.clear();
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
}
|
|
98
108
|
|
|
99
109
|
// src/sharedState.ts
|
|
100
110
|
var SharedStateConfig = {
|
|
@@ -109,41 +119,11 @@ var SharedStateConfig = {
|
|
|
109
119
|
*/
|
|
110
120
|
lazyLoad: false
|
|
111
121
|
};
|
|
112
|
-
|
|
113
|
-
dataByKey = /* @__PURE__ */ new Map();
|
|
114
|
-
subscribersByKey = /* @__PURE__ */ new Map();
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
get(key) {
|
|
118
|
-
return this.dataByKey.get(key);
|
|
119
|
-
}
|
|
120
|
-
set(key, value) {
|
|
121
|
-
const prev = this.dataByKey.get(key);
|
|
122
|
-
if (prev === value) return;
|
|
123
|
-
this.dataByKey.set(key, value);
|
|
124
|
-
this.notify(key, value, prev);
|
|
125
|
-
}
|
|
126
|
-
delete(key) {
|
|
127
|
-
this.dataByKey.delete(key);
|
|
128
|
-
}
|
|
129
|
-
setNoNotify(key, value) {
|
|
130
|
-
this.dataByKey.set(key, value);
|
|
131
|
-
}
|
|
132
|
-
// Subscribe to changes of a specific key
|
|
133
|
-
subscribe(key, subscriber) {
|
|
134
|
-
let subscribers = this.subscribersByKey.get(key);
|
|
135
|
-
if (!subscribers) {
|
|
136
|
-
subscribers = /* @__PURE__ */ new Set();
|
|
137
|
-
this.subscribersByKey.set(key, subscribers);
|
|
138
|
-
}
|
|
139
|
-
subscribers.add(subscriber);
|
|
140
|
-
return () => {
|
|
141
|
-
subscribers.delete(subscriber);
|
|
142
|
-
if (subscribers.size === 0) this.subscribersByKey.delete(key);
|
|
143
|
-
};
|
|
144
|
-
}
|
|
145
|
-
notify(key, next, prev) {
|
|
146
|
-
const subscribers = this.subscribersByKey.get(key);
|
|
122
|
+
function createPubSubStore() {
|
|
123
|
+
const dataByKey = /* @__PURE__ */ new Map();
|
|
124
|
+
const subscribersByKey = /* @__PURE__ */ new Map();
|
|
125
|
+
function notify(key, next, prev) {
|
|
126
|
+
const subscribers = subscribersByKey.get(key);
|
|
147
127
|
if (!subscribers) return;
|
|
148
128
|
const copy = Array.from(subscribers.values());
|
|
149
129
|
for (const subscriber of copy) {
|
|
@@ -154,116 +134,139 @@ var PubSubStore = class {
|
|
|
154
134
|
}
|
|
155
135
|
}
|
|
156
136
|
}
|
|
157
|
-
|
|
158
|
-
|
|
137
|
+
return {
|
|
138
|
+
// This could be undefined if T includes undefined, else it should never
|
|
139
|
+
// return undefined because the user has to always specify the default value.
|
|
140
|
+
get(key) {
|
|
141
|
+
return dataByKey.get(key);
|
|
142
|
+
},
|
|
143
|
+
set(key, value) {
|
|
144
|
+
const prev = dataByKey.get(key);
|
|
145
|
+
if (prev === value) return;
|
|
146
|
+
dataByKey.set(key, value);
|
|
147
|
+
notify(key, value, prev);
|
|
148
|
+
},
|
|
149
|
+
delete(key) {
|
|
150
|
+
dataByKey.delete(key);
|
|
151
|
+
},
|
|
152
|
+
setNoNotify(key, value) {
|
|
153
|
+
dataByKey.set(key, value);
|
|
154
|
+
},
|
|
155
|
+
// Subscribe to changes of a specific key
|
|
156
|
+
subscribe(key, subscriber) {
|
|
157
|
+
let subscribers = subscribersByKey.get(key);
|
|
158
|
+
if (!subscribers) {
|
|
159
|
+
subscribers = /* @__PURE__ */ new Set();
|
|
160
|
+
subscribersByKey.set(key, subscribers);
|
|
161
|
+
}
|
|
162
|
+
subscribers.add(subscriber);
|
|
163
|
+
return () => {
|
|
164
|
+
subscribers.delete(subscriber);
|
|
165
|
+
if (subscribers.size === 0) subscribersByKey.delete(key);
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
var pubSubStore = createPubSubStore();
|
|
159
171
|
var STORAGE_KEY = "state";
|
|
160
172
|
var stateKey = 0;
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
173
|
+
function sharedState(defaultValue, options) {
|
|
174
|
+
let readyResolve = () => {
|
|
175
|
+
};
|
|
176
|
+
const readyPromise = new Promise((resolve) => {
|
|
177
|
+
readyResolve = resolve;
|
|
178
|
+
});
|
|
179
|
+
const resolveReadyPromise = readyResolve;
|
|
180
|
+
const pubSubKey = String(stateKey++);
|
|
181
|
+
const storageAdapter = getStorageAdapter(
|
|
182
|
+
options,
|
|
183
|
+
SharedStateConfig.storeExpiryMs
|
|
184
|
+
);
|
|
185
|
+
pubSubStore.setNoNotify(pubSubKey, defaultValue);
|
|
186
|
+
const lazyLoad = options?.lazyLoad;
|
|
187
|
+
const lazy = lazyLoad !== void 0 ? lazyLoad : SharedStateConfig.lazyLoad;
|
|
188
|
+
const obj = {
|
|
189
|
+
/** Allow users to await for the completion of data init. */
|
|
190
|
+
readyPromise,
|
|
191
|
+
lazy,
|
|
192
|
+
initStarted: false,
|
|
193
|
+
initDone: false,
|
|
194
|
+
/** `subscribe` function required by useSyncExternalStore. */
|
|
195
|
+
subscribe(subscriber) {
|
|
196
|
+
return pubSubStore.subscribe(pubSubKey, () => subscriber());
|
|
197
|
+
},
|
|
198
|
+
/** Returns true if there is a backing store. */
|
|
199
|
+
hasStore() {
|
|
200
|
+
return Boolean(storageAdapter);
|
|
201
|
+
},
|
|
202
|
+
/**
|
|
203
|
+
* Set a value directly into the store. Does nothing if there is no
|
|
204
|
+
* backing store.
|
|
205
|
+
*
|
|
206
|
+
* Warning: If you want to set the value properly, you should use
|
|
207
|
+
* `setValue()` instead which also updates the in-memory copy.
|
|
208
|
+
*/
|
|
209
|
+
async setInStore(value) {
|
|
210
|
+
return await storageAdapter?.set(STORAGE_KEY, value);
|
|
211
|
+
},
|
|
212
|
+
/**
|
|
213
|
+
* Get a value directly from the store. Returns undefined if there is no
|
|
214
|
+
* backing store.
|
|
215
|
+
*/
|
|
216
|
+
async getFromStore() {
|
|
217
|
+
return await storageAdapter?.get(STORAGE_KEY);
|
|
218
|
+
},
|
|
219
|
+
/**
|
|
220
|
+
* Get the value of the shared state.
|
|
221
|
+
* `getSnapshot` function — must return same ref if value unchanged.
|
|
222
|
+
* Note that this would return the default value if data init from store is
|
|
223
|
+
* not completed yet.
|
|
224
|
+
*/
|
|
225
|
+
getSnapshot() {
|
|
226
|
+
return pubSubStore.get(pubSubKey);
|
|
227
|
+
},
|
|
228
|
+
/** Set the value of the shared state. */
|
|
229
|
+
setValue(next) {
|
|
230
|
+
if (typeof next === "function") {
|
|
231
|
+
const prev = pubSubStore.get(pubSubKey);
|
|
232
|
+
next = next(prev);
|
|
233
|
+
}
|
|
234
|
+
pubSubStore.set(pubSubKey, next);
|
|
235
|
+
void obj.setInStore(next);
|
|
236
|
+
},
|
|
237
|
+
/**
|
|
238
|
+
* Remove the value of the shared state. Actually this just sets the value
|
|
239
|
+
* back to the given default value.
|
|
240
|
+
*/
|
|
241
|
+
delete() {
|
|
242
|
+
pubSubStore.set(pubSubKey, defaultValue);
|
|
243
|
+
void storageAdapter?.set(STORAGE_KEY, defaultValue);
|
|
244
|
+
},
|
|
245
|
+
/** Initial default handling. */
|
|
246
|
+
async initDefaultValueOnce() {
|
|
247
|
+
if (obj.initDone || obj.initStarted) return;
|
|
248
|
+
obj.initStarted = true;
|
|
249
|
+
const stored = await obj.getFromStore();
|
|
250
|
+
const value = stored !== void 0 ? stored : defaultValue;
|
|
251
|
+
pubSubStore.set(pubSubKey, value);
|
|
252
|
+
obj.initDone = true;
|
|
253
|
+
resolveReadyPromise();
|
|
232
254
|
}
|
|
233
|
-
|
|
234
|
-
|
|
255
|
+
};
|
|
256
|
+
if (!lazy) {
|
|
257
|
+
void obj.initDefaultValueOnce();
|
|
235
258
|
}
|
|
236
|
-
|
|
237
|
-
* Remove the value of the shared state. Actually this just sets the value
|
|
238
|
-
* back to the given default value.
|
|
239
|
-
*/
|
|
240
|
-
delete() {
|
|
241
|
-
pubSubStore.set(this.pubSubKey, this.defaultValue);
|
|
242
|
-
void this.storageAdapter?.set(STORAGE_KEY, this.defaultValue);
|
|
243
|
-
}
|
|
244
|
-
/** Initial default handling. */
|
|
245
|
-
async initDefaultValueOnce() {
|
|
246
|
-
if (this.initDone || this.initStarted) return;
|
|
247
|
-
this.initStarted = true;
|
|
248
|
-
const stored = await this.getFromStore();
|
|
249
|
-
const value = stored !== void 0 ? stored : this.defaultValue;
|
|
250
|
-
pubSubStore.set(this.pubSubKey, value);
|
|
251
|
-
this.initDone = true;
|
|
252
|
-
this.resolveReadyPromise();
|
|
253
|
-
}
|
|
254
|
-
};
|
|
255
|
-
function sharedState(defaultValue, options) {
|
|
256
|
-
return new SharedState(defaultValue, options);
|
|
259
|
+
return obj;
|
|
257
260
|
}
|
|
258
261
|
function useSharedState(state) {
|
|
259
|
-
state.initDefaultValueOnce();
|
|
262
|
+
void state.initDefaultValueOnce();
|
|
260
263
|
const value = useSyncExternalStore(
|
|
261
264
|
(onStoreChange) => state.subscribe(onStoreChange),
|
|
262
265
|
() => state.getSnapshot(),
|
|
263
266
|
// getServerSnapshot is needed to prevent SSR dev errors in nextjs.
|
|
264
267
|
() => state.getSnapshot()
|
|
265
268
|
);
|
|
266
|
-
return [value, state.
|
|
269
|
+
return [value, state.setValue];
|
|
267
270
|
}
|
|
268
271
|
|
|
269
272
|
// src/stringify.ts
|
|
@@ -303,7 +306,7 @@ import { useMemo, useState } from "react";
|
|
|
303
306
|
function useDeepMemo(value) {
|
|
304
307
|
const [state, setState] = useState(value);
|
|
305
308
|
return useMemo(() => {
|
|
306
|
-
if (
|
|
309
|
+
if (isEqual(state, value)) {
|
|
307
310
|
return state;
|
|
308
311
|
}
|
|
309
312
|
setState(value);
|
|
@@ -331,282 +334,104 @@ var SharedQueryConfig = {
|
|
|
331
334
|
function configureSharedQuery(config) {
|
|
332
335
|
Object.assign(SharedQueryConfig, config);
|
|
333
336
|
}
|
|
334
|
-
var
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
expiryMs;
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
337
|
+
var seenQueryNames = /* @__PURE__ */ new Set();
|
|
338
|
+
function sharedQuery(options) {
|
|
339
|
+
if (seenQueryNames.has(options.queryName)) {
|
|
340
|
+
throw new Error(`Duplicate shared query name "${options.queryName}"`);
|
|
341
|
+
}
|
|
342
|
+
seenQueryNames.add(options.queryName);
|
|
343
|
+
const inflightQueries = /* @__PURE__ */ new Map();
|
|
344
|
+
const mountedKeys = /* @__PURE__ */ new Map();
|
|
345
|
+
const queryName = options.queryName;
|
|
346
|
+
const queryFn = options.queryFn;
|
|
347
|
+
const refetchFn = options.refetchFn;
|
|
348
|
+
const staleMs = options?.staleMs ?? SharedQueryConfig.staleMs;
|
|
349
|
+
const expiryMs = options?.expiryMs ?? SharedQueryConfig.expiryMs;
|
|
350
|
+
const refetchOnStale = options?.refetchOnStale ?? SharedQueryConfig.refetchOnStale;
|
|
351
|
+
const store = options.store;
|
|
352
|
+
if (store) {
|
|
353
|
+
if ("persistTo" in store && store.isValid) {
|
|
354
|
+
const isValid = store.isValid;
|
|
355
|
+
options = {
|
|
356
|
+
...options,
|
|
357
|
+
store: {
|
|
358
|
+
...store,
|
|
359
|
+
isValid: (u) => {
|
|
360
|
+
if (!u || typeof u !== "object") {
|
|
361
|
+
return false;
|
|
362
|
+
}
|
|
363
|
+
for (const v of Object.values(u)) {
|
|
364
|
+
if (!v || typeof v !== "object" || !("data" in v) || !("dataUpdatedMs" in v) || !("lastUpdatedMs" in v) || typeof v.dataUpdatedMs !== "number" || typeof v.lastUpdatedMs !== "number" || !isValid(v.data)) {
|
|
365
|
+
return false;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
return true;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
} else {
|
|
374
|
+
const persistTo = options.persistTo ?? SharedQueryConfig.persistTo;
|
|
375
|
+
if (persistTo) {
|
|
376
|
+
options = {
|
|
377
|
+
...options,
|
|
378
|
+
store: {
|
|
364
379
|
persistTo,
|
|
365
|
-
key:
|
|
366
|
-
expiryMs
|
|
367
|
-
}
|
|
368
|
-
}
|
|
380
|
+
key: queryName,
|
|
381
|
+
expiryMs
|
|
382
|
+
}
|
|
383
|
+
};
|
|
369
384
|
}
|
|
370
|
-
this.maxSize = options?.maxSize ?? SharedQueryConfig.maxSize;
|
|
371
|
-
this.maxBytes = options?.maxBytes ?? SharedQueryConfig.maxBytes;
|
|
372
|
-
this.keepLastNonEmptyData = Boolean(options?.keepLastNonEmptyData);
|
|
373
|
-
this.log = options?.log ?? SharedQueryConfig.log;
|
|
374
|
-
this.queryState = sharedState(
|
|
375
|
-
{},
|
|
376
|
-
// defaultValue
|
|
377
|
-
options
|
|
378
|
-
);
|
|
379
385
|
}
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
getAbortController(args) {
|
|
392
|
-
const queryKey = this.getQueryKey(args);
|
|
393
|
-
return this.getAbortControllerByKey(queryKey);
|
|
394
|
-
}
|
|
395
|
-
getAbortControllerByKey(queryKey) {
|
|
396
|
-
return this.inflightQueries.get(queryKey)?.abortController ?? null;
|
|
397
|
-
}
|
|
398
|
-
/**
|
|
399
|
-
* Get the AbortSignal to check for query abortions.
|
|
400
|
-
*
|
|
401
|
-
* Example:
|
|
402
|
-
* ```
|
|
403
|
-
* const getUserQuery = sharedQuery({
|
|
404
|
-
* queryName: "getUser",
|
|
405
|
-
* queryFn: (userId: string) => {
|
|
406
|
-
* // Get the signal for this current execution.
|
|
407
|
-
* const signal = getUserQuery.getAbortSignal([userId]);
|
|
408
|
-
*
|
|
409
|
-
* // Pass the signal to fetch so that it can be aborted.
|
|
410
|
-
* const response = await fetch(`/users/${userId}`, { signal });
|
|
411
|
-
*
|
|
412
|
-
* // Check for errors.
|
|
413
|
-
* if (!response.ok) {
|
|
414
|
-
* throw new Error(response.statusText);
|
|
415
|
-
* }
|
|
416
|
-
*
|
|
417
|
-
* // Return the data.
|
|
418
|
-
* return await response.json();
|
|
419
|
-
* },
|
|
420
|
-
* });
|
|
421
|
-
* ```
|
|
422
|
-
*/
|
|
423
|
-
getAbortSignal(args) {
|
|
424
|
-
const queryKey = this.getQueryKey(args);
|
|
425
|
-
return this.getAbortSignalByKey(queryKey);
|
|
426
|
-
}
|
|
427
|
-
getAbortSignalByKey(queryKey) {
|
|
428
|
-
return this.getAbortControllerByKey(queryKey)?.signal ?? null;
|
|
429
|
-
}
|
|
430
|
-
isStale(dataUpdatedMs) {
|
|
431
|
-
if (this.staleMs === 0) return true;
|
|
386
|
+
const maxSize = options?.maxSize ?? SharedQueryConfig.maxSize;
|
|
387
|
+
const maxBytes = options?.maxBytes ?? SharedQueryConfig.maxBytes;
|
|
388
|
+
const keepLastNonEmptyData = Boolean(options?.keepLastNonEmptyData);
|
|
389
|
+
const log = options?.log ?? SharedQueryConfig.log;
|
|
390
|
+
const queryState = sharedState(
|
|
391
|
+
{},
|
|
392
|
+
// defaultValue
|
|
393
|
+
options
|
|
394
|
+
);
|
|
395
|
+
function isStale(dataUpdatedMs) {
|
|
396
|
+
if (staleMs === 0) return true;
|
|
432
397
|
const ageMs = Date.now() - dataUpdatedMs;
|
|
433
|
-
return ageMs >
|
|
398
|
+
return ageMs > staleMs;
|
|
434
399
|
}
|
|
435
|
-
isExpired(dataUpdatedMs) {
|
|
436
|
-
if (
|
|
400
|
+
function isExpired(dataUpdatedMs) {
|
|
401
|
+
if (expiryMs === 0) return false;
|
|
437
402
|
const ageMs = Date.now() - dataUpdatedMs;
|
|
438
|
-
return ageMs >
|
|
403
|
+
return ageMs > expiryMs;
|
|
439
404
|
}
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
const startMs = Date.now();
|
|
443
|
-
await this.queryState.readyPromise;
|
|
444
|
-
const waitedMs = Date.now() - startMs;
|
|
445
|
-
const queryKey = this.getQueryKey(args);
|
|
446
|
-
const cached = this.queryState.getSnapshot()?.[queryKey];
|
|
447
|
-
if (waitedMs) {
|
|
448
|
-
this.log(`${queryKey}: Waited for ${waitedMs}ms before data is ready`);
|
|
449
|
-
}
|
|
450
|
-
if (cached?.data && !this.isExpired(cached.dataUpdatedMs ?? 0)) {
|
|
451
|
-
if (!this.isStale(cached.dataUpdatedMs ?? 0)) {
|
|
452
|
-
this.log(`Return fresh data ${queryKey} from cache without fetching`);
|
|
453
|
-
return cached;
|
|
454
|
-
}
|
|
455
|
-
if (this.refetchOnStale) {
|
|
456
|
-
this.log(`refetchOnStale for ${queryKey}`);
|
|
457
|
-
void this.dedupedFetch(queryKey, "query", ...args);
|
|
458
|
-
}
|
|
459
|
-
this.log(`Returning stale data for ${queryKey}`);
|
|
460
|
-
return cached;
|
|
461
|
-
}
|
|
462
|
-
const data = await this.dedupedFetch(queryKey, "query", ...args);
|
|
463
|
-
const now = Date.now();
|
|
464
|
-
return {
|
|
465
|
-
// Don't keep old error.
|
|
466
|
-
lastUpdatedMs: now,
|
|
467
|
-
loading: false,
|
|
468
|
-
data,
|
|
469
|
-
dataUpdatedMs: now
|
|
470
|
-
};
|
|
471
|
-
}
|
|
472
|
-
dedupedFetch(queryKey, queryType, ...args) {
|
|
473
|
-
const inflightQuery = this.inflightQueries.get(queryKey);
|
|
474
|
-
if (inflightQuery) {
|
|
475
|
-
this.log(
|
|
476
|
-
`Deduplicating inflight fetch for ${queryKey}, queryType=${queryType}`
|
|
477
|
-
);
|
|
478
|
-
return inflightQuery.promise;
|
|
479
|
-
}
|
|
480
|
-
const promise = this.startFetching(queryKey, queryType, args);
|
|
481
|
-
const abortController = new AbortController();
|
|
482
|
-
this.inflightQueries.set(queryKey, { promise, abortController });
|
|
483
|
-
return promise;
|
|
484
|
-
}
|
|
485
|
-
async startFetching(queryKey, queryType, args) {
|
|
486
|
-
this.log(`Start fetching ${queryKey}, queryType=${queryType}`);
|
|
487
|
-
try {
|
|
488
|
-
const data = await (queryType === "refetch" && this.refetchFn ? this.refetchFn(...args) : this.queryFn(...args));
|
|
489
|
-
this.log(`Successfully fetched ${queryKey}`);
|
|
490
|
-
this.setData(queryKey, data, Date.now());
|
|
491
|
-
return data;
|
|
492
|
-
} catch (e) {
|
|
493
|
-
this.log(`Failed to fetch ${queryKey} for queryType=${queryType}: ${e}`);
|
|
494
|
-
throw e;
|
|
495
|
-
} finally {
|
|
496
|
-
this.inflightQueries.delete(queryKey);
|
|
497
|
-
}
|
|
498
|
-
}
|
|
499
|
-
/** Get the entire stored entry for a query key. */
|
|
500
|
-
getQueryValue(queryKey) {
|
|
501
|
-
return this.queryState.getSnapshot()?.[queryKey];
|
|
502
|
-
}
|
|
503
|
-
/** Get the current stored data for a query key. */
|
|
504
|
-
getData(queryKey) {
|
|
505
|
-
const stateValue = this.queryState.getSnapshot()?.[queryKey];
|
|
506
|
-
if (stateValue?.lastUpdatedMs) {
|
|
507
|
-
if (this.isExpired(stateValue.lastUpdatedMs)) {
|
|
508
|
-
this.deleteData(queryKey);
|
|
509
|
-
return void 0;
|
|
510
|
-
}
|
|
511
|
-
}
|
|
512
|
-
return stateValue?.data;
|
|
513
|
-
}
|
|
514
|
-
/** Set the data directly if the user obtained it from somewhere else. */
|
|
515
|
-
setData(queryKey, data, dataUpdatedMs = Date.now()) {
|
|
516
|
-
const entry = {
|
|
517
|
-
lastUpdatedMs: Date.now(),
|
|
518
|
-
loading: false,
|
|
519
|
-
data,
|
|
520
|
-
dataUpdatedMs
|
|
521
|
-
};
|
|
522
|
-
this.queryState.setValue((prev) => ({ ...prev, [queryKey]: entry }));
|
|
523
|
-
this.enforceSizeLimit();
|
|
524
|
-
}
|
|
525
|
-
/**
|
|
526
|
-
* Do a new fetch even when the data is already cached, but don't fetch if
|
|
527
|
-
* another fetch is already inflight.
|
|
528
|
-
*/
|
|
529
|
-
async updateFromSource(...args) {
|
|
530
|
-
const key = this.getQueryKey(args);
|
|
531
|
-
return await this.dedupedFetch(key, "query", ...args);
|
|
532
|
-
}
|
|
533
|
-
/**
|
|
534
|
-
* Perform an explicit refetch. If `options.refetchFn` was not specified,
|
|
535
|
-
* then this would be identical to calling `this.updateFromSource()`.
|
|
536
|
-
*/
|
|
537
|
-
async refetch(...args) {
|
|
538
|
-
const key = this.getQueryKey(args);
|
|
539
|
-
return await this.dedupedFetch(key, "refetch", ...args);
|
|
540
|
-
}
|
|
541
|
-
/** Delete the cached data for one set of arguments. */
|
|
542
|
-
deleteData(queryKey) {
|
|
543
|
-
const record = this.queryState.getSnapshot();
|
|
544
|
-
const clone = { ...record };
|
|
545
|
-
delete clone[queryKey];
|
|
546
|
-
this.queryState.setValue(clone);
|
|
547
|
-
const inflight = this.inflightQueries.get(queryKey);
|
|
548
|
-
if (inflight) {
|
|
549
|
-
this.inflightQueries.delete(queryKey);
|
|
550
|
-
inflight.abortController.abort("Deleted");
|
|
551
|
-
}
|
|
552
|
-
}
|
|
553
|
-
/** Delete all currently cached data & all inflight promises. */
|
|
554
|
-
clear() {
|
|
555
|
-
this.queryState.delete();
|
|
556
|
-
for (const { abortController } of Array.from(
|
|
557
|
-
this.inflightQueries.values()
|
|
558
|
-
)) {
|
|
559
|
-
abortController.abort("Cleared");
|
|
560
|
-
}
|
|
561
|
-
this.inflightQueries.clear();
|
|
562
|
-
}
|
|
563
|
-
/** Keep track of mounted keys. */
|
|
564
|
-
mount(queryKey) {
|
|
565
|
-
this.mountedKeys.set(queryKey, (this.mountedKeys.get(queryKey) ?? 0) + 1);
|
|
566
|
-
}
|
|
567
|
-
/** Clean up mounted keys. */
|
|
568
|
-
unmount(queryKey) {
|
|
569
|
-
const count = this.mountedKeys.get(queryKey);
|
|
570
|
-
if (count === 1) {
|
|
571
|
-
this.mountedKeys.delete(queryKey);
|
|
572
|
-
} else {
|
|
573
|
-
this.mountedKeys.set(queryKey, (count ?? 0) - 1);
|
|
574
|
-
}
|
|
575
|
-
}
|
|
576
|
-
/**
|
|
577
|
-
* Discard the oldest entries if the size exceeds the limit. The last
|
|
578
|
-
* remaining record cannot be deleted no matter what the limit is.
|
|
579
|
-
*/
|
|
580
|
-
enforceSizeLimit() {
|
|
581
|
-
if (!this.maxSize && !this.maxBytes) {
|
|
405
|
+
function enforceSizeLimit() {
|
|
406
|
+
if (!maxSize && !maxBytes) {
|
|
582
407
|
return;
|
|
583
408
|
}
|
|
584
|
-
const oldState =
|
|
409
|
+
const oldState = queryState.getSnapshot();
|
|
585
410
|
const oldSize = Object.keys(oldState).length;
|
|
586
411
|
if (oldSize <= 1) {
|
|
587
412
|
return;
|
|
588
413
|
}
|
|
589
414
|
const oldByteSize = getByteSize(oldState);
|
|
590
|
-
let numToCut =
|
|
591
|
-
let bytesToCut =
|
|
415
|
+
let numToCut = maxSize ? oldSize - maxSize : 0;
|
|
416
|
+
let bytesToCut = maxBytes ? oldByteSize - maxBytes : 0;
|
|
592
417
|
if (numToCut <= 0 && bytesToCut <= 0) {
|
|
593
|
-
|
|
594
|
-
`No need to trim data for ${
|
|
418
|
+
log(
|
|
419
|
+
`No need to trim data for ${queryName}: size=${oldSize} (limit=${maxSize}), byteSize=${oldByteSize.toLocaleString()} (limit=${maxBytes.toLocaleString()})`
|
|
595
420
|
);
|
|
596
421
|
return;
|
|
597
422
|
}
|
|
598
|
-
|
|
599
|
-
`Need to trim ${
|
|
423
|
+
log(
|
|
424
|
+
`Need to trim ${queryName}: numToCut=${numToCut}, bytesToCut=${bytesToCut}`
|
|
600
425
|
);
|
|
601
426
|
const newState = { ...oldState };
|
|
602
427
|
let needUpdate = false;
|
|
603
428
|
const deleteKeyIfNotMounted = (key, expired) => {
|
|
604
|
-
if (
|
|
605
|
-
|
|
429
|
+
if (mountedKeys.has(key)) {
|
|
430
|
+
log(`Cannot clean up ${key} as it is mounted`);
|
|
606
431
|
return;
|
|
607
432
|
}
|
|
608
433
|
const byteSize = getByteSize(key) + getByteSize(newState[key]);
|
|
609
|
-
|
|
434
|
+
log(
|
|
610
435
|
`Cleaning up unmounted${expired ? ", expired" : ""} ${key} (byteSize=${byteSize.toLocaleString()})`
|
|
611
436
|
);
|
|
612
437
|
numToCut--;
|
|
@@ -618,7 +443,7 @@ var SharedQuery = class {
|
|
|
618
443
|
(entry1, entry2) => entry1[1].lastUpdatedMs - entry2[1].lastUpdatedMs
|
|
619
444
|
);
|
|
620
445
|
for (const [key] of entriesByTimeAscending) {
|
|
621
|
-
if (!
|
|
446
|
+
if (!isExpired(newState[key].lastUpdatedMs)) {
|
|
622
447
|
break;
|
|
623
448
|
}
|
|
624
449
|
deleteKeyIfNotMounted(key, true);
|
|
@@ -637,24 +462,220 @@ var SharedQuery = class {
|
|
|
637
462
|
}
|
|
638
463
|
}
|
|
639
464
|
if (!needUpdate) {
|
|
640
|
-
|
|
641
|
-
`No trimmable entries found for ${
|
|
465
|
+
log(
|
|
466
|
+
`No trimmable entries found for ${queryName}: size=${oldSize} (limit=${maxSize}), byteSize=${oldByteSize.toLocaleString()} (limit=${maxBytes.toLocaleString()})`
|
|
642
467
|
);
|
|
643
468
|
return;
|
|
644
469
|
}
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
`Trimmed data for ${
|
|
470
|
+
queryState.setValue(newState);
|
|
471
|
+
log(
|
|
472
|
+
`Trimmed data for ${queryName}: size=[${oldSize} -> ${Object.keys(newState).length}] (limit=${maxSize}), byteSize=[${oldByteSize.toLocaleString()} -> ${getByteSize(newState).toLocaleString()}] (limit=${maxBytes.toLocaleString()})`
|
|
648
473
|
);
|
|
649
474
|
}
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
475
|
+
function setData(queryKey, data, dataUpdatedMs = Date.now()) {
|
|
476
|
+
const entry = {
|
|
477
|
+
lastUpdatedMs: Date.now(),
|
|
478
|
+
loading: false,
|
|
479
|
+
data,
|
|
480
|
+
dataUpdatedMs
|
|
481
|
+
};
|
|
482
|
+
queryState.setValue((prev) => ({ ...prev, [queryKey]: entry }));
|
|
483
|
+
enforceSizeLimit();
|
|
655
484
|
}
|
|
656
|
-
|
|
657
|
-
|
|
485
|
+
async function startFetching(queryKey, queryType, args) {
|
|
486
|
+
log(`Start fetching ${queryKey}, queryType=${queryType}`);
|
|
487
|
+
try {
|
|
488
|
+
const data = await (queryType === "refetch" && refetchFn ? refetchFn(...args) : queryFn(...args));
|
|
489
|
+
log(`Successfully fetched ${queryKey}`);
|
|
490
|
+
setData(queryKey, data, Date.now());
|
|
491
|
+
return data;
|
|
492
|
+
} catch (e) {
|
|
493
|
+
log(`Failed to fetch ${queryKey} for queryType=${queryType}: ${e}`);
|
|
494
|
+
throw e;
|
|
495
|
+
} finally {
|
|
496
|
+
inflightQueries.delete(queryKey);
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
function dedupedFetch(queryKey, queryType, ...args) {
|
|
500
|
+
const inflightQuery = inflightQueries.get(queryKey);
|
|
501
|
+
if (inflightQuery) {
|
|
502
|
+
log(
|
|
503
|
+
`Deduplicating inflight fetch for ${queryKey}, queryType=${queryType}`
|
|
504
|
+
);
|
|
505
|
+
return inflightQuery.promise;
|
|
506
|
+
}
|
|
507
|
+
const promise = startFetching(queryKey, queryType, args);
|
|
508
|
+
const abortController = new AbortController();
|
|
509
|
+
inflightQueries.set(queryKey, { promise, abortController });
|
|
510
|
+
return promise;
|
|
511
|
+
}
|
|
512
|
+
const obj = {
|
|
513
|
+
queryName,
|
|
514
|
+
queryFn,
|
|
515
|
+
refetchFn,
|
|
516
|
+
expiryMs,
|
|
517
|
+
staleMs,
|
|
518
|
+
refetchOnStale,
|
|
519
|
+
maxSize,
|
|
520
|
+
maxBytes,
|
|
521
|
+
keepLastNonEmptyData,
|
|
522
|
+
log,
|
|
523
|
+
queryState,
|
|
524
|
+
/** Returns a key to identify requests based on the given args. */
|
|
525
|
+
getQueryKey(args) {
|
|
526
|
+
return queryName + ":" + stringifyDeterministicForKeys(args);
|
|
527
|
+
},
|
|
528
|
+
/**
|
|
529
|
+
* Get the AbortController to abort the inflight query (if any).
|
|
530
|
+
*
|
|
531
|
+
* Note that once you get the controller, you can also get the signal easily
|
|
532
|
+
* using `controller.signal`. So if you need both the controller and the
|
|
533
|
+
* signal, just use this function.
|
|
534
|
+
*/
|
|
535
|
+
getAbortController(args) {
|
|
536
|
+
const queryKey = obj.getQueryKey(args);
|
|
537
|
+
return obj.getAbortControllerByKey(queryKey);
|
|
538
|
+
},
|
|
539
|
+
getAbortControllerByKey(queryKey) {
|
|
540
|
+
return inflightQueries.get(queryKey)?.abortController ?? null;
|
|
541
|
+
},
|
|
542
|
+
/**
|
|
543
|
+
* Get the AbortSignal to check for query abortions.
|
|
544
|
+
*
|
|
545
|
+
* Example:
|
|
546
|
+
* ```
|
|
547
|
+
* const getUserQuery = sharedQuery({
|
|
548
|
+
* queryName: "getUser",
|
|
549
|
+
* queryFn: (userId: string) => {
|
|
550
|
+
* // Get the signal for this current execution.
|
|
551
|
+
* const signal = getUserQuery.getAbortSignal([userId]);
|
|
552
|
+
*
|
|
553
|
+
* // Pass the signal to fetch so that it can be aborted.
|
|
554
|
+
* const response = await fetch(`/users/${userId}`, { signal });
|
|
555
|
+
*
|
|
556
|
+
* // Check for errors.
|
|
557
|
+
* if (!response.ok) {
|
|
558
|
+
* throw new Error(response.statusText);
|
|
559
|
+
* }
|
|
560
|
+
*
|
|
561
|
+
* // Return the data.
|
|
562
|
+
* return await response.json();
|
|
563
|
+
* },
|
|
564
|
+
* });
|
|
565
|
+
* ```
|
|
566
|
+
*/
|
|
567
|
+
getAbortSignal(args) {
|
|
568
|
+
const queryKey = obj.getQueryKey(args);
|
|
569
|
+
return obj.getAbortSignalByKey(queryKey);
|
|
570
|
+
},
|
|
571
|
+
getAbortSignalByKey(queryKey) {
|
|
572
|
+
return obj.getAbortControllerByKey(queryKey)?.signal ?? null;
|
|
573
|
+
},
|
|
574
|
+
/** For cached data, we need to return the correct timestamps as well. */
|
|
575
|
+
async getCachedOrFetch(...args) {
|
|
576
|
+
const startMs = Date.now();
|
|
577
|
+
await queryState.readyPromise;
|
|
578
|
+
const waitedMs = Date.now() - startMs;
|
|
579
|
+
const queryKey = obj.getQueryKey(args);
|
|
580
|
+
const cached = queryState.getSnapshot()?.[queryKey];
|
|
581
|
+
if (waitedMs) {
|
|
582
|
+
log(`${queryKey}: Waited for ${waitedMs}ms before data is ready`);
|
|
583
|
+
}
|
|
584
|
+
if (cached?.data && !isExpired(cached.dataUpdatedMs ?? 0)) {
|
|
585
|
+
if (!isStale(cached.dataUpdatedMs ?? 0)) {
|
|
586
|
+
log(`Return fresh data ${queryKey} from cache without fetching`);
|
|
587
|
+
return cached;
|
|
588
|
+
}
|
|
589
|
+
if (refetchOnStale) {
|
|
590
|
+
log(`refetchOnStale for ${queryKey}`);
|
|
591
|
+
void dedupedFetch(queryKey, "query", ...args);
|
|
592
|
+
}
|
|
593
|
+
log(`Returning stale data for ${queryKey}`);
|
|
594
|
+
return cached;
|
|
595
|
+
}
|
|
596
|
+
const data = await dedupedFetch(queryKey, "query", ...args);
|
|
597
|
+
const now = Date.now();
|
|
598
|
+
return {
|
|
599
|
+
// Don't keep old error.
|
|
600
|
+
lastUpdatedMs: now,
|
|
601
|
+
loading: false,
|
|
602
|
+
data,
|
|
603
|
+
dataUpdatedMs: now
|
|
604
|
+
};
|
|
605
|
+
},
|
|
606
|
+
/** Get the entire stored entry for a query key. */
|
|
607
|
+
getQueryValue(queryKey) {
|
|
608
|
+
return queryState.getSnapshot()?.[queryKey];
|
|
609
|
+
},
|
|
610
|
+
/** Get the current stored data for a query key. */
|
|
611
|
+
getData(queryKey) {
|
|
612
|
+
const stateValue = queryState.getSnapshot()?.[queryKey];
|
|
613
|
+
if (stateValue?.lastUpdatedMs) {
|
|
614
|
+
if (isExpired(stateValue.lastUpdatedMs)) {
|
|
615
|
+
obj.deleteData(queryKey);
|
|
616
|
+
return void 0;
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
return stateValue?.data;
|
|
620
|
+
},
|
|
621
|
+
/** Set the data directly if the user obtained it from somewhere else. */
|
|
622
|
+
setData,
|
|
623
|
+
/**
|
|
624
|
+
* Do a new fetch even when the data is already cached, but don't fetch if
|
|
625
|
+
* another fetch is already inflight.
|
|
626
|
+
*/
|
|
627
|
+
async updateFromSource(...args) {
|
|
628
|
+
const key = obj.getQueryKey(args);
|
|
629
|
+
return await dedupedFetch(key, "query", ...args);
|
|
630
|
+
},
|
|
631
|
+
/**
|
|
632
|
+
* Perform an explicit refetch. If `options.refetchFn` was not specified,
|
|
633
|
+
* then this would be identical to calling `obj.updateFromSource()`.
|
|
634
|
+
*/
|
|
635
|
+
async refetch(...args) {
|
|
636
|
+
const key = obj.getQueryKey(args);
|
|
637
|
+
return await dedupedFetch(key, "refetch", ...args);
|
|
638
|
+
},
|
|
639
|
+
/** Delete the cached data for one set of arguments. */
|
|
640
|
+
deleteData(queryKey) {
|
|
641
|
+
const record = queryState.getSnapshot();
|
|
642
|
+
const clone = { ...record };
|
|
643
|
+
delete clone[queryKey];
|
|
644
|
+
queryState.setValue(clone);
|
|
645
|
+
const inflight = inflightQueries.get(queryKey);
|
|
646
|
+
if (inflight) {
|
|
647
|
+
inflightQueries.delete(queryKey);
|
|
648
|
+
inflight.abortController.abort("Deleted");
|
|
649
|
+
}
|
|
650
|
+
},
|
|
651
|
+
/** Delete all currently cached data & all inflight promises. */
|
|
652
|
+
clear() {
|
|
653
|
+
queryState.delete();
|
|
654
|
+
for (const { abortController } of Array.from(inflightQueries.values())) {
|
|
655
|
+
abortController.abort("Cleared");
|
|
656
|
+
}
|
|
657
|
+
inflightQueries.clear();
|
|
658
|
+
},
|
|
659
|
+
/** Keep track of mounted keys. */
|
|
660
|
+
mount(queryKey) {
|
|
661
|
+
mountedKeys.set(queryKey, (mountedKeys.get(queryKey) ?? 0) + 1);
|
|
662
|
+
},
|
|
663
|
+
/** Clean up mounted keys. */
|
|
664
|
+
unmount(queryKey) {
|
|
665
|
+
const count = mountedKeys.get(queryKey);
|
|
666
|
+
if (count === 1) {
|
|
667
|
+
mountedKeys.delete(queryKey);
|
|
668
|
+
} else {
|
|
669
|
+
mountedKeys.set(queryKey, (count ?? 0) - 1);
|
|
670
|
+
}
|
|
671
|
+
},
|
|
672
|
+
/**
|
|
673
|
+
* Discard the oldest entries if the size exceeds the limit. The last
|
|
674
|
+
* remaining record cannot be deleted no matter what the limit is.
|
|
675
|
+
*/
|
|
676
|
+
enforceSizeLimit
|
|
677
|
+
};
|
|
678
|
+
return obj;
|
|
658
679
|
}
|
|
659
680
|
var DEFAULT_QUERY_STATE_ENTRY = {
|
|
660
681
|
lastUpdatedMs: 0,
|
|
@@ -751,38 +772,18 @@ function useSharedQuery(query, ...args) {
|
|
|
751
772
|
return Boolean(controller);
|
|
752
773
|
},
|
|
753
774
|
refetch: () => {
|
|
754
|
-
|
|
775
|
+
return execute(true);
|
|
755
776
|
},
|
|
756
777
|
setData: (data, dataUpdatedMs) => {
|
|
757
|
-
|
|
758
|
-
setQueryStateValue((prev) => ({
|
|
759
|
-
...prev,
|
|
760
|
-
data,
|
|
761
|
-
dataUpdatedMs: dataUpdatedMs ?? now,
|
|
762
|
-
lastUpdatedMs: now,
|
|
763
|
-
loading: false
|
|
764
|
-
}));
|
|
778
|
+
query.setData(queryKey, data, dataUpdatedMs);
|
|
765
779
|
},
|
|
766
780
|
deleteData: () => {
|
|
767
|
-
|
|
768
|
-
const clone = { ...prev };
|
|
769
|
-
delete clone[queryKey];
|
|
770
|
-
return clone;
|
|
771
|
-
});
|
|
781
|
+
query.deleteData(queryKey);
|
|
772
782
|
}
|
|
773
783
|
};
|
|
774
|
-
}, [
|
|
775
|
-
execute,
|
|
776
|
-
lastNonEmptyData,
|
|
777
|
-
query,
|
|
778
|
-
queryKey,
|
|
779
|
-
queryStateValue,
|
|
780
|
-
setQueryState,
|
|
781
|
-
setQueryStateValue
|
|
782
|
-
]);
|
|
784
|
+
}, [execute, lastNonEmptyData, query, queryKey, queryStateValue]);
|
|
783
785
|
}
|
|
784
786
|
export {
|
|
785
|
-
SharedQuery,
|
|
786
787
|
SharedQueryConfig,
|
|
787
788
|
configureSharedQuery,
|
|
788
789
|
sharedQuery,
|