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