@choksheak/react-utils 0.2.6 → 0.2.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/fetcher.cjs +4 -4
- package/fetcher.d.mts +31 -4
- package/fetcher.d.ts +30 -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 +3 -3
- package/package.json +2 -1
- package/sharedQuery.cjs +82 -64
- package/sharedQuery.d.mts +145 -5
- package/sharedQuery.d.ts +144 -5
- 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 +89 -65
- package/sharedState.cjs +3 -1
- package/sharedState.d.mts +62 -9
- package/sharedState.d.ts +62 -9
- 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 +3 -1
package/fetcher.cjs
CHANGED
|
@@ -31,21 +31,21 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
31
31
|
var fetcher_exports = {};
|
|
32
32
|
__export(fetcher_exports, {
|
|
33
33
|
fetcher: () => fetcher,
|
|
34
|
-
|
|
34
|
+
simpleFetcher: () => simpleFetcher
|
|
35
35
|
});
|
|
36
36
|
module.exports = __toCommonJS(fetcher_exports);
|
|
37
37
|
var import_wretch = __toESM(require("wretch"));
|
|
38
38
|
var import_middlewares = require("wretch/middlewares");
|
|
39
|
-
var fetcher = (0, import_wretch.default)()
|
|
40
|
-
var fetcherWithRetries = (0, import_wretch.default)().middlewares([
|
|
39
|
+
var fetcher = (0, import_wretch.default)().middlewares([
|
|
41
40
|
(0, import_middlewares.retry)({
|
|
42
41
|
maxAttempts: 3,
|
|
43
42
|
delayTimer: 1e3,
|
|
44
43
|
retryOnNetworkError: true
|
|
45
44
|
})
|
|
46
45
|
]);
|
|
46
|
+
var simpleFetcher = (0, import_wretch.default)();
|
|
47
47
|
// Annotate the CommonJS export names for ESM import in node:
|
|
48
48
|
0 && (module.exports = {
|
|
49
49
|
fetcher,
|
|
50
|
-
|
|
50
|
+
simpleFetcher
|
|
51
51
|
});
|
package/fetcher.d.mts
CHANGED
|
@@ -1,8 +1,35 @@
|
|
|
1
1
|
import * as wretch from 'wretch';
|
|
2
2
|
|
|
3
|
-
/**
|
|
3
|
+
/**
|
|
4
|
+
* @packageDocumentation
|
|
5
|
+
*
|
|
6
|
+
* An alternate syntax to fetch() that allows function chaining to compose the
|
|
7
|
+
* entire fetch request. One can also compose partial fetcher objects to be
|
|
8
|
+
* reused in future requests.
|
|
9
|
+
*
|
|
10
|
+
* Examples:
|
|
11
|
+
* ```
|
|
12
|
+
* const userFetcher = fetcher
|
|
13
|
+
* .auth("Bearer TOPSECRET")
|
|
14
|
+
* .content("application/json")
|
|
15
|
+
* .url("/api/user");
|
|
16
|
+
*
|
|
17
|
+
* const user = await userFetcher.get();
|
|
18
|
+
*
|
|
19
|
+
* const updateResult = await userFetcher
|
|
20
|
+
* .body({ id: user.id, name: "New name"})
|
|
21
|
+
* .post();
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
/**
|
|
25
|
+
* Fetcher with default short retries. Use this to make your queries more
|
|
26
|
+
* robust without any additional work.
|
|
27
|
+
*/
|
|
4
28
|
declare const fetcher: wretch.Wretch<unknown, unknown, undefined, undefined>;
|
|
5
|
-
/**
|
|
6
|
-
|
|
29
|
+
/**
|
|
30
|
+
* Name this as `simpleFetcher` to make it easier to understand than `wretch`.
|
|
31
|
+
* Use this if you don't want retries. Use `fetcher` if you want retries.
|
|
32
|
+
*/
|
|
33
|
+
declare const simpleFetcher: wretch.Wretch<unknown, unknown, undefined, undefined>;
|
|
7
34
|
|
|
8
|
-
export { fetcher,
|
|
35
|
+
export { fetcher, simpleFetcher };
|
package/fetcher.d.ts
CHANGED
|
@@ -1,4 +1,31 @@
|
|
|
1
|
-
/**
|
|
1
|
+
/**
|
|
2
|
+
* @packageDocumentation
|
|
3
|
+
*
|
|
4
|
+
* An alternate syntax to fetch() that allows function chaining to compose the
|
|
5
|
+
* entire fetch request. One can also compose partial fetcher objects to be
|
|
6
|
+
* reused in future requests.
|
|
7
|
+
*
|
|
8
|
+
* Examples:
|
|
9
|
+
* ```
|
|
10
|
+
* const userFetcher = fetcher
|
|
11
|
+
* .auth("Bearer TOPSECRET")
|
|
12
|
+
* .content("application/json")
|
|
13
|
+
* .url("/api/user");
|
|
14
|
+
*
|
|
15
|
+
* const user = await userFetcher.get();
|
|
16
|
+
*
|
|
17
|
+
* const updateResult = await userFetcher
|
|
18
|
+
* .body({ id: user.id, name: "New name"})
|
|
19
|
+
* .post();
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
/**
|
|
23
|
+
* Fetcher with default short retries. Use this to make your queries more
|
|
24
|
+
* robust without any additional work.
|
|
25
|
+
*/
|
|
2
26
|
export declare const fetcher: import("wretch").Wretch<unknown, unknown, undefined, undefined>;
|
|
3
|
-
/**
|
|
4
|
-
|
|
27
|
+
/**
|
|
28
|
+
* Name this as `simpleFetcher` to make it easier to understand than `wretch`.
|
|
29
|
+
* Use this if you don't want retries. Use `fetcher` if you want retries.
|
|
30
|
+
*/
|
|
31
|
+
export declare const simpleFetcher: import("wretch").Wretch<unknown, unknown, undefined, undefined>;
|
package/fetcher.min.cjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
"use strict";var
|
|
1
|
+
"use strict";var a=Object.create;var m=Object.defineProperty;var d=Object.getOwnPropertyDescriptor;var f=Object.getOwnPropertyNames;var h=Object.getPrototypeOf,l=Object.prototype.hasOwnProperty;var n=(r,e)=>{for(var t in e)m(r,t,{get:e[t],enumerable:!0})},i=(r,e,t,c)=>{if(e&&typeof e=="object"||typeof e=="function")for(let o of f(e))!l.call(r,o)&&o!==t&&m(r,o,{get:()=>e[o],enumerable:!(c=d(e,o))||c.enumerable});return r};var w=(r,e,t)=>(t=r!=null?a(h(r)):{},i(e||!r||!r.__esModule?m(t,"default",{value:r,enumerable:!0}):t,r)),x=r=>i(m({},"__esModule",{value:!0}),r);var u={};n(u,{fetcher:()=>y,simpleFetcher:()=>k});module.exports=x(u);var p=w(require("wretch")),s=require("wretch/middlewares"),y=(0,p.default)().middlewares([(0,s.retry)({maxAttempts:3,delayTimer:1e3,retryOnNetworkError:!0})]),k=(0,p.default)();0&&(module.exports={fetcher,simpleFetcher});
|
|
2
2
|
//# sourceMappingURL=fetcher.min.cjs.map
|
package/fetcher.min.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/fetcher.ts"],"sourcesContent":["
|
|
1
|
+
{"version":3,"sources":["../src/fetcher.ts"],"sourcesContent":["/**\n * @packageDocumentation\n *\n * An alternate syntax to fetch() that allows function chaining to compose the\n * entire fetch request. One can also compose partial fetcher objects to be\n * reused in future requests.\n *\n * Examples:\n * ```\n * const userFetcher = fetcher\n * .auth(\"Bearer TOPSECRET\")\n * .content(\"application/json\")\n * .url(\"/api/user\");\n *\n * const user = await userFetcher.get();\n *\n * const updateResult = await userFetcher\n * .body({ id: user.id, name: \"New name\"})\n * .post();\n * ```\n */\n\nimport wretch from \"wretch\";\nimport { retry } from \"wretch/middlewares\";\n\n/**\n * Fetcher with default short retries. Use this to make your queries more\n * robust without any additional work.\n */\nexport const fetcher = wretch().middlewares([\n retry({\n maxAttempts: 3,\n delayTimer: 1000,\n retryOnNetworkError: true,\n }),\n]);\n\n/**\n * Name this as `simpleFetcher` to make it easier to understand than `wretch`.\n * Use this if you don't want retries. Use `fetcher` if you want retries.\n */\nexport const simpleFetcher = wretch();\n"],"mappings":"0jBAAA,IAAAA,EAAA,GAAAC,EAAAD,EAAA,aAAAE,EAAA,kBAAAC,IAAA,eAAAC,EAAAJ,GAsBA,IAAAK,EAAmB,qBACnBC,EAAsB,8BAMTJ,KAAU,EAAAK,SAAO,EAAE,YAAY,IAC1C,SAAM,CACJ,YAAa,EACb,WAAY,IACZ,oBAAqB,EACvB,CAAC,CACH,CAAC,EAMYJ,KAAgB,EAAAI,SAAO","names":["fetcher_exports","__export","fetcher","simpleFetcher","__toCommonJS","import_wretch","import_middlewares","wretch"]}
|
package/fetcher.min.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import r from"wretch";import{retry as e}from"wretch/middlewares";var m=r()
|
|
1
|
+
import r from"wretch";import{retry as e}from"wretch/middlewares";var m=r().middlewares([e({maxAttempts:3,delayTimer:1e3,retryOnNetworkError:!0})]),p=r();export{m as fetcher,p as simpleFetcher};
|
|
2
2
|
//# sourceMappingURL=fetcher.min.mjs.map
|
package/fetcher.min.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/fetcher.ts"],"sourcesContent":["
|
|
1
|
+
{"version":3,"sources":["../src/fetcher.ts"],"sourcesContent":["/**\n * @packageDocumentation\n *\n * An alternate syntax to fetch() that allows function chaining to compose the\n * entire fetch request. One can also compose partial fetcher objects to be\n * reused in future requests.\n *\n * Examples:\n * ```\n * const userFetcher = fetcher\n * .auth(\"Bearer TOPSECRET\")\n * .content(\"application/json\")\n * .url(\"/api/user\");\n *\n * const user = await userFetcher.get();\n *\n * const updateResult = await userFetcher\n * .body({ id: user.id, name: \"New name\"})\n * .post();\n * ```\n */\n\nimport wretch from \"wretch\";\nimport { retry } from \"wretch/middlewares\";\n\n/**\n * Fetcher with default short retries. Use this to make your queries more\n * robust without any additional work.\n */\nexport const fetcher = wretch().middlewares([\n retry({\n maxAttempts: 3,\n delayTimer: 1000,\n retryOnNetworkError: true,\n }),\n]);\n\n/**\n * Name this as `simpleFetcher` to make it easier to understand than `wretch`.\n * Use this if you don't want retries. Use `fetcher` if you want retries.\n */\nexport const simpleFetcher = wretch();\n"],"mappings":"AAsBA,OAAOA,MAAY,SACnB,OAAS,SAAAC,MAAa,qBAMf,IAAMC,EAAUF,EAAO,EAAE,YAAY,CAC1CC,EAAM,CACJ,YAAa,EACb,WAAY,IACZ,oBAAqB,EACvB,CAAC,CACH,CAAC,EAMYE,EAAgBH,EAAO","names":["wretch","retry","fetcher","simpleFetcher"]}
|
package/fetcher.mjs
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
// src/fetcher.ts
|
|
2
2
|
import wretch from "wretch";
|
|
3
3
|
import { retry } from "wretch/middlewares";
|
|
4
|
-
var fetcher = wretch()
|
|
5
|
-
var fetcherWithRetries = wretch().middlewares([
|
|
4
|
+
var fetcher = wretch().middlewares([
|
|
6
5
|
retry({
|
|
7
6
|
maxAttempts: 3,
|
|
8
7
|
delayTimer: 1e3,
|
|
9
8
|
retryOnNetworkError: true
|
|
10
9
|
})
|
|
11
10
|
]);
|
|
11
|
+
var simpleFetcher = wretch();
|
|
12
12
|
export {
|
|
13
13
|
fetcher,
|
|
14
|
-
|
|
14
|
+
simpleFetcher
|
|
15
15
|
};
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@choksheak/react-utils",
|
|
3
3
|
"license": "The Unlicense",
|
|
4
|
-
"version": "0.2.
|
|
4
|
+
"version": "0.2.8",
|
|
5
5
|
"description": "Miscellaneous React+TypeScript utilities with support for full tree-shaking.",
|
|
6
6
|
"private": false,
|
|
7
7
|
"files": [
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
"test:watch": "vitest",
|
|
18
18
|
"ci": "npm run lint && npm run tsc && npm run build && npm run test",
|
|
19
19
|
"ls": "lint-staged",
|
|
20
|
+
"clean": "rm -rf dist",
|
|
20
21
|
"docs": "rm -rf docs && typedoc `find src -name '*.ts' ! -name '*.test.ts'` --out docs --name 'Chok’s React Utilities'",
|
|
21
22
|
"gen-types": "tsc --emitDeclarationOnly --declaration",
|
|
22
23
|
"gen-exports": "tsx scripts/gen-exports.ts",
|
package/sharedQuery.cjs
CHANGED
|
@@ -214,7 +214,9 @@ var SharedState = class {
|
|
|
214
214
|
options,
|
|
215
215
|
SharedStateDefaults.storeExpiryMs
|
|
216
216
|
);
|
|
217
|
-
this.setValueBounded = (
|
|
217
|
+
this.setValueBounded = (next) => {
|
|
218
|
+
this.setValue(next);
|
|
219
|
+
};
|
|
218
220
|
pubSubStore.setNoNotify(this.pubSubKey, defaultValue);
|
|
219
221
|
const lazyLoad = options?.lazyLoad;
|
|
220
222
|
const lazy = lazyLoad !== void 0 ? lazyLoad : SharedStateDefaults.lazyLoad;
|
|
@@ -319,24 +321,30 @@ function useDeepMemo(value) {
|
|
|
319
321
|
|
|
320
322
|
// src/sharedQuery.ts
|
|
321
323
|
var SharedQueryDefaults = {
|
|
322
|
-
|
|
324
|
+
/** Refetch on every page load. */
|
|
323
325
|
staleMs: 0,
|
|
324
|
-
|
|
326
|
+
/** Keep in cache for a long time. */
|
|
325
327
|
expiryMs: 30 * import_timeConstants2.MS_PER_DAY,
|
|
326
|
-
|
|
328
|
+
/** Always refetch automatically if the data is stale. */
|
|
327
329
|
refetchOnStale: true,
|
|
328
|
-
|
|
330
|
+
/** No default persistence configured. */
|
|
329
331
|
persistTo: void 0,
|
|
330
|
-
|
|
332
|
+
/** 100 is not too large, but please tweak accordingly. */
|
|
331
333
|
maxSize: 100,
|
|
332
|
-
|
|
334
|
+
/** 100kb limit before discarding old records. */
|
|
333
335
|
maxBytes: 1e5,
|
|
334
|
-
|
|
336
|
+
/** Default to log to console. */
|
|
335
337
|
log: (...args) => console.log("[sharedQuery]", ...args)
|
|
336
338
|
};
|
|
337
339
|
var SharedQuery = class {
|
|
338
340
|
inflightQueries = /* @__PURE__ */ new Map();
|
|
339
341
|
queryState;
|
|
342
|
+
/**
|
|
343
|
+
* Keep track of the mounted query keys. This handles the case where an
|
|
344
|
+
* earlier-started query returns later, and when it returns, it was already
|
|
345
|
+
* unmounted, thus making it eligible for clean up right after fetch.
|
|
346
|
+
*/
|
|
347
|
+
mountedKeys = /* @__PURE__ */ new Map();
|
|
340
348
|
queryName;
|
|
341
349
|
queryFn;
|
|
342
350
|
expiryMs;
|
|
@@ -344,6 +352,7 @@ var SharedQuery = class {
|
|
|
344
352
|
refetchOnStale;
|
|
345
353
|
maxSize;
|
|
346
354
|
maxBytes;
|
|
355
|
+
keepLastNonEmptyData;
|
|
347
356
|
log;
|
|
348
357
|
constructor(options) {
|
|
349
358
|
this.queryName = options.queryName;
|
|
@@ -361,6 +370,7 @@ var SharedQuery = class {
|
|
|
361
370
|
}
|
|
362
371
|
this.maxSize = options?.maxSize ?? SharedQueryDefaults.maxSize;
|
|
363
372
|
this.maxBytes = options?.maxBytes ?? SharedQueryDefaults.maxBytes;
|
|
373
|
+
this.keepLastNonEmptyData = Boolean(options?.keepLastNonEmptyData);
|
|
364
374
|
this.log = options?.log ?? SharedQueryDefaults.log;
|
|
365
375
|
this.queryState = sharedState(
|
|
366
376
|
{},
|
|
@@ -523,6 +533,19 @@ var SharedQuery = class {
|
|
|
523
533
|
}
|
|
524
534
|
this.inflightQueries.clear();
|
|
525
535
|
}
|
|
536
|
+
/** Keep track of mounted keys. */
|
|
537
|
+
mount(queryKey) {
|
|
538
|
+
this.mountedKeys.set(queryKey, (this.mountedKeys.get(queryKey) ?? 0) + 1);
|
|
539
|
+
}
|
|
540
|
+
/** Clean up mounted keys. */
|
|
541
|
+
unmount(queryKey) {
|
|
542
|
+
const count = this.mountedKeys.get(queryKey);
|
|
543
|
+
if (count === 1) {
|
|
544
|
+
this.mountedKeys.delete(queryKey);
|
|
545
|
+
} else {
|
|
546
|
+
this.mountedKeys.set(queryKey, (count ?? 0) - 1);
|
|
547
|
+
}
|
|
548
|
+
}
|
|
526
549
|
/**
|
|
527
550
|
* Discard the oldest entries if the size exceeds the limit. The last
|
|
528
551
|
* remaining record cannot be deleted no matter what the limit is.
|
|
@@ -537,62 +560,48 @@ var SharedQuery = class {
|
|
|
537
560
|
return;
|
|
538
561
|
}
|
|
539
562
|
const oldByteSize = (0, import_object_sizeof.default)(oldState);
|
|
540
|
-
let
|
|
563
|
+
let numToCut = this.maxSize ? oldSize - this.maxSize : 0;
|
|
564
|
+
let bytesToCut = this.maxBytes ? oldByteSize - this.maxBytes : 0;
|
|
565
|
+
if (numToCut <= 0 && bytesToCut <= 0) {
|
|
566
|
+
this.log(
|
|
567
|
+
`No need to trim data for ${this.queryName}: size=${oldSize} (limit=${this.maxSize}), byteSize=${oldByteSize.toLocaleString()} (limit=${this.maxBytes.toLocaleString()})`
|
|
568
|
+
);
|
|
569
|
+
return;
|
|
570
|
+
}
|
|
571
|
+
this.log(
|
|
572
|
+
`Need to trim ${this.queryName}: numToCut=${numToCut}, bytesToCut=${bytesToCut}`
|
|
573
|
+
);
|
|
574
|
+
const newState = { ...oldState };
|
|
541
575
|
let needUpdate = false;
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
);
|
|
548
|
-
|
|
549
|
-
needUpdate = true;
|
|
550
|
-
Object.entries(newState).sort(
|
|
551
|
-
(entry1, entry2) => (
|
|
552
|
-
// Sort descending.
|
|
553
|
-
entry2[1].lastUpdatedMs - entry1[1].lastUpdatedMs
|
|
554
|
-
)
|
|
555
|
-
).slice(-numToCut).forEach(([key]) => {
|
|
556
|
-
delete newState[key];
|
|
557
|
-
});
|
|
576
|
+
const entriesByTimeAscending = Object.entries(newState).sort(
|
|
577
|
+
(entry1, entry2) => entry1[1].lastUpdatedMs - entry2[1].lastUpdatedMs
|
|
578
|
+
);
|
|
579
|
+
for (const [key] of entriesByTimeAscending) {
|
|
580
|
+
if (this.mountedKeys.has(key)) {
|
|
581
|
+
this.log(`Cannot clean up ${key} as it is mounted`);
|
|
582
|
+
continue;
|
|
558
583
|
}
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
needUpdate = true;
|
|
570
|
-
}
|
|
571
|
-
const entriesByTimeAscending = Object.entries(newState).sort(
|
|
572
|
-
(entry1, entry2) => entry1[1].lastUpdatedMs - entry2[1].lastUpdatedMs
|
|
573
|
-
);
|
|
574
|
-
for (
|
|
575
|
-
let i = 0;
|
|
576
|
-
// Leave the last record entriesByTimeAscending[len-1] untouched.
|
|
577
|
-
i < entriesByTimeAscending.length - 1 && bytesToCut > 0;
|
|
578
|
-
i++
|
|
579
|
-
) {
|
|
580
|
-
const key = entriesByTimeAscending[i][0];
|
|
581
|
-
const thisSize = (0, import_object_sizeof.default)(key) + (0, import_object_sizeof.default)(newState[key]);
|
|
582
|
-
delete newState[key];
|
|
583
|
-
bytesToCut -= thisSize;
|
|
584
|
-
}
|
|
584
|
+
const byteSize = (0, import_object_sizeof.default)(key) + (0, import_object_sizeof.default)(newState[key]);
|
|
585
|
+
this.log(
|
|
586
|
+
`Cleaning up unmounted ${key} (byteSize=${byteSize.toLocaleString()})`
|
|
587
|
+
);
|
|
588
|
+
numToCut--;
|
|
589
|
+
bytesToCut -= byteSize;
|
|
590
|
+
delete newState[key];
|
|
591
|
+
needUpdate = true;
|
|
592
|
+
if (numToCut <= 0 && bytesToCut <= 0) {
|
|
593
|
+
break;
|
|
585
594
|
}
|
|
586
595
|
}
|
|
587
|
-
if (needUpdate) {
|
|
588
|
-
this.queryState.setValue(newState);
|
|
596
|
+
if (!needUpdate) {
|
|
589
597
|
this.log(
|
|
590
|
-
`
|
|
598
|
+
`No trimmable entries found for ${this.queryName}: size=${oldSize} (limit=${this.maxSize}), byteSize=${oldByteSize.toLocaleString()} (limit=${this.maxBytes.toLocaleString()})`
|
|
591
599
|
);
|
|
592
600
|
return;
|
|
593
601
|
}
|
|
602
|
+
this.queryState.setValue(newState);
|
|
594
603
|
this.log(
|
|
595
|
-
`
|
|
604
|
+
`Trimmed data for ${this.queryName}: size=[${oldSize} -> ${Object.keys(newState).length}] (limit=${this.maxSize}), byteSize=[${oldByteSize.toLocaleString()} -> ${(0, import_object_sizeof.default)(newState).toLocaleString()}] (limit=${this.maxBytes.toLocaleString()})`
|
|
596
605
|
);
|
|
597
606
|
}
|
|
598
607
|
};
|
|
@@ -616,23 +625,29 @@ function useSharedQuery(query, args = []) {
|
|
|
616
625
|
() => query.getQueryKey(stableArgs),
|
|
617
626
|
[query, stableArgs]
|
|
618
627
|
);
|
|
619
|
-
const queryStateValue = queryState[queryKey];
|
|
628
|
+
const queryStateValue = queryState[queryKey] ?? DEFAULT_QUERY_STATE_ENTRY;
|
|
620
629
|
const setQueryStateValue = (0, import_react3.useCallback)(
|
|
621
|
-
(
|
|
630
|
+
(next) => {
|
|
622
631
|
setQueryState((prev) => {
|
|
623
632
|
const clone = { ...prev };
|
|
624
|
-
clone[queryKey] = typeof
|
|
633
|
+
clone[queryKey] = typeof next === "function" ? next(clone[queryKey] ?? DEFAULT_QUERY_STATE_ENTRY) : next;
|
|
625
634
|
return clone;
|
|
626
635
|
});
|
|
627
636
|
},
|
|
628
637
|
[queryKey, setQueryState]
|
|
629
638
|
);
|
|
639
|
+
const [lastNonEmptyData, setLastNonEmptyData] = (0, import_react3.useState)();
|
|
640
|
+
(0, import_react3.useEffect)(() => {
|
|
641
|
+
if (query.keepLastNonEmptyData && queryStateValue.data !== void 0) {
|
|
642
|
+
setLastNonEmptyData(queryStateValue.data);
|
|
643
|
+
}
|
|
644
|
+
}, [query.keepLastNonEmptyData, queryStateValue.data]);
|
|
630
645
|
const execute = (0, import_react3.useCallback)(
|
|
631
646
|
async (forceRefresh) => {
|
|
632
647
|
query.log(`Begin executing shared query ${queryKey}`);
|
|
633
648
|
setQueryStateValue((prev) => ({
|
|
634
649
|
// Keep old data and error.
|
|
635
|
-
...prev
|
|
650
|
+
...prev,
|
|
636
651
|
lastUpdatedMs: Date.now(),
|
|
637
652
|
loading: true
|
|
638
653
|
}));
|
|
@@ -653,7 +668,7 @@ function useSharedQuery(query, args = []) {
|
|
|
653
668
|
const now = Date.now();
|
|
654
669
|
setQueryStateValue((prev) => ({
|
|
655
670
|
// Keep old data.
|
|
656
|
-
...prev
|
|
671
|
+
...prev,
|
|
657
672
|
lastUpdatedMs: now,
|
|
658
673
|
loading: false,
|
|
659
674
|
error: e,
|
|
@@ -666,15 +681,17 @@ function useSharedQuery(query, args = []) {
|
|
|
666
681
|
);
|
|
667
682
|
(0, import_react3.useEffect)(() => {
|
|
668
683
|
isMounted.current = true;
|
|
684
|
+
query.mount(queryKey);
|
|
669
685
|
void execute(false);
|
|
670
686
|
return () => {
|
|
671
687
|
isMounted.current = false;
|
|
688
|
+
query.unmount(queryKey);
|
|
672
689
|
};
|
|
673
|
-
}, [execute]);
|
|
690
|
+
}, [execute, query, queryKey]);
|
|
674
691
|
return (0, import_react3.useMemo)(() => {
|
|
675
|
-
const state = queryStateValue ?? DEFAULT_QUERY_STATE_ENTRY;
|
|
676
692
|
return {
|
|
677
|
-
...
|
|
693
|
+
...queryStateValue,
|
|
694
|
+
lastNonEmptyData,
|
|
678
695
|
// This works only if the user-given queryFn supports abort. If not,
|
|
679
696
|
// this function doesn't do anything, since we don't have any means to
|
|
680
697
|
// abort the running queryFn.
|
|
@@ -689,7 +706,7 @@ function useSharedQuery(query, args = []) {
|
|
|
689
706
|
setData: (data, dataUpdatedMs) => {
|
|
690
707
|
const now = Date.now();
|
|
691
708
|
setQueryStateValue((prev) => ({
|
|
692
|
-
...prev
|
|
709
|
+
...prev,
|
|
693
710
|
data,
|
|
694
711
|
dataUpdatedMs: dataUpdatedMs ?? now,
|
|
695
712
|
lastUpdatedMs: now,
|
|
@@ -706,6 +723,7 @@ function useSharedQuery(query, args = []) {
|
|
|
706
723
|
};
|
|
707
724
|
}, [
|
|
708
725
|
execute,
|
|
726
|
+
lastNonEmptyData,
|
|
709
727
|
query,
|
|
710
728
|
queryKey,
|
|
711
729
|
queryStateValue,
|
package/sharedQuery.d.mts
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { SharedStateOptions, SharedState } from './sharedState.mjs';
|
|
2
|
+
import 'react';
|
|
2
3
|
import './utils/storage.mjs';
|
|
3
4
|
|
|
4
5
|
/**
|
|
6
|
+
* @packageDocumentation
|
|
7
|
+
*
|
|
5
8
|
* The "shared query" is a data fetching convenience library that helps users
|
|
6
9
|
* to load data, check for staleness and expiration, cache in memory, local
|
|
7
10
|
* storage or indexed db, and be able to manipulate the data anytime.
|
|
@@ -12,11 +15,81 @@ import './utils/storage.mjs';
|
|
|
12
15
|
* records and the total byte size of data can be limited so as to ensure the
|
|
13
16
|
* query does not take up too much memory.
|
|
14
17
|
*
|
|
15
|
-
*
|
|
18
|
+
* Example:
|
|
19
|
+
* ```
|
|
20
|
+
* import { sharedQuery, useSharedQuery } from "@choksheak/react-utils/sharedQuery";
|
|
21
|
+
*
|
|
22
|
+
* // Create a new shared query in the top level scope.
|
|
23
|
+
* // The query function `queryFn` here does not take any arguments.
|
|
24
|
+
* //
|
|
25
|
+
* // Only the queryName and queryFn are required and everything else is
|
|
26
|
+
* // optional.
|
|
27
|
+
* //
|
|
28
|
+
* // The queryName must be unique, otherwise we will throw an
|
|
29
|
+
* // error. This is because defining the same queryName for two different
|
|
30
|
+
* // queries implies that the queries are the same, thus violating the idea
|
|
31
|
+
* // of a "shared" query, and almost certainly points to a bug in the code.
|
|
32
|
+
* export const usersQuery = sharedQuery({
|
|
33
|
+
* queryName: "users",
|
|
34
|
+
* queryFn: listUsers,
|
|
35
|
+
* persistTo: "indexedDb",
|
|
36
|
+
* staleMs: MS_PER_DAY,
|
|
37
|
+
* });
|
|
38
|
+
*
|
|
39
|
+
* // Note that the query will only start fetching on mount in React.
|
|
40
|
+
* // But you can do `usersQuery.getCachedOrFetch()` here to prefetch the
|
|
41
|
+
* // data if necessary.
|
|
42
|
+
* usersQuery.getCachedOrFetch();
|
|
43
|
+
*
|
|
44
|
+
* // Example of using a query function with arguments.
|
|
45
|
+
* export const getUserQuery = sharedQuery({
|
|
46
|
+
* queryName: "getUser",
|
|
47
|
+
* queryFn: getUser, // getUser(userId: string) => User
|
|
48
|
+
* persistTo: "indexedDb",
|
|
49
|
+
* staleMs: MS_PER_DAY,
|
|
50
|
+
* });
|
|
51
|
+
*
|
|
52
|
+
* export const UsersComponent: React.FC = () => {
|
|
53
|
+
* // Second argument is optional, and is an array of arguments to be
|
|
54
|
+
* // passed to the `queryFn`.
|
|
55
|
+
* const users = useSharedQuery(usersQuery);
|
|
56
|
+
*
|
|
57
|
+
* return (
|
|
58
|
+
* <>
|
|
59
|
+
* <h1>List of Users</h1>
|
|
60
|
+
*
|
|
61
|
+
* <button onclick={users.refetch}>Refresh</button>
|
|
62
|
+
*
|
|
63
|
+
* {users.loading && <Loader />}
|
|
64
|
+
*
|
|
65
|
+
* {users.error && !users.data && <p>Error: {String(users.error)}</p>}
|
|
66
|
+
*
|
|
67
|
+
* <p>Data: {users.data ? JSON.stringify(users.data) : "-"}</p>
|
|
68
|
+
* </>
|
|
69
|
+
* );
|
|
70
|
+
* };
|
|
71
|
+
*
|
|
72
|
+
* export const AnotherComponent: React.FC = () => {
|
|
73
|
+
* // This query will be deduped with the one above, sharing the same
|
|
74
|
+
* // data and the same fetches.
|
|
75
|
+
* const users = useSharedQuery(usersQuery);
|
|
76
|
+
* ...
|
|
77
|
+
* };
|
|
78
|
+
*
|
|
79
|
+
* export const DisplayOneUser: React.FC<{userId: string}> = ({userId}) => {
|
|
80
|
+
* // Example of using the query with an argument. Whenever the argument
|
|
81
|
+
* // changes, the data will be fetched and updated automatically.
|
|
82
|
+
* const user = useSharedQuery(getUserQuery, [userId]);
|
|
83
|
+
*
|
|
84
|
+
* return <>User: {user.data ? JSON.stringify(user.data) : "-"}</>;
|
|
85
|
+
* };
|
|
86
|
+
* ```
|
|
16
87
|
*/
|
|
17
88
|
|
|
89
|
+
/** Type of the queryFn used to fetch the data from source. */
|
|
18
90
|
type QueryFn<TArgs extends unknown[], TData> = (...args: TArgs) => Promise<TData> | TData;
|
|
19
|
-
|
|
91
|
+
/** An entry for a persisted state corresponding to one query key. */
|
|
92
|
+
type QueryStateValue<TData> = Readonly<{
|
|
20
93
|
/**
|
|
21
94
|
* When this record was last updated. Used for LRU determination. If you want
|
|
22
95
|
* the data age, please use `dataUpdatedMs` instead.
|
|
@@ -32,8 +105,14 @@ type QueryStateValue<TData> = {
|
|
|
32
105
|
*/
|
|
33
106
|
error?: unknown;
|
|
34
107
|
errorUpdatedMs?: number;
|
|
35
|
-
}
|
|
36
|
-
|
|
108
|
+
}>;
|
|
109
|
+
/** Return type for useSharedQuery(). */
|
|
110
|
+
type UseQueryResult<TData> = QueryStateValue<TData> & Readonly<{
|
|
111
|
+
/**
|
|
112
|
+
* Return the last non-empty data if you want to display the last loaded data
|
|
113
|
+
* while waiting for the new data to load.
|
|
114
|
+
*/
|
|
115
|
+
lastNonEmptyData: TData | undefined;
|
|
37
116
|
/**
|
|
38
117
|
* Cancels the inflight query (if any). Returns true if canceled.
|
|
39
118
|
*/
|
|
@@ -48,37 +127,93 @@ type UseQueryResult<TData> = QueryStateValue<TData> & {
|
|
|
48
127
|
*/
|
|
49
128
|
setData: (data: TData, dataUpdatedMs?: number) => void;
|
|
50
129
|
deleteData: () => void;
|
|
51
|
-
}
|
|
130
|
+
}>;
|
|
131
|
+
/** List of all available persistence options. */
|
|
52
132
|
type PersistTo = "localStorage" | "indexedDb";
|
|
53
133
|
/**
|
|
54
134
|
* The full query state is a record of all the fetch states mapped by
|
|
55
135
|
* query key to the state entry.
|
|
56
136
|
*/
|
|
57
137
|
type SharedQueryState<TData> = Record<string, QueryStateValue<TData>>;
|
|
138
|
+
/** Options to configure a shared query. */
|
|
58
139
|
type SharedQueryOptions<TArgs extends unknown[], TData> = {
|
|
140
|
+
/**
|
|
141
|
+
* The name could have been auto-generated, but we let the user give us a
|
|
142
|
+
* human-readable name that can be identified quickly in the logs.
|
|
143
|
+
* Note that the queryKey is a string formed by the queryName plus the query
|
|
144
|
+
* arguments.
|
|
145
|
+
*/
|
|
59
146
|
queryName: string;
|
|
147
|
+
/**
|
|
148
|
+
* Function to fetch data.
|
|
149
|
+
*/
|
|
60
150
|
queryFn: QueryFn<TArgs, TData>;
|
|
151
|
+
/**
|
|
152
|
+
* ms before data considered stale; 0 means always stale.
|
|
153
|
+
*/
|
|
61
154
|
staleMs?: number;
|
|
155
|
+
/**
|
|
156
|
+
* ms before data is removed from cache; 0 means never expire.
|
|
157
|
+
*/
|
|
62
158
|
expiryMs?: number;
|
|
159
|
+
/**
|
|
160
|
+
* Trigger background re-fetch if stale.
|
|
161
|
+
*/
|
|
63
162
|
refetchOnStale?: boolean;
|
|
163
|
+
/**
|
|
164
|
+
* Shortcut to set the localStorageKey or indexedDbKey automatically from
|
|
165
|
+
* queryName. This is important because it avoids requiring the user to
|
|
166
|
+
* specify the same queryName twice everytime.
|
|
167
|
+
*/
|
|
64
168
|
persistTo?: PersistTo;
|
|
169
|
+
/**
|
|
170
|
+
* Max number of entries to keep. The oldest entries will be discarded.
|
|
171
|
+
* The last record cannot be discarded. 0 means no limit.
|
|
172
|
+
*/
|
|
65
173
|
maxSize?: number;
|
|
174
|
+
/**
|
|
175
|
+
* Max number of bytes to keep. The oldest entries will be discarded.
|
|
176
|
+
* The last record cannot be discarded. 0 means no limit.
|
|
177
|
+
*/
|
|
66
178
|
maxBytes?: number;
|
|
179
|
+
/**
|
|
180
|
+
* Keep a copy of the last non-empty data so that when you change the
|
|
181
|
+
* query arguments, you can still display the old data while waiting for the
|
|
182
|
+
* new data to load.
|
|
183
|
+
*/
|
|
184
|
+
keepLastNonEmptyData?: boolean;
|
|
185
|
+
/**
|
|
186
|
+
* Customize the logger.
|
|
187
|
+
*/
|
|
67
188
|
log?: (...args: unknown[]) => void;
|
|
68
189
|
} & SharedStateOptions<SharedQueryState<TData>>;
|
|
69
190
|
/** Users can override these values globally. */
|
|
70
191
|
declare const SharedQueryDefaults: {
|
|
192
|
+
/** Refetch on every page load. */
|
|
71
193
|
staleMs: number;
|
|
194
|
+
/** Keep in cache for a long time. */
|
|
72
195
|
expiryMs: number;
|
|
196
|
+
/** Always refetch automatically if the data is stale. */
|
|
73
197
|
refetchOnStale: boolean;
|
|
198
|
+
/** No default persistence configured. */
|
|
74
199
|
persistTo: PersistTo | undefined;
|
|
200
|
+
/** 100 is not too large, but please tweak accordingly. */
|
|
75
201
|
maxSize: number;
|
|
202
|
+
/** 100kb limit before discarding old records. */
|
|
76
203
|
maxBytes: number;
|
|
204
|
+
/** Default to log to console. */
|
|
77
205
|
log: (...args: unknown[]) => void;
|
|
78
206
|
};
|
|
207
|
+
/** Please use sharedQuery() instead. */
|
|
79
208
|
declare class SharedQuery<TArgs extends unknown[], TData> {
|
|
80
209
|
private readonly inflightQueries;
|
|
81
210
|
readonly queryState: SharedState<SharedQueryState<TData>>;
|
|
211
|
+
/**
|
|
212
|
+
* Keep track of the mounted query keys. This handles the case where an
|
|
213
|
+
* earlier-started query returns later, and when it returns, it was already
|
|
214
|
+
* unmounted, thus making it eligible for clean up right after fetch.
|
|
215
|
+
*/
|
|
216
|
+
private readonly mountedKeys;
|
|
82
217
|
readonly queryName: string;
|
|
83
218
|
private readonly queryFn;
|
|
84
219
|
readonly expiryMs: number;
|
|
@@ -86,6 +221,7 @@ declare class SharedQuery<TArgs extends unknown[], TData> {
|
|
|
86
221
|
readonly refetchOnStale: boolean;
|
|
87
222
|
readonly maxSize: number;
|
|
88
223
|
readonly maxBytes: number;
|
|
224
|
+
readonly keepLastNonEmptyData: boolean;
|
|
89
225
|
readonly log: (...args: unknown[]) => void;
|
|
90
226
|
constructor(options: SharedQueryOptions<TArgs, TData>);
|
|
91
227
|
/** Returns a key to identify requests based on the given args. */
|
|
@@ -146,6 +282,10 @@ declare class SharedQuery<TArgs extends unknown[], TData> {
|
|
|
146
282
|
deleteData(queryKey: string): void;
|
|
147
283
|
/** Delete all currently cached data & all inflight promises. */
|
|
148
284
|
clear(): void;
|
|
285
|
+
/** Keep track of mounted keys. */
|
|
286
|
+
mount(queryKey: string): void;
|
|
287
|
+
/** Clean up mounted keys. */
|
|
288
|
+
unmount(queryKey: string): void;
|
|
149
289
|
/**
|
|
150
290
|
* Discard the oldest entries if the size exceeds the limit. The last
|
|
151
291
|
* remaining record cannot be deleted no matter what the limit is.
|