@reykjavik/webtools 0.2.6 → 0.2.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,23 @@
4
4
 
5
5
  - ... <!-- Add new lines here. -->
6
6
 
7
+ ## 0.2.8
8
+
9
+ _2025-09-17_
10
+
11
+ - `@reykjavik/webtools/async`:
12
+ - feat: Add `debounce` and `throttle` helpers
13
+ - `@reykjavik/webtools/hooks` (new module):
14
+ - feat: Add `useDebounced` and `useThrottled` hooks
15
+
16
+ ## 0.2.7
17
+
18
+ _2025-09-01_
19
+
20
+ - `@reykjavik/webtools/fixIcelandicLocale`:
21
+ - fix: Check for support of each Intl class separately
22
+ - docs: Improve JSDocs and README for `asError()` and `ErrorFromPayload`
23
+
7
24
  ## 0.2.6
8
25
 
9
26
  _2025-06-20_
package/README.md CHANGED
@@ -29,6 +29,11 @@ bun add @reykjavik/webtools
29
29
  - [`@reykjavik/webtools/async`](#reykjavikwebtoolsasync)
30
30
  - [`promiseAllObject`](#promiseallobject)
31
31
  - [`maxWait`](#maxwait)
32
+ - [`debounce`](#debounce)
33
+ - [`throttle`](#throttle)
34
+ - [`@reykjavik/webtools/hoooks`](#reykjavikwebtoolshoooks)
35
+ - [`useDebounced`](#usedebounced)
36
+ - [`useThrottled`](#usethrottled)
32
37
  - [`@reykjavik/webtools/errorhandling`](#reykjavikwebtoolserrorhandling)
33
38
  - [`asError`](#aserror)
34
39
  - [`Result` Singleton](#result-singleton)
@@ -369,6 +374,188 @@ console.log(posts?.reason); // undefined | unknown
369
374
 
370
375
  ---
371
376
 
377
+ ### `debounce`
378
+
379
+ **Syntax:**
380
+ `debounce<A extends Array<unknown>>(func: (...args: A) => void, delay: number, immediate?: boolean): ((...args: A) => void) & { cancel: (finish?: boolean) => void; }`
381
+
382
+ Returns a debounced function that only runs after `delay` milliseconds of
383
+ quiet-time, and can optionally be made to run `immediate`ly on first call
384
+ before dbouncing subsequent calls.
385
+
386
+ ```ts
387
+ import { debounce } from '@reykjavik/webtools/async';
388
+
389
+ // Basic usage:
390
+ const sayHello = debounce((namme: string) => {
391
+ console.log('Hello ' + name);
392
+ }, 200);
393
+
394
+ sayHello('Alice');
395
+ sayHello('Bob');
396
+ sayHello('Charlie');
397
+ sayHello('Dorothy');
398
+ // Only "Hello Dorothy" is logged, 200ms after the last call
399
+
400
+ // With `immediate` param set to true:
401
+ const sayHi = debounce(
402
+ (namme: string) => console.log('Hi ' + name),
403
+ 200,
404
+ true
405
+ );
406
+ sayHi('Alice');
407
+ sayHi('Bob');
408
+ sayHi('Charlie');
409
+ sayHi('Dorothy');
410
+ // "Hi Alice" is logged immediately
411
+ // Then "Hi Dorothy" is logged, 200ms after the last call
412
+ ```
413
+
414
+ The returned function has a nice `.cancel()` method, which can optionally
415
+ invoke the function before cancelling, if it had a debounce pending.
416
+
417
+ ```ts
418
+ sayHello('Erica');
419
+ sayHello('Fiona');
420
+ sayHello.cancel();
421
+ // Nothing is logged because the debounce was cancelled
422
+
423
+ sayHello('George');
424
+ sayHello('Harold');
425
+ sayHello.cancel(true); // `finish` parmeter is true
426
+ // "Hello Harold" is logged immediately because it was pending
427
+ ```
428
+
429
+ ---
430
+
431
+ ### `throttle`
432
+
433
+ **Syntax:**
434
+ `throttle<A extends Array<unknown>>(func: (...args: A) => void, delay: number, skipFirst?: boolean): ((...args: A) => void) & { finish: (cancel?: boolean) => void; }`
435
+
436
+ Returns a throttled function that never runs more often than every `delay`
437
+ milliseconds. It can optionally made to `skipFirst` invocation.
438
+
439
+ ```ts
440
+ import { throttle } from '@reykjavik/webtools/async';
441
+
442
+ // Basic usage:
443
+ const sayHello = throttle((name: string) => {
444
+ console.log('Hello ' + name);
445
+ }, 200);
446
+
447
+ sayHello('Alice');
448
+ sayHello('Bob');
449
+ sayHello('Charlie');
450
+ sayHello('Dorothy');
451
+ // Only "Hello Alice" is logged immediately. The other calls were throttled.
452
+
453
+ // With `skipFirst` param set to true:
454
+ const sayHi = throttle(
455
+ (name: string) => console.log('Hi ' + name),
456
+ 200,
457
+ true
458
+ );
459
+ sayHi('Alice');
460
+ sayHi('Bob');
461
+ sayHi('Charlie');
462
+ sayHi('Dorothy');
463
+ // Nothing is logged. The first call was skipped, and the rest were throttled.
464
+ ```
465
+
466
+ The returned function also has a nice `.finish()` method to reset the throttle
467
+ timer. By default it instantly invokes the function, if the last call was
468
+ throttled (skipped)-.
469
+
470
+ ```ts
471
+ sayHello('Erica');
472
+ sayHello('Fiona');
473
+ sayHello.finish();
474
+ // "Hello Fiona" is logged immediately because it was pending
475
+
476
+ sayHello('George');
477
+ sayHello('Harold');
478
+ sayHello.finish(true); // `cancel` parmeter is true
479
+ // Nothing is logged because the pending call was cancelled
480
+ ```
481
+
482
+ ---
483
+
484
+ ## `@reykjavik/webtools/hoooks`
485
+
486
+ Some useful React hooks.
487
+
488
+ ### `useDebounced`
489
+
490
+ **Syntax:**
491
+ `useDebounced<A extends Array<unknown>>(func: (...args: A) => void, delay: number, immediate?: boolean): ((...args: A) => void) & { cancel: (finish?: boolean) => void; }`
492
+
493
+ Returns a stable debounced function that invokes the supplied function after
494
+ the specified delay. When the component unmounts, any pending (debounced)
495
+ calls are automatically cancelled.
496
+
497
+ **NOTE:** The supplied callback does not need to be memoized. The debouncer
498
+ will always invoke the last supplied version.
499
+
500
+ ```ts
501
+ import { useDebounced } from '@reykjavik/webtools/hoooks';
502
+
503
+ const MyComponent = () => {
504
+ const renderDate = new Date();
505
+ const debouncedSearch = useDebounced((query: string) => {
506
+ console.log('Searching for:', query, 'at', renderDate.toISOString());
507
+ }, 200);
508
+ return (
509
+ <input
510
+ type="text"
511
+ onChange={(e) => {
512
+ debouncedSearch(e.currentTarget.value);
513
+ }}
514
+ />
515
+ );
516
+ };
517
+ ```
518
+
519
+ See [`debounce`](#debounce) for more details about the parameters and the
520
+ returned debounced function's `.cancel()` method.
521
+
522
+ ### `useThrottled`
523
+
524
+ **Syntax:**
525
+ `useThrottled<A extends Array<unknown>>(func: (...args: A) => void, delay: number, skipFirst?: boolean): ((...args: A) => void) & { finish: (cancel?: boolean) => void; }`
526
+
527
+ Returns a stable throttler function that throttles the supplied function.
528
+
529
+ **NOTE:** The supplied callback does not need to be memoized. The throttler
530
+ will always invoke the last supplied version.
531
+
532
+ ```ts
533
+ import { useThrottled } from '@reykjavik/webtools/hoooks';
534
+
535
+ const MyComponent = () => {
536
+ const renderDate = new Date();
537
+ const throttledReportPosition = useThrottled((x: number, y: number) => {
538
+ console.log('Mouse position:', x, ',', y, 'at', renderDate.toISOString());
539
+ }, 200);
540
+
541
+ return (
542
+ <div
543
+ style={{ width: '300px', height: '300px', background: '#eee' }}
544
+ onMouseMove={(e) => {
545
+ throttledReportPosition(e.clientX, e.clientY);
546
+ }}
547
+ >
548
+ Move your mouse here.
549
+ </div>
550
+ );
551
+ };
552
+ ```
553
+
554
+ See [`throttle`](#throttle) for more details about the parameters and the
555
+ returned throttled function's `.finish()` method.
556
+
557
+ ---
558
+
372
559
  ## `@reykjavik/webtools/errorhandling`
373
560
 
374
561
  A small set of lightweight tools for handling errors and promises in a safer,
@@ -386,7 +573,8 @@ Guarantees that a caught (`catch (e)`) value of `unknown` type, is indeed an
386
573
 
387
574
  If the input is an `Error` instance, it is returned as-is. If the input is
388
575
  something else it is wrapped in a new `ErrorFromPayload` instance, and the
389
- original value is stored in as a `payload` property.
576
+ original value is stored in as a `payload` property, and it's `.toString()` is
577
+ used for the `message` property.
390
578
 
391
579
  ```ts
392
580
  import { asError, type ErrorFromPayload } from '@reykjavik/webtools/errorhandling';
@@ -395,21 +583,23 @@ const theError = new Error('Something went wrong');
395
583
  try {
396
584
  throw theError;
397
585
  } catch (err) {
586
+ // theError is an instance of Error so it's returned as-is
398
587
  const error = asError(theError);
399
588
  console.error(error === theError); // true
400
- console.error('patload' in error); // false
589
+ console.error('payload' in error); // false
401
590
  }
402
591
 
403
- const someObject = ['Something went wrong'];
592
+ const someObject = ['Oops', 'Something went wrong'];
404
593
  try {
405
594
  throw someObject;
406
595
  } catch (err) {
596
+ // the thrown someObject is not an Error so an `ErrorFromPayload` is returned
407
597
  const error = asError(someObject);
408
598
  console.error(error === someObject); // false
409
- console.error(error.message === someObject.join(',')); // false
410
599
  console.error(error instanceOf ErrorFromPayload); // true
411
600
 
412
601
  console.error(error.payload === someObject); // true
602
+ console.error(error.message === someObject.join(',')); // true
413
603
  }
414
604
  ```
415
605
 
package/async.d.ts CHANGED
@@ -31,4 +31,43 @@ export declare function maxWait<PromiseMap extends PlainObj>(timeout: number, pr
31
31
  * @see https://github.com/reykjavikcity/webtools/blob/v0.2/README.md#promiseallobject
32
32
  */
33
33
  export declare const promiseAllObject: <T extends PlainObj>(promisesMap: T) => Promise<{ -readonly [K in keyof T]: Awaited<T[K]>; }>;
34
+ type Cancellable<A extends Array<unknown>> = ((...args: A) => void) & {
35
+ /**
36
+ * Cancels any pending invocation of the debounced function. \
37
+ * If `finish` is true and if a debounce is pending, the function is invoked
38
+ * before cancelling.
39
+ */
40
+ cancel(finish?: boolean): void;
41
+ };
42
+ /**
43
+ * Returns a debounced function that only runs after `delay` milliseconds
44
+ * of quiet-time. \
45
+ * The returned function also has a nice `.cancel()` method.
46
+ *
47
+ * @see https://github.com/reykjavikcity/webtools/blob/v0.2/README.md#debounce
48
+ */
49
+ export declare const debounce: {
50
+ <A extends Array<unknown>>(func: (...args: A) => void, delay: number, immediate?: boolean): Cancellable<A>;
51
+ d(delay: number, immediate?: boolean): Cancellable<[fn: (...args: Array<any>) => void, ...args: any[]]>;
52
+ };
53
+ type Finishable<A extends Array<unknown>> = ((...args: A) => void) & {
54
+ /**
55
+ * Immediately invokes the function if there is a pending throttle and its last
56
+ * invoication was throttled (cancelled). \
57
+ * If `cancel` is true then only the timer is reset without invoking function.
58
+ */
59
+ finish(cancel?: boolean): void;
60
+ };
61
+ /**
62
+ * Returns a throttled function that never runs more often than
63
+ * every `delay` milliseconds. \
64
+ * The returned function also has a nice `.finish()` method to reset the
65
+ * throttle timer
66
+ *
67
+ * @see https://github.com/reykjavikcity/webtools/blob/v0.2/README.md#throttle
68
+ */
69
+ export declare const throttle: {
70
+ <A extends Array<unknown>>(func: (...args: A) => void, delay: number, skipFirst?: boolean): Finishable<A>;
71
+ d(delay: number, skipFirst?: boolean): Finishable<[fn: (...args: Array<any>) => void, ...args: any[]]>;
72
+ };
34
73
  export {};
package/async.js CHANGED
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.promiseAllObject = exports.addLag = exports.sleep = void 0;
3
+ exports.throttle = exports.debounce = exports.promiseAllObject = exports.addLag = exports.sleep = void 0;
4
4
  exports.maxWait = maxWait;
5
5
  /**
6
6
  * Simple sleep function. Returns a promise that resolves after `length`
@@ -79,3 +79,107 @@ const promiseAllObject = (promisesMap) => Promise.all(Object.values(promisesMap)
79
79
  return resolvedMap;
80
80
  });
81
81
  exports.promiseAllObject = promiseAllObject;
82
+ /**
83
+ * Returns a debounced function that only runs after `delay` milliseconds
84
+ * of quiet-time. \
85
+ * The returned function also has a nice `.cancel()` method.
86
+ *
87
+ * @see https://github.com/reykjavikcity/webtools/blob/v0.2/README.md#debounce
88
+ */
89
+ /*#__NO_SIDE_EFFECTS__*/
90
+ const debounce = (
91
+ /** The function to debounce */
92
+ func,
93
+ /** The delay, in milliseconds, to wait before running the function */
94
+ delay,
95
+ /** Whether to run the function at the start of the delay instead of the end */
96
+ immediate) => {
97
+ let timeout;
98
+ let _args;
99
+ let _this;
100
+ const debouncedFn = function (...args) {
101
+ _args = args;
102
+ _this = this;
103
+ immediate && !timeout && func.apply(_this, _args);
104
+ timeout && clearTimeout(timeout);
105
+ timeout = setTimeout(() => {
106
+ debouncedFn.cancel(true);
107
+ }, delay);
108
+ };
109
+ debouncedFn.cancel = (finish) => {
110
+ timeout && clearTimeout(timeout);
111
+ finish && timeout && func.apply(_this, _args);
112
+ timeout = undefined;
113
+ };
114
+ return debouncedFn;
115
+ };
116
+ exports.debounce = debounce;
117
+ /**
118
+ * Sugar to produce a dynamic debounced function that accepts its contents/behavior at call time.
119
+ *
120
+ * Usage:
121
+ * ```ts
122
+ * const myDebouncer = debounce.d(500);
123
+ * myDebouncer(() => { alert('Hello world'); });
124
+ * myDebouncer(() => { alert('I mean: Howdy world!'); });
125
+ * myDebouncer((name) => { alert('Wazzap ' + name); }, 'world');
126
+ * ```
127
+ *
128
+ * Not documented in README as its usefulness is still uncertain.
129
+ */
130
+ exports.debounce.d = (delay, immediate) => (0, exports.debounce)(function (fn, ...args) {
131
+ fn.apply(this, args);
132
+ }, delay, immediate);
133
+ /**
134
+ * Returns a throttled function that never runs more often than
135
+ * every `delay` milliseconds. \
136
+ * The returned function also has a nice `.finish()` method to reset the
137
+ * throttle timer
138
+ *
139
+ * @see https://github.com/reykjavikcity/webtools/blob/v0.2/README.md#throttle
140
+ */
141
+ /*#__NO_SIDE_EFFECTS__*/
142
+ const throttle = (
143
+ /** The function to throttle */
144
+ func,
145
+ /** The delay, in milliseconds, to wait between invocations */
146
+ delay,
147
+ /** Whether to skip the first invocation instead of running it immediately */
148
+ skipFirst) => {
149
+ let timeout;
150
+ let throttled = 0;
151
+ let _args;
152
+ let _this;
153
+ const throttledFn = function (...args) {
154
+ _args = args;
155
+ _this = this;
156
+ if (!throttled) {
157
+ skipFirst ? throttled++ : func.apply(_this, _args);
158
+ timeout = setTimeout(throttledFn.finish, delay); // Go home TypeScript, you're drunk!
159
+ }
160
+ throttled++;
161
+ };
162
+ throttledFn.finish = (cancel) => {
163
+ timeout && clearTimeout(timeout);
164
+ !cancel && throttled && func.apply(_this, _args);
165
+ throttled = 0;
166
+ };
167
+ return throttledFn;
168
+ };
169
+ exports.throttle = throttle;
170
+ /**
171
+ * Sugar to produce a dynamic debounced function that accepts its contents/behavior at call time.
172
+ *
173
+ * Usage:
174
+ * ```ts
175
+ * const myThrottler = throttle.d(500);
176
+ * myThrottler(() => { alert('Hello world'); });
177
+ * myThrottler(() => { alert('I mean: Howdy world!'); });
178
+ * myThrottler((name) => { alert('Wazzap ' + name); }, 'world');
179
+ * ```
180
+ *
181
+ * Not documented in README as its usefulness is still uncertain.
182
+ */
183
+ exports.throttle.d = (delay, skipFirst) => (0, exports.throttle)(function (fn, ...args) {
184
+ fn.apply(this, args);
185
+ }, delay, skipFirst);
@@ -1,10 +1,18 @@
1
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.
2
+ * Error subclass for thrown NON-Error values that got turned into an actual
3
+ * Error, with the original thrown value as the `payload` property.
4
4
  *
5
5
  * @see https://github.com/reykjavikcity/webtools/blob/v0.2/README.md#aserror
6
6
  */
7
7
  export declare class ErrorFromPayload extends Error {
8
+ /**
9
+ * This payload property is only set if the original `throw` value was NOT
10
+ * `instanceof Error`.
11
+ *
12
+ * In such cases it contains the thrown value as is, and the `message`
13
+ * property of this `ErrorFromPayload` instance is set to the `.toString()`
14
+ * representation of the payload.
15
+ */
8
16
  payload?: unknown;
9
17
  constructor(payload: unknown);
10
18
  name: string;
@@ -50,7 +58,7 @@ export type ResultTupleObj<T, E extends Error = Error> = SuccessResult<T> | Fail
50
58
  * `[error, results]` tuple with the `result` and `error` also attached as
51
59
  * named properties.
52
60
  *
53
- * Works on both promises and sync callback functions.
61
+ * Works on both promises and (synchronous) callback functions.
54
62
  *
55
63
  * @see https://github.com/reykjavikcity/webtools/blob/v0.2/README.md#resultcatch
56
64
  */
package/errorhandling.js CHANGED
@@ -2,8 +2,8 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.Result = exports.asError = exports.ErrorFromPayload = void 0;
4
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.
5
+ * Error subclass for thrown NON-Error values that got turned into an actual
6
+ * Error, with the original thrown value as the `payload` property.
7
7
  *
8
8
  * @see https://github.com/reykjavikcity/webtools/blob/v0.2/README.md#aserror
9
9
  */
package/esm/async.d.ts CHANGED
@@ -31,4 +31,43 @@ export declare function maxWait<PromiseMap extends PlainObj>(timeout: number, pr
31
31
  * @see https://github.com/reykjavikcity/webtools/blob/v0.2/README.md#promiseallobject
32
32
  */
33
33
  export declare const promiseAllObject: <T extends PlainObj>(promisesMap: T) => Promise<{ -readonly [K in keyof T]: Awaited<T[K]>; }>;
34
+ type Cancellable<A extends Array<unknown>> = ((...args: A) => void) & {
35
+ /**
36
+ * Cancels any pending invocation of the debounced function. \
37
+ * If `finish` is true and if a debounce is pending, the function is invoked
38
+ * before cancelling.
39
+ */
40
+ cancel(finish?: boolean): void;
41
+ };
42
+ /**
43
+ * Returns a debounced function that only runs after `delay` milliseconds
44
+ * of quiet-time. \
45
+ * The returned function also has a nice `.cancel()` method.
46
+ *
47
+ * @see https://github.com/reykjavikcity/webtools/blob/v0.2/README.md#debounce
48
+ */
49
+ export declare const debounce: {
50
+ <A extends Array<unknown>>(func: (...args: A) => void, delay: number, immediate?: boolean): Cancellable<A>;
51
+ d(delay: number, immediate?: boolean): Cancellable<[fn: (...args: Array<any>) => void, ...args: any[]]>;
52
+ };
53
+ type Finishable<A extends Array<unknown>> = ((...args: A) => void) & {
54
+ /**
55
+ * Immediately invokes the function if there is a pending throttle and its last
56
+ * invoication was throttled (cancelled). \
57
+ * If `cancel` is true then only the timer is reset without invoking function.
58
+ */
59
+ finish(cancel?: boolean): void;
60
+ };
61
+ /**
62
+ * Returns a throttled function that never runs more often than
63
+ * every `delay` milliseconds. \
64
+ * The returned function also has a nice `.finish()` method to reset the
65
+ * throttle timer
66
+ *
67
+ * @see https://github.com/reykjavikcity/webtools/blob/v0.2/README.md#throttle
68
+ */
69
+ export declare const throttle: {
70
+ <A extends Array<unknown>>(func: (...args: A) => void, delay: number, skipFirst?: boolean): Finishable<A>;
71
+ d(delay: number, skipFirst?: boolean): Finishable<[fn: (...args: Array<any>) => void, ...args: any[]]>;
72
+ };
34
73
  export {};
package/esm/async.js CHANGED
@@ -72,3 +72,105 @@ export const promiseAllObject = (promisesMap) => Promise.all(Object.values(promi
72
72
  }
73
73
  return resolvedMap;
74
74
  });
75
+ /**
76
+ * Returns a debounced function that only runs after `delay` milliseconds
77
+ * of quiet-time. \
78
+ * The returned function also has a nice `.cancel()` method.
79
+ *
80
+ * @see https://github.com/reykjavikcity/webtools/blob/v0.2/README.md#debounce
81
+ */
82
+ /*#__NO_SIDE_EFFECTS__*/
83
+ export const debounce = (
84
+ /** The function to debounce */
85
+ func,
86
+ /** The delay, in milliseconds, to wait before running the function */
87
+ delay,
88
+ /** Whether to run the function at the start of the delay instead of the end */
89
+ immediate) => {
90
+ let timeout;
91
+ let _args;
92
+ let _this;
93
+ const debouncedFn = function (...args) {
94
+ _args = args;
95
+ _this = this;
96
+ immediate && !timeout && func.apply(_this, _args);
97
+ timeout && clearTimeout(timeout);
98
+ timeout = setTimeout(() => {
99
+ debouncedFn.cancel(true);
100
+ }, delay);
101
+ };
102
+ debouncedFn.cancel = (finish) => {
103
+ timeout && clearTimeout(timeout);
104
+ finish && timeout && func.apply(_this, _args);
105
+ timeout = undefined;
106
+ };
107
+ return debouncedFn;
108
+ };
109
+ /**
110
+ * Sugar to produce a dynamic debounced function that accepts its contents/behavior at call time.
111
+ *
112
+ * Usage:
113
+ * ```ts
114
+ * const myDebouncer = debounce.d(500);
115
+ * myDebouncer(() => { alert('Hello world'); });
116
+ * myDebouncer(() => { alert('I mean: Howdy world!'); });
117
+ * myDebouncer((name) => { alert('Wazzap ' + name); }, 'world');
118
+ * ```
119
+ *
120
+ * Not documented in README as its usefulness is still uncertain.
121
+ */
122
+ debounce.d = (delay, immediate) => debounce(function (fn, ...args) {
123
+ fn.apply(this, args);
124
+ }, delay, immediate);
125
+ /**
126
+ * Returns a throttled function that never runs more often than
127
+ * every `delay` milliseconds. \
128
+ * The returned function also has a nice `.finish()` method to reset the
129
+ * throttle timer
130
+ *
131
+ * @see https://github.com/reykjavikcity/webtools/blob/v0.2/README.md#throttle
132
+ */
133
+ /*#__NO_SIDE_EFFECTS__*/
134
+ export const throttle = (
135
+ /** The function to throttle */
136
+ func,
137
+ /** The delay, in milliseconds, to wait between invocations */
138
+ delay,
139
+ /** Whether to skip the first invocation instead of running it immediately */
140
+ skipFirst) => {
141
+ let timeout;
142
+ let throttled = 0;
143
+ let _args;
144
+ let _this;
145
+ const throttledFn = function (...args) {
146
+ _args = args;
147
+ _this = this;
148
+ if (!throttled) {
149
+ skipFirst ? throttled++ : func.apply(_this, _args);
150
+ timeout = setTimeout(throttledFn.finish, delay); // Go home TypeScript, you're drunk!
151
+ }
152
+ throttled++;
153
+ };
154
+ throttledFn.finish = (cancel) => {
155
+ timeout && clearTimeout(timeout);
156
+ !cancel && throttled && func.apply(_this, _args);
157
+ throttled = 0;
158
+ };
159
+ return throttledFn;
160
+ };
161
+ /**
162
+ * Sugar to produce a dynamic debounced function that accepts its contents/behavior at call time.
163
+ *
164
+ * Usage:
165
+ * ```ts
166
+ * const myThrottler = throttle.d(500);
167
+ * myThrottler(() => { alert('Hello world'); });
168
+ * myThrottler(() => { alert('I mean: Howdy world!'); });
169
+ * myThrottler((name) => { alert('Wazzap ' + name); }, 'world');
170
+ * ```
171
+ *
172
+ * Not documented in README as its usefulness is still uncertain.
173
+ */
174
+ throttle.d = (delay, skipFirst) => throttle(function (fn, ...args) {
175
+ fn.apply(this, args);
176
+ }, delay, skipFirst);
@@ -1,10 +1,18 @@
1
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.
2
+ * Error subclass for thrown NON-Error values that got turned into an actual
3
+ * Error, with the original thrown value as the `payload` property.
4
4
  *
5
5
  * @see https://github.com/reykjavikcity/webtools/blob/v0.2/README.md#aserror
6
6
  */
7
7
  export declare class ErrorFromPayload extends Error {
8
+ /**
9
+ * This payload property is only set if the original `throw` value was NOT
10
+ * `instanceof Error`.
11
+ *
12
+ * In such cases it contains the thrown value as is, and the `message`
13
+ * property of this `ErrorFromPayload` instance is set to the `.toString()`
14
+ * representation of the payload.
15
+ */
8
16
  payload?: unknown;
9
17
  constructor(payload: unknown);
10
18
  name: string;
@@ -50,7 +58,7 @@ export type ResultTupleObj<T, E extends Error = Error> = SuccessResult<T> | Fail
50
58
  * `[error, results]` tuple with the `result` and `error` also attached as
51
59
  * named properties.
52
60
  *
53
- * Works on both promises and sync callback functions.
61
+ * Works on both promises and (synchronous) callback functions.
54
62
  *
55
63
  * @see https://github.com/reykjavikcity/webtools/blob/v0.2/README.md#resultcatch
56
64
  */
@@ -1,6 +1,6 @@
1
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.
2
+ * Error subclass for thrown NON-Error values that got turned into an actual
3
+ * Error, with the original thrown value as the `payload` property.
4
4
  *
5
5
  * @see https://github.com/reykjavikcity/webtools/blob/v0.2/README.md#aserror
6
6
  */
@@ -7,8 +7,12 @@ import { _PatchedCollator, _PatchedDateTimeFormat, _patchedDateToLocaleDateStrin
7
7
  if (!Intl.Collator.supportedLocalesOf(['is']).length) {
8
8
  Intl.Collator = _PatchedCollator;
9
9
  String.prototype.localeCompare = _patchedStringLocaleCompare;
10
+ }
11
+ if (!Intl.NumberFormat.supportedLocalesOf(['is']).length) {
10
12
  Intl.NumberFormat = _PatchedNumberFormat;
11
13
  Number.prototype.toLocaleString = _patchedNumberToLocaleString;
14
+ }
15
+ if (!Intl.DateTimeFormat.supportedLocalesOf(['is']).length) {
12
16
  Intl.DateTimeFormat = _PatchedDateTimeFormat;
13
17
  Date.prototype.toLocaleString = _patchedDateToLocaleString;
14
18
  Date.prototype.toLocaleDateString = _patchedDateToLocaleDateString;
package/esm/hooks.d.ts ADDED
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Returns a stable debounced function that invokes the supplied function
3
+ * after the specified delay. \
4
+ * When the component unmounts, any pending (debounced) calls are automatically cancelled.
5
+ *
6
+ * **NOTE:** The supplied callback does not need to be memoized. The debouncer
7
+ * will always invoke the last supplied version.
8
+ *
9
+ * @see https://github.com/reykjavikcity/webtools/blob/v0.2/README.md#usedebounced
10
+ */
11
+ export declare const useDebounced: <A extends Array<unknown>>(
12
+ /** The function to debounce */
13
+ func: (...args: A) => void,
14
+ /** The delay, in milliseconds, to wait before running the function */
15
+ delay: number,
16
+ /** Whether to run the function at the start of the delay instead of the end */
17
+ immediate?: boolean) => ((...args: A) => void) & {
18
+ cancel(finish?: boolean): void;
19
+ };
20
+ /**
21
+ * Returns a stable throttler function that throttles the supplied function.
22
+ *
23
+ * **NOTE:** The supplied callback does not need to be memoized. The throttler
24
+ * will always invoke the last supplied version.
25
+ *
26
+ * @see https://github.com/reykjavikcity/webtools/blob/v0.2/README.md#usethrottled
27
+ */
28
+ export declare const useThrottled: <A extends Array<unknown>>(
29
+ /** The function to throttle */
30
+ func: (...args: A) => void,
31
+ /** The delay, in milliseconds, to wait between invocations. */
32
+ delay: number,
33
+ /** Whether to skip the first invocation instead of running it immediately. */
34
+ skipFirst?: boolean) => ((...args: A) => void) & {
35
+ finish(cancel?: boolean): void;
36
+ };
package/esm/hooks.js ADDED
@@ -0,0 +1,46 @@
1
+ import { useEffect, useMemo, useRef } from 'react';
2
+ import { debounce, throttle } from './async.js';
3
+ /**
4
+ * Returns a stable debounced function that invokes the supplied function
5
+ * after the specified delay. \
6
+ * When the component unmounts, any pending (debounced) calls are automatically cancelled.
7
+ *
8
+ * **NOTE:** The supplied callback does not need to be memoized. The debouncer
9
+ * will always invoke the last supplied version.
10
+ *
11
+ * @see https://github.com/reykjavikcity/webtools/blob/v0.2/README.md#usedebounced
12
+ */
13
+ export const useDebounced = (
14
+ /** The function to debounce */
15
+ func,
16
+ /** The delay, in milliseconds, to wait before running the function */
17
+ delay,
18
+ /** Whether to run the function at the start of the delay instead of the end */
19
+ immediate) => {
20
+ const fn = useRef();
21
+ fn.current = func;
22
+ const debouncedFunc = useMemo(() => debounce((...args) => fn.current(...args), delay, immediate), [delay, immediate]);
23
+ useEffect(() => debouncedFunc.cancel, [debouncedFunc]);
24
+ return debouncedFunc;
25
+ };
26
+ /**
27
+ * Returns a stable throttler function that throttles the supplied function.
28
+ *
29
+ * **NOTE:** The supplied callback does not need to be memoized. The throttler
30
+ * will always invoke the last supplied version.
31
+ *
32
+ * @see https://github.com/reykjavikcity/webtools/blob/v0.2/README.md#usethrottled
33
+ */
34
+ export const useThrottled = (
35
+ /** The function to throttle */
36
+ func,
37
+ /** The delay, in milliseconds, to wait between invocations. */
38
+ delay,
39
+ /** Whether to skip the first invocation instead of running it immediately. */
40
+ skipFirst) => {
41
+ const fn = useRef();
42
+ fn.current = func;
43
+ const throttledFunc = useMemo(() => throttle((...args) => fn.current(...args), delay, skipFirst), [delay, skipFirst]);
44
+ // NOTE: We don't need to run throttled.finish() on unmount, as the default behavior is no-op.
45
+ return throttledFunc;
46
+ };
package/esm/http.d.ts CHANGED
@@ -11,6 +11,7 @@ export declare const HTTP_103_EarlyHints = 103;
11
11
  export declare const HTTP_200_OK = 200;
12
12
  /** The request succeeded, and a new resource was created as a result. This is typically the response sent after POST or PUT requests. */
13
13
  export declare const HTTP_201_Created = 201;
14
+ /** The request has been received but not yet acted upon. Another process or server handles the request. */
14
15
  export declare const HTTP_202_Accepted = 202;
15
16
  /** The returned metadata is not necessarily complete. */
16
17
  export declare const HTTP_203_NonAuthoritativeInformation = 203;
package/esm/http.js CHANGED
@@ -12,7 +12,7 @@ export const HTTP_103_EarlyHints = 103;
12
12
  export const HTTP_200_OK = 200;
13
13
  /** The request succeeded, and a new resource was created as a result. This is typically the response sent after POST or PUT requests. */
14
14
  export const HTTP_201_Created = 201;
15
- /* The request has been received but not yet acted upon. Another process or server handles the request. */
15
+ /** The request has been received but not yet acted upon. Another process or server handles the request. */
16
16
  export const HTTP_202_Accepted = 202;
17
17
  /** The returned metadata is not necessarily complete. */
18
18
  export const HTTP_203_NonAuthoritativeInformation = 203;
package/esm/index.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ /// <reference path="./hooks.d.ts" />
1
2
  /// <reference path="./vanillaExtract.d.ts" />
2
3
  /// <reference path="./http.d.ts" />
3
4
  /// <reference path="./async.d.ts" />
@@ -9,8 +9,12 @@ const fixIcelandicLocale_privates_js_1 = require("./fixIcelandicLocale.privates.
9
9
  if (!Intl.Collator.supportedLocalesOf(['is']).length) {
10
10
  Intl.Collator = fixIcelandicLocale_privates_js_1._PatchedCollator;
11
11
  String.prototype.localeCompare = fixIcelandicLocale_privates_js_1._patchedStringLocaleCompare;
12
+ }
13
+ if (!Intl.NumberFormat.supportedLocalesOf(['is']).length) {
12
14
  Intl.NumberFormat = fixIcelandicLocale_privates_js_1._PatchedNumberFormat;
13
15
  Number.prototype.toLocaleString = fixIcelandicLocale_privates_js_1._patchedNumberToLocaleString;
16
+ }
17
+ if (!Intl.DateTimeFormat.supportedLocalesOf(['is']).length) {
14
18
  Intl.DateTimeFormat = fixIcelandicLocale_privates_js_1._PatchedDateTimeFormat;
15
19
  Date.prototype.toLocaleString = fixIcelandicLocale_privates_js_1._patchedDateToLocaleString;
16
20
  Date.prototype.toLocaleDateString = fixIcelandicLocale_privates_js_1._patchedDateToLocaleDateString;
package/hooks.d.ts ADDED
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Returns a stable debounced function that invokes the supplied function
3
+ * after the specified delay. \
4
+ * When the component unmounts, any pending (debounced) calls are automatically cancelled.
5
+ *
6
+ * **NOTE:** The supplied callback does not need to be memoized. The debouncer
7
+ * will always invoke the last supplied version.
8
+ *
9
+ * @see https://github.com/reykjavikcity/webtools/blob/v0.2/README.md#usedebounced
10
+ */
11
+ export declare const useDebounced: <A extends Array<unknown>>(
12
+ /** The function to debounce */
13
+ func: (...args: A) => void,
14
+ /** The delay, in milliseconds, to wait before running the function */
15
+ delay: number,
16
+ /** Whether to run the function at the start of the delay instead of the end */
17
+ immediate?: boolean) => ((...args: A) => void) & {
18
+ cancel(finish?: boolean): void;
19
+ };
20
+ /**
21
+ * Returns a stable throttler function that throttles the supplied function.
22
+ *
23
+ * **NOTE:** The supplied callback does not need to be memoized. The throttler
24
+ * will always invoke the last supplied version.
25
+ *
26
+ * @see https://github.com/reykjavikcity/webtools/blob/v0.2/README.md#usethrottled
27
+ */
28
+ export declare const useThrottled: <A extends Array<unknown>>(
29
+ /** The function to throttle */
30
+ func: (...args: A) => void,
31
+ /** The delay, in milliseconds, to wait between invocations. */
32
+ delay: number,
33
+ /** Whether to skip the first invocation instead of running it immediately. */
34
+ skipFirst?: boolean) => ((...args: A) => void) & {
35
+ finish(cancel?: boolean): void;
36
+ };
package/hooks.js ADDED
@@ -0,0 +1,51 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.useThrottled = exports.useDebounced = void 0;
4
+ const react_1 = require("react");
5
+ const async_js_1 = require("./async.js");
6
+ /**
7
+ * Returns a stable debounced function that invokes the supplied function
8
+ * after the specified delay. \
9
+ * When the component unmounts, any pending (debounced) calls are automatically cancelled.
10
+ *
11
+ * **NOTE:** The supplied callback does not need to be memoized. The debouncer
12
+ * will always invoke the last supplied version.
13
+ *
14
+ * @see https://github.com/reykjavikcity/webtools/blob/v0.2/README.md#usedebounced
15
+ */
16
+ const useDebounced = (
17
+ /** The function to debounce */
18
+ func,
19
+ /** The delay, in milliseconds, to wait before running the function */
20
+ delay,
21
+ /** Whether to run the function at the start of the delay instead of the end */
22
+ immediate) => {
23
+ const fn = (0, react_1.useRef)();
24
+ fn.current = func;
25
+ const debouncedFunc = (0, react_1.useMemo)(() => (0, async_js_1.debounce)((...args) => fn.current(...args), delay, immediate), [delay, immediate]);
26
+ (0, react_1.useEffect)(() => debouncedFunc.cancel, [debouncedFunc]);
27
+ return debouncedFunc;
28
+ };
29
+ exports.useDebounced = useDebounced;
30
+ /**
31
+ * Returns a stable throttler function that throttles the supplied function.
32
+ *
33
+ * **NOTE:** The supplied callback does not need to be memoized. The throttler
34
+ * will always invoke the last supplied version.
35
+ *
36
+ * @see https://github.com/reykjavikcity/webtools/blob/v0.2/README.md#usethrottled
37
+ */
38
+ const useThrottled = (
39
+ /** The function to throttle */
40
+ func,
41
+ /** The delay, in milliseconds, to wait between invocations. */
42
+ delay,
43
+ /** Whether to skip the first invocation instead of running it immediately. */
44
+ skipFirst) => {
45
+ const fn = (0, react_1.useRef)();
46
+ fn.current = func;
47
+ const throttledFunc = (0, react_1.useMemo)(() => (0, async_js_1.throttle)((...args) => fn.current(...args), delay, skipFirst), [delay, skipFirst]);
48
+ // NOTE: We don't need to run throttled.finish() on unmount, as the default behavior is no-op.
49
+ return throttledFunc;
50
+ };
51
+ exports.useThrottled = useThrottled;
package/http.d.ts CHANGED
@@ -11,6 +11,7 @@ export declare const HTTP_103_EarlyHints = 103;
11
11
  export declare const HTTP_200_OK = 200;
12
12
  /** The request succeeded, and a new resource was created as a result. This is typically the response sent after POST or PUT requests. */
13
13
  export declare const HTTP_201_Created = 201;
14
+ /** The request has been received but not yet acted upon. Another process or server handles the request. */
14
15
  export declare const HTTP_202_Accepted = 202;
15
16
  /** The returned metadata is not necessarily complete. */
16
17
  export declare const HTTP_203_NonAuthoritativeInformation = 203;
package/http.js CHANGED
@@ -16,7 +16,7 @@ exports.HTTP_103_EarlyHints = 103;
16
16
  exports.HTTP_200_OK = 200;
17
17
  /** The request succeeded, and a new resource was created as a result. This is typically the response sent after POST or PUT requests. */
18
18
  exports.HTTP_201_Created = 201;
19
- /* The request has been received but not yet acted upon. Another process or server handles the request. */
19
+ /** The request has been received but not yet acted upon. Another process or server handles the request. */
20
20
  exports.HTTP_202_Accepted = 202;
21
21
  /** The returned metadata is not necessarily complete. */
22
22
  exports.HTTP_203_NonAuthoritativeInformation = 203;
package/index.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ /// <reference path="./hooks.d.ts" />
1
2
  /// <reference path="./vanillaExtract.d.ts" />
2
3
  /// <reference path="./http.d.ts" />
3
4
  /// <reference path="./async.d.ts" />
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reykjavik/webtools",
3
- "version": "0.2.6",
3
+ "version": "0.2.8",
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",
@@ -44,6 +44,10 @@
44
44
  "**/fixIcelandicLocale.js"
45
45
  ],
46
46
  "exports": {
47
+ "./hooks": {
48
+ "import": "./esm/hooks.js",
49
+ "require": "./hooks.js"
50
+ },
47
51
  ".": {
48
52
  "import": "./esm/index.js",
49
53
  "require": "./index.js"