@reykjavik/webtools 0.3.5 → 0.3.6
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/CHANGELOG.md +10 -1
- package/README.md +80 -9
- package/async.d.ts +51 -0
- package/async.js +58 -1
- package/errorhandling.d.ts +1 -1
- package/esm/async.d.ts +51 -0
- package/esm/async.js +56 -0
- package/esm/errorhandling.d.ts +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,11 +4,20 @@
|
|
|
4
4
|
|
|
5
5
|
- ... <!-- Add new lines here. -->
|
|
6
6
|
|
|
7
|
+
## 0.3.6
|
|
8
|
+
|
|
9
|
+
_2026-02-24_
|
|
10
|
+
|
|
11
|
+
- `@reykjavik/webtools/async`:
|
|
12
|
+
- feat: Add `cachifyAsync` helper for simple (yet robust) caching of async
|
|
13
|
+
functions
|
|
14
|
+
|
|
7
15
|
## 0.3.5
|
|
8
16
|
|
|
9
17
|
_2026-02-18_
|
|
10
18
|
|
|
11
|
-
-
|
|
19
|
+
- `@reykjavik/webtools/errorhandling`:
|
|
20
|
+
- fix: `Result.ErrorOf<T>` not handling functions with parameters correctly
|
|
12
21
|
|
|
13
22
|
## 0.3.4
|
|
14
23
|
|
package/README.md
CHANGED
|
@@ -30,6 +30,7 @@ bun add @reykjavik/webtools
|
|
|
30
30
|
- [`promiseAllObject`](#promiseallobject)
|
|
31
31
|
- [`maxWait`](#maxwait)
|
|
32
32
|
- [`debounce`](#debounce)
|
|
33
|
+
- [`cachifyAsync`](#cachifyasync)
|
|
33
34
|
- [`throttle`](#throttle)
|
|
34
35
|
- [`@reykjavik/webtools/hoooks`](#reykjavikwebtoolshoooks)
|
|
35
36
|
- [`useDebounced`](#usedebounced)
|
|
@@ -307,20 +308,21 @@ detection test.)
|
|
|
307
308
|
|
|
308
309
|
**`Intl.NumberFormat` and `toLocaleString`:**
|
|
309
310
|
|
|
310
|
-
- The `style: "unit"` option is not supported and prints units in Danish. (
|
|
311
|
-
many units and unit-variants…)
|
|
311
|
+
- The `style: "unit"` option is not supported and prints units in Danish. (So,
|
|
312
|
+
so (!!) many units and unit-variants… Impractical to handle size-wise.)
|
|
312
313
|
- The `currencyDisplay: "name"` option is not supported and prints the
|
|
313
|
-
currency's full name in Danish.
|
|
314
|
+
currency's full name in Danish. (Impractical to handle size-wise.)
|
|
314
315
|
|
|
315
316
|
**`Intl.DateTimeFormat` and `toLocaleDateString`:**
|
|
316
317
|
|
|
317
318
|
- The `month: 'narrow'` and `weekday: 'narrow'` options are not supported, and
|
|
318
|
-
print the corresponding Danish initials.
|
|
319
|
+
print the corresponding Danish initials. (Near impossible to patch because
|
|
320
|
+
the Danish initials are ambigious)
|
|
319
321
|
- For `timeZoneName` the values `"long"`, `"shortGeneric"` and `"longGeneric"`
|
|
320
|
-
will appear in Danish.
|
|
322
|
+
will appear in Danish. (Impractical to handle size-wise.)
|
|
321
323
|
- The `timeStyle: 'full'` option prints the timezone names in Danish
|
|
322
324
|
- The `dayPeriod` option has a couple of slight mismatches, at 5 am and 12
|
|
323
|
-
noon.
|
|
325
|
+
noon. (Completely harmless.)
|
|
324
326
|
|
|
325
327
|
We eagerly accept bugfixes, additions, etc. to this module!
|
|
326
328
|
|
|
@@ -439,6 +441,67 @@ sayHello.cancel(true); // `finish` parmeter is true
|
|
|
439
441
|
|
|
440
442
|
---
|
|
441
443
|
|
|
444
|
+
### `cachifyAsync`
|
|
445
|
+
|
|
446
|
+
**Syntax:**
|
|
447
|
+
`cachifyAsync<R, F extends (...args: any[]) => Promise<Result.TupleObj<R>>>(opts: { fn: F; ttl: TTL; throttle?: TTL; customTtl?: (args: Parameters<F>, result: Result.TupleObj<R>) => TTL | undefined; getKey?: (...args: Parameters<F>) => string; returnStale?: boolean }): F`
|
|
448
|
+
|
|
449
|
+
Wraps an async function with a simple, robust caching layer. Returns a
|
|
450
|
+
function with the same signature as `fn`, but with caching applied.
|
|
451
|
+
|
|
452
|
+
The caching strategy is simple. If `fn` resolves to an error result, the error
|
|
453
|
+
is cached for a short time (default: `30s`) to avoid hammering the underlying
|
|
454
|
+
function, and a stale (last successful) result is returned if available. The
|
|
455
|
+
error result is only while waiting for the issue to be resolved. Return stale
|
|
456
|
+
(last successful) result while throttling.
|
|
457
|
+
|
|
458
|
+
- No max size or eviction strategy—intended for caching a small, clearly
|
|
459
|
+
bounded number of different cache "keys" (e.g. per language).
|
|
460
|
+
|
|
461
|
+
**Options:**
|
|
462
|
+
|
|
463
|
+
- `fn: <T>(...args: ay[]) => Promise<Result.TupleObj<T>>` — The async function
|
|
464
|
+
to cache.
|
|
465
|
+
- `ttl: TTL` — How long to cache successful results. Number values are treated
|
|
466
|
+
as seconds. (See (`TTL` type)[#type-ttl]).
|
|
467
|
+
- `throttle? TTL` — The minimum time between retries for error results.
|
|
468
|
+
Numbers are treated as seconds.
|
|
469
|
+
- `customTtl?: (args: Parameters<typeof fn>, result: Result.TupleObj<T>) => TTL | undefined;`
|
|
470
|
+
— set a custom TTL on success and/or error results. Return `undefined` to
|
|
471
|
+
use the default `ttl`/`throttle` values.
|
|
472
|
+
- `getKey?: (...args: Parameters<typeof fn>) => string` — Creates a custom
|
|
473
|
+
cache key for the current result set. Default: `JSON.stringify(args)`.
|
|
474
|
+
- `returnStale?: boolean` — Whether to return stale (last successful) result
|
|
475
|
+
when `fn` resolves to an error result. Defaults to `true`.
|
|
476
|
+
|
|
477
|
+
**Example:**
|
|
478
|
+
|
|
479
|
+
```ts
|
|
480
|
+
import { cachifyAsync } from '@reykjavik/webtools/async';
|
|
481
|
+
import { Result } from '@reykjavik/webtools/errorhandling';
|
|
482
|
+
|
|
483
|
+
const fetchUser = async (id: string) =>
|
|
484
|
+
Result.ify(fetch(`/api/user/${id}`).then((r) => r.json()));
|
|
485
|
+
|
|
486
|
+
const cachedFetchUser = cachifyAsync({
|
|
487
|
+
fn: fetchUser,
|
|
488
|
+
ttl: '10m',
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
// ---------------------****---------------------------------------
|
|
492
|
+
// Usage:
|
|
493
|
+
|
|
494
|
+
const result = await cachedFetchUser('123');
|
|
495
|
+
|
|
496
|
+
if (result.error) {
|
|
497
|
+
// handle error
|
|
498
|
+
} else {
|
|
499
|
+
// use result.result
|
|
500
|
+
}
|
|
501
|
+
```
|
|
502
|
+
|
|
503
|
+
---
|
|
504
|
+
|
|
442
505
|
### `throttle`
|
|
443
506
|
|
|
444
507
|
**Syntax:**
|
|
@@ -621,7 +684,7 @@ handling `ResultTupleObj` instances:
|
|
|
621
684
|
|
|
622
685
|
- `Result.Success`
|
|
623
686
|
- `Result.Fail`
|
|
624
|
-
- `Result.catch`
|
|
687
|
+
- `Result.catch` / `Result.ify`
|
|
625
688
|
- `Result.map`
|
|
626
689
|
- `Result.throw`
|
|
627
690
|
|
|
@@ -855,7 +918,9 @@ import { Result } from '@reykjavik/webtools/errorhandling';
|
|
|
855
918
|
type ResTpl = Result.Tuple<string, Error>;
|
|
856
919
|
type ResTplPromise = Promise<Result.Tuple<number, Error>>;
|
|
857
920
|
type ResTplFn = (arg: unknown) => Result.Tuple<boolean, Error>;
|
|
858
|
-
type ResTplPromiseFn = (
|
|
921
|
+
type ResTplPromiseFn = (
|
|
922
|
+
arg: unknown
|
|
923
|
+
) => Promise<Result.TupleObj<Date, Error>>;
|
|
859
924
|
|
|
860
925
|
type Payload1 = Result.PayloadOf<ResTpl>; // string
|
|
861
926
|
type Payload2 = Result.PayloadOf<ResTplPromise>; // number
|
|
@@ -863,6 +928,9 @@ type Payload3 = Result.PayloadOf<ResTplFn>; // boolean
|
|
|
863
928
|
type Payload4 = Result.PayloadOf<ResTplPromiseFn>; // Date
|
|
864
929
|
```
|
|
865
930
|
|
|
931
|
+
NOTE: This type also works for [`ResultTupleObj`](#type-resulttupleobj) as
|
|
932
|
+
it's a subtype of `ResultTuple`.
|
|
933
|
+
|
|
866
934
|
---
|
|
867
935
|
|
|
868
936
|
### Type `Result.ErrorOf`
|
|
@@ -881,7 +949,7 @@ type ResTplPromise = Promise<Result.Tuple<number, RangeError>>;
|
|
|
881
949
|
type ResTplFn = (arg: unknown) => Result.Tuple<boolean, RangeError>;
|
|
882
950
|
type ResTplPromiseFn = (
|
|
883
951
|
arg: unknown
|
|
884
|
-
) => Promise<Result.
|
|
952
|
+
) => Promise<Result.TupleÞObj<Date, RangeError>>;
|
|
885
953
|
|
|
886
954
|
type Error1 = Result.ErrorOf<ResTpl>; // RangeError
|
|
887
955
|
type Error2 = Result.ErrorOf<ResTplPromise>; // RangeError
|
|
@@ -889,6 +957,9 @@ type Error3 = Result.ErrorOf<ResTplFn>; // RangeError
|
|
|
889
957
|
type Error4 = Result.ErrorOf<ResTplPromiseFn>; // RangeError
|
|
890
958
|
```
|
|
891
959
|
|
|
960
|
+
NOTE: This type also works for [`ResultTupleObj`](#type-resulttupleobj) as
|
|
961
|
+
it's a subtype of `ResultTuple`.
|
|
962
|
+
|
|
892
963
|
---
|
|
893
964
|
|
|
894
965
|
## `@reykjavik/webtools/SiteImprove`
|
package/async.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { EitherObj } from '@reykjavik/hanna-utils';
|
|
2
|
+
import { Result } from './errorhandling.js';
|
|
3
|
+
import { TTL } from './http.js';
|
|
2
4
|
type PlainObj = Record<string, unknown>;
|
|
3
5
|
/**
|
|
4
6
|
* Simple sleep function. Returns a promise that resolves after `length`
|
|
@@ -70,4 +72,53 @@ export declare const throttle: {
|
|
|
70
72
|
<A extends Array<unknown>>(func: (...args: A) => void, delay: number, skipFirst?: boolean): Finishable<A>;
|
|
71
73
|
d(delay: number, skipFirst?: boolean): Finishable<[fn: (...args: Array<any>) => void, ...args: any[]]>;
|
|
72
74
|
};
|
|
75
|
+
/**
|
|
76
|
+
* Wraps an async function with a simple, but fairly robust caching layer.
|
|
77
|
+
*
|
|
78
|
+
* Successful results are cached for `ttlMs`, while error results are
|
|
79
|
+
* throttled to avoid hammering the underlying function.
|
|
80
|
+
*
|
|
81
|
+
* Has no max size or eviction strategy; intended for caching a small,
|
|
82
|
+
* clearly bounded number of different cache "keys" (e.g. per language).
|
|
83
|
+
*
|
|
84
|
+
* @see https://github.com/reykjavikcity/webtools/blob/v0.3/README.md#cachifyasync
|
|
85
|
+
*/
|
|
86
|
+
export declare const cachifyAsync: <R, F extends (...args: Array<any>) => Promise<Result.TupleObj<R>>>(opts: {
|
|
87
|
+
/** The async function to cache. */
|
|
88
|
+
fn: F;
|
|
89
|
+
/** How long to cache successful results. Number values are treated as seconds */
|
|
90
|
+
ttl: TTL;
|
|
91
|
+
/**
|
|
92
|
+
* The minimum time between retries for error results, to avoid hammering
|
|
93
|
+
* the underlying function while waiting for the issue to be (hopefully)
|
|
94
|
+
* resolved.
|
|
95
|
+
*
|
|
96
|
+
* Raw numbers are treated as seconds.
|
|
97
|
+
*
|
|
98
|
+
* Default: '30s'
|
|
99
|
+
*/
|
|
100
|
+
throttle?: TTL;
|
|
101
|
+
/**
|
|
102
|
+
* Function to optionally set a custom TTL on success and/or error results,
|
|
103
|
+
* when the promise resolves.
|
|
104
|
+
*
|
|
105
|
+
* If `undefined` is returned, the default `ttlMs` and` `throttleMs` settings
|
|
106
|
+
* are used.
|
|
107
|
+
*/
|
|
108
|
+
customTtl?: (args: Parameters<F>, result: Result.TupleObj<R>) => TTL | undefined;
|
|
109
|
+
/**
|
|
110
|
+
* Creates a custom cache key for the current result set
|
|
111
|
+
*
|
|
112
|
+
* Default: `JSON.stringify` of the function arguments
|
|
113
|
+
*
|
|
114
|
+
*/
|
|
115
|
+
getKey?: (...args: Parameters<F>) => string;
|
|
116
|
+
/**
|
|
117
|
+
* Whether to return stale (last successful) result when `fn` resolves to an
|
|
118
|
+
* error result.
|
|
119
|
+
*
|
|
120
|
+
* Default: `true`
|
|
121
|
+
*/
|
|
122
|
+
returnStale?: boolean;
|
|
123
|
+
}) => F;
|
|
73
124
|
export {};
|
package/async.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.throttle = exports.debounce = exports.promiseAllObject = exports.addLag = exports.sleep = void 0;
|
|
3
|
+
exports.cachifyAsync = exports.throttle = exports.debounce = exports.promiseAllObject = exports.addLag = exports.sleep = void 0;
|
|
4
4
|
exports.maxWait = maxWait;
|
|
5
|
+
const http_js_1 = require("./http.js");
|
|
5
6
|
/**
|
|
6
7
|
* Simple sleep function. Returns a promise that resolves after `length`
|
|
7
8
|
* milliseconds.
|
|
@@ -183,3 +184,59 @@ exports.throttle = throttle;
|
|
|
183
184
|
exports.throttle.d = (delay, skipFirst) => (0, exports.throttle)(function (fn, ...args) {
|
|
184
185
|
fn.apply(this, args);
|
|
185
186
|
}, delay, skipFirst);
|
|
187
|
+
// ---------------------------------------------------------------------------
|
|
188
|
+
// Wrap toSec to use a 90% shorter TTL in development mode
|
|
189
|
+
const toSec = process.env.NODE_ENV === 'production' ? http_js_1.toSec : (val) => (0, http_js_1.toSec)(val) / 10;
|
|
190
|
+
const DEFAULT_THROTTLING_MS = '30s';
|
|
191
|
+
/**
|
|
192
|
+
* Wraps an async function with a simple, but fairly robust caching layer.
|
|
193
|
+
*
|
|
194
|
+
* Successful results are cached for `ttlMs`, while error results are
|
|
195
|
+
* throttled to avoid hammering the underlying function.
|
|
196
|
+
*
|
|
197
|
+
* Has no max size or eviction strategy; intended for caching a small,
|
|
198
|
+
* clearly bounded number of different cache "keys" (e.g. per language).
|
|
199
|
+
*
|
|
200
|
+
* @see https://github.com/reykjavikcity/webtools/blob/v0.3/README.md#cachifyasync
|
|
201
|
+
*/
|
|
202
|
+
const cachifyAsync = (opts) => {
|
|
203
|
+
const { fn, getKey = (...args) => JSON.stringify(args), customTtl, returnStale } = opts;
|
|
204
|
+
// Set up the cache object
|
|
205
|
+
const TTL_SEC = toSec(opts.ttl);
|
|
206
|
+
const THROTTLING_SEC = toSec(opts.throttle || 0) || Math.min(toSec(DEFAULT_THROTTLING_MS), TTL_SEC);
|
|
207
|
+
const _cache = new Map();
|
|
208
|
+
return (async (...args) => {
|
|
209
|
+
const now = Date.now();
|
|
210
|
+
const key = getKey(...args);
|
|
211
|
+
const cached = _cache.get(key);
|
|
212
|
+
if (cached && now < cached.freshUntil) {
|
|
213
|
+
return cached.data;
|
|
214
|
+
}
|
|
215
|
+
const lastData = returnStale !== false && (cached === null || cached === void 0 ? void 0 : cached.data);
|
|
216
|
+
const entry = {
|
|
217
|
+
// Set an initial "fresh until" that's longer than TTL_SEC to cover
|
|
218
|
+
// (somewhat) safely the time it takes for the promise to resolve,
|
|
219
|
+
// so that we don't trigger multiple calls to `fn` in parallel
|
|
220
|
+
// TODO: Build in a proper AbortSignal timeout, etc. to handle this more robustly
|
|
221
|
+
freshUntil: now + (TTL_SEC + 60) * 1000,
|
|
222
|
+
data: fn(...args).then((result) => {
|
|
223
|
+
const customTtlSec = toSec((customTtl === null || customTtl === void 0 ? void 0 : customTtl(args, result)) || 0);
|
|
224
|
+
entry.freshUntil = now + (customTtlSec || TTL_SEC) * 1000;
|
|
225
|
+
if (result.error) {
|
|
226
|
+
if (!customTtlSec) {
|
|
227
|
+
// Set shorter TTL on errors to allow quicker retries
|
|
228
|
+
entry.freshUntil = now + THROTTLING_SEC * 1000;
|
|
229
|
+
}
|
|
230
|
+
if (lastData) {
|
|
231
|
+
// Return last known good data if available, even if it's a bit stale
|
|
232
|
+
return lastData;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
return result;
|
|
236
|
+
}),
|
|
237
|
+
};
|
|
238
|
+
_cache.set(key, entry);
|
|
239
|
+
return entry.data;
|
|
240
|
+
});
|
|
241
|
+
};
|
|
242
|
+
exports.cachifyAsync = cachifyAsync;
|
package/errorhandling.d.ts
CHANGED
|
@@ -119,7 +119,7 @@ export declare namespace Result {
|
|
|
119
119
|
* Extracts the error type `E` from a `Result.Tuple<T, E>`-like
|
|
120
120
|
* type, a `Promise` of such type, or a function returning either of those.
|
|
121
121
|
*
|
|
122
|
-
* @see https://github.com/reykjavikcity/webtools/blob/v0.3/README.md#type-
|
|
122
|
+
* @see https://github.com/reykjavikcity/webtools/blob/v0.3/README.md#type-resulterrorof
|
|
123
123
|
*/
|
|
124
124
|
type ErrorOf<T extends ResultTuple<unknown> | Promise<ResultTuple<unknown>> | ((...args: Array<any>) => ResultTuple<unknown> | Promise<ResultTuple<unknown>>)> = T extends [infer E, undefined?] ? E : T extends Promise<infer P> ? P extends [infer E, undefined?] ? E : never : T extends (...args: Array<any>) => infer R ? R extends [infer E, undefined?] ? E : R extends Promise<infer P> ? P extends [infer E, undefined?] ? E : never : never : never;
|
|
125
125
|
}
|
package/esm/async.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { EitherObj } from '@reykjavik/hanna-utils';
|
|
2
|
+
import { Result } from './errorhandling.js';
|
|
3
|
+
import { TTL } from './http.js';
|
|
2
4
|
type PlainObj = Record<string, unknown>;
|
|
3
5
|
/**
|
|
4
6
|
* Simple sleep function. Returns a promise that resolves after `length`
|
|
@@ -70,4 +72,53 @@ export declare const throttle: {
|
|
|
70
72
|
<A extends Array<unknown>>(func: (...args: A) => void, delay: number, skipFirst?: boolean): Finishable<A>;
|
|
71
73
|
d(delay: number, skipFirst?: boolean): Finishable<[fn: (...args: Array<any>) => void, ...args: any[]]>;
|
|
72
74
|
};
|
|
75
|
+
/**
|
|
76
|
+
* Wraps an async function with a simple, but fairly robust caching layer.
|
|
77
|
+
*
|
|
78
|
+
* Successful results are cached for `ttlMs`, while error results are
|
|
79
|
+
* throttled to avoid hammering the underlying function.
|
|
80
|
+
*
|
|
81
|
+
* Has no max size or eviction strategy; intended for caching a small,
|
|
82
|
+
* clearly bounded number of different cache "keys" (e.g. per language).
|
|
83
|
+
*
|
|
84
|
+
* @see https://github.com/reykjavikcity/webtools/blob/v0.3/README.md#cachifyasync
|
|
85
|
+
*/
|
|
86
|
+
export declare const cachifyAsync: <R, F extends (...args: Array<any>) => Promise<Result.TupleObj<R>>>(opts: {
|
|
87
|
+
/** The async function to cache. */
|
|
88
|
+
fn: F;
|
|
89
|
+
/** How long to cache successful results. Number values are treated as seconds */
|
|
90
|
+
ttl: TTL;
|
|
91
|
+
/**
|
|
92
|
+
* The minimum time between retries for error results, to avoid hammering
|
|
93
|
+
* the underlying function while waiting for the issue to be (hopefully)
|
|
94
|
+
* resolved.
|
|
95
|
+
*
|
|
96
|
+
* Raw numbers are treated as seconds.
|
|
97
|
+
*
|
|
98
|
+
* Default: '30s'
|
|
99
|
+
*/
|
|
100
|
+
throttle?: TTL;
|
|
101
|
+
/**
|
|
102
|
+
* Function to optionally set a custom TTL on success and/or error results,
|
|
103
|
+
* when the promise resolves.
|
|
104
|
+
*
|
|
105
|
+
* If `undefined` is returned, the default `ttlMs` and` `throttleMs` settings
|
|
106
|
+
* are used.
|
|
107
|
+
*/
|
|
108
|
+
customTtl?: (args: Parameters<F>, result: Result.TupleObj<R>) => TTL | undefined;
|
|
109
|
+
/**
|
|
110
|
+
* Creates a custom cache key for the current result set
|
|
111
|
+
*
|
|
112
|
+
* Default: `JSON.stringify` of the function arguments
|
|
113
|
+
*
|
|
114
|
+
*/
|
|
115
|
+
getKey?: (...args: Parameters<F>) => string;
|
|
116
|
+
/**
|
|
117
|
+
* Whether to return stale (last successful) result when `fn` resolves to an
|
|
118
|
+
* error result.
|
|
119
|
+
*
|
|
120
|
+
* Default: `true`
|
|
121
|
+
*/
|
|
122
|
+
returnStale?: boolean;
|
|
123
|
+
}) => F;
|
|
73
124
|
export {};
|
package/esm/async.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { toSec as _toSec } from './http.js';
|
|
1
2
|
/**
|
|
2
3
|
* Simple sleep function. Returns a promise that resolves after `length`
|
|
3
4
|
* milliseconds.
|
|
@@ -174,3 +175,58 @@ skipFirst) => {
|
|
|
174
175
|
throttle.d = (delay, skipFirst) => throttle(function (fn, ...args) {
|
|
175
176
|
fn.apply(this, args);
|
|
176
177
|
}, delay, skipFirst);
|
|
178
|
+
// ---------------------------------------------------------------------------
|
|
179
|
+
// Wrap toSec to use a 90% shorter TTL in development mode
|
|
180
|
+
const toSec = process.env.NODE_ENV === 'production' ? _toSec : (val) => _toSec(val) / 10;
|
|
181
|
+
const DEFAULT_THROTTLING_MS = '30s';
|
|
182
|
+
/**
|
|
183
|
+
* Wraps an async function with a simple, but fairly robust caching layer.
|
|
184
|
+
*
|
|
185
|
+
* Successful results are cached for `ttlMs`, while error results are
|
|
186
|
+
* throttled to avoid hammering the underlying function.
|
|
187
|
+
*
|
|
188
|
+
* Has no max size or eviction strategy; intended for caching a small,
|
|
189
|
+
* clearly bounded number of different cache "keys" (e.g. per language).
|
|
190
|
+
*
|
|
191
|
+
* @see https://github.com/reykjavikcity/webtools/blob/v0.3/README.md#cachifyasync
|
|
192
|
+
*/
|
|
193
|
+
export const cachifyAsync = (opts) => {
|
|
194
|
+
const { fn, getKey = (...args) => JSON.stringify(args), customTtl, returnStale } = opts;
|
|
195
|
+
// Set up the cache object
|
|
196
|
+
const TTL_SEC = toSec(opts.ttl);
|
|
197
|
+
const THROTTLING_SEC = toSec(opts.throttle || 0) || Math.min(toSec(DEFAULT_THROTTLING_MS), TTL_SEC);
|
|
198
|
+
const _cache = new Map();
|
|
199
|
+
return (async (...args) => {
|
|
200
|
+
const now = Date.now();
|
|
201
|
+
const key = getKey(...args);
|
|
202
|
+
const cached = _cache.get(key);
|
|
203
|
+
if (cached && now < cached.freshUntil) {
|
|
204
|
+
return cached.data;
|
|
205
|
+
}
|
|
206
|
+
const lastData = returnStale !== false && (cached === null || cached === void 0 ? void 0 : cached.data);
|
|
207
|
+
const entry = {
|
|
208
|
+
// Set an initial "fresh until" that's longer than TTL_SEC to cover
|
|
209
|
+
// (somewhat) safely the time it takes for the promise to resolve,
|
|
210
|
+
// so that we don't trigger multiple calls to `fn` in parallel
|
|
211
|
+
// TODO: Build in a proper AbortSignal timeout, etc. to handle this more robustly
|
|
212
|
+
freshUntil: now + (TTL_SEC + 60) * 1000,
|
|
213
|
+
data: fn(...args).then((result) => {
|
|
214
|
+
const customTtlSec = toSec((customTtl === null || customTtl === void 0 ? void 0 : customTtl(args, result)) || 0);
|
|
215
|
+
entry.freshUntil = now + (customTtlSec || TTL_SEC) * 1000;
|
|
216
|
+
if (result.error) {
|
|
217
|
+
if (!customTtlSec) {
|
|
218
|
+
// Set shorter TTL on errors to allow quicker retries
|
|
219
|
+
entry.freshUntil = now + THROTTLING_SEC * 1000;
|
|
220
|
+
}
|
|
221
|
+
if (lastData) {
|
|
222
|
+
// Return last known good data if available, even if it's a bit stale
|
|
223
|
+
return lastData;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
return result;
|
|
227
|
+
}),
|
|
228
|
+
};
|
|
229
|
+
_cache.set(key, entry);
|
|
230
|
+
return entry.data;
|
|
231
|
+
});
|
|
232
|
+
};
|
package/esm/errorhandling.d.ts
CHANGED
|
@@ -119,7 +119,7 @@ export declare namespace Result {
|
|
|
119
119
|
* Extracts the error type `E` from a `Result.Tuple<T, E>`-like
|
|
120
120
|
* type, a `Promise` of such type, or a function returning either of those.
|
|
121
121
|
*
|
|
122
|
-
* @see https://github.com/reykjavikcity/webtools/blob/v0.3/README.md#type-
|
|
122
|
+
* @see https://github.com/reykjavikcity/webtools/blob/v0.3/README.md#type-resulterrorof
|
|
123
123
|
*/
|
|
124
124
|
type ErrorOf<T extends ResultTuple<unknown> | Promise<ResultTuple<unknown>> | ((...args: Array<any>) => ResultTuple<unknown> | Promise<ResultTuple<unknown>>)> = T extends [infer E, undefined?] ? E : T extends Promise<infer P> ? P extends [infer E, undefined?] ? E : never : T extends (...args: Array<any>) => infer R ? R extends [infer E, undefined?] ? E : R extends Promise<infer P> ? P extends [infer E, undefined?] ? E : never : never : never;
|
|
125
125
|
}
|
package/package.json
CHANGED