@esmalley/ts-utils 6.4.5 → 7.0.0
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/README.md +435 -24
- package/dist/cjs/index.js +7 -7
- package/dist/cjs/index.js.map +4 -4
- package/dist/esm/index.js +7 -7
- package/dist/esm/index.js.map +4 -4
- package/dist/types/Arithmetic.d.ts +92 -0
- package/dist/types/Arithmetic.d.ts.map +1 -1
- package/dist/types/Arrayifier.d.ts +83 -6
- package/dist/types/Arrayifier.d.ts.map +1 -1
- package/dist/types/CSV.d.ts +14 -0
- package/dist/types/CSV.d.ts.map +1 -1
- package/dist/types/Color.d.ts +21 -5
- package/dist/types/Color.d.ts.map +1 -1
- package/dist/types/Dates.d.ts +39 -0
- package/dist/types/Dates.d.ts.map +1 -1
- package/dist/types/Kontororu/Socket.d.ts.map +1 -1
- package/dist/types/Kontororu/Store.d.ts.map +1 -1
- package/dist/types/Kontororu.d.ts +12 -5
- package/dist/types/Kontororu.d.ts.map +1 -1
- package/dist/types/Numbers.d.ts +78 -0
- package/dist/types/Numbers.d.ts.map +1 -0
- package/dist/types/Objector.d.ts +14 -0
- package/dist/types/Objector.d.ts.map +1 -1
- package/dist/types/Sorter.d.ts.map +1 -1
- package/dist/types/Style.d.ts.map +1 -1
- package/dist/types/Tasker.d.ts +109 -0
- package/dist/types/Tasker.d.ts.map +1 -0
- package/dist/types/Textor.d.ts +10 -0
- package/dist/types/Textor.d.ts.map +1 -1
- package/dist/types/Toaster.d.ts +26 -1
- package/dist/types/Toaster.d.ts.map +1 -1
- package/dist/types/UuidService.d.ts +9 -2
- package/dist/types/UuidService.d.ts.map +1 -1
- package/dist/types/index.d.ts +2 -0
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +7 -2
package/README.md
CHANGED
|
@@ -20,15 +20,19 @@ A modular collection of TypeScript utilities for modern web development.
|
|
|
20
20
|
* [CSV](#csv)
|
|
21
21
|
* [Dates](#dates)
|
|
22
22
|
* [Kontororu (Events)](#kontororu-events)
|
|
23
|
+
* [Numbers](#numbers)
|
|
23
24
|
* [Objector](#objector)
|
|
24
25
|
* [Sorter](#sorter)
|
|
25
26
|
* [Style](#style)
|
|
27
|
+
* [Tasker](#tasker)
|
|
26
28
|
* [Textor](#textor)
|
|
27
29
|
* [Theme](#theme)
|
|
28
30
|
* [Toaster](#toaster)
|
|
31
|
+
* [UuidService](#uuidservice)
|
|
29
32
|
|
|
30
33
|
|
|
31
34
|
* [Testing](#testing)
|
|
35
|
+
* [Benchmarks](#benchmarks)
|
|
32
36
|
* [License](#license)
|
|
33
37
|
|
|
34
38
|
---
|
|
@@ -76,6 +80,48 @@ console.log(actualHealth); // Output: 100
|
|
|
76
80
|
```
|
|
77
81
|
|
|
78
82
|
|
|
83
|
+
### `lerp(a, b, amount)`
|
|
84
|
+
|
|
85
|
+
Linearly interpolates between two numbers. `amount` is clamped to 0–1, so the result never overshoots.
|
|
86
|
+
|
|
87
|
+
```ts
|
|
88
|
+
Arithmetic.lerp(0, 100, 0.25); // 25
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### `round(value, precision?)`
|
|
92
|
+
|
|
93
|
+
Rounds to a fixed number of decimal places, handling the half-way cases that naive scaling gets wrong. A negative precision rounds to tens, hundreds, and so on.
|
|
94
|
+
|
|
95
|
+
```ts
|
|
96
|
+
Arithmetic.round(1.005, 2); // 1.01 (not 1 as with Math.round(v * 100) / 100)
|
|
97
|
+
Arithmetic.round(1234, -2); // 1200
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### `mapRange(value, inMin, inMax, outMin, outMax)` / `normalize(value, min, max)`
|
|
101
|
+
|
|
102
|
+
Re-maps a number from one range onto another. `normalize` is the special case that maps onto 0–1, and is the inverse of `lerp`.
|
|
103
|
+
|
|
104
|
+
```ts
|
|
105
|
+
// A 1000-2000 rating expressed as a 0-100 score
|
|
106
|
+
Arithmetic.mapRange(1500, 1000, 2000, 0, 100); // 50
|
|
107
|
+
Arithmetic.normalize(1500, 1000, 2000); // 0.5
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### Descriptive statistics
|
|
111
|
+
|
|
112
|
+
`sum`, `mean`, `median`, `mode`, `variance`, `stdDev`, and `percentile`.
|
|
113
|
+
|
|
114
|
+
Aggregates over an empty list return `NaN` rather than `0`, so an empty data set is visibly empty instead of silently reading as a real zero. `variance` and `stdDev` are sample statistics (dividing by `n - 1`); pass `true` as the second argument for the population form.
|
|
115
|
+
|
|
116
|
+
```ts
|
|
117
|
+
const scores = [88, 72, 95, 64, 79];
|
|
118
|
+
|
|
119
|
+
Arithmetic.mean(scores); // 79.6
|
|
120
|
+
Arithmetic.median(scores); // 79
|
|
121
|
+
Arithmetic.stdDev(scores); // 11.97...
|
|
122
|
+
Arithmetic.percentile(scores, 90); // 92.2
|
|
123
|
+
Arithmetic.mode([1, 2, 2, 3]); // [2] (every tied value, in first-seen order)
|
|
124
|
+
```
|
|
79
125
|
|
|
80
126
|
---
|
|
81
127
|
|
|
@@ -112,7 +158,62 @@ const matchups = Arrayifier.getCombinations(players, 2);
|
|
|
112
158
|
|
|
113
159
|
```
|
|
114
160
|
|
|
161
|
+
A three-argument form, `getCombinations(arr, n, r)`, draws from only the first `n` elements.
|
|
162
|
+
|
|
163
|
+
### `groupBy(arr, keyFn)` / `countBy(arr, keyFn)`
|
|
164
|
+
|
|
165
|
+
Buckets or counts items by a derived key.
|
|
166
|
+
|
|
167
|
+
```ts
|
|
168
|
+
Arrayifier.groupBy(games, (g) => g.season);
|
|
169
|
+
// { 2025: [...], 2026: [...] }
|
|
170
|
+
|
|
171
|
+
Arrayifier.countBy(games, (g) => g.status);
|
|
172
|
+
// { final: 12, live: 3 }
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
### `chunk(arr, size)`
|
|
176
|
+
|
|
177
|
+
Splits an array into consecutive chunks of at most `size`.
|
|
178
|
+
|
|
179
|
+
```ts
|
|
180
|
+
Arrayifier.chunk([1, 2, 3, 4, 5], 2); // [[1, 2], [3, 4], [5]]
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
### `unique(arr)` / `uniqueBy(arr, keyFn)`
|
|
184
|
+
|
|
185
|
+
Removes duplicates, keeping the first occurrence. `unique` compares by identity; `uniqueBy` compares a derived key, which is what you want for objects.
|
|
186
|
+
|
|
187
|
+
```ts
|
|
188
|
+
Arrayifier.unique([1, 2, 2, 3]); // [1, 2, 3]
|
|
189
|
+
Arrayifier.uniqueBy(players, (p) => p.team_id);
|
|
190
|
+
```
|
|
115
191
|
|
|
192
|
+
### `sortBy(arr, keyFn, direction?)`
|
|
193
|
+
|
|
194
|
+
Sorts by a derived value without mutating the input. Numbers compare numerically, everything else as strings, and null/undefined keys sort last in both directions.
|
|
195
|
+
|
|
196
|
+
```ts
|
|
197
|
+
Arrayifier.sortBy(teams, (t) => t.rating, 'desc');
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
### `partition(arr, predicate)`
|
|
201
|
+
|
|
202
|
+
Splits into the items that satisfy the predicate and those that do not.
|
|
203
|
+
|
|
204
|
+
```ts
|
|
205
|
+
const [wins, losses] = Arrayifier.partition(games, (g) => g.won);
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
### `range(start, end?, step?)`
|
|
209
|
+
|
|
210
|
+
A sequence of numbers, excluding the end. With one argument, counts from 0.
|
|
211
|
+
|
|
212
|
+
```ts
|
|
213
|
+
Arrayifier.range(4); // [0, 1, 2, 3]
|
|
214
|
+
Arrayifier.range(1, 4); // [1, 2, 3]
|
|
215
|
+
Arrayifier.range(3, 0, -1); // [3, 2, 1]
|
|
216
|
+
```
|
|
116
217
|
|
|
117
218
|
---
|
|
118
219
|
|
|
@@ -173,7 +274,20 @@ console.log(overlay); // Output: rgba(0, 0, 0, 0.5)
|
|
|
173
274
|
|
|
174
275
|
```
|
|
175
276
|
|
|
277
|
+
### Conversions: `hexToRgb`, `rgbToHex`, `rgbToHsl`, `hslToRgb`
|
|
176
278
|
|
|
279
|
+
The full set of conversions is public, so you can move between representations in either direction. `hexToRgb` accepts shorthand (`#fff`) and throws on malformed input rather than silently reporting black. `rgbToHex` rounds and clamps its channels, so an out-of-range value cannot produce a malformed string.
|
|
280
|
+
|
|
281
|
+
```ts
|
|
282
|
+
Color.hexToRgb('#ff8000'); // [255, 128, 0]
|
|
283
|
+
Color.rgbToHex(255, 128, 0); // '#FF8000'
|
|
284
|
+
Color.rgbToHsl(255, 0, 0); // [0, 100, 50] (hue 0-360, sat/lightness 0-100)
|
|
285
|
+
Color.hslToRgb(0, 100, 50); // [255, 0, 0]
|
|
286
|
+
|
|
287
|
+
// Rotate a hue without leaving hex
|
|
288
|
+
const [h, s, l] = Color.rgbToHsl(...Color.hexToRgb('#3498db'));
|
|
289
|
+
const shifted = Color.rgbToHex(...Color.hslToRgb((h + 180) % 360, s, l));
|
|
290
|
+
```
|
|
177
291
|
|
|
178
292
|
---
|
|
179
293
|
|
|
@@ -181,6 +295,27 @@ console.log(overlay); // Output: rgba(0, 0, 0, 0.5)
|
|
|
181
295
|
|
|
182
296
|
Utilities for data exportation.
|
|
183
297
|
|
|
298
|
+
### `stringify(data)`
|
|
299
|
+
|
|
300
|
+
Converts a nested object into an RFC 4180 CSV string. Headers are the union of every row's keys, so a row carrying extra columns is not truncated to the shape of the first one. Values containing a comma, quote or newline are quoted, and embedded quotes are doubled. `null` and `undefined` become empty cells, while `0`, `false` and `''` survive as written.
|
|
301
|
+
|
|
302
|
+
* **Example 1: Building a CSV in Memory**
|
|
303
|
+
```ts
|
|
304
|
+
import { CSV } from '@esmalley/ts-utils';
|
|
305
|
+
|
|
306
|
+
const csv = CSV.stringify({
|
|
307
|
+
1: { team: 'Tigers', wins: 0, note: 'Rebuilding, "again"' },
|
|
308
|
+
2: { team: 'Bears', wins: 12, streak: 3 },
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
// team,wins,note,streak
|
|
312
|
+
// Tigers,0,"Rebuilding, ""again""",
|
|
313
|
+
// Bears,12,,3
|
|
314
|
+
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
|
|
184
319
|
### `download(data)`
|
|
185
320
|
|
|
186
321
|
Takes a nested object and triggers a browser download of a generated `.csv` file.
|
|
@@ -257,6 +392,154 @@ console.log(Dates.isSameDay(d1, d2)); // true
|
|
|
257
392
|
|
|
258
393
|
```
|
|
259
394
|
|
|
395
|
+
### Period boundaries
|
|
396
|
+
|
|
397
|
+
`getStartOfDay` / `getEndOfDay`, `getStartOfMonth` / `getEndOfMonth`, and `getDaysInMonth`.
|
|
398
|
+
|
|
399
|
+
```ts
|
|
400
|
+
Dates.getEndOfDay('2026-03-15'); // 2026-03-15 23:59:59.999 local
|
|
401
|
+
Dates.getEndOfMonth('2026-02-10'); // 2026-02-28 23:59:59.999
|
|
402
|
+
Dates.getDaysInMonth('2024-02-01'); // 29
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
### `eachDayOfInterval(start, end)`
|
|
406
|
+
|
|
407
|
+
Every day from `start` to `end` inclusive, as local midnights. Steps by calendar day, so it stays correct across DST boundaries.
|
|
408
|
+
|
|
409
|
+
* **Example: Building a month grid**
|
|
410
|
+
Pair with `getStartOfGrid` / `getEndOfGrid` to get whole Sunday-to-Saturday weeks covering the month.
|
|
411
|
+
```ts
|
|
412
|
+
const cells = Dates.eachDayOfInterval(
|
|
413
|
+
Dates.getStartOfGrid('2026-03-01'),
|
|
414
|
+
Dates.getEndOfGrid('2026-03-01'),
|
|
415
|
+
);
|
|
416
|
+
// cells.length is always a multiple of 7
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
### `isBetween(date, start, end, inclusive?)`
|
|
420
|
+
|
|
421
|
+
Whether a date falls between two others. The bounds may be given in either order, and comparison is by exact instant — pair with `getStartOfDay` / `getEndOfDay` for a whole-day range.
|
|
422
|
+
|
|
423
|
+
```ts
|
|
424
|
+
Dates.isBetween('2026-03-05', '2026-03-01', '2026-03-10'); // true
|
|
425
|
+
```
|
|
426
|
+
|
|
427
|
+
---
|
|
428
|
+
|
|
429
|
+
## Tasker
|
|
430
|
+
|
|
431
|
+
Wrappers for controlling how and when other functions run.
|
|
432
|
+
|
|
433
|
+
### `debounce(fn, wait, leading?)`
|
|
434
|
+
|
|
435
|
+
Delays a function until it has stopped being called for `wait` ms. The returned function carries `cancel()`, `flush()`, and `pending()`.
|
|
436
|
+
|
|
437
|
+
* **Example: Search-as-you-type**
|
|
438
|
+
```ts
|
|
439
|
+
import { Tasker } from '@esmalley/ts-utils';
|
|
440
|
+
|
|
441
|
+
const search = Tasker.debounce((term: string) => runQuery(term), 300);
|
|
442
|
+
|
|
443
|
+
search('gon');
|
|
444
|
+
search('gonz'); // only this one runs, 300ms after the last keystroke
|
|
445
|
+
```
|
|
446
|
+
|
|
447
|
+
### `throttle(fn, wait)`
|
|
448
|
+
|
|
449
|
+
Allows a function to run at most once per `wait` ms. The first call runs immediately; a call made during the cooling-off period runs when it ends, carrying the most recent arguments.
|
|
450
|
+
|
|
451
|
+
```ts
|
|
452
|
+
window.addEventListener('scroll', Tasker.throttle(() => measure(), 100));
|
|
453
|
+
```
|
|
454
|
+
|
|
455
|
+
### `retry(fn, options?)`
|
|
456
|
+
|
|
457
|
+
Calls an async function until it succeeds, backing off between attempts. Rethrows the final error once attempts are exhausted.
|
|
458
|
+
|
|
459
|
+
```ts
|
|
460
|
+
const data = await Tasker.retry(() => fetchRatings(), {
|
|
461
|
+
attempts: 5,
|
|
462
|
+
delay: 500,
|
|
463
|
+
onRetry: (error, attempt) => console.warn(`retry ${attempt}`, error),
|
|
464
|
+
});
|
|
465
|
+
```
|
|
466
|
+
|
|
467
|
+
### `backoff(attempt, options?)`
|
|
468
|
+
|
|
469
|
+
The delay before a given retry attempt, growing exponentially and capped at `maxDelay`. Pass `jitter` to randomize across `0..computed` and avoid a thundering herd.
|
|
470
|
+
|
|
471
|
+
```ts
|
|
472
|
+
Tasker.backoff(0); // 1000
|
|
473
|
+
Tasker.backoff(3); // 8000
|
|
474
|
+
```
|
|
475
|
+
|
|
476
|
+
### `memoize(fn, keyFn?)` / `once(fn)`
|
|
477
|
+
|
|
478
|
+
Caches results by arguments, or restricts a function to a single call. The memoized function carries `clear()` and `size()`.
|
|
479
|
+
|
|
480
|
+
```ts
|
|
481
|
+
const ratingFor = Tasker.memoize((teamId: number) => compute(teamId));
|
|
482
|
+
const init = Tasker.once(() => connect());
|
|
483
|
+
```
|
|
484
|
+
|
|
485
|
+
### `sleep(ms)`
|
|
486
|
+
|
|
487
|
+
Resolves after a delay.
|
|
488
|
+
|
|
489
|
+
```ts
|
|
490
|
+
await Tasker.sleep(250);
|
|
491
|
+
```
|
|
492
|
+
|
|
493
|
+
---
|
|
494
|
+
|
|
495
|
+
## Numbers
|
|
496
|
+
|
|
497
|
+
Human-readable number formatting.
|
|
498
|
+
|
|
499
|
+
### `formatOrdinal(n)` / `ordinalSuffix(n)`
|
|
500
|
+
|
|
501
|
+
A number with its English ordinal suffix, or just the suffix.
|
|
502
|
+
|
|
503
|
+
```ts
|
|
504
|
+
Numbers.formatOrdinal(1); // '1st'
|
|
505
|
+
Numbers.formatOrdinal(23); // '23rd'
|
|
506
|
+
Numbers.ordinalSuffix(11); // 'th'
|
|
507
|
+
```
|
|
508
|
+
|
|
509
|
+
### `format(value, decimals?, locale?)` / `formatSigned(value, decimals?)`
|
|
510
|
+
|
|
511
|
+
Locale grouping separators, optionally with an explicit sign for deltas such as a change in rank.
|
|
512
|
+
|
|
513
|
+
```ts
|
|
514
|
+
Numbers.format(1234567); // '1,234,567'
|
|
515
|
+
Numbers.format(1234.5, 2); // '1,234.50'
|
|
516
|
+
Numbers.formatSigned(3); // '+3'
|
|
517
|
+
```
|
|
518
|
+
|
|
519
|
+
### `formatCompact(value, decimals?)`
|
|
520
|
+
|
|
521
|
+
A large number shortened with a magnitude suffix. Implemented directly rather than through Intl's compact notation, whose exact output varies with the runtime's ICU data.
|
|
522
|
+
|
|
523
|
+
```ts
|
|
524
|
+
Numbers.formatCompact(1234); // '1.2K'
|
|
525
|
+
Numbers.formatCompact(2_000_000); // '2M'
|
|
526
|
+
```
|
|
527
|
+
|
|
528
|
+
### `formatPercent(value, decimals?, fromRatio?)`
|
|
529
|
+
|
|
530
|
+
A ratio as a percentage. Pass `false` for `fromRatio` if the input is already scaled.
|
|
531
|
+
|
|
532
|
+
```ts
|
|
533
|
+
Numbers.formatPercent(0.1234); // '12.3%'
|
|
534
|
+
```
|
|
535
|
+
|
|
536
|
+
### `formatDuration(ms, parts?)` / `formatBytes(bytes, decimals?)`
|
|
537
|
+
|
|
538
|
+
```ts
|
|
539
|
+
Numbers.formatDuration(3_725_000); // '1h 2m'
|
|
540
|
+
Numbers.formatDuration(3_725_000, 3); // '1h 2m 5s'
|
|
541
|
+
Numbers.formatBytes(1536); // '1.5 KB'
|
|
542
|
+
```
|
|
260
543
|
|
|
261
544
|
---
|
|
262
545
|
|
|
@@ -293,6 +576,29 @@ Objector.extender(defaults, userConfig);
|
|
|
293
576
|
|
|
294
577
|
```
|
|
295
578
|
|
|
579
|
+
### `deepEqual(a, b)`
|
|
580
|
+
|
|
581
|
+
Structurally compares two values — the counterpart to `deepClone`. Handles the same shapes (Date, RegExp, Map, Set, arrays, plain objects, symbol keys) and tolerates circular references.
|
|
582
|
+
|
|
583
|
+
`NaN` equals `NaN` and `+0` does not equal `-0`, matching `Object.is` rather than `===`. Objects must share a prototype to be considered equal.
|
|
584
|
+
|
|
585
|
+
* **Example 1: Change detection**
|
|
586
|
+
```ts
|
|
587
|
+
if (!Objector.deepEqual(previousFilters, nextFilters)) {
|
|
588
|
+
refetch();
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
```
|
|
592
|
+
|
|
593
|
+
* **Example 2: Verifying a clone**
|
|
594
|
+
```ts
|
|
595
|
+
const copy = Objector.deepClone(state);
|
|
596
|
+
Objector.deepEqual(state, copy); // true
|
|
597
|
+
copy.user.id = 2;
|
|
598
|
+
Objector.deepEqual(state, copy); // false
|
|
599
|
+
|
|
600
|
+
```
|
|
601
|
+
|
|
296
602
|
|
|
297
603
|
|
|
298
604
|
---
|
|
@@ -362,6 +668,14 @@ const shadow = Style.getShadow(4);
|
|
|
362
668
|
|
|
363
669
|
String manipulation and linguistic utility functions.
|
|
364
670
|
|
|
671
|
+
### `toKebabCase(str)`
|
|
672
|
+
|
|
673
|
+
Converts camelCase or PascalCase to kebab-case. Used internally to turn CSS-in-JS property names into real CSS properties, but general purpose.
|
|
674
|
+
|
|
675
|
+
```ts
|
|
676
|
+
Textor.toKebabCase('backgroundColor'); // 'background-color'
|
|
677
|
+
```
|
|
678
|
+
|
|
365
679
|
### `levenshtein(a, b)`
|
|
366
680
|
|
|
367
681
|
Calculates the Levenshtein distance between two strings. This is a string metric for measuring the difference between two sequences (the minimum number of single-character edits required to change one word into the other).
|
|
@@ -479,27 +793,6 @@ console.log(light.background.main); // #ffffff
|
|
|
479
793
|
|
|
480
794
|
|
|
481
795
|
|
|
482
|
-
### Palette Accessors (e.g., `getGrey()`, `getAmber()`, etc.)
|
|
483
|
-
|
|
484
|
-
The class provides access to the standard Material Design color ramps.
|
|
485
|
-
|
|
486
|
-
* **Example 1: Using Specific Color Weights**
|
|
487
|
-
```ts
|
|
488
|
-
const theme = new Theme('light');
|
|
489
|
-
const greys = theme.getGrey();
|
|
490
|
-
|
|
491
|
-
const dividerStyle = {
|
|
492
|
-
backgroundColor: greys[300] // Light grey for dividers
|
|
493
|
-
};
|
|
494
|
-
|
|
495
|
-
const secondaryText = {
|
|
496
|
-
color: greys[600] // Medium grey for subtext
|
|
497
|
-
};
|
|
498
|
-
|
|
499
|
-
```
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
796
|
---
|
|
504
797
|
|
|
505
798
|
## Toaster
|
|
@@ -528,7 +821,7 @@ const unsubscribe = toaster.subscribe((newList) => {
|
|
|
528
821
|
|
|
529
822
|
### `add(message, type)`
|
|
530
823
|
|
|
531
|
-
Adds a new notification to the stack. Defaults to `'info'` type. Automatically triggers a close request after 4 seconds.
|
|
824
|
+
Adds a new notification to the stack and returns its id, which `requestClose(id)` and `remove(id)` take. Defaults to `'info'` type. Automatically triggers a close request after `Toaster.AUTO_DISMISS_MS` (4 seconds); the exit phase then lasts `Toaster.EXIT_ANIMATION_MS` (500ms). Both are public constants, so a UI can drive its animation from the same numbers.
|
|
532
825
|
|
|
533
826
|
* **Example 1: Error Handling**
|
|
534
827
|
```ts
|
|
@@ -546,13 +839,20 @@ try {
|
|
|
546
839
|
```ts
|
|
547
840
|
toaster.add("You have a new message."); // Defaults to 'info'
|
|
548
841
|
|
|
842
|
+
// Hold on to the id to dismiss a toast before it auto-dismisses.
|
|
843
|
+
const id = toaster.add("Uploading...");
|
|
844
|
+
await upload();
|
|
845
|
+
toaster.remove(id);
|
|
846
|
+
|
|
549
847
|
```
|
|
550
848
|
|
|
551
849
|
|
|
552
850
|
|
|
553
851
|
### `requestClose(id)`
|
|
554
852
|
|
|
555
|
-
Starts the "exit" phase for a toast. It marks the toast as `exiting: true
|
|
853
|
+
Starts the "exit" phase for a toast. It marks the toast as `exiting: true` so the UI can play a fade-out animation.
|
|
854
|
+
|
|
855
|
+
Your UI is expected to call `remove(id)` once that animation ends — that is the normal path, and it is what gets the toast off the list. As a safety net, the toast is also dropped 500ms later in case the animation callback never arrives (a backgrounded tab, an unmount mid-animation, reduced-motion settings). That late removal is silent when the UI already handled it, so it costs no extra render.
|
|
556
856
|
|
|
557
857
|
* **Example 1: Manual Dismiss Button**
|
|
558
858
|
```ts
|
|
@@ -563,11 +863,24 @@ const handleClose = (toastId) => {
|
|
|
563
863
|
|
|
564
864
|
```
|
|
565
865
|
|
|
866
|
+
* **Example 2: Removing when the animation ends**
|
|
867
|
+
```tsx
|
|
868
|
+
<div
|
|
869
|
+
className={isExiting ? 'toast-fade-out' : 'toast-slide-up'}
|
|
870
|
+
onAnimationEnd={() => {
|
|
871
|
+
if (isExiting) {
|
|
872
|
+
toaster.remove(toast.id);
|
|
873
|
+
}
|
|
874
|
+
}}
|
|
875
|
+
>
|
|
876
|
+
|
|
877
|
+
```
|
|
878
|
+
|
|
566
879
|
|
|
567
880
|
|
|
568
881
|
### `remove(id)`
|
|
569
882
|
|
|
570
|
-
Immediately removes a toast from the list without waiting for an animation or timeout.
|
|
883
|
+
Immediately removes a toast from the list without waiting for an animation or timeout. Removing an id that is not present is a no-op and notifies nobody.
|
|
571
884
|
|
|
572
885
|
* **Example 1: Force Clearing a specific alert**
|
|
573
886
|
```ts
|
|
@@ -610,6 +923,104 @@ bus.addEventListener('data', (payload) => console.log(payload));
|
|
|
610
923
|
|
|
611
924
|
```
|
|
612
925
|
|
|
926
|
+
---
|
|
927
|
+
|
|
928
|
+
## UuidService
|
|
929
|
+
|
|
930
|
+
A singleton that generates UUIDv7 identifiers — time-ordered, so they sort chronologically and index well as a primary key.
|
|
931
|
+
|
|
932
|
+
### `generateUUIDv7Bytes()`
|
|
933
|
+
|
|
934
|
+
Returns the raw 16 bytes. Monotonic within a millisecond via a 12-bit counter, and holds steady if the wall clock jumps backwards.
|
|
935
|
+
|
|
936
|
+
```ts
|
|
937
|
+
import { uuidService } from '@esmalley/ts-utils';
|
|
938
|
+
|
|
939
|
+
const bytes = uuidService.generateUUIDv7Bytes();
|
|
940
|
+
const id = uuidService.binToUuid(bytes);
|
|
941
|
+
// '01997e4c-1f3a-7b21-9c04-5e6f70818293'
|
|
942
|
+
```
|
|
943
|
+
|
|
944
|
+
### `binToUuid(buffer)` / `uuidToBin(uuid)`
|
|
945
|
+
|
|
946
|
+
Converts between the 16-byte form and the canonical dashed string. Storing the binary form in a `BINARY(16)` column costs 16 bytes instead of 36.
|
|
947
|
+
|
|
948
|
+
```ts
|
|
949
|
+
const bin = uuidService.uuidToBin(id);
|
|
950
|
+
uuidService.binToUuid(bin) === id; // true
|
|
951
|
+
```
|
|
952
|
+
|
|
953
|
+
### `isValid(uuid)`
|
|
954
|
+
|
|
955
|
+
Whether a string is a well-formed UUID, dashed or bare.
|
|
956
|
+
|
|
957
|
+
```ts
|
|
958
|
+
uuidService.isValid(id); // true
|
|
959
|
+
uuidService.isValid('not-a-id'); // false
|
|
960
|
+
```
|
|
961
|
+
|
|
962
|
+
---
|
|
963
|
+
|
|
964
|
+
# Testing
|
|
965
|
+
|
|
966
|
+
Unit tests run on Jest in ESM mode:
|
|
967
|
+
|
|
968
|
+
```bash
|
|
969
|
+
npm test # type-checks, then runs every suite
|
|
970
|
+
npm test -- Arithmetic # one file
|
|
971
|
+
npm run typecheck # tsc --noEmit on its own
|
|
972
|
+
```
|
|
973
|
+
|
|
974
|
+
`npm test` runs `typecheck` first via `pretest`. That matters because ts-jest is configured with `isolatedModules: true`, which makes it transpile tests without type-checking them — so Jest alone will happily run a test file full of type errors. `tsc --noEmit` covers `src`, `tests` and everything else the root `tsconfig.json` includes, while `npm run build` stays scoped to `src`.
|
|
975
|
+
|
|
976
|
+
## Benchmarks
|
|
977
|
+
|
|
978
|
+
Correctness is covered by Jest; execution time is covered by a separate benchmark suite built on [tinybench](https://github.com/tinylibs/tinybench). Benchmarks live in `bench/*.bench.js` and read like tests:
|
|
979
|
+
|
|
980
|
+
```js
|
|
981
|
+
import { Arithmetic } from '../dist/esm/index.js';
|
|
982
|
+
import { suite, bench } from './harness.js';
|
|
983
|
+
|
|
984
|
+
const ratings = Array.from({ length: 10_000 }, () => Math.random() * 2000);
|
|
985
|
+
|
|
986
|
+
suite('Arithmetic (10k values)', () => {
|
|
987
|
+
bench('mean()', () => Arithmetic.mean(ratings));
|
|
988
|
+
bench('median()', () => Arithmetic.median(ratings));
|
|
989
|
+
});
|
|
990
|
+
```
|
|
991
|
+
|
|
992
|
+
They import from `dist/esm`, not `src`, so they measure what consumers actually install. Build first:
|
|
993
|
+
|
|
994
|
+
```bash
|
|
995
|
+
npm run build
|
|
996
|
+
npm run bench # run everything
|
|
997
|
+
npm run bench -- Arithmetic # filter by suite or bench name
|
|
998
|
+
npm run bench -- --time=1000 # sample each bench for 1000ms (default 500)
|
|
999
|
+
```
|
|
1000
|
+
|
|
1001
|
+
### Catching regressions
|
|
1002
|
+
|
|
1003
|
+
Record the current numbers, make a change, then re-run to see the difference:
|
|
1004
|
+
|
|
1005
|
+
```bash
|
|
1006
|
+
npm run bench:save # writes bench/baseline.json
|
|
1007
|
+
# ...optimize something...
|
|
1008
|
+
npm run bench # each row gains a delta column
|
|
1009
|
+
```
|
|
1010
|
+
|
|
1011
|
+
```
|
|
1012
|
+
Arithmetic (10k values)
|
|
1013
|
+
mean() 12,001 ops/s +-0.9% 84.59 us 1,419 runs same
|
|
1014
|
+
median() 471 ops/s +-4.1% 2.17 ms 64 runs +18.2%
|
|
1015
|
+
```
|
|
1016
|
+
|
|
1017
|
+
Deltas are throughput, so a positive number is faster. Anything under 5% reports as `same`, because run-to-run noise on a working machine swamps it. The baseline is machine-specific and git-ignored — compare against your own hardware, never someone else's.
|
|
1018
|
+
|
|
1019
|
+
Two annotations flag numbers you should not trust:
|
|
1020
|
+
|
|
1021
|
+
* **`unstable`** — the margin of error exceeded 5%. Re-run with a longer `--time`, or close whatever else is busy.
|
|
1022
|
+
* **`faster than the clock` / `at timer resolution`** — the work finished faster than the platform timer can resolve, so the figure is approximate. Benchmark a realistic batch instead of a single call.
|
|
1023
|
+
|
|
613
1024
|
|
|
614
1025
|
|
|
615
1026
|
|