@forthtilliath/ts-kit 0.2.0 → 0.4.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 +104 -19
- package/dist/array/getMostRecentIds.d.ts +21 -0
- package/dist/array/getMostRecentIds.d.ts.map +1 -0
- package/dist/array/getMostRecentIds.js +30 -0
- package/dist/array/index.d.ts +3 -0
- package/dist/array/index.d.ts.map +1 -1
- package/dist/array/index.js +3 -0
- package/dist/array/nextInCycle.d.ts +15 -0
- package/dist/array/nextInCycle.d.ts.map +1 -0
- package/dist/array/nextInCycle.js +19 -0
- package/dist/array/rankByNameMatch.d.ts +19 -0
- package/dist/array/rankByNameMatch.d.ts.map +1 -0
- package/dist/array/rankByNameMatch.js +45 -0
- package/dist/async/debounce.d.ts +20 -0
- package/dist/async/debounce.d.ts.map +1 -0
- package/dist/async/debounce.js +44 -0
- package/dist/async/index.d.ts +7 -0
- package/dist/async/index.d.ts.map +1 -0
- package/dist/async/index.js +6 -0
- package/dist/async/memoize.d.ts +19 -0
- package/dist/async/memoize.d.ts.map +1 -0
- package/dist/async/memoize.js +25 -0
- package/dist/async/retry.d.ts +25 -0
- package/dist/async/retry.d.ts.map +1 -0
- package/dist/async/retry.js +33 -0
- package/dist/async/sleep.d.ts +9 -0
- package/dist/async/sleep.d.ts.map +1 -0
- package/dist/async/sleep.js +10 -0
- package/dist/async/throttle.d.ts +20 -0
- package/dist/async/throttle.d.ts.map +1 -0
- package/dist/async/throttle.js +43 -0
- package/dist/async/withTimeout.d.ts +19 -0
- package/dist/async/withTimeout.d.ts.map +1 -0
- package/dist/async/withTimeout.js +34 -0
- package/dist/classes/array.d.ts +88 -0
- package/dist/classes/array.d.ts.map +1 -1
- package/dist/classes/array.js +74 -0
- package/dist/classes/index.d.ts +2 -0
- package/dist/classes/index.d.ts.map +1 -0
- package/dist/classes/index.js +1 -0
- package/dist/date/formatRelativeTime.d.ts +13 -0
- package/dist/date/formatRelativeTime.d.ts.map +1 -0
- package/dist/date/formatRelativeTime.js +30 -0
- package/dist/date/getPeriodStartMs.d.ts +16 -0
- package/dist/date/getPeriodStartMs.d.ts.map +1 -0
- package/dist/date/getPeriodStartMs.js +24 -0
- package/dist/date/index.d.ts +5 -0
- package/dist/date/index.d.ts.map +1 -0
- package/dist/date/index.js +4 -0
- package/dist/date/isSameDay.d.ts +11 -0
- package/dist/date/isSameDay.d.ts.map +1 -0
- package/dist/date/isSameDay.js +14 -0
- package/dist/date/startOfDay.d.ts +10 -0
- package/dist/date/startOfDay.d.ts.map +1 -0
- package/dist/date/startOfDay.js +13 -0
- package/dist/files/index.d.ts +2 -0
- package/dist/files/index.d.ts.map +1 -0
- package/dist/files/index.js +1 -0
- package/dist/index.d.ts +10 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +15 -1
- package/dist/number/clamp.d.ts +13 -0
- package/dist/number/clamp.d.ts.map +1 -0
- package/dist/number/clamp.js +14 -0
- package/dist/number/formatBytes.d.ts +12 -0
- package/dist/number/formatBytes.d.ts.map +1 -0
- package/dist/number/formatBytes.js +19 -0
- package/dist/number/formatCsvNumber.d.ts +14 -0
- package/dist/number/formatCsvNumber.d.ts.map +1 -0
- package/dist/number/formatCsvNumber.js +15 -0
- package/dist/number/formatDuration.d.ts +12 -0
- package/dist/number/formatDuration.d.ts.map +1 -0
- package/dist/number/formatDuration.js +29 -0
- package/dist/number/index.d.ts +6 -0
- package/dist/number/index.d.ts.map +1 -0
- package/dist/number/index.js +5 -0
- package/dist/number/round.d.ts +12 -0
- package/dist/number/round.d.ts.map +1 -0
- package/dist/number/round.js +14 -0
- package/dist/object/deepClone.d.ts +12 -0
- package/dist/object/deepClone.d.ts.map +1 -0
- package/dist/object/deepClone.js +16 -0
- package/dist/object/deepEqual.d.ts +12 -0
- package/dist/object/deepEqual.d.ts.map +1 -0
- package/dist/object/deepEqual.js +31 -0
- package/dist/object/groupBy.d.ts +12 -0
- package/dist/object/groupBy.d.ts.map +1 -0
- package/dist/object/groupBy.js +18 -0
- package/dist/object/index.d.ts +8 -0
- package/dist/object/index.d.ts.map +1 -0
- package/dist/object/index.js +7 -0
- package/dist/object/mapValues.d.ts +11 -0
- package/dist/object/mapValues.d.ts.map +1 -0
- package/dist/object/mapValues.js +16 -0
- package/dist/object/omit.d.ts +11 -0
- package/dist/object/omit.d.ts.map +1 -0
- package/dist/object/omit.js +16 -0
- package/dist/object/pick.d.ts +11 -0
- package/dist/object/pick.d.ts.map +1 -0
- package/dist/object/pick.js +18 -0
- package/dist/object/uniqueBy.d.ts +13 -0
- package/dist/object/uniqueBy.d.ts.map +1 -0
- package/dist/object/uniqueBy.js +23 -0
- package/dist/string/capitalize.d.ts +11 -0
- package/dist/string/capitalize.d.ts.map +1 -0
- package/dist/string/capitalize.js +14 -0
- package/dist/string/escapeCsvField.d.ts +14 -0
- package/dist/string/escapeCsvField.d.ts.map +1 -0
- package/dist/string/escapeCsvField.js +17 -0
- package/dist/string/escapeHtml.d.ts +12 -0
- package/dist/string/escapeHtml.d.ts.map +1 -0
- package/dist/string/escapeHtml.js +17 -0
- package/dist/string/index.d.ts +8 -0
- package/dist/string/index.d.ts.map +1 -0
- package/dist/string/index.js +7 -0
- package/dist/string/normalizeForSearch.d.ts +12 -0
- package/dist/string/normalizeForSearch.d.ts.map +1 -0
- package/dist/string/normalizeForSearch.js +25 -0
- package/dist/string/pluralize.d.ts +17 -0
- package/dist/string/pluralize.d.ts.map +1 -0
- package/dist/string/pluralize.js +26 -0
- package/dist/string/slugify.d.ts +12 -0
- package/dist/string/slugify.d.ts.map +1 -0
- package/dist/string/slugify.js +18 -0
- package/dist/string/truncate.d.ts +13 -0
- package/dist/string/truncate.d.ts.map +1 -0
- package/dist/string/truncate.js +17 -0
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -1,11 +1,8 @@
|
|
|
1
1
|
# @forthtilliath/ts-kit
|
|
2
2
|
|
|
3
|
-
Framework-agnostic TypeScript utility functions — arrays,
|
|
4
|
-
downloads, and a couple of
|
|
5
|
-
|
|
6
|
-
> **Status**: not currently consumed by any other package in this monorepo.
|
|
7
|
-
> Built, linted, tested and type-checked like everything else, but nothing
|
|
8
|
-
> imports it yet.
|
|
3
|
+
Framework-agnostic TypeScript utility functions — arrays, objects, strings,
|
|
4
|
+
async control-flow, dates, numbers, browser file downloads, and a couple of
|
|
5
|
+
`Array` extensions with better type-narrowing.
|
|
9
6
|
|
|
10
7
|
## Install
|
|
11
8
|
|
|
@@ -21,31 +18,78 @@ Within the workspace:
|
|
|
21
18
|
|
|
22
19
|
## Usage
|
|
23
20
|
|
|
24
|
-
|
|
25
|
-
|
|
21
|
+
Import everything you need from the package root — a single barrel re-exports
|
|
22
|
+
every category, so there's no need to remember (or type out) one import line
|
|
23
|
+
per module:
|
|
24
|
+
|
|
25
|
+
```ts
|
|
26
|
+
import { chunk, clamp, debounce, pick, slugify } from "@forthtilliath/ts-kit";
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Subpath imports still work if you'd rather import one module at a time (e.g.
|
|
30
|
+
to keep a bundle's included code obvious at a glance):
|
|
26
31
|
|
|
27
32
|
```ts
|
|
28
33
|
import { chunk } from "@forthtilliath/ts-kit/array/chunk";
|
|
29
|
-
import { flattenDeep } from "@forthtilliath/ts-kit/array/flattenDeep";
|
|
30
|
-
import { FArray } from "@forthtilliath/ts-kit/classes/array";
|
|
31
|
-
import {
|
|
32
|
-
downloadText,
|
|
33
|
-
downloadTextBlob,
|
|
34
|
-
} from "@forthtilliath/ts-kit/files/downloadText";
|
|
35
|
-
import { avg } from "@forthtilliath/ts-kit/maths/average";
|
|
36
|
-
import { sum } from "@forthtilliath/ts-kit/maths/sum";
|
|
37
34
|
```
|
|
38
35
|
|
|
39
36
|
### `array`
|
|
40
37
|
|
|
41
38
|
- `chunk(array, size)` — splits an array into chunks of a given size.
|
|
42
39
|
- `flattenDeep(array)` — recursively flattens nested arrays.
|
|
40
|
+
- `getMostRecentIds(rows, limit?)` — most recently occurring distinct ids
|
|
41
|
+
(`{ id, occurredAt }[]`), most recent first, deduplicated, limited
|
|
42
|
+
(default 5).
|
|
43
|
+
- `nextInCycle(ids, currentId)` — next id in a short list (e.g. cycling
|
|
44
|
+
through recents on tap) — wraps to the first if `currentId` is at the end
|
|
45
|
+
or no longer in the list.
|
|
46
|
+
- `rankByNameMatch(items, query, getName)` — ranks `items` by relevance to
|
|
47
|
+
`query`: earlier match position first, then shorter name — for a
|
|
48
|
+
search-as-you-type list.
|
|
49
|
+
|
|
50
|
+
### `async`
|
|
51
|
+
|
|
52
|
+
- `sleep(ms)` — resolves after a delay.
|
|
53
|
+
- `retry(fn, options)` — retries an async operation with exponential backoff.
|
|
54
|
+
- `debounce(fn, wait)` — delays `fn` until calls stop for `wait` ms; exposes
|
|
55
|
+
`cancel()` / `flush()`.
|
|
56
|
+
- `throttle(fn, wait)` — runs `fn` at most once per `wait` ms (leading +
|
|
57
|
+
trailing edge); exposes `cancel()`.
|
|
58
|
+
- `memoize(fn, getKey?)` — caches `fn`'s results per argument tuple; exposes
|
|
59
|
+
the underlying `cache` map.
|
|
60
|
+
- `withTimeout(promise, ms, message?)` — rejects with a `TimeoutError` if
|
|
61
|
+
`promise` doesn't settle in time.
|
|
43
62
|
|
|
44
63
|
### `classes`
|
|
45
64
|
|
|
46
|
-
- `FArray` — an `Array` subclass
|
|
47
|
-
|
|
48
|
-
|
|
65
|
+
- `FArray` — an `Array` subclass that fixes a handful of spots where the
|
|
66
|
+
native `Array` typings are wrong or overly restrictive:
|
|
67
|
+
- `includes()` narrows the type of the searched value when it's found
|
|
68
|
+
(the native `Array.prototype.includes` only narrows the _array's_
|
|
69
|
+
element type).
|
|
70
|
+
- `indexOf()` / `lastIndexOf()` accept a search value outside of the
|
|
71
|
+
array's element type instead of rejecting it at compile-time (same
|
|
72
|
+
root cause as `includes`, fixed the same way).
|
|
73
|
+
- `filter(Boolean)` strips `null`/`undefined` from the result's type
|
|
74
|
+
(the native typings don't recognize `Boolean` as a type guard, so the
|
|
75
|
+
result keeps the nullish types even though they're removed at runtime).
|
|
76
|
+
- `FArray.from(...)` / `FArray.of(...)` are typed to return an `FArray`
|
|
77
|
+
instead of a plain array (the native static typings return a plain
|
|
78
|
+
array even though these methods already construct an instance of the
|
|
79
|
+
calling class at runtime).
|
|
80
|
+
- `toArray()` converts an `FArray` back into a plain `Array` instance —
|
|
81
|
+
rarely needed since `FArray` is already assignable to `T[]` anywhere a
|
|
82
|
+
plain array is expected, but useful when the runtime type matters too.
|
|
83
|
+
|
|
84
|
+
### `date`
|
|
85
|
+
|
|
86
|
+
- `startOfDay(date?)` — returns a new `Date` at local midnight.
|
|
87
|
+
- `isSameDay(a, b)` — checks whether two dates fall on the same local day.
|
|
88
|
+
- `formatRelativeTime(date, baseDate?, locale?)` — human-readable relative
|
|
89
|
+
time ("3 hours ago"), backed by `Intl.RelativeTimeFormat`.
|
|
90
|
+
- `getPeriodStartMs(period, now?)` — start timestamp (ms) for a
|
|
91
|
+
`"today" | "7d" | "30d" | "all"` period filter, or `null` for `"all"`.
|
|
92
|
+
`"today"` is the current calendar day, not a rolling 24h window.
|
|
49
93
|
|
|
50
94
|
### `files`
|
|
51
95
|
|
|
@@ -54,11 +98,52 @@ import { sum } from "@forthtilliath/ts-kit/maths/sum";
|
|
|
54
98
|
- `downloadTextBlob(filename, text)` — same, but via a `Blob` + object URL
|
|
55
99
|
(better for larger content).
|
|
56
100
|
|
|
101
|
+
### `id`
|
|
102
|
+
|
|
103
|
+
- `randomId()` — `crypto.randomUUID()` with fallbacks for older/insecure
|
|
104
|
+
contexts.
|
|
105
|
+
|
|
57
106
|
### `maths`
|
|
58
107
|
|
|
59
108
|
- `sum(numbers)` — sum of an array of numbers.
|
|
60
109
|
- `avg(numbers)` — average of an array of numbers.
|
|
61
110
|
|
|
111
|
+
### `number`
|
|
112
|
+
|
|
113
|
+
- `clamp(value, min, max)` — clamps a number between two bounds.
|
|
114
|
+
- `round(value, decimals?)` — rounds to a given number of decimal places.
|
|
115
|
+
- `formatBytes(bytes, decimals?)` — human-readable byte size ("1.5 MB").
|
|
116
|
+
- `formatDuration(ms)` — human-readable duration ("1d 1h 1m 1s").
|
|
117
|
+
- `formatCsvNumber(value, decimals?)` — formats a number with a comma
|
|
118
|
+
decimal separator (French-locale spreadsheets) instead of JS's dot.
|
|
119
|
+
|
|
120
|
+
### `object`
|
|
121
|
+
|
|
122
|
+
- `pick(obj, keys)` / `omit(obj, keys)` — keep or drop a set of keys.
|
|
123
|
+
- `groupBy(array, getKey)` — groups elements by a computed key.
|
|
124
|
+
- `uniqueBy(array, getKey)` — deduplicates elements by a computed key.
|
|
125
|
+
- `deepClone(value)` — deep clone (via `structuredClone`, with a JSON
|
|
126
|
+
fallback).
|
|
127
|
+
- `deepEqual(a, b)` — recursive structural equality check.
|
|
128
|
+
- `mapValues(obj, fn)` — maps every value of an object through `fn`.
|
|
129
|
+
|
|
130
|
+
### `string`
|
|
131
|
+
|
|
132
|
+
- `capitalize(str)` — uppercases the first letter, lowercases the rest.
|
|
133
|
+
- `truncate(str, maxLength, suffix?)` — truncates with an ellipsis (or custom
|
|
134
|
+
suffix).
|
|
135
|
+
- `slugify(str)` — URL-friendly slug (accents stripped, lower-cased,
|
|
136
|
+
hyphenated).
|
|
137
|
+
- `pluralize(count, singular, plural?)` — naive English pluralization, with an
|
|
138
|
+
override for irregular words.
|
|
139
|
+
- `normalizeForSearch(text)` — lowercases, trims, strips accents, and expands
|
|
140
|
+
œ/æ ligatures (which `normalize("NFD")` alone doesn't decompose) — for
|
|
141
|
+
accent/case-insensitive search matching.
|
|
142
|
+
- `escapeCsvField(value)` — quotes and escapes a CSV field (RFC 4180) only if
|
|
143
|
+
it contains a `"`, `;`, or newline.
|
|
144
|
+
- `escapeHtml(text)` — basic HTML entity escaping (`&`, `<`, `>`, `"`) for
|
|
145
|
+
inserting user text into an HTML template.
|
|
146
|
+
|
|
62
147
|
## Scripts
|
|
63
148
|
|
|
64
149
|
```bash
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export interface RecentIdRow {
|
|
2
|
+
id: number | null;
|
|
3
|
+
occurredAt: string;
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* Returns the most recently occurring distinct ids, most recent first —
|
|
7
|
+
* useful for a "recents" picker (a food, a container, or any other dated
|
|
8
|
+
* entity). Rows with a `null` id (a reference deleted since) are ignored:
|
|
9
|
+
* there's nothing left to offer for them.
|
|
10
|
+
*
|
|
11
|
+
* @param rows - Rows carrying an id (or `null`) and an ISO/parsable date.
|
|
12
|
+
* @param limit - Max number of ids to return. Defaults to `5`.
|
|
13
|
+
* @returns Distinct ids, most recent first, limited to `limit`.
|
|
14
|
+
* @example
|
|
15
|
+
* getMostRecentIds([
|
|
16
|
+
* { id: 1, occurredAt: "2026-01-01T00:00:00.000Z" },
|
|
17
|
+
* { id: 2, occurredAt: "2026-01-03T00:00:00.000Z" },
|
|
18
|
+
* ]); // => [2, 1]
|
|
19
|
+
*/
|
|
20
|
+
export declare function getMostRecentIds(rows: RecentIdRow[], limit?: number): number[];
|
|
21
|
+
//# sourceMappingURL=getMostRecentIds.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"getMostRecentIds.d.ts","sourceRoot":"","sources":["../../src/array/getMostRecentIds.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,WAAW;IAC1B,EAAE,EAAE,MAAM,GAAG,IAAI,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;CACpB;AAID;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,gBAAgB,CAC9B,IAAI,EAAE,WAAW,EAAE,EACnB,KAAK,SAAgB,GACpB,MAAM,EAAE,CAcV"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
const DEFAULT_LIMIT = 5;
|
|
2
|
+
/**
|
|
3
|
+
* Returns the most recently occurring distinct ids, most recent first —
|
|
4
|
+
* useful for a "recents" picker (a food, a container, or any other dated
|
|
5
|
+
* entity). Rows with a `null` id (a reference deleted since) are ignored:
|
|
6
|
+
* there's nothing left to offer for them.
|
|
7
|
+
*
|
|
8
|
+
* @param rows - Rows carrying an id (or `null`) and an ISO/parsable date.
|
|
9
|
+
* @param limit - Max number of ids to return. Defaults to `5`.
|
|
10
|
+
* @returns Distinct ids, most recent first, limited to `limit`.
|
|
11
|
+
* @example
|
|
12
|
+
* getMostRecentIds([
|
|
13
|
+
* { id: 1, occurredAt: "2026-01-01T00:00:00.000Z" },
|
|
14
|
+
* { id: 2, occurredAt: "2026-01-03T00:00:00.000Z" },
|
|
15
|
+
* ]); // => [2, 1]
|
|
16
|
+
*/
|
|
17
|
+
export function getMostRecentIds(rows, limit = DEFAULT_LIMIT) {
|
|
18
|
+
const sorted = [...rows].sort((a, b) => new Date(b.occurredAt).getTime() - new Date(a.occurredAt).getTime());
|
|
19
|
+
const seen = new Set();
|
|
20
|
+
const result = [];
|
|
21
|
+
for (const row of sorted) {
|
|
22
|
+
if (row.id == null || seen.has(row.id))
|
|
23
|
+
continue;
|
|
24
|
+
seen.add(row.id);
|
|
25
|
+
result.push(row.id);
|
|
26
|
+
if (result.length >= limit)
|
|
27
|
+
break;
|
|
28
|
+
}
|
|
29
|
+
return result;
|
|
30
|
+
}
|
package/dist/array/index.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/array/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAC;AAC3B,cAAc,kBAAkB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/array/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAC;AAC3B,cAAc,kBAAkB,CAAC;AACjC,cAAc,uBAAuB,CAAC;AACtC,cAAc,kBAAkB,CAAC;AACjC,cAAc,sBAAsB,CAAC"}
|
package/dist/array/index.js
CHANGED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Returns the next element in a short list (e.g. cycling through a few
|
|
3
|
+
* recent items on tap). Wraps to the first element if the current one is no
|
|
4
|
+
* longer in the list (deleted, or out of range) or was the last one.
|
|
5
|
+
*
|
|
6
|
+
* @param ids - The list to cycle through.
|
|
7
|
+
* @param currentId - The currently selected id, or `null` for none.
|
|
8
|
+
* @returns The next id, or `null` if `ids` is empty.
|
|
9
|
+
* @example
|
|
10
|
+
* nextInCycle([1, 2, 3], 1); // => 2
|
|
11
|
+
* nextInCycle([1, 2, 3], 3); // => 1
|
|
12
|
+
* nextInCycle([1, 2, 3], null); // => 1
|
|
13
|
+
*/
|
|
14
|
+
export declare function nextInCycle(ids: number[], currentId: number | null): number | null;
|
|
15
|
+
//# sourceMappingURL=nextInCycle.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"nextInCycle.d.ts","sourceRoot":"","sources":["../../src/array/nextInCycle.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,wBAAgB,WAAW,CACzB,GAAG,EAAE,MAAM,EAAE,EACb,SAAS,EAAE,MAAM,GAAG,IAAI,GACvB,MAAM,GAAG,IAAI,CAIf"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Returns the next element in a short list (e.g. cycling through a few
|
|
3
|
+
* recent items on tap). Wraps to the first element if the current one is no
|
|
4
|
+
* longer in the list (deleted, or out of range) or was the last one.
|
|
5
|
+
*
|
|
6
|
+
* @param ids - The list to cycle through.
|
|
7
|
+
* @param currentId - The currently selected id, or `null` for none.
|
|
8
|
+
* @returns The next id, or `null` if `ids` is empty.
|
|
9
|
+
* @example
|
|
10
|
+
* nextInCycle([1, 2, 3], 1); // => 2
|
|
11
|
+
* nextInCycle([1, 2, 3], 3); // => 1
|
|
12
|
+
* nextInCycle([1, 2, 3], null); // => 1
|
|
13
|
+
*/
|
|
14
|
+
export function nextInCycle(ids, currentId) {
|
|
15
|
+
if (ids.length === 0)
|
|
16
|
+
return null;
|
|
17
|
+
const currentIndex = currentId == null ? -1 : ids.indexOf(currentId);
|
|
18
|
+
return ids[(currentIndex + 1) % ids.length] ?? null;
|
|
19
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ranks `items` by relevance to a text search: match position in the name
|
|
3
|
+
* (earlier is more relevant), then name length. Generic so it can be reused
|
|
4
|
+
* for any list of items with a displayable name — typically for a
|
|
5
|
+
* search-as-you-type list.
|
|
6
|
+
*
|
|
7
|
+
* @param items - The items to rank.
|
|
8
|
+
* @param query - The search text.
|
|
9
|
+
* @param getName - Reads the displayable name off an item.
|
|
10
|
+
* @returns Only the matching items, ranked most relevant first.
|
|
11
|
+
* @example
|
|
12
|
+
* rankByNameMatch(
|
|
13
|
+
* [{ label: "Salad with potato" }, { label: "Potato" }],
|
|
14
|
+
* "potato",
|
|
15
|
+
* (item) => item.label,
|
|
16
|
+
* ); // => [{ label: "Potato" }, { label: "Salad with potato" }]
|
|
17
|
+
*/
|
|
18
|
+
export declare function rankByNameMatch<T>(items: T[], query: string, getName: (item: T) => string): T[];
|
|
19
|
+
//# sourceMappingURL=rankByNameMatch.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rankByNameMatch.d.ts","sourceRoot":"","sources":["../../src/array/rankByNameMatch.ts"],"names":[],"mappings":"AAiBA;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,eAAe,CAAC,CAAC,EAC/B,KAAK,EAAE,CAAC,EAAE,EACV,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,MAAM,GAC3B,CAAC,EAAE,CAiBL"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { normalizeForSearch } from "../string/normalizeForSearch.js";
|
|
2
|
+
// Caches each item's normalized name by object reference: useful for fixed
|
|
3
|
+
// lists (module-level constants) searched on every keystroke in a search
|
|
4
|
+
// field, to avoid renormalizing every name on each call.
|
|
5
|
+
const normalizedNameCache = new WeakMap();
|
|
6
|
+
function getNormalizedName(item, getName) {
|
|
7
|
+
if (typeof item !== "object" || item === null)
|
|
8
|
+
return normalizeForSearch(getName(item));
|
|
9
|
+
const cached = normalizedNameCache.get(item);
|
|
10
|
+
if (cached !== undefined)
|
|
11
|
+
return cached;
|
|
12
|
+
const normalized = normalizeForSearch(getName(item));
|
|
13
|
+
normalizedNameCache.set(item, normalized);
|
|
14
|
+
return normalized;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Ranks `items` by relevance to a text search: match position in the name
|
|
18
|
+
* (earlier is more relevant), then name length. Generic so it can be reused
|
|
19
|
+
* for any list of items with a displayable name — typically for a
|
|
20
|
+
* search-as-you-type list.
|
|
21
|
+
*
|
|
22
|
+
* @param items - The items to rank.
|
|
23
|
+
* @param query - The search text.
|
|
24
|
+
* @param getName - Reads the displayable name off an item.
|
|
25
|
+
* @returns Only the matching items, ranked most relevant first.
|
|
26
|
+
* @example
|
|
27
|
+
* rankByNameMatch(
|
|
28
|
+
* [{ label: "Salad with potato" }, { label: "Potato" }],
|
|
29
|
+
* "potato",
|
|
30
|
+
* (item) => item.label,
|
|
31
|
+
* ); // => [{ label: "Potato" }, { label: "Salad with potato" }]
|
|
32
|
+
*/
|
|
33
|
+
export function rankByNameMatch(items, query, getName) {
|
|
34
|
+
const normalizedQuery = normalizeForSearch(query);
|
|
35
|
+
const matches = [];
|
|
36
|
+
for (const item of items) {
|
|
37
|
+
const matchIndex = getNormalizedName(item, getName).indexOf(normalizedQuery);
|
|
38
|
+
if (matchIndex !== -1) {
|
|
39
|
+
matches.push({ item, matchIndex });
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
matches.sort((a, b) => a.matchIndex - b.matchIndex ||
|
|
43
|
+
getName(a.item).length - getName(b.item).length);
|
|
44
|
+
return matches.map((m) => m.item);
|
|
45
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export interface DebouncedFunction<Args extends unknown[]> {
|
|
2
|
+
(...args: Args): void;
|
|
3
|
+
/** Cancels a pending invocation, if any. */
|
|
4
|
+
cancel: () => void;
|
|
5
|
+
/** Immediately invokes a pending call, if any. */
|
|
6
|
+
flush: () => void;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Returns a debounced version of `fn` that only runs after `wait`
|
|
10
|
+
* milliseconds have elapsed without another call.
|
|
11
|
+
*
|
|
12
|
+
* @param fn - The function to debounce.
|
|
13
|
+
* @param wait - The delay in milliseconds.
|
|
14
|
+
* @returns The debounced function, with `cancel()` and `flush()` helpers.
|
|
15
|
+
* @example
|
|
16
|
+
* const onResize = debounce(() => recompute(), 200);
|
|
17
|
+
* window.addEventListener("resize", onResize);
|
|
18
|
+
*/
|
|
19
|
+
export declare function debounce<Args extends unknown[]>(fn: (...args: Args) => void, wait: number): DebouncedFunction<Args>;
|
|
20
|
+
//# sourceMappingURL=debounce.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"debounce.d.ts","sourceRoot":"","sources":["../../src/async/debounce.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,iBAAiB,CAAC,IAAI,SAAS,OAAO,EAAE;IACvD,CAAC,GAAG,IAAI,EAAE,IAAI,GAAG,IAAI,CAAC;IACtB,4CAA4C;IAC5C,MAAM,EAAE,MAAM,IAAI,CAAC;IACnB,kDAAkD;IAClD,KAAK,EAAE,MAAM,IAAI,CAAC;CACnB;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,QAAQ,CAAC,IAAI,SAAS,OAAO,EAAE,EAC7C,EAAE,EAAE,CAAC,GAAG,IAAI,EAAE,IAAI,KAAK,IAAI,EAC3B,IAAI,EAAE,MAAM,GACX,iBAAiB,CAAC,IAAI,CAAC,CA+BzB"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Returns a debounced version of `fn` that only runs after `wait`
|
|
3
|
+
* milliseconds have elapsed without another call.
|
|
4
|
+
*
|
|
5
|
+
* @param fn - The function to debounce.
|
|
6
|
+
* @param wait - The delay in milliseconds.
|
|
7
|
+
* @returns The debounced function, with `cancel()` and `flush()` helpers.
|
|
8
|
+
* @example
|
|
9
|
+
* const onResize = debounce(() => recompute(), 200);
|
|
10
|
+
* window.addEventListener("resize", onResize);
|
|
11
|
+
*/
|
|
12
|
+
export function debounce(fn, wait) {
|
|
13
|
+
let timer;
|
|
14
|
+
let pendingArgs;
|
|
15
|
+
const debounced = ((...args) => {
|
|
16
|
+
pendingArgs = args;
|
|
17
|
+
if (timer !== undefined)
|
|
18
|
+
clearTimeout(timer);
|
|
19
|
+
timer = setTimeout(() => {
|
|
20
|
+
timer = undefined;
|
|
21
|
+
const callArgs = pendingArgs;
|
|
22
|
+
pendingArgs = undefined;
|
|
23
|
+
if (callArgs)
|
|
24
|
+
fn(...callArgs);
|
|
25
|
+
}, wait);
|
|
26
|
+
});
|
|
27
|
+
debounced.cancel = () => {
|
|
28
|
+
if (timer !== undefined)
|
|
29
|
+
clearTimeout(timer);
|
|
30
|
+
timer = undefined;
|
|
31
|
+
pendingArgs = undefined;
|
|
32
|
+
};
|
|
33
|
+
debounced.flush = () => {
|
|
34
|
+
if (timer === undefined)
|
|
35
|
+
return;
|
|
36
|
+
clearTimeout(timer);
|
|
37
|
+
timer = undefined;
|
|
38
|
+
const callArgs = pendingArgs;
|
|
39
|
+
pendingArgs = undefined;
|
|
40
|
+
if (callArgs)
|
|
41
|
+
fn(...callArgs);
|
|
42
|
+
};
|
|
43
|
+
return debounced;
|
|
44
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/async/index.ts"],"names":[],"mappings":"AAAA,cAAc,eAAe,CAAC;AAC9B,cAAc,cAAc,CAAC;AAC7B,cAAc,YAAY,CAAC;AAC3B,cAAc,YAAY,CAAC;AAC3B,cAAc,eAAe,CAAC;AAC9B,cAAc,kBAAkB,CAAC"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export interface MemoizedFunction<Args extends unknown[], R> {
|
|
2
|
+
(...args: Args): R;
|
|
3
|
+
/** The underlying cache, exposed for inspection or manual clearing. */
|
|
4
|
+
cache: Map<string, R>;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Caches the result of `fn` per argument tuple (JSON-stringified by
|
|
8
|
+
* default).
|
|
9
|
+
*
|
|
10
|
+
* @param fn - The function to memoize.
|
|
11
|
+
* @param getKey - Computes the cache key for a set of arguments (defaults to `JSON.stringify`).
|
|
12
|
+
* @returns A memoized version of `fn`, with a `cache` map exposed.
|
|
13
|
+
* @example
|
|
14
|
+
* const square = memoize((n: number) => expensiveSquare(n));
|
|
15
|
+
* square(4); // computed
|
|
16
|
+
* square(4); // cached
|
|
17
|
+
*/
|
|
18
|
+
export declare function memoize<Args extends unknown[], R>(fn: (...args: Args) => R, getKey?: (...args: Args) => string): MemoizedFunction<Args, R>;
|
|
19
|
+
//# sourceMappingURL=memoize.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"memoize.d.ts","sourceRoot":"","sources":["../../src/async/memoize.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,gBAAgB,CAAC,IAAI,SAAS,OAAO,EAAE,EAAE,CAAC;IACzD,CAAC,GAAG,IAAI,EAAE,IAAI,GAAG,CAAC,CAAC;IACnB,uEAAuE;IACvE,KAAK,EAAE,GAAG,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC;CACvB;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,OAAO,CAAC,IAAI,SAAS,OAAO,EAAE,EAAE,CAAC,EAC/C,EAAE,EAAE,CAAC,GAAG,IAAI,EAAE,IAAI,KAAK,CAAC,EACxB,MAAM,GAAE,CAAC,GAAG,IAAI,EAAE,IAAI,KAAK,MAA0C,GACpE,gBAAgB,CAAC,IAAI,EAAE,CAAC,CAAC,CAa3B"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Caches the result of `fn` per argument tuple (JSON-stringified by
|
|
3
|
+
* default).
|
|
4
|
+
*
|
|
5
|
+
* @param fn - The function to memoize.
|
|
6
|
+
* @param getKey - Computes the cache key for a set of arguments (defaults to `JSON.stringify`).
|
|
7
|
+
* @returns A memoized version of `fn`, with a `cache` map exposed.
|
|
8
|
+
* @example
|
|
9
|
+
* const square = memoize((n: number) => expensiveSquare(n));
|
|
10
|
+
* square(4); // computed
|
|
11
|
+
* square(4); // cached
|
|
12
|
+
*/
|
|
13
|
+
export function memoize(fn, getKey = (...args) => JSON.stringify(args)) {
|
|
14
|
+
const cache = new Map();
|
|
15
|
+
const memoized = ((...args) => {
|
|
16
|
+
const key = getKey(...args);
|
|
17
|
+
if (cache.has(key))
|
|
18
|
+
return cache.get(key);
|
|
19
|
+
const result = fn(...args);
|
|
20
|
+
cache.set(key, result);
|
|
21
|
+
return result;
|
|
22
|
+
});
|
|
23
|
+
memoized.cache = cache;
|
|
24
|
+
return memoized;
|
|
25
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export interface RetryOptions {
|
|
2
|
+
/** Maximum number of attempts, including the first one (default: 3). */
|
|
3
|
+
attempts?: number;
|
|
4
|
+
/** Base delay in milliseconds before retrying (default: 200). */
|
|
5
|
+
delayMs?: number;
|
|
6
|
+
/** Backoff multiplier applied to `delayMs` after each failed attempt (default: 2). */
|
|
7
|
+
backoffFactor?: number;
|
|
8
|
+
/** Called with the error and the 1-based attempt number after each failure. */
|
|
9
|
+
onRetry?: (error: unknown, attempt: number) => void;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Retries an async operation with exponential backoff.
|
|
13
|
+
*
|
|
14
|
+
* @param fn - The operation to retry. Receives the 1-based attempt number.
|
|
15
|
+
* @param options - Retry configuration.
|
|
16
|
+
* @returns The result of the first successful attempt.
|
|
17
|
+
* @throws The last error, if every attempt failed.
|
|
18
|
+
* @example
|
|
19
|
+
* const data = await retry(() => fetch("/api").then((r) => r.json()), {
|
|
20
|
+
* attempts: 5,
|
|
21
|
+
* delayMs: 300,
|
|
22
|
+
* });
|
|
23
|
+
*/
|
|
24
|
+
export declare function retry<T>(fn: (attempt: number) => Promise<T>, options?: RetryOptions): Promise<T>;
|
|
25
|
+
//# sourceMappingURL=retry.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"retry.d.ts","sourceRoot":"","sources":["../../src/async/retry.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,YAAY;IAC3B,wEAAwE;IACxE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,iEAAiE;IACjE,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,sFAAsF;IACtF,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,+EAA+E;IAC/E,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;CACrD;AAED;;;;;;;;;;;;GAYG;AACH,wBAAsB,KAAK,CAAC,CAAC,EAC3B,EAAE,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC,CAAC,CAAC,EACnC,OAAO,GAAE,YAAiB,GACzB,OAAO,CAAC,CAAC,CAAC,CAoBZ"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { sleep } from "./sleep.js";
|
|
2
|
+
/**
|
|
3
|
+
* Retries an async operation with exponential backoff.
|
|
4
|
+
*
|
|
5
|
+
* @param fn - The operation to retry. Receives the 1-based attempt number.
|
|
6
|
+
* @param options - Retry configuration.
|
|
7
|
+
* @returns The result of the first successful attempt.
|
|
8
|
+
* @throws The last error, if every attempt failed.
|
|
9
|
+
* @example
|
|
10
|
+
* const data = await retry(() => fetch("/api").then((r) => r.json()), {
|
|
11
|
+
* attempts: 5,
|
|
12
|
+
* delayMs: 300,
|
|
13
|
+
* });
|
|
14
|
+
*/
|
|
15
|
+
export async function retry(fn, options = {}) {
|
|
16
|
+
const { attempts = 3, delayMs = 200, backoffFactor = 2, onRetry } = options;
|
|
17
|
+
let lastError;
|
|
18
|
+
let currentDelay = delayMs;
|
|
19
|
+
for (let attempt = 1; attempt <= attempts; attempt++) {
|
|
20
|
+
try {
|
|
21
|
+
return await fn(attempt);
|
|
22
|
+
}
|
|
23
|
+
catch (error) {
|
|
24
|
+
lastError = error;
|
|
25
|
+
onRetry?.(error, attempt);
|
|
26
|
+
if (attempt < attempts) {
|
|
27
|
+
await sleep(currentDelay);
|
|
28
|
+
currentDelay *= backoffFactor;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
throw lastError;
|
|
33
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sleep.d.ts","sourceRoot":"","sources":["../../src/async/sleep.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,wBAAgB,KAAK,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAE/C"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export interface ThrottledFunction<Args extends unknown[]> {
|
|
2
|
+
(...args: Args): void;
|
|
3
|
+
/** Cancels any pending trailing invocation. */
|
|
4
|
+
cancel: () => void;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Returns a throttled version of `fn` that runs at most once every `wait`
|
|
8
|
+
* milliseconds: immediately on the first call (leading edge), then once more
|
|
9
|
+
* at the end of the window if further calls came in during it (trailing
|
|
10
|
+
* edge).
|
|
11
|
+
*
|
|
12
|
+
* @param fn - The function to throttle.
|
|
13
|
+
* @param wait - The minimum delay in milliseconds between calls.
|
|
14
|
+
* @returns The throttled function, with a `cancel()` helper.
|
|
15
|
+
* @example
|
|
16
|
+
* const onScroll = throttle(() => updatePosition(), 100);
|
|
17
|
+
* window.addEventListener("scroll", onScroll);
|
|
18
|
+
*/
|
|
19
|
+
export declare function throttle<Args extends unknown[]>(fn: (...args: Args) => void, wait: number): ThrottledFunction<Args>;
|
|
20
|
+
//# sourceMappingURL=throttle.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"throttle.d.ts","sourceRoot":"","sources":["../../src/async/throttle.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,iBAAiB,CAAC,IAAI,SAAS,OAAO,EAAE;IACvD,CAAC,GAAG,IAAI,EAAE,IAAI,GAAG,IAAI,CAAC;IACtB,+CAA+C;IAC/C,MAAM,EAAE,MAAM,IAAI,CAAC;CACpB;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,QAAQ,CAAC,IAAI,SAAS,OAAO,EAAE,EAC7C,EAAE,EAAE,CAAC,GAAG,IAAI,EAAE,IAAI,KAAK,IAAI,EAC3B,IAAI,EAAE,MAAM,GACX,iBAAiB,CAAC,IAAI,CAAC,CAkCzB"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Returns a throttled version of `fn` that runs at most once every `wait`
|
|
3
|
+
* milliseconds: immediately on the first call (leading edge), then once more
|
|
4
|
+
* at the end of the window if further calls came in during it (trailing
|
|
5
|
+
* edge).
|
|
6
|
+
*
|
|
7
|
+
* @param fn - The function to throttle.
|
|
8
|
+
* @param wait - The minimum delay in milliseconds between calls.
|
|
9
|
+
* @returns The throttled function, with a `cancel()` helper.
|
|
10
|
+
* @example
|
|
11
|
+
* const onScroll = throttle(() => updatePosition(), 100);
|
|
12
|
+
* window.addEventListener("scroll", onScroll);
|
|
13
|
+
*/
|
|
14
|
+
export function throttle(fn, wait) {
|
|
15
|
+
let lastCallTime;
|
|
16
|
+
let timer;
|
|
17
|
+
let pendingArgs;
|
|
18
|
+
const throttled = ((...args) => {
|
|
19
|
+
const now = Date.now();
|
|
20
|
+
if (lastCallTime === undefined || now - lastCallTime >= wait) {
|
|
21
|
+
lastCallTime = now;
|
|
22
|
+
fn(...args);
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
pendingArgs = args;
|
|
26
|
+
timer ??= setTimeout(() => {
|
|
27
|
+
lastCallTime = Date.now();
|
|
28
|
+
timer = undefined;
|
|
29
|
+
const callArgs = pendingArgs;
|
|
30
|
+
pendingArgs = undefined;
|
|
31
|
+
if (callArgs)
|
|
32
|
+
fn(...callArgs);
|
|
33
|
+
}, wait - (now - lastCallTime));
|
|
34
|
+
});
|
|
35
|
+
throttled.cancel = () => {
|
|
36
|
+
if (timer !== undefined)
|
|
37
|
+
clearTimeout(timer);
|
|
38
|
+
timer = undefined;
|
|
39
|
+
pendingArgs = undefined;
|
|
40
|
+
lastCallTime = undefined;
|
|
41
|
+
};
|
|
42
|
+
return throttled;
|
|
43
|
+
}
|