@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 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
- - fix: `Result.ErrorOf<T>` not handling functions with parameters correctly
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. (Soo
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 = (arg: unknown) => Promise<Result.Tuple<Date, Error>>;
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.Tuple<Date, RangeError>>;
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;
@@ -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-resultpayloadof
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
+ };
@@ -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-resultpayloadof
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reykjavik/webtools",
3
- "version": "0.3.5",
3
+ "version": "0.3.6",
4
4
  "description": "Misc. JS/TS helpers used by Reykjavík City's web dev teams.",
5
5
  "main": "index.js",
6
6
  "repository": "ssh://git@github.com:reykjavikcity/webtools.git",