@reykjavik/webtools 0.3.5 → 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.
@@ -0,0 +1,317 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.createAlerterStore = void 0;
37
+ const hanna_utils_1 = require("@reykjavik/hanna-utils");
38
+ const v = __importStar(require("valibot"));
39
+ const messageSchema = v.union([
40
+ v.string(),
41
+ v.array(v.union([
42
+ // text nodes
43
+ v.string(),
44
+ // link elements
45
+ v.object({
46
+ tag: v.literal('a'),
47
+ text: v.string(),
48
+ href: v.string(),
49
+ target: v.optional(v.string()),
50
+ hrefLang: v.optional(v.string()),
51
+ lang: v.optional(v.string()),
52
+ }),
53
+ // strong/bold elements
54
+ v.object({
55
+ tag: v.literal('strong'),
56
+ text: v.string(),
57
+ }),
58
+ ])),
59
+ ]);
60
+ // ---------------------------------------------------------------------------
61
+ const defaultKey = 'app~alerts';
62
+ const defaultAlertLevels = ['info', 'warning', 'success', 'error'];
63
+ const defaultDurations = {
64
+ BLINK: 2000,
65
+ SHORT: 4000,
66
+ MEDIUM: 8000,
67
+ LONG: 16000,
68
+ XLONG: 32000,
69
+ INDEFINITE: 0,
70
+ };
71
+ const defaultDefaultDuration = 'SHORT';
72
+ const storeKeys = {};
73
+ /**
74
+ * Factory function that creates an alerter store singleton with optional
75
+ * configuration for the genarated alerts.
76
+ *
77
+ * @see https://github.com/reykjavikcity/webtools/blob/v0.3/README.md#createalerterstore
78
+ */
79
+ /*#__NO_SIDE_EFFECTS__*/
80
+ const createAlerterStore = (cfg = {}) => {
81
+ const STORE_KEY = cfg.key || defaultKey;
82
+ if (storeKeys[STORE_KEY]) {
83
+ throw new Error(`An alerter store with key "${STORE_KEY}" already exists.`);
84
+ }
85
+ storeKeys[STORE_KEY] = true;
86
+ const storgae = cfg.storage || (typeof sessionStorage !== 'undefined' ? sessionStorage : undefined);
87
+ const alertLevels = cfg.levels || defaultAlertLevels;
88
+ const durations = cfg.durations || defaultDurations;
89
+ const DEFAULT_DURATION = cfg.durations
90
+ ? cfg.defaultDuration
91
+ : defaultDefaultDuration;
92
+ const _notificationSchema = v.object({
93
+ level: v.picklist(alertLevels),
94
+ message: messageSchema,
95
+ type: v.optional(cfg.types ? v.picklist(cfg.types) : v.string()),
96
+ flags: v.optional(v.array(cfg.flags ? v.picklist(cfg.flags) : v.string())),
97
+ duration: v.optional(v.number()),
98
+ id: v.string(),
99
+ });
100
+ const alertsSchema = v.object({
101
+ active: v.array(_notificationSchema),
102
+ pending: v.array(v.intersect([_notificationSchema, v.object({ showAt: v.number() })])),
103
+ });
104
+ /** Global array of inflight alert notifications */
105
+ const alerts = {
106
+ active: [],
107
+ pending: [],
108
+ };
109
+ const _saveAlertsToStorage = storgae
110
+ ? () => storgae.setItem(STORE_KEY, JSON.stringify(alerts))
111
+ : () => undefined;
112
+ // ---------------------------------------------------------------------------
113
+ /** Array of callbacks to call whenever alerts are activated or cleared */
114
+ const subscriptions = [];
115
+ let isEmitting;
116
+ /** Calls all subscribed callbacks with the currently active alerts and the event type. */
117
+ const emitEvent = (meta, callback) => {
118
+ if (callback) {
119
+ setTimeout(() => callback(alerts.active, meta));
120
+ return;
121
+ }
122
+ _saveAlertsToStorage();
123
+ clearTimeout(isEmitting);
124
+ // For consistency, always delay pinging the subscribed callbacks until next tick.
125
+ isEmitting = setTimeout(() => {
126
+ subscriptions.forEach((callback) => {
127
+ callback(alerts.active, meta);
128
+ });
129
+ });
130
+ };
131
+ const clearAlert = (id) => {
132
+ let found = false;
133
+ alerts.active.some((alert, idx) => {
134
+ if (alert.id === id) {
135
+ alerts.active = alerts.active.toSpliced(idx, 1);
136
+ found = true;
137
+ return true;
138
+ }
139
+ });
140
+ alerts.pending.some((alert, idx) => {
141
+ if (alert.id === id) {
142
+ alerts.pending = alerts.pending.toSpliced(idx, 1);
143
+ found = true;
144
+ return true;
145
+ }
146
+ });
147
+ if (found) {
148
+ emitEvent({ type: 'clear', ids: [id] });
149
+ }
150
+ };
151
+ // ---------------------------------------------------------------------------
152
+ const _unsubscribe = (callback) => {
153
+ const idx = subscriptions.indexOf(callback);
154
+ if (idx > -1) {
155
+ subscriptions.splice(idx, 1);
156
+ }
157
+ };
158
+ const subscribe = (callback) => {
159
+ if (subscriptions.indexOf(callback) === -1) {
160
+ subscriptions.push(callback);
161
+ // Should we allow opting-out of immediate invocations via function param?
162
+ if (alerts.active.length) {
163
+ emitEvent({
164
+ type: 'add',
165
+ ids: alerts.active.map((t) => t.id),
166
+ }, callback);
167
+ }
168
+ }
169
+ return () => _unsubscribe(callback);
170
+ };
171
+ // ---------------------------------------------------------------------------
172
+ const buildNotification = (_payload, level) => {
173
+ // Strip away duration and delay (not part of the notification object)
174
+ const { duration, delay, ...payload } = _payload;
175
+ const durationMs = durations[(duration || DEFAULT_DURATION || '')] || 0;
176
+ return {
177
+ ...payload,
178
+ level,
179
+ id: (0, hanna_utils_1.dumbId)(), // Make unique ID for the notification
180
+ ...(durationMs && { duration: durationMs }),
181
+ ...(delay && delay > 50 && { showAt: Date.now() + delay }),
182
+ };
183
+ };
184
+ // add dismiss and setFlags dispatcher functions
185
+ // this way they stay stable across updates to the same alert, which can be useful for UI components that want to update or dismiss an alert after it's been rendered.
186
+ const addMethodsToAlertInfo = (notification) => {
187
+ const id = notification.id;
188
+ return {
189
+ ...notification,
190
+ dismiss: () => clearAlert(id),
191
+ setFlags: (value) => {
192
+ const notification = alerts.active.find((t) => t.id === id);
193
+ if (!notification) {
194
+ return;
195
+ }
196
+ const oldFlags = notification.flags;
197
+ const flags = typeof value === 'string'
198
+ ? [value]
199
+ : Array.isArray(value)
200
+ ? [...value]
201
+ : value(notification.flags);
202
+ if (flags === oldFlags) {
203
+ return;
204
+ }
205
+ alerts.active = alerts.active.toSpliced(alerts.active.findIndex((t) => t.id === id), 1, { ...notification, flags });
206
+ emitEvent({ type: 'change', ids: [id] });
207
+ },
208
+ };
209
+ };
210
+ const preparePendingAlertActivation = ({ id, showAt }) => {
211
+ setTimeout(() => {
212
+ const idx = alerts.pending.findIndex((t) => t.id === id);
213
+ if (idx === -1) {
214
+ return;
215
+ }
216
+ // Find and remove the pending in alert in one fell swoop
217
+ const { showAt, ...clonedAsActive } = alerts.pending.splice(idx, 1)[0];
218
+ alerts.active = [...alerts.active, addMethodsToAlertInfo(clonedAsActive)];
219
+ emitEvent({ type: 'add', ids: [id] });
220
+ }, showAt - Date.now());
221
+ };
222
+ const _addAlert = (payload, level) => {
223
+ const notification = buildNotification(payload, level);
224
+ if (!notification.showAt) {
225
+ // Notification starts active. Clone the array.
226
+ alerts.active = [...alerts.active, addMethodsToAlertInfo(notification)];
227
+ emitEvent({ type: 'add', ids: [notification.id] });
228
+ return;
229
+ }
230
+ // Set up delayed dispatch of the notification
231
+ // Store it as pending
232
+ alerts.pending.push(notification);
233
+ // Persist the updated alerts state immediately
234
+ _saveAlertsToStorage();
235
+ // Set up a timer for make it active
236
+ preparePendingAlertActivation(notification);
237
+ };
238
+ const alerter = (0, hanna_utils_1.ObjectFromEntries)(alertLevels.map((level) => [
239
+ level,
240
+ (payload) => _addAlert(payload, level),
241
+ ]));
242
+ // ---------------------------------------------------------------------------
243
+ // On module load, read saved alerts from the storage (def. `sessionStorage`),
244
+ // if available.
245
+ // This allows us to persist alerts across page reloads,
246
+ // but NOT across tabs or browser sessions.
247
+ storgae &&
248
+ (() => {
249
+ const storedAlerts = storgae.getItem(STORE_KEY);
250
+ if (!storedAlerts) {
251
+ return;
252
+ }
253
+ try {
254
+ // NOTE: This TypeAssertion is wrong rn and only becomes true after
255
+ // `addMethodsToAlertInfo` is called on the active alerts.
256
+ // Also TS has a hard time understanding the dynamically generated
257
+ // schema and that they're actually correct in terms of the configuration
258
+ // type params `Level`, `Type` and `Flag`.
259
+ const paesed = v.parse(alertsSchema, JSON.parse(storedAlerts));
260
+ alerts.pending = paesed.pending;
261
+ alerts.active = paesed.active.map(addMethodsToAlertInfo);
262
+ }
263
+ catch (e) {
264
+ console.error('Failed to parse stored alerts:', e);
265
+ return;
266
+ }
267
+ const now = Date.now();
268
+ const newActive = [];
269
+ // Check pending alerts and set up timers, unless they should already
270
+ // be active, in which case move them to active immediately.
271
+ alerts.pending = alerts.pending.filter((alert) => {
272
+ if (alert.showAt <= now) {
273
+ // Remove pending alerts that should now be active. Store them in `newActive`
274
+ newActive.push(alert);
275
+ return false;
276
+ }
277
+ // Keep the rest, and set up timers to activate them later.
278
+ preparePendingAlertActivation(alert);
279
+ return true;
280
+ });
281
+ // Append `newActive` to the active alerts array
282
+ alerts.active.push(...newActive
283
+ // Make sure alerts with earlier `showAt` are shown first
284
+ .sort((a, b) => a.showAt - b.showAt)
285
+ // remove showAt, not part of active alerts
286
+ .map(({ showAt, ...rest }) => addMethodsToAlertInfo(rest)));
287
+ // Save the (possibly) cleaned up alerts back to the storage, because why not. :-D
288
+ _saveAlertsToStorage();
289
+ })();
290
+ return {
291
+ /**
292
+ * Singleton object with methods for showing alerts of different levels.
293
+ * Pass a payload object to the method of the level you want to dispatch,
294
+ * and the alert will be added to the store.
295
+ *
296
+ * Use `subscribeToAlerts` elsewhere in the app to subscribe to alert
297
+ * notifications and display them.
298
+ *
299
+ * @see https://github.com/reykjavikcity/webtools/blob/v0.3/README.md#createalerterstore
300
+ */
301
+ alerter,
302
+ /**
303
+ * Subscribes to alert events. The provided callback will be called whenever a
304
+ * alert is added or cleared.
305
+ *
306
+ * The callback is called immediately upon subscription if there are already
307
+ * active alerts.
308
+ *
309
+ * Returns an unsubscribe function that can be called to stop receiving alert
310
+ * events.
311
+ *
312
+ * @see https://github.com/reykjavikcity/webtools/blob/v0.3/README.md#createalerterstore
313
+ */
314
+ subscribe,
315
+ };
316
+ };
317
+ exports.createAlerterStore = createAlerterStore;
@@ -0,0 +1,36 @@
1
+ import React, { ReactNode } from 'react';
2
+ import { AlertMessage } from './index.js';
3
+ type SubsScriber<AlertInfo> = (callback: (alerts: Array<AlertInfo>, _type: string) => void) => () => void;
4
+ /**
5
+ * Factory function that creates a React subscription hook and a container
6
+ * component linked to a specific alerter store subscibe function.
7
+ *
8
+ * @see https://github.com/reykjavikcity/webtools/blob/v0.3/README.md#makereactsubscription
9
+ */
10
+ export declare const makeReactSubscription: <AlertInfo>(subscribe: SubsScriber<AlertInfo>) => {
11
+ useAlerter: () => AlertInfo[];
12
+ AlertsContainer: (props: {
13
+ children: (alerts: Array<AlertInfo>) => ReactNode;
14
+ }) => React.ReactNode;
15
+ };
16
+ /**
17
+ * Helper to render an alerter alert message, which can be a simple string or a
18
+ * more complex array of strings and objects representing links and rich (bold)
19
+ * text formatting.
20
+ *
21
+ * @see https://github.com/reykjavikcity/webtools/blob/v0.3/README.md#renderalertmessage
22
+ */
23
+ export declare const renderAlertMessage: {
24
+ (message: AlertMessage, linkComponent?: renderAlertMessage.LinkRenderer): ReactNode;
25
+ withLinkRenderer(LinkCompnent: renderAlertMessage.LinkRenderer): (message: AlertMessage) => React.ReactNode;
26
+ };
27
+ export declare namespace renderAlertMessage {
28
+ type LinkRendererProps = Omit<Extract<AlertMessage[number], {
29
+ tag: 'a';
30
+ }>, 'tag' | 'text'> & {
31
+ children: ReactNode;
32
+ };
33
+ export type LinkRenderer = (props: LinkRendererProps) => ReactNode;
34
+ export {};
35
+ }
36
+ export {};
@@ -0,0 +1,96 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.renderAlertMessage = exports.makeReactSubscription = void 0;
37
+ const react_1 = __importStar(require("react"));
38
+ /**
39
+ * Factory function that creates a React subscription hook and a container
40
+ * component linked to a specific alerter store subscibe function.
41
+ *
42
+ * @see https://github.com/reykjavikcity/webtools/blob/v0.3/README.md#makereactsubscription
43
+ */
44
+ /*#__NO_SIDE_EFFECTS__*/
45
+ const makeReactSubscription = (subscribe) => {
46
+ const useAlerter = () => {
47
+ const [alerts, setAlerts] = (0, react_1.useState)([]);
48
+ (0, react_1.useEffect)(() => subscribe((alerts) => setAlerts(alerts)), []);
49
+ return alerts;
50
+ };
51
+ const AlertsContainer = (props) => {
52
+ const alerts = useAlerter();
53
+ return props.children(alerts);
54
+ };
55
+ return {
56
+ useAlerter,
57
+ AlertsContainer,
58
+ };
59
+ };
60
+ exports.makeReactSubscription = makeReactSubscription;
61
+ /**
62
+ * Helper to render an alerter alert message, which can be a simple string or a
63
+ * more complex array of strings and objects representing links and rich (bold)
64
+ * text formatting.
65
+ *
66
+ * @see https://github.com/reykjavikcity/webtools/blob/v0.3/README.md#renderalertmessage
67
+ */
68
+ /*#__NO_SIDE_EFFECTS__*/
69
+ const renderAlertMessage = (message, linkComponent) => {
70
+ const Link = linkComponent || 'a';
71
+ return typeof message === 'string'
72
+ ? message
73
+ : message.flatMap((part, i) => {
74
+ if (typeof part === 'string') {
75
+ return ` ${part}`;
76
+ }
77
+ if (part.tag === 'a') {
78
+ const { text, tag, ...linkProps } = part;
79
+ return [
80
+ ' ',
81
+ react_1.default.createElement(Link, { key: i, ...linkProps }, text),
82
+ ];
83
+ }
84
+ return [' ', react_1.default.createElement(part.tag, { key: i }, part.text)];
85
+ });
86
+ };
87
+ exports.renderAlertMessage = renderAlertMessage;
88
+ /**
89
+ * Retrns a curried version of `renderAlertMessage` that uses the provided
90
+ * `LinkComponent` for rendering links in alert messages.
91
+ *
92
+ * @see https://github.com/reykjavikcity/webtools/blob/v0.3/README.md#renderalertmessagewithlinkrenderer
93
+ */
94
+ /*#__NO_SIDE_EFFECTS__*/
95
+ exports.renderAlertMessage.withLinkRenderer =
96
+ (LinkCompnent) => (message) => (0, exports.renderAlertMessage)(message, LinkCompnent);
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 `ttl` and` `throttle` 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,60 @@ 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
+ /*#__NO_SIDE_EFFECTS__*/
203
+ const cachifyAsync = (opts) => {
204
+ const { fn, getKey = (...args) => JSON.stringify(args), customTtl, returnStale } = opts;
205
+ // Set up the cache object
206
+ const TTL_SEC = toSec(opts.ttl);
207
+ const THROTTLING_SEC = toSec(opts.throttle || 0) || Math.min(toSec(DEFAULT_THROTTLING_MS), TTL_SEC);
208
+ const _cache = new Map();
209
+ return (async (...args) => {
210
+ const now = Date.now();
211
+ const key = getKey(...args);
212
+ const cached = _cache.get(key);
213
+ if (cached && now < cached.freshUntil) {
214
+ return cached.data;
215
+ }
216
+ const lastData = returnStale !== false && (cached === null || cached === void 0 ? void 0 : cached.data);
217
+ const entry = {
218
+ // Set an initial "fresh until" that's longer than TTL_SEC to cover
219
+ // (somewhat) safely the time it takes for the promise to resolve,
220
+ // so that we don't trigger multiple calls to `fn` in parallel
221
+ // TODO: Build in a proper AbortSignal timeout, etc. to handle this more robustly
222
+ freshUntil: now + (TTL_SEC + 60) * 1000,
223
+ data: fn(...args).then((result) => {
224
+ const customTtlSec = toSec((customTtl === null || customTtl === void 0 ? void 0 : customTtl(args, result)) || 0);
225
+ entry.freshUntil = now + (customTtlSec || TTL_SEC) * 1000;
226
+ if (result.error) {
227
+ if (!customTtlSec) {
228
+ // Set shorter TTL on errors to allow quicker retries
229
+ entry.freshUntil = now + THROTTLING_SEC * 1000;
230
+ }
231
+ if (lastData) {
232
+ // Return last known good data if available, even if it's a bit stale
233
+ return lastData;
234
+ }
235
+ }
236
+ return result;
237
+ }),
238
+ };
239
+ _cache.set(key, entry);
240
+ return entry.data;
241
+ });
242
+ };
243
+ 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
  }