@forthtilliath/ts-kit 0.10.0 → 0.12.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 +100 -22
- package/dist/array/flattenDeep.d.ts +7 -5
- package/dist/array/flattenDeep.d.ts.map +1 -1
- package/dist/array/flattenDeep.js +8 -5
- package/dist/array/index.d.ts +1 -0
- package/dist/array/index.d.ts.map +1 -1
- package/dist/array/index.js +1 -0
- package/dist/array/shuffle.d.ts +14 -0
- package/dist/array/shuffle.d.ts.map +1 -0
- package/dist/array/shuffle.js +20 -0
- package/dist/async/abortReason.d.ts +9 -0
- package/dist/async/abortReason.d.ts.map +1 -0
- package/dist/async/abortReason.js +11 -0
- package/dist/async/memoize.d.ts +2 -18
- package/dist/async/memoize.d.ts.map +1 -1
- package/dist/async/memoize.js +4 -25
- package/dist/async/retry.d.ts +15 -3
- package/dist/async/retry.d.ts.map +1 -1
- package/dist/async/retry.js +15 -11
- package/dist/async/sleep.d.ts +2 -1
- package/dist/async/sleep.d.ts.map +1 -1
- package/dist/async/sleep.js +23 -2
- package/dist/async/withTimeout.d.ts +11 -4
- package/dist/async/withTimeout.d.ts.map +1 -1
- package/dist/async/withTimeout.js +31 -9
- package/dist/csv/escapeCsvField.d.ts +21 -0
- package/dist/csv/escapeCsvField.d.ts.map +1 -0
- package/dist/csv/escapeCsvField.js +28 -0
- package/dist/csv/formatCsvNumber.d.ts +14 -0
- package/dist/csv/formatCsvNumber.d.ts.map +1 -0
- package/dist/csv/formatCsvNumber.js +15 -0
- package/dist/csv/index.d.ts +2 -0
- package/dist/csv/index.d.ts.map +1 -1
- package/dist/csv/index.js +2 -0
- package/dist/date/dateInTimeZone.d.ts +22 -0
- package/dist/date/dateInTimeZone.d.ts.map +1 -0
- package/dist/date/dateInTimeZone.js +35 -0
- package/dist/date/daysUntil.d.ts +15 -0
- package/dist/date/daysUntil.d.ts.map +1 -0
- package/dist/date/daysUntil.js +22 -0
- package/dist/date/formatRecentDate.d.ts +22 -0
- package/dist/date/formatRecentDate.d.ts.map +1 -0
- package/dist/date/formatRecentDate.js +46 -0
- package/dist/date/index.d.ts +7 -0
- package/dist/date/index.d.ts.map +1 -1
- package/dist/date/index.js +7 -0
- package/dist/date/mondayFirstWeekday.d.ts +12 -0
- package/dist/date/mondayFirstWeekday.d.ts.map +1 -0
- package/dist/date/mondayFirstWeekday.js +15 -0
- package/dist/date/parseIsoDate.d.ts +12 -0
- package/dist/date/parseIsoDate.d.ts.map +1 -0
- package/dist/date/parseIsoDate.js +13 -0
- package/dist/date/parseSqliteDate.d.ts +13 -0
- package/dist/date/parseSqliteDate.d.ts.map +1 -0
- package/dist/date/parseSqliteDate.js +17 -0
- package/dist/date/todayInTimeZone.d.ts +14 -0
- package/dist/date/todayInTimeZone.d.ts.map +1 -0
- package/dist/date/todayInTimeZone.js +15 -0
- package/dist/files/downloadText.d.ts.map +1 -1
- package/dist/files/downloadText.js +9 -1
- package/dist/files/formatFileTimestamp.d.ts +22 -0
- package/dist/files/formatFileTimestamp.d.ts.map +1 -0
- package/dist/files/formatFileTimestamp.js +20 -0
- package/dist/files/index.d.ts +1 -0
- package/dist/files/index.d.ts.map +1 -1
- package/dist/files/index.js +1 -0
- package/dist/function/index.d.ts +2 -0
- package/dist/function/index.d.ts.map +1 -0
- package/dist/function/index.js +1 -0
- package/dist/function/memoize.d.ts +34 -0
- package/dist/function/memoize.d.ts.map +1 -0
- package/dist/function/memoize.js +50 -0
- package/dist/ics/escapeIcsText.d.ts +12 -0
- package/dist/ics/escapeIcsText.d.ts.map +1 -0
- package/dist/ics/escapeIcsText.js +17 -0
- package/dist/ics/foldIcsLine.d.ts +15 -0
- package/dist/ics/foldIcsLine.d.ts.map +1 -0
- package/dist/ics/foldIcsLine.js +40 -0
- package/dist/ics/icsTimestamp.d.ts +10 -0
- package/dist/ics/icsTimestamp.d.ts.map +1 -0
- package/dist/ics/icsTimestamp.js +11 -0
- package/dist/ics/index.d.ts +6 -0
- package/dist/ics/index.d.ts.map +1 -0
- package/dist/ics/index.js +5 -0
- package/dist/ics/toIcsDate.d.ts +11 -0
- package/dist/ics/toIcsDate.d.ts.map +1 -0
- package/dist/ics/toIcsDate.js +12 -0
- package/dist/ics/toIcsDateTime.d.ts +13 -0
- package/dist/ics/toIcsDateTime.d.ts.map +1 -0
- package/dist/ics/toIcsDateTime.js +16 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/maths/average.d.ts +2 -7
- package/dist/maths/average.d.ts.map +1 -1
- package/dist/maths/average.js +4 -10
- package/dist/maths/sum.d.ts +2 -7
- package/dist/maths/sum.d.ts.map +1 -1
- package/dist/maths/sum.js +4 -9
- package/dist/number/average.d.ts +13 -0
- package/dist/number/average.d.ts.map +1 -0
- package/dist/number/average.js +15 -0
- package/dist/number/formatBytes.d.ts.map +1 -1
- package/dist/number/formatBytes.js +6 -2
- package/dist/number/formatCsvNumber.d.ts +2 -13
- package/dist/number/formatCsvNumber.d.ts.map +1 -1
- package/dist/number/formatCsvNumber.js +4 -15
- package/dist/number/index.d.ts +2 -0
- package/dist/number/index.d.ts.map +1 -1
- package/dist/number/index.js +2 -0
- package/dist/number/sum.d.ts +10 -0
- package/dist/number/sum.d.ts.map +1 -0
- package/dist/number/sum.js +11 -0
- package/dist/object/deepEqual.d.ts +6 -0
- package/dist/object/deepEqual.d.ts.map +1 -1
- package/dist/object/deepEqual.js +45 -11
- package/dist/string/escapeCsvField.d.ts +2 -13
- package/dist/string/escapeCsvField.d.ts.map +1 -1
- package/dist/string/escapeCsvField.js +4 -17
- package/dist/string/escapeHtml.d.ts +6 -4
- package/dist/string/escapeHtml.d.ts.map +1 -1
- package/dist/string/escapeHtml.js +8 -5
- package/dist/string/pluralize.d.ts +30 -7
- package/dist/string/pluralize.d.ts.map +1 -1
- package/dist/string/pluralize.js +48 -15
- package/dist/version/compareVersions.d.ts +8 -1
- package/dist/version/compareVersions.d.ts.map +1 -1
- package/dist/version/compareVersions.js +63 -9
- package/package.json +7 -6
package/README.md
CHANGED
|
@@ -33,6 +33,18 @@ to keep a bundle's included code obvious at a glance):
|
|
|
33
33
|
import { chunk } from "@forthtilliath/ts-kit/array/chunk";
|
|
34
34
|
```
|
|
35
35
|
|
|
36
|
+
### Browser-only functions
|
|
37
|
+
|
|
38
|
+
Everything runs in any JavaScript runtime (browser, Node, React Native,
|
|
39
|
+
Angular SSR…) **except** these, which need a DOM (`document`, `Image`,
|
|
40
|
+
`URL.createObjectURL`) and throw elsewhere — only call them client-side:
|
|
41
|
+
|
|
42
|
+
- `csv/downloadCsv`
|
|
43
|
+
- `files/downloadText`, `files/downloadTextBlob`
|
|
44
|
+
- `image/compressImage`
|
|
45
|
+
|
|
46
|
+
Importing them is harmless anywhere: the DOM is only touched when called.
|
|
47
|
+
|
|
36
48
|
### `array`
|
|
37
49
|
|
|
38
50
|
- `chunk(array, size)` — splits an array into chunks of a given size.
|
|
@@ -40,7 +52,8 @@ import { chunk } from "@forthtilliath/ts-kit/array/chunk";
|
|
|
40
52
|
persist a user-customized column list (order + visibility, e.g. in
|
|
41
53
|
`localStorage`). Only `key`/`visible` are stored; on read, unknown keys are
|
|
42
54
|
dropped, new columns appended, and invalid input falls back to `defaults`.
|
|
43
|
-
- `flattenDeep(array)` — recursively flattens nested arrays
|
|
55
|
+
- `flattenDeep(array)` — recursively flattens nested arrays, typed as the
|
|
56
|
+
innermost element type (`number[][][]` → `number[]`).
|
|
44
57
|
- `getMostRecentIds(rows, limit?)` — most recently occurring distinct ids
|
|
45
58
|
(`{ id, occurredAt }[]`), most recent first, deduplicated, limited
|
|
46
59
|
(default 5).
|
|
@@ -50,6 +63,8 @@ import { chunk } from "@forthtilliath/ts-kit/array/chunk";
|
|
|
50
63
|
- `rankByNameMatch(items, query, getName)` — ranks `items` by relevance to
|
|
51
64
|
`query`: earlier match position first, then shorter name — for a
|
|
52
65
|
search-as-you-type list.
|
|
66
|
+
- `shuffle(array, random?)` — shuffled copy (Fisher–Yates); `random`
|
|
67
|
+
(default `Math.random`) can be swapped for a seeded generator in tests.
|
|
53
68
|
- `sortUpcomingFirst(items, getDates, { now? })` — upcoming items first
|
|
54
69
|
(nearest first), then past ones (most recent first), undated last — for a
|
|
55
70
|
list of events. An item may carry several dates: it stays upcoming while
|
|
@@ -60,16 +75,23 @@ import { chunk } from "@forthtilliath/ts-kit/array/chunk";
|
|
|
60
75
|
|
|
61
76
|
### `async`
|
|
62
77
|
|
|
63
|
-
- `sleep(ms)` — resolves after a delay
|
|
78
|
+
- `sleep(ms, signal?)` — resolves after a delay; rejects early if `signal`
|
|
79
|
+
aborts.
|
|
64
80
|
- `retry(fn, options)` — retries an async operation with exponential backoff.
|
|
81
|
+
Options: `attempts` (3), `delayMs` (200), `backoffFactor` (2),
|
|
82
|
+
`maxDelayMs`, `shouldRetry(error, attempt)` (return `false` to rethrow at
|
|
83
|
+
once, e.g. on a 4xx), `onRetry(error, attempt)` (called before each actual
|
|
84
|
+
retry), `signal`.
|
|
65
85
|
- `debounce(fn, wait)` — delays `fn` until calls stop for `wait` ms; exposes
|
|
66
86
|
`cancel()` / `flush()`.
|
|
67
87
|
- `throttle(fn, wait)` — runs `fn` at most once per `wait` ms (leading +
|
|
68
88
|
trailing edge); exposes `cancel()`.
|
|
69
|
-
- `
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
`
|
|
89
|
+
- `withTimeout(operation, ms, message?)` — rejects with a `TimeoutError` if
|
|
90
|
+
`operation` doesn't settle in time. Pass a function `(signal) => promise`
|
|
91
|
+
instead of a promise to also **cancel** the operation on timeout (e.g.
|
|
92
|
+
`withTimeout((signal) => fetch(url, { signal }), 5000)`).
|
|
93
|
+
- `memoize` — moved to [`function`](#function); still importable from
|
|
94
|
+
`async/memoize` (deprecated).
|
|
73
95
|
|
|
74
96
|
### `classes`
|
|
75
97
|
|
|
@@ -97,30 +119,82 @@ import { chunk } from "@forthtilliath/ts-kit/array/chunk";
|
|
|
97
119
|
- `parseCsvLine(line, delimiter?)` — splits one CSV line into trimmed cells;
|
|
98
120
|
quoted cells may contain the delimiter, `""` stands for a literal quote.
|
|
99
121
|
Delimiter defaults to `","`.
|
|
100
|
-
- `toCsv(rows, delimiter?)` — serializes rows to CSV, every cell quoted
|
|
101
|
-
escaped, rows joined with `\n`.
|
|
122
|
+
- `toCsv(rows, delimiter?)` — serializes whole rows to CSV, every cell quoted
|
|
123
|
+
and escaped, rows joined with `\n`. Delimiter defaults to `","`.
|
|
124
|
+
- `escapeCsvField(value, delimiter?)` — escapes a single field (RFC 4180),
|
|
125
|
+
quoting it only if it contains a `"`, the delimiter or a line break — to
|
|
126
|
+
build rows cell by cell. Delimiter defaults to `";"` (French-locale Excel).
|
|
127
|
+
- `formatCsvNumber(value, decimals?)` — formats a number with a comma decimal
|
|
128
|
+
separator (French-locale spreadsheets) instead of JS's dot.
|
|
102
129
|
- `downloadCsv(filename, content)` — browser download as `text/csv`, with a
|
|
103
130
|
UTF-8 BOM so Excel reads accented characters.
|
|
104
131
|
|
|
132
|
+
`escapeCsvField` and `formatCsvNumber` used to live in `string/` and
|
|
133
|
+
`number/`; those deep imports still work (deprecated).
|
|
134
|
+
|
|
105
135
|
### `date`
|
|
106
136
|
|
|
107
137
|
- `startOfDay(date?)` — returns a new `Date` at local midnight.
|
|
108
138
|
- `isSameDay(a, b)` — checks whether two dates fall on the same local day.
|
|
139
|
+
- `parseIsoDate(iso)` — a `YYYY-MM-DD` calendar date as **local** midnight
|
|
140
|
+
(`new Date("2026-03-14")` is UTC midnight, the previous day in the Americas).
|
|
141
|
+
- `parseSqliteDate(value)` — reads a SQLite `current_timestamp` value
|
|
142
|
+
(`"2026-10-09 19:46:00"`, UTC without a zone marker) as **UTC** instead of
|
|
143
|
+
local time; full ISO strings are read as they are.
|
|
144
|
+
- `mondayFirstWeekday(date)` — weekday index with Monday = `0` … Sunday = `6`,
|
|
145
|
+
from a `Date` or a `YYYY-MM-DD` string; offsets a Monday-first month grid.
|
|
109
146
|
- `formatRelativeTime(date, baseDate?, locale?)` — human-readable relative
|
|
110
147
|
time ("3 hours ago"), backed by `Intl.RelativeTimeFormat`.
|
|
148
|
+
- `formatRecentDate(date, { now?, locale? })` — a past moment as a "last
|
|
149
|
+
saved…" line reads best: `"aujourd'hui à 21:42"`, `"hier à 08:05"`, then
|
|
150
|
+
`"le 11 octobre 2026"` (`locale: "en"`: `"today at…"`, `"on October 11, 2026"`).
|
|
111
151
|
- `getPeriodStartMs(period, now?)` — start timestamp (ms) for a
|
|
112
152
|
`"today" | "7d" | "30d" | "all"` period filter, or `null` for `"all"`.
|
|
113
153
|
`"today"` is the current calendar day, not a rolling 24h window.
|
|
154
|
+
- `dateInTimeZone(date, timeZone)` — the calendar date of an instant in an
|
|
155
|
+
IANA time zone, `{ year, month, day, iso }` (`month` from 1 to 12), whatever
|
|
156
|
+
the machine's own time zone.
|
|
157
|
+
- `todayInTimeZone(timeZone, now?)` — today's date in that time zone: on a
|
|
158
|
+
server running in UTC, `new Date()` is still "yesterday" in Paris just
|
|
159
|
+
after midnight.
|
|
160
|
+
- `daysUntil(date, timeZone, now?)` — calendar days between today and a date,
|
|
161
|
+
both taken in that time zone (`0` today, `1` tomorrow, `-1` yesterday);
|
|
162
|
+
unaffected by the time of day or daylight saving time.
|
|
114
163
|
|
|
115
164
|
### `files`
|
|
116
165
|
|
|
117
166
|
- `downloadText(filename, text)` — triggers a browser download of a text file
|
|
118
167
|
via a `data:` URI.
|
|
119
168
|
- `downloadTextBlob(filename, text)` — same, but via a `Blob` + object URL
|
|
120
|
-
(better for larger content).
|
|
169
|
+
(better for larger content). The object URL is revoked 40 s later, not
|
|
170
|
+
right after the click (which can cancel the download in Firefox/Safari).
|
|
171
|
+
- `formatFileTimestamp(date?, { seconds?, separator? })` — compact **local**
|
|
172
|
+
timestamp for file names, `"20261008-0902"` by default (`toISOString()` is
|
|
173
|
+
UTC and its `:` is rejected by Windows); names sort chronologically.
|
|
174
|
+
|
|
175
|
+
### `function`
|
|
176
|
+
|
|
177
|
+
- `memoize(fn, getKeyOrOptions?)` — caches `fn`'s results per argument tuple
|
|
178
|
+
(JSON-stringified by default); exposes the underlying `cache` map. Pass a
|
|
179
|
+
key function, or `{ getKey, maxSize }` to evict the least recently used
|
|
180
|
+
result beyond `maxSize`. A returned promise that rejects is evicted, so the
|
|
181
|
+
next call retries instead of failing forever.
|
|
121
182
|
- `sanitizeFileName(name)` — replaces characters forbidden in file names
|
|
122
183
|
(`\ / : * ? " < > |`) with hyphens, collapses whitespace and trims.
|
|
123
184
|
|
|
185
|
+
### `ics`
|
|
186
|
+
|
|
187
|
+
Building blocks for writing an iCalendar (`.ics`, RFC 5545) file.
|
|
188
|
+
|
|
189
|
+
- `escapeIcsText(text)` — escapes a TEXT value (`SUMMARY`, `DESCRIPTION`…):
|
|
190
|
+
`\`, `;`, `,` and line breaks.
|
|
191
|
+
- `foldIcsLine(line, maxOctets?)` — folds a content line past 75 **octets**
|
|
192
|
+
(UTF-8 bytes, not characters) without splitting a character.
|
|
193
|
+
- `toIcsDate(isoDate)` — `"2026-07-14"` → `"20260714"`, for all-day events.
|
|
194
|
+
- `toIcsDateTime(isoDate, "HH:MM")` — floating local date-time,
|
|
195
|
+
`"20260714T063000"`.
|
|
196
|
+
- `icsTimestamp(date?)` — UTC date-time for `DTSTAMP`, `"20260714T080509Z"`.
|
|
197
|
+
|
|
124
198
|
### `id`
|
|
125
199
|
|
|
126
200
|
- `randomId()` — `crypto.randomUUID()` with fallbacks for older/insecure
|
|
@@ -141,22 +215,21 @@ import { chunk } from "@forthtilliath/ts-kit/array/chunk";
|
|
|
141
215
|
`bold`-aware segments), ready for a UI layer to render without a full
|
|
142
216
|
Markdown dependency.
|
|
143
217
|
|
|
144
|
-
### `maths`
|
|
145
|
-
|
|
146
|
-
- `sum(numbers)` — sum of an array of numbers.
|
|
147
|
-
- `avg(numbers)` — average of an array of numbers.
|
|
148
|
-
|
|
149
218
|
### `number`
|
|
150
219
|
|
|
220
|
+
- `sum(numbers)` — sum of an array of numbers (`0` if empty).
|
|
221
|
+
- `avg(numbers)` — average of an array of numbers; `NaN` if empty (no
|
|
222
|
+
meaningful average of nothing, like lodash's `mean`).
|
|
151
223
|
- `clamp(value, min, max)` — clamps a number between two bounds.
|
|
152
224
|
- `round(value, decimals?)` — rounds to a given number of decimal places.
|
|
153
225
|
- `formatBytes(bytes, decimals?)` — human-readable byte size ("1.5 MB").
|
|
154
226
|
- `formatDuration(ms)` — human-readable duration ("1d 1h 1m 1s").
|
|
155
|
-
- `formatCsvNumber(value, decimals?)` — formats a number with a comma
|
|
156
|
-
decimal separator (French-locale spreadsheets) instead of JS's dot.
|
|
157
227
|
- `padNumber(n, total, minWidth?)` — zero-pads `n` to the width of `total`
|
|
158
228
|
(at least `minWidth`, default 2) so numbered file names sort correctly.
|
|
159
229
|
|
|
230
|
+
`sum` and `avg` used to live in `maths/`; those deep imports still work
|
|
231
|
+
(deprecated).
|
|
232
|
+
|
|
160
233
|
### `object`
|
|
161
234
|
|
|
162
235
|
- `pick(obj, keys)` / `omit(obj, keys)` — keep or drop a set of keys.
|
|
@@ -174,15 +247,20 @@ import { chunk } from "@forthtilliath/ts-kit/array/chunk";
|
|
|
174
247
|
suffix).
|
|
175
248
|
- `slugify(str)` — URL-friendly slug (accents stripped, lower-cased,
|
|
176
249
|
hyphenated).
|
|
177
|
-
- `pluralize(count, singular, plural?)` —
|
|
178
|
-
|
|
250
|
+
- `pluralize(count, singular, plural? | { plural?, locale? })` — picks the
|
|
251
|
+
singular or the plural form. Which counts take the singular follows the
|
|
252
|
+
locale's rules (`Intl.PluralRules`, `"en"` by default): only ±1 in
|
|
253
|
+
English, 0 and 1.5 too in French. Without an explicit plural form, a naive
|
|
254
|
+
suffix: English `s`/`es`/`ies`, French `s` (nothing after `s`/`x`/`z`),
|
|
255
|
+
`s` elsewhere.
|
|
256
|
+
- `createPluralize(locale)` — `pluralize` bound to a locale, declared once per
|
|
257
|
+
app: `const plural = createPluralize("fr"); plural(n, "est", "sont")`.
|
|
179
258
|
- `normalizeForSearch(text)` — lowercases, trims, strips accents, and expands
|
|
180
259
|
œ/æ ligatures (which `normalize("NFD")` alone doesn't decompose) — for
|
|
181
260
|
accent/case-insensitive search matching.
|
|
182
|
-
- `
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
inserting user text into an HTML template.
|
|
261
|
+
- `escapeHtml(text)` — HTML entity escaping (`&`, `<`, `>`, `"`, `'`) for
|
|
262
|
+
inserting user text into an HTML template, in text content or in an
|
|
263
|
+
attribute quoted with `"` or `'`.
|
|
186
264
|
|
|
187
265
|
### `version`
|
|
188
266
|
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Recursively flattens a nested array.
|
|
2
|
+
* Recursively flattens a nested array, at any depth.
|
|
3
3
|
*
|
|
4
|
-
* @
|
|
5
|
-
* @
|
|
6
|
-
*
|
|
4
|
+
* @param array - The array to flatten (left untouched).
|
|
5
|
+
* @returns A new array with every nested element at the top level, typed as
|
|
6
|
+
* the innermost element type (`number[][][]` → `number[]`).
|
|
7
|
+
* @example
|
|
8
|
+
* flattenDeep([1, [2, [3, [4]]]]); // => [1, 2, 3, 4]
|
|
7
9
|
*/
|
|
8
|
-
export declare function flattenDeep<T>(array: T
|
|
10
|
+
export declare function flattenDeep<T extends readonly unknown[]>(array: T): FlatArray<T, 20>[];
|
|
9
11
|
//# sourceMappingURL=flattenDeep.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"flattenDeep.d.ts","sourceRoot":"","sources":["../../src/array/flattenDeep.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"flattenDeep.d.ts","sourceRoot":"","sources":["../../src/array/flattenDeep.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,wBAAgB,WAAW,CAAC,CAAC,SAAS,SAAS,OAAO,EAAE,EACtD,KAAK,EAAE,CAAC,GACP,SAAS,CAAC,CAAC,EAAE,EAAE,CAAC,EAAE,CAGpB"}
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Recursively flattens a nested array.
|
|
2
|
+
* Recursively flattens a nested array, at any depth.
|
|
3
3
|
*
|
|
4
|
-
* @
|
|
5
|
-
* @
|
|
6
|
-
*
|
|
4
|
+
* @param array - The array to flatten (left untouched).
|
|
5
|
+
* @returns A new array with every nested element at the top level, typed as
|
|
6
|
+
* the innermost element type (`number[][][]` → `number[]`).
|
|
7
|
+
* @example
|
|
8
|
+
* flattenDeep([1, [2, [3, [4]]]]); // => [1, 2, 3, 4]
|
|
7
9
|
*/
|
|
8
10
|
export function flattenDeep(array) {
|
|
9
|
-
|
|
11
|
+
// 20 is the deepest level the built-in `FlatArray` type can unwind.
|
|
12
|
+
return array.flat(Infinity);
|
|
10
13
|
}
|
package/dist/array/index.d.ts
CHANGED
|
@@ -4,6 +4,7 @@ export * from "./flattenDeep.js";
|
|
|
4
4
|
export * from "./getMostRecentIds.js";
|
|
5
5
|
export * from "./nextInCycle.js";
|
|
6
6
|
export * from "./rankByNameMatch.js";
|
|
7
|
+
export * from "./shuffle.js";
|
|
7
8
|
export * from "./sortUpcomingFirst.js";
|
|
8
9
|
export * from "./swapItems.js";
|
|
9
10
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/array/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAC;AAC3B,cAAc,0BAA0B,CAAC;AACzC,cAAc,kBAAkB,CAAC;AACjC,cAAc,uBAAuB,CAAC;AACtC,cAAc,kBAAkB,CAAC;AACjC,cAAc,sBAAsB,CAAC;AACrC,cAAc,wBAAwB,CAAC;AACvC,cAAc,gBAAgB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/array/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAC;AAC3B,cAAc,0BAA0B,CAAC;AACzC,cAAc,kBAAkB,CAAC;AACjC,cAAc,uBAAuB,CAAC;AACtC,cAAc,kBAAkB,CAAC;AACjC,cAAc,sBAAsB,CAAC;AACrC,cAAc,cAAc,CAAC;AAC7B,cAAc,wBAAwB,CAAC;AACvC,cAAc,gBAAgB,CAAC"}
|
package/dist/array/index.js
CHANGED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Returns a shuffled copy of an array (Fisher–Yates), leaving the original
|
|
3
|
+
* untouched.
|
|
4
|
+
*
|
|
5
|
+
* @param array - The array to shuffle.
|
|
6
|
+
* @param random - Returns a number in `[0, 1)`. Defaults to `Math.random`;
|
|
7
|
+
* pass a seeded generator for deterministic tests.
|
|
8
|
+
* @returns A new array holding the same elements in random order.
|
|
9
|
+
* @example
|
|
10
|
+
* shuffle([1, 2, 3, 4]); // => e.g. [3, 1, 4, 2]
|
|
11
|
+
* shuffle(["a", "b", "c"], () => 0); // => ["b", "c", "a"]
|
|
12
|
+
*/
|
|
13
|
+
export declare function shuffle<T>(array: readonly T[], random?: () => number): T[];
|
|
14
|
+
//# sourceMappingURL=shuffle.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"shuffle.d.ts","sourceRoot":"","sources":["../../src/array/shuffle.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,wBAAgB,OAAO,CAAC,CAAC,EACvB,KAAK,EAAE,SAAS,CAAC,EAAE,EACnB,MAAM,GAAE,MAAM,MAAoB,GACjC,CAAC,EAAE,CAOL"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Returns a shuffled copy of an array (Fisher–Yates), leaving the original
|
|
3
|
+
* untouched.
|
|
4
|
+
*
|
|
5
|
+
* @param array - The array to shuffle.
|
|
6
|
+
* @param random - Returns a number in `[0, 1)`. Defaults to `Math.random`;
|
|
7
|
+
* pass a seeded generator for deterministic tests.
|
|
8
|
+
* @returns A new array holding the same elements in random order.
|
|
9
|
+
* @example
|
|
10
|
+
* shuffle([1, 2, 3, 4]); // => e.g. [3, 1, 4, 2]
|
|
11
|
+
* shuffle(["a", "b", "c"], () => 0); // => ["b", "c", "a"]
|
|
12
|
+
*/
|
|
13
|
+
export function shuffle(array, random = Math.random) {
|
|
14
|
+
const result = [...array];
|
|
15
|
+
for (let i = result.length - 1; i > 0; i--) {
|
|
16
|
+
const j = Math.floor(random() * (i + 1));
|
|
17
|
+
[result[i], result[j]] = [result[j], result[i]];
|
|
18
|
+
}
|
|
19
|
+
return result;
|
|
20
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The error to reject with once `signal` is aborted: its `reason` (what was
|
|
3
|
+
* passed to `abort()`), or a standard `AbortError` when the runtime doesn't
|
|
4
|
+
* provide one (some React Native polyfills).
|
|
5
|
+
*
|
|
6
|
+
* Internal helper — not part of the package's public barrel.
|
|
7
|
+
*/
|
|
8
|
+
export declare function abortReason(signal: AbortSignal): unknown;
|
|
9
|
+
//# sourceMappingURL=abortReason.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"abortReason.d.ts","sourceRoot":"","sources":["../../src/async/abortReason.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,wBAAgB,WAAW,CAAC,MAAM,EAAE,WAAW,GAAG,OAAO,CAKxD"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The error to reject with once `signal` is aborted: its `reason` (what was
|
|
3
|
+
* passed to `abort()`), or a standard `AbortError` when the runtime doesn't
|
|
4
|
+
* provide one (some React Native polyfills).
|
|
5
|
+
*
|
|
6
|
+
* Internal helper — not part of the package's public barrel.
|
|
7
|
+
*/
|
|
8
|
+
export function abortReason(signal) {
|
|
9
|
+
return (signal.reason ??
|
|
10
|
+
new DOMException("This operation was aborted", "AbortError"));
|
|
11
|
+
}
|
package/dist/async/memoize.d.ts
CHANGED
|
@@ -1,19 +1,3 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
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>;
|
|
1
|
+
/** @deprecated Import from `@forthtilliath/ts-kit/function/memoize` instead. */
|
|
2
|
+
export { memoize, type MemoizedFunction, type MemoizeOptions, } from "../function/memoize.js";
|
|
19
3
|
//# sourceMappingURL=memoize.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"memoize.d.ts","sourceRoot":"","sources":["../../src/async/memoize.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"memoize.d.ts","sourceRoot":"","sources":["../../src/async/memoize.ts"],"names":[],"mappings":"AAGA,gFAAgF;AAChF,OAAO,EACL,OAAO,EACP,KAAK,gBAAgB,EACrB,KAAK,cAAc,GACpB,MAAM,wBAAwB,CAAC"}
|
package/dist/async/memoize.js
CHANGED
|
@@ -1,25 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
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
|
-
}
|
|
1
|
+
// `memoize` isn't async-specific: it moved to `function/`. Re-exported here so
|
|
2
|
+
// the `@forthtilliath/ts-kit/async/memoize` deep import keeps working.
|
|
3
|
+
/** @deprecated Import from `@forthtilliath/ts-kit/function/memoize` instead. */
|
|
4
|
+
export { memoize, } from "../function/memoize.js";
|
package/dist/async/retry.d.ts
CHANGED
|
@@ -5,8 +5,18 @@ export interface RetryOptions {
|
|
|
5
5
|
delayMs?: number;
|
|
6
6
|
/** Backoff multiplier applied to `delayMs` after each failed attempt (default: 2). */
|
|
7
7
|
backoffFactor?: number;
|
|
8
|
-
/**
|
|
8
|
+
/** Upper bound for a single delay between two attempts (default: none). */
|
|
9
|
+
maxDelayMs?: number;
|
|
10
|
+
/**
|
|
11
|
+
* Decides whether a failure is worth retrying — return `false` to rethrow
|
|
12
|
+
* it right away (e.g. a 4xx response that won't fix itself). Retries every
|
|
13
|
+
* error by default.
|
|
14
|
+
*/
|
|
15
|
+
shouldRetry?: (error: unknown, attempt: number) => boolean;
|
|
16
|
+
/** Called with the error and the 1-based attempt number before each retry. */
|
|
9
17
|
onRetry?: (error: unknown, attempt: number) => void;
|
|
18
|
+
/** Stops retrying (rejecting with the abort reason) once aborted. */
|
|
19
|
+
signal?: AbortSignal;
|
|
10
20
|
}
|
|
11
21
|
/**
|
|
12
22
|
* Retries an async operation with exponential backoff.
|
|
@@ -14,11 +24,13 @@ export interface RetryOptions {
|
|
|
14
24
|
* @param fn - The operation to retry. Receives the 1-based attempt number.
|
|
15
25
|
* @param options - Retry configuration.
|
|
16
26
|
* @returns The result of the first successful attempt.
|
|
17
|
-
* @throws The last error,
|
|
27
|
+
* @throws The last error once attempts are exhausted, the first error
|
|
28
|
+
* `shouldRetry` declines, or the abort reason once `signal` is aborted.
|
|
18
29
|
* @example
|
|
19
|
-
* const data = await retry(() =>
|
|
30
|
+
* const data = await retry(() => fetchJson("/api"), {
|
|
20
31
|
* attempts: 5,
|
|
21
32
|
* delayMs: 300,
|
|
33
|
+
* shouldRetry: (error) => !(error instanceof HttpError && error.status < 500),
|
|
22
34
|
* });
|
|
23
35
|
*/
|
|
24
36
|
export declare function retry<T>(fn: (attempt: number) => Promise<T>, options?: RetryOptions): Promise<T>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"retry.d.ts","sourceRoot":"","sources":["../../src/async/retry.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"retry.d.ts","sourceRoot":"","sources":["../../src/async/retry.ts"],"names":[],"mappings":"AAGA,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,2EAA2E;IAC3E,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;;OAIG;IACH,WAAW,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC;IAC3D,8EAA8E;IAC9E,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IACpD,qEAAqE;IACrE,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB;AAED;;;;;;;;;;;;;;GAcG;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,CA0BZ"}
|
package/dist/async/retry.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { abortReason } from "./abortReason.js";
|
|
1
2
|
import { sleep } from "./sleep.js";
|
|
2
3
|
/**
|
|
3
4
|
* Retries an async operation with exponential backoff.
|
|
@@ -5,29 +6,32 @@ import { sleep } from "./sleep.js";
|
|
|
5
6
|
* @param fn - The operation to retry. Receives the 1-based attempt number.
|
|
6
7
|
* @param options - Retry configuration.
|
|
7
8
|
* @returns The result of the first successful attempt.
|
|
8
|
-
* @throws The last error,
|
|
9
|
+
* @throws The last error once attempts are exhausted, the first error
|
|
10
|
+
* `shouldRetry` declines, or the abort reason once `signal` is aborted.
|
|
9
11
|
* @example
|
|
10
|
-
* const data = await retry(() =>
|
|
12
|
+
* const data = await retry(() => fetchJson("/api"), {
|
|
11
13
|
* attempts: 5,
|
|
12
14
|
* delayMs: 300,
|
|
15
|
+
* shouldRetry: (error) => !(error instanceof HttpError && error.status < 500),
|
|
13
16
|
* });
|
|
14
17
|
*/
|
|
15
18
|
export async function retry(fn, options = {}) {
|
|
16
|
-
const { attempts = 3, delayMs = 200, backoffFactor = 2, onRetry } = options;
|
|
17
|
-
|
|
19
|
+
const { attempts = 3, delayMs = 200, backoffFactor = 2, maxDelayMs = Infinity, shouldRetry, onRetry, signal, } = options;
|
|
20
|
+
const maxAttempts = Math.max(1, attempts);
|
|
18
21
|
let currentDelay = delayMs;
|
|
19
|
-
for (let attempt = 1
|
|
22
|
+
for (let attempt = 1;; attempt++) {
|
|
23
|
+
if (signal?.aborted)
|
|
24
|
+
throw abortReason(signal);
|
|
20
25
|
try {
|
|
21
26
|
return await fn(attempt);
|
|
22
27
|
}
|
|
23
28
|
catch (error) {
|
|
24
|
-
|
|
29
|
+
const canRetry = attempt < maxAttempts && (shouldRetry?.(error, attempt) ?? true);
|
|
30
|
+
if (!canRetry)
|
|
31
|
+
throw error;
|
|
25
32
|
onRetry?.(error, attempt);
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
currentDelay *= backoffFactor;
|
|
29
|
-
}
|
|
33
|
+
await sleep(Math.min(currentDelay, maxDelayMs), signal);
|
|
34
|
+
currentDelay *= backoffFactor;
|
|
30
35
|
}
|
|
31
36
|
}
|
|
32
|
-
throw lastError;
|
|
33
37
|
}
|
package/dist/async/sleep.d.ts
CHANGED
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
* Resolves after the given number of milliseconds.
|
|
3
3
|
*
|
|
4
4
|
* @param ms - The delay in milliseconds.
|
|
5
|
+
* @param signal - Rejects early (with the abort reason) when aborted.
|
|
5
6
|
* @example
|
|
6
7
|
* await sleep(1000); // waits 1 second
|
|
7
8
|
*/
|
|
8
|
-
export declare function sleep(ms: number): Promise<void>;
|
|
9
|
+
export declare function sleep(ms: number, signal?: AbortSignal): Promise<void>;
|
|
9
10
|
//# sourceMappingURL=sleep.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sleep.d.ts","sourceRoot":"","sources":["../../src/async/sleep.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"sleep.d.ts","sourceRoot":"","sources":["../../src/async/sleep.ts"],"names":[],"mappings":"AAEA;;;;;;;GAOG;AAIH,wBAAgB,KAAK,CAAC,EAAE,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAiBrE"}
|
package/dist/async/sleep.js
CHANGED
|
@@ -1,10 +1,31 @@
|
|
|
1
|
+
import { abortReason } from "./abortReason.js";
|
|
1
2
|
/**
|
|
2
3
|
* Resolves after the given number of milliseconds.
|
|
3
4
|
*
|
|
4
5
|
* @param ms - The delay in milliseconds.
|
|
6
|
+
* @param signal - Rejects early (with the abort reason) when aborted.
|
|
5
7
|
* @example
|
|
6
8
|
* await sleep(1000); // waits 1 second
|
|
7
9
|
*/
|
|
8
|
-
|
|
9
|
-
|
|
10
|
+
// Rejects with the caller's own abort reason, whatever its type — like
|
|
11
|
+
// `fetch` does.
|
|
12
|
+
/* eslint-disable @typescript-eslint/prefer-promise-reject-errors */
|
|
13
|
+
export function sleep(ms, signal) {
|
|
14
|
+
return new Promise((resolve, reject) => {
|
|
15
|
+
if (signal?.aborted) {
|
|
16
|
+
reject(abortReason(signal));
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
const onAbort = () => {
|
|
20
|
+
clearTimeout(timer);
|
|
21
|
+
if (signal)
|
|
22
|
+
reject(abortReason(signal));
|
|
23
|
+
};
|
|
24
|
+
/* eslint-enable @typescript-eslint/prefer-promise-reject-errors */
|
|
25
|
+
const timer = setTimeout(() => {
|
|
26
|
+
signal?.removeEventListener("abort", onAbort);
|
|
27
|
+
resolve();
|
|
28
|
+
}, ms);
|
|
29
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
30
|
+
});
|
|
10
31
|
}
|
|
@@ -6,14 +6,21 @@ export declare class TimeoutError extends Error {
|
|
|
6
6
|
constructor(message?: string);
|
|
7
7
|
}
|
|
8
8
|
/**
|
|
9
|
-
* Rejects with a {@link TimeoutError} if `
|
|
9
|
+
* Rejects with a {@link TimeoutError} if `operation` has not settled within
|
|
10
10
|
* `ms` milliseconds.
|
|
11
11
|
*
|
|
12
|
-
*
|
|
12
|
+
* Pass a function rather than a promise to also **cancel** the operation on
|
|
13
|
+
* timeout: it receives an `AbortSignal`, aborted (with the `TimeoutError`)
|
|
14
|
+
* when time runs out — hand it to `fetch` or any abortable API. A plain
|
|
15
|
+
* promise can only be given up on, not stopped.
|
|
16
|
+
*
|
|
17
|
+
* @param operation - The promise to race against the timeout, or a function
|
|
18
|
+
* starting the operation with the given `AbortSignal`.
|
|
13
19
|
* @param ms - The timeout in milliseconds.
|
|
14
20
|
* @param message - Optional error message (defaults to a generic one).
|
|
15
21
|
* @example
|
|
16
|
-
* await withTimeout(fetch("/api"), 5000);
|
|
22
|
+
* await withTimeout((signal) => fetch("/api", { signal }), 5000); // aborted on timeout
|
|
23
|
+
* await withTimeout(somePromise, 5000); // only stops waiting
|
|
17
24
|
*/
|
|
18
|
-
export declare function withTimeout<T>(
|
|
25
|
+
export declare function withTimeout<T>(operation: Promise<T> | ((signal: AbortSignal) => Promise<T>), ms: number, message?: string): Promise<T>;
|
|
19
26
|
//# sourceMappingURL=withTimeout.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"withTimeout.d.ts","sourceRoot":"","sources":["../../src/async/withTimeout.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,qBAAa,YAAa,SAAQ,KAAK;gBACzB,OAAO,SAAwB;CAI5C;AAED
|
|
1
|
+
{"version":3,"file":"withTimeout.d.ts","sourceRoot":"","sources":["../../src/async/withTimeout.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,qBAAa,YAAa,SAAQ,KAAK;gBACzB,OAAO,SAAwB;CAI5C;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,WAAW,CAAC,CAAC,EAC3B,SAAS,EAAE,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,WAAW,KAAK,OAAO,CAAC,CAAC,CAAC,CAAC,EAC7D,EAAE,EAAE,MAAM,EACV,OAAO,CAAC,EAAE,MAAM,GACf,OAAO,CAAC,CAAC,CAAC,CA2BZ"}
|
|
@@ -9,26 +9,48 @@ export class TimeoutError extends Error {
|
|
|
9
9
|
}
|
|
10
10
|
}
|
|
11
11
|
/**
|
|
12
|
-
* Rejects with a {@link TimeoutError} if `
|
|
12
|
+
* Rejects with a {@link TimeoutError} if `operation` has not settled within
|
|
13
13
|
* `ms` milliseconds.
|
|
14
14
|
*
|
|
15
|
-
*
|
|
15
|
+
* Pass a function rather than a promise to also **cancel** the operation on
|
|
16
|
+
* timeout: it receives an `AbortSignal`, aborted (with the `TimeoutError`)
|
|
17
|
+
* when time runs out — hand it to `fetch` or any abortable API. A plain
|
|
18
|
+
* promise can only be given up on, not stopped.
|
|
19
|
+
*
|
|
20
|
+
* @param operation - The promise to race against the timeout, or a function
|
|
21
|
+
* starting the operation with the given `AbortSignal`.
|
|
16
22
|
* @param ms - The timeout in milliseconds.
|
|
17
23
|
* @param message - Optional error message (defaults to a generic one).
|
|
18
24
|
* @example
|
|
19
|
-
* await withTimeout(fetch("/api"), 5000);
|
|
25
|
+
* await withTimeout((signal) => fetch("/api", { signal }), 5000); // aborted on timeout
|
|
26
|
+
* await withTimeout(somePromise, 5000); // only stops waiting
|
|
20
27
|
*/
|
|
21
|
-
export function withTimeout(
|
|
28
|
+
export function withTimeout(operation, ms, message) {
|
|
29
|
+
const controller = new AbortController();
|
|
22
30
|
return new Promise((resolve, reject) => {
|
|
23
31
|
const timer = setTimeout(() => {
|
|
24
|
-
|
|
32
|
+
const error = new TimeoutError(message);
|
|
33
|
+
controller.abort(error);
|
|
34
|
+
reject(error);
|
|
25
35
|
}, ms);
|
|
36
|
+
const fail = (error) => {
|
|
37
|
+
clearTimeout(timer);
|
|
38
|
+
reject(error instanceof Error ? error : new Error(String(error)));
|
|
39
|
+
};
|
|
40
|
+
let promise;
|
|
41
|
+
try {
|
|
42
|
+
promise =
|
|
43
|
+
typeof operation === "function"
|
|
44
|
+
? operation(controller.signal)
|
|
45
|
+
: operation;
|
|
46
|
+
}
|
|
47
|
+
catch (error) {
|
|
48
|
+
fail(error);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
26
51
|
promise.then((value) => {
|
|
27
52
|
clearTimeout(timer);
|
|
28
53
|
resolve(value);
|
|
29
|
-
},
|
|
30
|
-
clearTimeout(timer);
|
|
31
|
-
reject(error instanceof Error ? error : new Error(String(error)));
|
|
32
|
-
});
|
|
54
|
+
}, fail);
|
|
33
55
|
});
|
|
34
56
|
}
|