@reykjavik/webtools 0.1.30 → 0.1.32

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,6 +4,23 @@
4
4
 
5
5
  - ... <!-- Add new lines here. -->
6
6
 
7
+ ## 0.1.32
8
+
9
+ _2024-10-02_
10
+
11
+ - feat: Add module `@reykjavik/webtools/errorhandling` with `asError` and
12
+ `Result.*` helpers for safe, structured error handling with discriminated
13
+ `[error, result]` tuples.
14
+
15
+ ## 0.1.31
16
+
17
+ _2024-09-23_
18
+
19
+ - `@reykjavik/webtools/async`:
20
+ - feat: `maxWait` returns full `PromiseSettledResult` objects
21
+ - fix: `maxWait` result objects should remain stable
22
+ - fix: `maxWait` should distinguish between unresolved and rejected promises
23
+
7
24
  ## 0.1.30
8
25
 
9
26
  _2024-09-15_
package/README.md CHANGED
@@ -24,11 +24,21 @@ bun add @reykjavik/webtools
24
24
  - [Type `TTLConfig`](#type-ttlconfig)
25
25
  - [`toSec` TTL helper](#tosec-ttl-helper)
26
26
  - [`toMs` duration helper](#toms-duration-helper)
27
+ - [`@reykjavik/webtools/fixIcelandicLocale`](#reykjavikwebtoolsfixicelandiclocale)
28
+ - [Limitations](#limitations)
27
29
  - [`@reykjavik/webtools/async`](#reykjavikwebtoolsasync)
28
30
  - [`promiseAllObject`](#promiseallobject)
29
31
  - [`maxWait`](#maxwait)
30
- - [`@reykjavik/webtools/fixIcelandicLocale`](#reykjavikwebtoolsfixicelandiclocale)
31
- - [Limitations](#limitations)
32
+ - [`@reykjavik/webtools/errorhandling`](#reykjavikwebtoolserrorhandling)
33
+ - [`asError`](#aserror)
34
+ - [`Result` Singleton](#result-singleton)
35
+ - [Type `ResultTuple`](#type-resulttuple)
36
+ - [Type `ResultTupleObj`](#type-resulttupleobj)
37
+ - [`Result.catch`](#resultcatch)
38
+ - [`Result.map`](#resultmap)
39
+ - [`Result.Success`](#resultsuccess)
40
+ - [`Result.Fail`](#resultfail)
41
+ - [`Result.throw`](#resultthrow)
32
42
  - [`@reykjavik/webtools/SiteImprove`](#reykjavikwebtoolssiteimprove)
33
43
  - [`SiteImprove` component](#siteimprove-component)
34
44
  - [`pingSiteImprove` helper](#pingsiteimprove-helper)
@@ -236,6 +246,70 @@ const ttlSec = toMs(ttl);
236
246
 
237
247
  ---
238
248
 
249
+ ## `@reykjavik/webtools/fixIcelandicLocale`
250
+
251
+ As of early 2024, Google Chrome still does not support the Icelandic locale
252
+ `is`/`is-IS` in any way. Meanwhile other browsers have supported it for over a
253
+ decade.
254
+
255
+ This module patches the following methods/classes by substituting the `is`
256
+ locale with `da` (Danish) and apply a few post-hoc fixes to their return
257
+ values.
258
+
259
+ - `Intl.Collator` and `String.prototype.localeCompare` (\*)
260
+ - `Intl.NumberFormat` and `Number.prototype.toLocaleString` (\*)
261
+ - `Intl.DateTimeFormat` and `Date.prototype.toLocaleString`,
262
+ `.toLocaleDateString`, and `.toLocaleTimeString` (\*)
263
+ - `Intl.RelativeDateFormat`
264
+ - `Intl.PluralRules`
265
+ - `Intl.ListFormat`
266
+
267
+ (\*) The results are quite usable, but not entirely perfect. The
268
+ limitations/caveats are listed below.
269
+
270
+ To apply the patch, simply "side-effect import" this module at the top of your
271
+ app's entry point:
272
+
273
+ ```ts
274
+ import '@reykjavik/webtools/fixIcelandicLocale';
275
+
276
+ // Then continue with your day and use `localeCompare` and other Intl.* methods
277
+ // as you normally would. (See "limitations" below.)
278
+ ```
279
+
280
+ (**NOTE** The patch is only applied in engines that fail a simple feature
281
+ detection test.)
282
+
283
+ ### Limitations
284
+
285
+ **`Intl.Collator` and `localeCompare`:**
286
+
287
+ - It sorts initial letters correctly but in the rest of the string, it
288
+ incorrectly treats `ð` and `d` as the same letter (most of the time), and
289
+ lumps the acute-accented characters `á`, `é`, `í`, `ó`, `ú` and `ý` in with
290
+ their non-accented counterparts.
291
+
292
+ **`Intl.NumberFormat` and `toLocaleString`:**
293
+
294
+ - The `style: "unit"` option is not supported and prints units in Danish. (Soo
295
+ many units and unit-variants…)
296
+ - The `currencyDisplay: "name"` option is not supported and prints the
297
+ currency's full name in Danish.
298
+
299
+ **`Intl.DateTimeFormat` and `toLocaleDateString`:**
300
+
301
+ - The `month: 'narrow'` and `weekday: 'narrow'` options are not supported, and
302
+ print the corresponding Danish initials.
303
+ - For `timeZoneName` the values `"long"`, `"shortGeneric"` and `"longGeneric"`
304
+ will appear in Danish.
305
+ - The `timeStyle: 'full'` option prints the timezone names in Danish
306
+ - The `dayPeriod` option has a couple of slight mismatches, at 5 am and 12
307
+ noon.
308
+
309
+ We eagerly accept bugfixes, additions, etc. to this module!
310
+
311
+ ---
312
+
239
313
  ## `@reykjavik/webtools/async`
240
314
 
241
315
  Contains a few small helpers for working with async functions and promises.
@@ -265,23 +339,21 @@ const { user, posts } = await promiseAllObject({
265
339
 
266
340
  **Syntax:** `maxWait(timeout: number, promises: Array<any>): Promise<void>`
267
341
  **Syntax:**
268
- `maxWait<T extends PlainObj>(timeout: number, promises: T): Promise<{ [K in keyof T]: { value: Awaited<T[K]> } | undefined }>`
342
+ `maxWait<T extends PlainObj>(timeout: number, promises: T): Promise<{ [K in keyof T]: PromiseSettledResult<T[K]> } | undefined }>`
269
343
 
270
- This somewhat esoteric helper resolves soon as all of the passed `promises`
271
- have resolved, or after `timeout` milliseconds — whichever comes first.
344
+ This somewhat esoteric helper resolves soon when all of the passed `promises`
345
+ have settled (resolved or rejected), OR after `timeout` milliseconds —
346
+ whichever comes first.
272
347
 
273
348
  If an object is passed, the resolved value will be an object with the same
274
- keys, with undefined values for any promises that didn't resolve in time, and
275
- the resolved values in a `value` container object.
276
-
277
- If any of the promises reject, their values become undefined in the returned
278
- object.
349
+ keys, and any settled values in a `PromiseSettledResult` object, and
350
+ `undefined` for any promises that didn't settle in time.
279
351
 
280
352
  ```ts
281
353
  import { maxWait } from '@reykjavik/webtools/async';
282
354
 
283
- const user = fetchUser();
284
- const posts = fetchPosts();
355
+ const user = fetchUser(); // Promise<User>
356
+ const posts = fetchPosts(); // Promise<Array<Post>>
285
357
 
286
358
  // Array of promises resolves to void
287
359
  await maxWait(500, [user, posts]);
@@ -291,72 +363,237 @@ const { user, posts } = await maxWait(500, { user, posts });
291
363
 
292
364
  console.log(user?.value); // undefined | User
293
365
  console.log(posts?.value); // undefined | Array<Post>
366
+ console.log(posts?.status); // 'fulfilled' | 'rejected'
367
+ console.log(posts?.reason); // undefined | unknown
294
368
  ```
295
369
 
296
370
  ---
297
371
 
298
- ## `@reykjavik/webtools/fixIcelandicLocale`
372
+ ## `@reykjavik/webtools/errorhandling`
299
373
 
300
- As of early 2024, Google Chrome still does not support the Icelandic locale
301
- `is`/`is-IS` in any way. Meanwhile other browsers have supported it for over a
302
- decade.
374
+ A small set of lightweight tools for handling errors and promises in a safer,
375
+ more structured, FP-style way.
303
376
 
304
- This module patches the following methods/classes by substituting the `is`
305
- locale with `da` (Danish) and apply a few post-hoc fixes to their return
306
- values.
377
+ Errors are always the first return value to promote early, explicit error
378
+ handling.
307
379
 
308
- - `Intl.Collator` and `String.prototype.localeCompare` (\*)
309
- - `Intl.NumberFormat` and `Number.prototype.toLocaleString` (\*)
310
- - `Intl.DateTimeFormat` and `Date.prototype.toLocaleString`,
311
- `.toLocaleDateString`, and `.toLocaleTimeString` (\*)
312
- - `Intl.RelativeDateFormat`
313
- - `Intl.PluralRules`
314
- - `Intl.ListFormat`
380
+ ### `asError`
315
381
 
316
- (\*) The results are quite usable, but not entirely perfect. The
317
- limitations/caveats are listed below.
382
+ **Syntax:** `asError(maybeError: unknown): ErrorFromPayload`
318
383
 
319
- To apply the patch, simply "side-effect import" this module at the top of your
320
- app's entry point:
384
+ Guarantees that a caught (`catch (e)`) value of `unknown` type, is indeed an
385
+ `Error` instance.
386
+
387
+ If the input is an `Error` instance, it is returned as-is. If the input is
388
+ something else it is wrapped in a new `ErrorFromPayload` instance, and the
389
+ original value is stored in a `payload`
321
390
 
322
391
  ```ts
323
- import '@reykjavik/webtools/fixIcelandicLocale';
392
+ import { asError, type ErrorFromPayload } from '@reykjavik/webtools/errorhandling';
393
+
394
+ const theError = new Error('Something went wrong');
395
+ try {
396
+ throw theError;
397
+ } catch (err) {
398
+ const error = asError(theError);
399
+ console.error(error === theError); // true
400
+ console.error('patload' in error); // false
401
+ }
324
402
 
325
- // Then continue with your day and use `localeCompare` and other Intl.* methods
326
- // as you normally would. (See "limitations" below.)
403
+ const someObject = ['Something went wrong'];
404
+ try {
405
+ throw someObject;
406
+ } catch (err) {
407
+ const error = asError(someObject);
408
+ console.error(error === someObject); // false
409
+ console.error(error.message === someObject.join(',')); // false
410
+ console.error(error instanceOf ErrorFromPayload); // true
411
+ console.error(error.payload === someObject); // true
412
+ }
327
413
  ```
328
414
 
329
- (**NOTE** The patch is only applied in engines that fail a simple feature
330
- detection test.)
415
+ ### `Result` Singleton
331
416
 
332
- ### Limitations
417
+ Singleton object with the following small methods for creating, mapping or
418
+ handling `ResultTupleObj` instances:
333
419
 
334
- **`Intl.Collator` and `localeCompare`:**
420
+ - `Result.Success`
421
+ - `Result.Fail`
422
+ - `Result.catch`
423
+ - `Result.map`
424
+ - `Result.throw`
335
425
 
336
- - It incorrectly treats `ð` and `d` as the same letter (most of the time), and
337
- the acute-accented characters `á`, `é`, `í`, `ó`, `ú` and `ý` get lumped in
338
- with their non-accented counterparts (unless the compared).
339
- We fix this only for the first letter in the string, but not for the rest of
340
- it.
426
+ ### Type `ResultTuple`
341
427
 
342
- **`Intl.NumberFormat` and `toLocaleString`:**
428
+ **Syntax:** `ResultTuple<ResultType, OptionalErrorType>`
343
429
 
344
- - The `style: "unit"` option is not supported and prints units in Danish. (Soo
345
- many units and unit-variants…)
346
- - The `currencyDisplay: "name"` option is not supported and prints the
347
- currency's full name in Danish.
430
+ (Also aliased as `Result.Tuple`)
348
431
 
349
- **`Intl.DateTimeFormat` and `toLocaleDateString`:**
432
+ Simple bare-bones discriminated tuple type for a `[error, result]` pair.
350
433
 
351
- - The `month: 'narrow'` and `weekday: 'narrow'` options are not supported, and
352
- print the corresponding Danish initials.
353
- - For `timeZoneName` the values `"long"`, `"shortGeneric"` and `"longGeneric"`
354
- will appear in Danish.
355
- - The `timeStyle: 'full'` option prints the timezone names in Danish
356
- - The `dayPeriod` option has a couple of slight mismatches, at 5 am and 12
357
- noon.
434
+ ```ts
435
+ import { type ResultTuple } from '@reykjavik/webtools/errorhandling';
358
436
 
359
- We eagerly accept bugfixes, additions, etc. to this module!
437
+ declare const myResult: ResultTuple<string, Error>;
438
+
439
+ const [error, result] = myResult;
440
+ // Either `error` or `result` will be `undefined`
441
+
442
+ if (error) {
443
+ // Here `error` is an Error instance
444
+ console.error(error.message);
445
+ } else {
446
+ // Here `result` is guaranteed to be a string
447
+ console.log(result);
448
+ }
449
+ ```
450
+
451
+ ### Type `ResultTupleObj`
452
+
453
+ **Syntax:** `ResultTupleObj<ResultType, OptionalErrorType>`
454
+
455
+ (Also aliased as `Result.TupleObj`)
456
+
457
+ Discriminated tuple type for a `[error, result]` pair (same as `ResultTuple`)
458
+ but with named properties `error` and `result` attached for dev convenience.
459
+
460
+ ```ts
461
+ import { type ResultTuple } from '@reykjavik/webtools/errorhandling';
462
+
463
+ declare const myResult: ResultTuple<string, Error>;
464
+
465
+ const [error, result] = myResult;
466
+ // Either `error` or `result` will be `undefined`
467
+
468
+ if (error) {
469
+ // Here `error` is an Error instance
470
+ console.error(error.message);
471
+ } else {
472
+ // Here `result` is guaranteed to be a string
473
+ console.log(result);
474
+ }
475
+
476
+ // But `myResults` also has named properties, for convenience
477
+ if (myResult.error) {
478
+ // Here `myResult.error` is an Error instance
479
+ console.error(myResult.error.message);
480
+ } else {
481
+ // Here `myResult.result` is a string
482
+ console.log(myResult.result);
483
+ }
484
+ ```
485
+
486
+ ### `Result.catch`
487
+
488
+ **Syntax:** `Result.catch<T, Err>(callback: () => T): ResultTupleObj<T, Err>`
489
+ **Syntax:**
490
+ `Result.catch<T, Err>(promise: Promise<T>): Promise<ResultTupleObj<T, Err>>`
491
+
492
+ Error handling utility that wraps a promise or a callback function.
493
+
494
+ Catches errors and returns a `ResultTupleObj` — a nice discriminated
495
+ `[error, results]` tuple with the `result` and `error` also attached as named
496
+ properties.
497
+
498
+ Works on both promises and sync callback functions.
499
+
500
+ ```ts
501
+ import { Result } from '@reykjavik/webtools/errorhandling';
502
+
503
+ // Callback:
504
+ const [error, fooObject] = Result.catch(() => getFooSyncMayThrow());
505
+ // Promise:
506
+ const [error, fooObject] = await Result.catch(getFooPromiseMayThrow());
507
+
508
+ // Example of object property access:
509
+ const fooQuery = await Result.catch(getFooPromiseMayThrow());
510
+ if (fooQuery.error) {
511
+ console.log(fooQuery.error === fooQuery[0]); // true
512
+ throw fooQuery.error;
513
+ }
514
+ console.log(fooQuery.result === fooQuery[1]); // true
515
+ fooQuery.result; // Guaranteed to be defined
516
+ ```
517
+
518
+ This function acts as the inverse of [`Result.throw()`](#resultthrow).
519
+
520
+ ### `Result.map`
521
+
522
+ **Syntax:**
523
+ `Result.map<T, T2, E>(result: ResultTuple<T, E>, mapResult: (resultValue: T) => T2): ResultTuple<T2, E>`
524
+
525
+ Helper to map a `ResultTuple`-like object to a new `ResultTupleObj` object,
526
+ applying a transformation function to the result, but retaining the error
527
+ as-is.
528
+
529
+ ```ts
530
+ import { Result } from '@reykjavik/webtools/errorhandling';
531
+
532
+ const getStrLength = (str: string) => str.length;
533
+
534
+ const resultTuple =
535
+ Math.random() < 0.5 ? [new Error('Fail')] : [undefined, 'Hello!'];
536
+
537
+ const [error, mappedResult] = Result.map(resultTuple, getStrLength);
538
+
539
+ if (result) {
540
+ console.log(result); // 6
541
+ }
542
+ ```
543
+
544
+ ### `Result.Success`
545
+
546
+ **Syntax:** `Result.Success<T>(result: T): ResultTuple<T>`
547
+
548
+ Factory for creating a successful `ResultTupleObj`.
549
+
550
+ ```ts
551
+ import { Result } from '@reykjavik/webtools/errorhandling';
552
+
553
+ const happyResult: Result.SuccessObj<string> =
554
+ Result.Success('My result value');
555
+
556
+ console.log(happyResult.error); // undefined
557
+ console.log(happyResult[0]); // undefined
558
+ console.log(happyResult.result); // 'My result value'
559
+ console.log(happyResult[1]); // 'My result value'
560
+ ```
561
+
562
+ ### `Result.Fail`
563
+
564
+ **Syntax:** `Result.Fail<E extends Error>(err: T): ResultTuple<unknown, Err>`
565
+
566
+ Factory for creating a failed `ResultTupleObj`.
567
+
568
+ ```ts
569
+ import { Result } from '@reykjavik/webtools/errorhandling';
570
+
571
+ const happyResult: Result.FailObj<string> = Result.Fail(new Error('Oh no!'));
572
+
573
+ console.log(happyResult.error.message); // 'Oh no!'
574
+ console.log(happyResult[0].message); // 'Oh no!'
575
+ console.log(happyResult.result); // undefined
576
+ console.log(happyResult[1]); // undefined
577
+ ```
578
+
579
+ ### `Result.throw`
580
+
581
+ **Syntax:** `Result.throw<T>(result: ResultTuple<T>): T`
582
+
583
+ Unwraps a discriminated `ResultTuple`-like `[error, result]` tuple and throws
584
+ if there's an error, but returns the result otherwise.
585
+
586
+ ```ts
587
+ import { Result } from '@reykjavik/webtools/errorhandling';
588
+
589
+ try {
590
+ const fooResults = Result.throw(await getFooResultsTuple());
591
+ } catch (fooError) {
592
+ // error is
593
+ }
594
+ ```
595
+
596
+ This function acts as the inverse of [`Result.catch()`](#resultcatch).
360
597
 
361
598
  ---
362
599
 
package/async.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { EitherObj } from '@reykjavik/hanna-utils';
1
2
  type PlainObj = Record<string, unknown>;
2
3
  /**
3
4
  * Simple sleep function. Returns a promise that resolves after `length`
@@ -5,16 +6,19 @@ type PlainObj = Record<string, unknown>;
5
6
  */
6
7
  export declare const sleep: (length: number) => Promise<void>;
7
8
  /**
8
- * Resolves soon as all of the passed `promises` have resolved, or after
9
- * `timeout` milliseconds whichever comes first.
9
+ * Returns a function that adds lag/delay to a promise chain,
10
+ * passing the promise payload through.
11
+ */
12
+ export declare const addLag: (length: number) => <T>(res: T) => Promise<T>;
13
+ /**
14
+ * Resolves as soon as all of the passed `promises` have resolved/settled,
15
+ * or after `timeout` milliseconds — whichever comes first.
10
16
  *
11
17
  * @see https://github.com/reykjavikcity/webtools/blob/v0.1/README.md#maxwait
12
18
  */
13
19
  export declare function maxWait(timeout: number, promises: Array<unknown>): Promise<void>;
14
20
  export declare function maxWait<PromiseMap extends PlainObj>(timeout: number, promises: PromiseMap): Promise<{
15
- -readonly [K in keyof PromiseMap]: {
16
- value: Awaited<PromiseMap[K]>;
17
- } | undefined;
21
+ -readonly [K in keyof PromiseMap]: EitherObj<PromiseFulfilledResult<Awaited<PromiseMap[K]>>, PromiseRejectedResult> | undefined;
18
22
  }>;
19
23
  /**
20
24
  * A variation of `Promise.all()` that accepts an object with named promises
package/async.js CHANGED
@@ -1,12 +1,18 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.promiseAllObject = exports.maxWait = exports.sleep = void 0;
3
+ exports.promiseAllObject = exports.maxWait = exports.addLag = exports.sleep = void 0;
4
4
  /**
5
5
  * Simple sleep function. Returns a promise that resolves after `length`
6
6
  * milliseconds.
7
7
  */
8
8
  const sleep = (length) => new Promise((resolve) => setTimeout(resolve, length));
9
9
  exports.sleep = sleep;
10
+ /**
11
+ * Returns a function that adds lag/delay to a promise chain,
12
+ * passing the promise payload through.
13
+ */
14
+ const addLag = (length) => (res) => (0, exports.sleep)(length).then(() => res);
15
+ exports.addLag = addLag;
10
16
  function maxWait(timeout, promises) {
11
17
  if (Array.isArray(promises)) {
12
18
  return Promise.race([
@@ -19,17 +25,17 @@ function maxWait(timeout, promises) {
19
25
  Object.entries(promises).forEach(([key, value]) => {
20
26
  if (value instanceof Promise) {
21
27
  retObj[key] = undefined;
22
- value
23
- .then((value) => {
24
- retObj[key] = { value };
25
- })
26
- .catch(() => undefined);
28
+ value.then((value) => {
29
+ retObj[key] = { status: 'fulfilled', value };
30
+ }, (reason) => {
31
+ retObj[key] = { status: 'rejected', reason };
32
+ });
27
33
  }
28
34
  else {
29
- retObj[key] = { value };
35
+ retObj[key] = { status: 'fulfilled', value };
30
36
  }
31
37
  });
32
- return (0, exports.sleep)(0).then(() => retObj);
38
+ return Promise.resolve().then(() => ({ ...retObj }));
33
39
  });
34
40
  }
35
41
  exports.maxWait = maxWait;
@@ -0,0 +1,99 @@
1
+ /**
2
+ * Error subclass for thrown values that got cought and turned into an actual
3
+ * Error, with the thrown value as the `payload` property.
4
+ *
5
+ * @see https://github.com/reykjavikcity/webtools/blob/v0.1/README.md#aserror
6
+ */
7
+ export declare class ErrorFromPayload extends Error {
8
+ payload?: unknown;
9
+ constructor(payload: unknown);
10
+ name: string;
11
+ }
12
+ /**v
13
+ * Guarantees that a caught (`catch (e)`) value of `unknown` type,
14
+ * is indeed an `Error` instance.
15
+ *
16
+ *If the input is an `Error` instance, it is returned as-is. If the input is
17
+ * something else it is wrapped in a new `ErrorFromPayload` instance, and the
18
+ * original value is stored in a `payload`
19
+ *
20
+ * @see https://github.com/reykjavikcity/webtools/blob/v0.1/README.md#aserror
21
+ */
22
+ export declare const asError: (maybeError: unknown) => ErrorFromPayload;
23
+ type SuccessResult<T> = [error: undefined, result: T] & {
24
+ error?: undefined;
25
+ result: T;
26
+ };
27
+ type FailResult<E extends Error> = [error: E] & {
28
+ error: E;
29
+ result?: undefined;
30
+ };
31
+ /**
32
+ * Simple bare-bones discriminated tuple type for a [error, result] pair.
33
+ *
34
+ * @see https://github.com/reykjavikcity/webtools/blob/v0.1/README.md#type-resulttuple
35
+ */
36
+ export type ResultTuple<T, E extends Error = Error> = [error: undefined, result: T] | [error: E];
37
+ /**
38
+ * Discriminated tuple type for a `[error, result]` pair (same as `ResultTuple`)
39
+ * but with named properties `error` and `result` attached for dev convenience.
40
+ *
41
+ * @see https://github.com/reykjavikcity/webtools/blob/v0.1/README.md#type-resulttupleobj
42
+ */
43
+ export type ResultTupleObj<T, E extends Error = Error> = SuccessResult<T> | FailResult<E>;
44
+ /**
45
+ * Error handling utility that wraps a promise or a callback function.
46
+ *
47
+ * Catches errors and returns a `ResultTupleObj` — a nice discriminated
48
+ * `[error, results]` tuple with the `result` and `error` also attached as
49
+ * named properties.
50
+ *
51
+ * Works on both promises and sync callback functions.
52
+ *
53
+ * @see https://github.com/reykjavikcity/webtools/blob/v0.1/README.md#resultcatch
54
+ */
55
+ declare function catch_<T, E extends Error = ErrorFromPayload>(promise: Promise<T>): Promise<ResultTupleObj<T, E>>;
56
+ declare function catch_<T, E extends Error = ErrorFromPayload>(callback: () => T): ResultTupleObj<T, E>;
57
+ /**
58
+ * Singleton object with small methods for creating, mapping or handling
59
+ * `ResultTupleObj` instances.
60
+ *
61
+ * @see https://github.com/reykjavikcity/webtools/blob/v0.1/README.md#result-singleton
62
+ */
63
+ export declare const Result: {
64
+ /**
65
+ * Factory for creating a successful `Result.TupleObj`.
66
+ *
67
+ * @see https://github.com/reykjavikcity/webtools/blob/v0.1/README.md#resultsuccess
68
+ */
69
+ Success: <T>(result: T) => SuccessResult<T>;
70
+ /**
71
+ * Factory for creating a failed `Result.TupleObj`.
72
+ *
73
+ * @see https://github.com/reykjavikcity/webtools/blob/v0.1/README.md#resultsfail
74
+ */
75
+ Fail: <E extends Error = Error>(e: unknown) => FailResult<E>;
76
+ catch: typeof catch_;
77
+ /**
78
+ * Helper to map a `ResultTuple`-like object to a new `ResultTupleObj`
79
+ * object, applying a transformation function to the result, but retaining
80
+ * the error as-is.
81
+ *
82
+ * @see https://github.com/reykjavikcity/webtools/blob/v0.1/README.md#resulmap
83
+ */
84
+ map: <T_1, T2, E_1 extends Error>(result: ResultTuple<T_1, E_1>, mapFn: (resultValue: T_1) => T2) => ResultTupleObj<T2, E_1>;
85
+ /**
86
+ * Unwraps a discriminated [error, result] `Result.Tuple`-like object
87
+ * and throws if there's an error, but returns the result otherwise.
88
+ *
89
+ * @see https://github.com/reykjavikcity/webtools/blob/v0.1/README.md#resulthrow
90
+ */
91
+ throw: <T_2>(result: ResultTuple<T_2, Error>) => T_2;
92
+ };
93
+ export declare namespace Result {
94
+ type Tuple<T, E extends Error = Error> = ResultTuple<T, E>;
95
+ type TupleObj<T, E extends Error = Error> = ResultTupleObj<T, E>;
96
+ type SuccessObj<T> = SuccessResult<T>;
97
+ type FailObj<E extends Error> = FailResult<E>;
98
+ }
99
+ export {};
@@ -0,0 +1,107 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.Result = exports.asError = exports.ErrorFromPayload = void 0;
4
+ /**
5
+ * Error subclass for thrown values that got cought and turned into an actual
6
+ * Error, with the thrown value as the `payload` property.
7
+ *
8
+ * @see https://github.com/reykjavikcity/webtools/blob/v0.1/README.md#aserror
9
+ */
10
+ class ErrorFromPayload extends Error {
11
+ constructor(payload) {
12
+ if (process.env.NODE_ENV !== 'production' && payload instanceof Error) {
13
+ throw new Error('Do not pass an Error instance as payload, just use it directly');
14
+ }
15
+ const message = (payload != null ? String(payload) : '') || 'Threw a falsy/empty value';
16
+ super(message);
17
+ this.name = 'ErrorFromPayload';
18
+ this.payload = payload;
19
+ }
20
+ }
21
+ exports.ErrorFromPayload = ErrorFromPayload;
22
+ /**v
23
+ * Guarantees that a caught (`catch (e)`) value of `unknown` type,
24
+ * is indeed an `Error` instance.
25
+ *
26
+ *If the input is an `Error` instance, it is returned as-is. If the input is
27
+ * something else it is wrapped in a new `ErrorFromPayload` instance, and the
28
+ * original value is stored in a `payload`
29
+ *
30
+ * @see https://github.com/reykjavikcity/webtools/blob/v0.1/README.md#aserror
31
+ */
32
+ const asError = (maybeError) => {
33
+ if (maybeError instanceof Error) {
34
+ return maybeError;
35
+ }
36
+ return new ErrorFromPayload(maybeError);
37
+ };
38
+ exports.asError = asError;
39
+ const Success = (result) => {
40
+ const tuple = [undefined, result];
41
+ tuple.result = result;
42
+ return tuple;
43
+ };
44
+ const Fail = (e) => {
45
+ const tuple = [(0, exports.asError)(e)];
46
+ tuple.error = tuple[0];
47
+ return tuple;
48
+ };
49
+ function catch_(something) {
50
+ if (something instanceof Promise) {
51
+ return something.then((result) => Success(result), (e) => Fail(e));
52
+ }
53
+ try {
54
+ return Success(something());
55
+ }
56
+ catch (e) {
57
+ return Fail(e);
58
+ }
59
+ }
60
+ /**
61
+ * Singleton object with small methods for creating, mapping or handling
62
+ * `ResultTupleObj` instances.
63
+ *
64
+ * @see https://github.com/reykjavikcity/webtools/blob/v0.1/README.md#result-singleton
65
+ */
66
+ exports.Result = {
67
+ /**
68
+ * Factory for creating a successful `Result.TupleObj`.
69
+ *
70
+ * @see https://github.com/reykjavikcity/webtools/blob/v0.1/README.md#resultsuccess
71
+ */
72
+ Success,
73
+ /**
74
+ * Factory for creating a failed `Result.TupleObj`.
75
+ *
76
+ * @see https://github.com/reykjavikcity/webtools/blob/v0.1/README.md#resultsfail
77
+ */
78
+ Fail,
79
+ // NOTE: The JSDoc must be placed above the `catch_` function above.
80
+ catch: catch_,
81
+ /**
82
+ * Helper to map a `ResultTuple`-like object to a new `ResultTupleObj`
83
+ * object, applying a transformation function to the result, but retaining
84
+ * the error as-is.
85
+ *
86
+ * @see https://github.com/reykjavikcity/webtools/blob/v0.1/README.md#resulmap
87
+ */
88
+ map: (result, mapFn) => {
89
+ const [error, resultValue] = result;
90
+ if (error) {
91
+ return Fail(error);
92
+ }
93
+ return Success(mapFn(resultValue));
94
+ },
95
+ /**
96
+ * Unwraps a discriminated [error, result] `Result.Tuple`-like object
97
+ * and throws if there's an error, but returns the result otherwise.
98
+ *
99
+ * @see https://github.com/reykjavikcity/webtools/blob/v0.1/README.md#resulthrow
100
+ */
101
+ throw: (result) => {
102
+ if (result[0]) {
103
+ throw result[0];
104
+ }
105
+ return result[1];
106
+ },
107
+ };
package/esm/async.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { EitherObj } from '@reykjavik/hanna-utils';
1
2
  type PlainObj = Record<string, unknown>;
2
3
  /**
3
4
  * Simple sleep function. Returns a promise that resolves after `length`
@@ -5,16 +6,19 @@ type PlainObj = Record<string, unknown>;
5
6
  */
6
7
  export declare const sleep: (length: number) => Promise<void>;
7
8
  /**
8
- * Resolves soon as all of the passed `promises` have resolved, or after
9
- * `timeout` milliseconds whichever comes first.
9
+ * Returns a function that adds lag/delay to a promise chain,
10
+ * passing the promise payload through.
11
+ */
12
+ export declare const addLag: (length: number) => <T>(res: T) => Promise<T>;
13
+ /**
14
+ * Resolves as soon as all of the passed `promises` have resolved/settled,
15
+ * or after `timeout` milliseconds — whichever comes first.
10
16
  *
11
17
  * @see https://github.com/reykjavikcity/webtools/blob/v0.1/README.md#maxwait
12
18
  */
13
19
  export declare function maxWait(timeout: number, promises: Array<unknown>): Promise<void>;
14
20
  export declare function maxWait<PromiseMap extends PlainObj>(timeout: number, promises: PromiseMap): Promise<{
15
- -readonly [K in keyof PromiseMap]: {
16
- value: Awaited<PromiseMap[K]>;
17
- } | undefined;
21
+ -readonly [K in keyof PromiseMap]: EitherObj<PromiseFulfilledResult<Awaited<PromiseMap[K]>>, PromiseRejectedResult> | undefined;
18
22
  }>;
19
23
  /**
20
24
  * A variation of `Promise.all()` that accepts an object with named promises
package/esm/async.js CHANGED
@@ -3,6 +3,11 @@
3
3
  * milliseconds.
4
4
  */
5
5
  export const sleep = (length) => new Promise((resolve) => setTimeout(resolve, length));
6
+ /**
7
+ * Returns a function that adds lag/delay to a promise chain,
8
+ * passing the promise payload through.
9
+ */
10
+ export const addLag = (length) => (res) => sleep(length).then(() => res);
6
11
  export function maxWait(timeout, promises) {
7
12
  if (Array.isArray(promises)) {
8
13
  return Promise.race([
@@ -15,17 +20,17 @@ export function maxWait(timeout, promises) {
15
20
  Object.entries(promises).forEach(([key, value]) => {
16
21
  if (value instanceof Promise) {
17
22
  retObj[key] = undefined;
18
- value
19
- .then((value) => {
20
- retObj[key] = { value };
21
- })
22
- .catch(() => undefined);
23
+ value.then((value) => {
24
+ retObj[key] = { status: 'fulfilled', value };
25
+ }, (reason) => {
26
+ retObj[key] = { status: 'rejected', reason };
27
+ });
23
28
  }
24
29
  else {
25
- retObj[key] = { value };
30
+ retObj[key] = { status: 'fulfilled', value };
26
31
  }
27
32
  });
28
- return sleep(0).then(() => retObj);
33
+ return Promise.resolve().then(() => ({ ...retObj }));
29
34
  });
30
35
  }
31
36
  // ---------------------------------------------------------------------------
@@ -0,0 +1,99 @@
1
+ /**
2
+ * Error subclass for thrown values that got cought and turned into an actual
3
+ * Error, with the thrown value as the `payload` property.
4
+ *
5
+ * @see https://github.com/reykjavikcity/webtools/blob/v0.1/README.md#aserror
6
+ */
7
+ export declare class ErrorFromPayload extends Error {
8
+ payload?: unknown;
9
+ constructor(payload: unknown);
10
+ name: string;
11
+ }
12
+ /**v
13
+ * Guarantees that a caught (`catch (e)`) value of `unknown` type,
14
+ * is indeed an `Error` instance.
15
+ *
16
+ *If the input is an `Error` instance, it is returned as-is. If the input is
17
+ * something else it is wrapped in a new `ErrorFromPayload` instance, and the
18
+ * original value is stored in a `payload`
19
+ *
20
+ * @see https://github.com/reykjavikcity/webtools/blob/v0.1/README.md#aserror
21
+ */
22
+ export declare const asError: (maybeError: unknown) => ErrorFromPayload;
23
+ type SuccessResult<T> = [error: undefined, result: T] & {
24
+ error?: undefined;
25
+ result: T;
26
+ };
27
+ type FailResult<E extends Error> = [error: E] & {
28
+ error: E;
29
+ result?: undefined;
30
+ };
31
+ /**
32
+ * Simple bare-bones discriminated tuple type for a [error, result] pair.
33
+ *
34
+ * @see https://github.com/reykjavikcity/webtools/blob/v0.1/README.md#type-resulttuple
35
+ */
36
+ export type ResultTuple<T, E extends Error = Error> = [error: undefined, result: T] | [error: E];
37
+ /**
38
+ * Discriminated tuple type for a `[error, result]` pair (same as `ResultTuple`)
39
+ * but with named properties `error` and `result` attached for dev convenience.
40
+ *
41
+ * @see https://github.com/reykjavikcity/webtools/blob/v0.1/README.md#type-resulttupleobj
42
+ */
43
+ export type ResultTupleObj<T, E extends Error = Error> = SuccessResult<T> | FailResult<E>;
44
+ /**
45
+ * Error handling utility that wraps a promise or a callback function.
46
+ *
47
+ * Catches errors and returns a `ResultTupleObj` — a nice discriminated
48
+ * `[error, results]` tuple with the `result` and `error` also attached as
49
+ * named properties.
50
+ *
51
+ * Works on both promises and sync callback functions.
52
+ *
53
+ * @see https://github.com/reykjavikcity/webtools/blob/v0.1/README.md#resultcatch
54
+ */
55
+ declare function catch_<T, E extends Error = ErrorFromPayload>(promise: Promise<T>): Promise<ResultTupleObj<T, E>>;
56
+ declare function catch_<T, E extends Error = ErrorFromPayload>(callback: () => T): ResultTupleObj<T, E>;
57
+ /**
58
+ * Singleton object with small methods for creating, mapping or handling
59
+ * `ResultTupleObj` instances.
60
+ *
61
+ * @see https://github.com/reykjavikcity/webtools/blob/v0.1/README.md#result-singleton
62
+ */
63
+ export declare const Result: {
64
+ /**
65
+ * Factory for creating a successful `Result.TupleObj`.
66
+ *
67
+ * @see https://github.com/reykjavikcity/webtools/blob/v0.1/README.md#resultsuccess
68
+ */
69
+ Success: <T>(result: T) => SuccessResult<T>;
70
+ /**
71
+ * Factory for creating a failed `Result.TupleObj`.
72
+ *
73
+ * @see https://github.com/reykjavikcity/webtools/blob/v0.1/README.md#resultsfail
74
+ */
75
+ Fail: <E extends Error = Error>(e: unknown) => FailResult<E>;
76
+ catch: typeof catch_;
77
+ /**
78
+ * Helper to map a `ResultTuple`-like object to a new `ResultTupleObj`
79
+ * object, applying a transformation function to the result, but retaining
80
+ * the error as-is.
81
+ *
82
+ * @see https://github.com/reykjavikcity/webtools/blob/v0.1/README.md#resulmap
83
+ */
84
+ map: <T_1, T2, E_1 extends Error>(result: ResultTuple<T_1, E_1>, mapFn: (resultValue: T_1) => T2) => ResultTupleObj<T2, E_1>;
85
+ /**
86
+ * Unwraps a discriminated [error, result] `Result.Tuple`-like object
87
+ * and throws if there's an error, but returns the result otherwise.
88
+ *
89
+ * @see https://github.com/reykjavikcity/webtools/blob/v0.1/README.md#resulthrow
90
+ */
91
+ throw: <T_2>(result: ResultTuple<T_2, Error>) => T_2;
92
+ };
93
+ export declare namespace Result {
94
+ type Tuple<T, E extends Error = Error> = ResultTuple<T, E>;
95
+ type TupleObj<T, E extends Error = Error> = ResultTupleObj<T, E>;
96
+ type SuccessObj<T> = SuccessResult<T>;
97
+ type FailObj<E extends Error> = FailResult<E>;
98
+ }
99
+ export {};
@@ -0,0 +1,102 @@
1
+ /**
2
+ * Error subclass for thrown values that got cought and turned into an actual
3
+ * Error, with the thrown value as the `payload` property.
4
+ *
5
+ * @see https://github.com/reykjavikcity/webtools/blob/v0.1/README.md#aserror
6
+ */
7
+ export class ErrorFromPayload extends Error {
8
+ constructor(payload) {
9
+ if (process.env.NODE_ENV !== 'production' && payload instanceof Error) {
10
+ throw new Error('Do not pass an Error instance as payload, just use it directly');
11
+ }
12
+ const message = (payload != null ? String(payload) : '') || 'Threw a falsy/empty value';
13
+ super(message);
14
+ this.name = 'ErrorFromPayload';
15
+ this.payload = payload;
16
+ }
17
+ }
18
+ /**v
19
+ * Guarantees that a caught (`catch (e)`) value of `unknown` type,
20
+ * is indeed an `Error` instance.
21
+ *
22
+ *If the input is an `Error` instance, it is returned as-is. If the input is
23
+ * something else it is wrapped in a new `ErrorFromPayload` instance, and the
24
+ * original value is stored in a `payload`
25
+ *
26
+ * @see https://github.com/reykjavikcity/webtools/blob/v0.1/README.md#aserror
27
+ */
28
+ export const asError = (maybeError) => {
29
+ if (maybeError instanceof Error) {
30
+ return maybeError;
31
+ }
32
+ return new ErrorFromPayload(maybeError);
33
+ };
34
+ const Success = (result) => {
35
+ const tuple = [undefined, result];
36
+ tuple.result = result;
37
+ return tuple;
38
+ };
39
+ const Fail = (e) => {
40
+ const tuple = [asError(e)];
41
+ tuple.error = tuple[0];
42
+ return tuple;
43
+ };
44
+ function catch_(something) {
45
+ if (something instanceof Promise) {
46
+ return something.then((result) => Success(result), (e) => Fail(e));
47
+ }
48
+ try {
49
+ return Success(something());
50
+ }
51
+ catch (e) {
52
+ return Fail(e);
53
+ }
54
+ }
55
+ /**
56
+ * Singleton object with small methods for creating, mapping or handling
57
+ * `ResultTupleObj` instances.
58
+ *
59
+ * @see https://github.com/reykjavikcity/webtools/blob/v0.1/README.md#result-singleton
60
+ */
61
+ export const Result = {
62
+ /**
63
+ * Factory for creating a successful `Result.TupleObj`.
64
+ *
65
+ * @see https://github.com/reykjavikcity/webtools/blob/v0.1/README.md#resultsuccess
66
+ */
67
+ Success,
68
+ /**
69
+ * Factory for creating a failed `Result.TupleObj`.
70
+ *
71
+ * @see https://github.com/reykjavikcity/webtools/blob/v0.1/README.md#resultsfail
72
+ */
73
+ Fail,
74
+ // NOTE: The JSDoc must be placed above the `catch_` function above.
75
+ catch: catch_,
76
+ /**
77
+ * Helper to map a `ResultTuple`-like object to a new `ResultTupleObj`
78
+ * object, applying a transformation function to the result, but retaining
79
+ * the error as-is.
80
+ *
81
+ * @see https://github.com/reykjavikcity/webtools/blob/v0.1/README.md#resulmap
82
+ */
83
+ map: (result, mapFn) => {
84
+ const [error, resultValue] = result;
85
+ if (error) {
86
+ return Fail(error);
87
+ }
88
+ return Success(mapFn(resultValue));
89
+ },
90
+ /**
91
+ * Unwraps a discriminated [error, result] `Result.Tuple`-like object
92
+ * and throws if there's an error, but returns the result otherwise.
93
+ *
94
+ * @see https://github.com/reykjavikcity/webtools/blob/v0.1/README.md#resulthrow
95
+ */
96
+ throw: (result) => {
97
+ if (result[0]) {
98
+ throw result[0];
99
+ }
100
+ return result[1];
101
+ },
102
+ };
package/esm/index.d.ts CHANGED
@@ -2,6 +2,7 @@
2
2
  /// <reference path="./http.d.ts" />
3
3
  /// <reference path="./async.d.ts" />
4
4
  /// <reference path="./fixIcelandicLocale.d.ts" />
5
+ /// <reference path="./errorhandling.d.ts" />
5
6
  /// <reference path="./remix/http.d.ts" />
6
7
  /// <reference path="./SiteImprove.d.tsx" />
7
8
  /// <reference path="./CookieHubConsent.d.tsx" />
package/index.d.ts CHANGED
@@ -2,6 +2,7 @@
2
2
  /// <reference path="./http.d.ts" />
3
3
  /// <reference path="./async.d.ts" />
4
4
  /// <reference path="./fixIcelandicLocale.d.ts" />
5
+ /// <reference path="./errorhandling.d.ts" />
5
6
  /// <reference path="./remix/http.d.ts" />
6
7
  /// <reference path="./SiteImprove.d.tsx" />
7
8
  /// <reference path="./CookieHubConsent.d.tsx" />
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reykjavik/webtools",
3
- "version": "0.1.30",
3
+ "version": "0.1.32",
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",
@@ -64,6 +64,10 @@
64
64
  "import": "./esm/fixIcelandicLocale.js",
65
65
  "require": "./fixIcelandicLocale.js"
66
66
  },
67
+ "./errorhandling": {
68
+ "import": "./esm/errorhandling.js",
69
+ "require": "./errorhandling.js"
70
+ },
67
71
  "./remix/http": {
68
72
  "import": "./esm/remix/http.js",
69
73
  "require": "./remix/http.js"