@forthtilliath/ts-kit 0.10.0 → 0.11.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.
Files changed (87) hide show
  1. package/README.md +72 -22
  2. package/dist/array/flattenDeep.d.ts +7 -5
  3. package/dist/array/flattenDeep.d.ts.map +1 -1
  4. package/dist/array/flattenDeep.js +8 -5
  5. package/dist/async/abortReason.d.ts +9 -0
  6. package/dist/async/abortReason.d.ts.map +1 -0
  7. package/dist/async/abortReason.js +11 -0
  8. package/dist/async/memoize.d.ts +2 -18
  9. package/dist/async/memoize.d.ts.map +1 -1
  10. package/dist/async/memoize.js +4 -25
  11. package/dist/async/retry.d.ts +15 -3
  12. package/dist/async/retry.d.ts.map +1 -1
  13. package/dist/async/retry.js +15 -11
  14. package/dist/async/sleep.d.ts +2 -1
  15. package/dist/async/sleep.d.ts.map +1 -1
  16. package/dist/async/sleep.js +23 -2
  17. package/dist/async/withTimeout.d.ts +11 -4
  18. package/dist/async/withTimeout.d.ts.map +1 -1
  19. package/dist/async/withTimeout.js +31 -9
  20. package/dist/csv/escapeCsvField.d.ts +21 -0
  21. package/dist/csv/escapeCsvField.d.ts.map +1 -0
  22. package/dist/csv/escapeCsvField.js +28 -0
  23. package/dist/csv/formatCsvNumber.d.ts +14 -0
  24. package/dist/csv/formatCsvNumber.d.ts.map +1 -0
  25. package/dist/csv/formatCsvNumber.js +15 -0
  26. package/dist/csv/index.d.ts +2 -0
  27. package/dist/csv/index.d.ts.map +1 -1
  28. package/dist/csv/index.js +2 -0
  29. package/dist/date/dateInTimeZone.d.ts +22 -0
  30. package/dist/date/dateInTimeZone.d.ts.map +1 -0
  31. package/dist/date/dateInTimeZone.js +35 -0
  32. package/dist/date/daysUntil.d.ts +15 -0
  33. package/dist/date/daysUntil.d.ts.map +1 -0
  34. package/dist/date/daysUntil.js +22 -0
  35. package/dist/date/index.d.ts +3 -0
  36. package/dist/date/index.d.ts.map +1 -1
  37. package/dist/date/index.js +3 -0
  38. package/dist/date/todayInTimeZone.d.ts +14 -0
  39. package/dist/date/todayInTimeZone.d.ts.map +1 -0
  40. package/dist/date/todayInTimeZone.js +15 -0
  41. package/dist/files/downloadText.d.ts.map +1 -1
  42. package/dist/files/downloadText.js +9 -1
  43. package/dist/function/index.d.ts +2 -0
  44. package/dist/function/index.d.ts.map +1 -0
  45. package/dist/function/index.js +1 -0
  46. package/dist/function/memoize.d.ts +34 -0
  47. package/dist/function/memoize.d.ts.map +1 -0
  48. package/dist/function/memoize.js +50 -0
  49. package/dist/index.d.ts +1 -1
  50. package/dist/index.d.ts.map +1 -1
  51. package/dist/index.js +1 -1
  52. package/dist/maths/average.d.ts +2 -7
  53. package/dist/maths/average.d.ts.map +1 -1
  54. package/dist/maths/average.js +4 -10
  55. package/dist/maths/sum.d.ts +2 -7
  56. package/dist/maths/sum.d.ts.map +1 -1
  57. package/dist/maths/sum.js +4 -9
  58. package/dist/number/average.d.ts +13 -0
  59. package/dist/number/average.d.ts.map +1 -0
  60. package/dist/number/average.js +15 -0
  61. package/dist/number/formatBytes.d.ts.map +1 -1
  62. package/dist/number/formatBytes.js +6 -2
  63. package/dist/number/formatCsvNumber.d.ts +2 -13
  64. package/dist/number/formatCsvNumber.d.ts.map +1 -1
  65. package/dist/number/formatCsvNumber.js +4 -15
  66. package/dist/number/index.d.ts +2 -0
  67. package/dist/number/index.d.ts.map +1 -1
  68. package/dist/number/index.js +2 -0
  69. package/dist/number/sum.d.ts +10 -0
  70. package/dist/number/sum.d.ts.map +1 -0
  71. package/dist/number/sum.js +11 -0
  72. package/dist/object/deepEqual.d.ts +6 -0
  73. package/dist/object/deepEqual.d.ts.map +1 -1
  74. package/dist/object/deepEqual.js +45 -11
  75. package/dist/string/escapeCsvField.d.ts +2 -13
  76. package/dist/string/escapeCsvField.d.ts.map +1 -1
  77. package/dist/string/escapeCsvField.js +4 -17
  78. package/dist/string/escapeHtml.d.ts +6 -4
  79. package/dist/string/escapeHtml.d.ts.map +1 -1
  80. package/dist/string/escapeHtml.js +8 -5
  81. package/dist/string/pluralize.d.ts +30 -7
  82. package/dist/string/pluralize.d.ts.map +1 -1
  83. package/dist/string/pluralize.js +48 -15
  84. package/dist/version/compareVersions.d.ts +8 -1
  85. package/dist/version/compareVersions.d.ts.map +1 -1
  86. package/dist/version/compareVersions.js +63 -9
  87. 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).
@@ -60,16 +73,23 @@ import { chunk } from "@forthtilliath/ts-kit/array/chunk";
60
73
 
61
74
  ### `async`
62
75
 
63
- - `sleep(ms)` — resolves after a delay.
76
+ - `sleep(ms, signal?)` — resolves after a delay; rejects early if `signal`
77
+ aborts.
64
78
  - `retry(fn, options)` — retries an async operation with exponential backoff.
79
+ Options: `attempts` (3), `delayMs` (200), `backoffFactor` (2),
80
+ `maxDelayMs`, `shouldRetry(error, attempt)` (return `false` to rethrow at
81
+ once, e.g. on a 4xx), `onRetry(error, attempt)` (called before each actual
82
+ retry), `signal`.
65
83
  - `debounce(fn, wait)` — delays `fn` until calls stop for `wait` ms; exposes
66
84
  `cancel()` / `flush()`.
67
85
  - `throttle(fn, wait)` — runs `fn` at most once per `wait` ms (leading +
68
86
  trailing edge); exposes `cancel()`.
69
- - `memoize(fn, getKey?)` — caches `fn`'s results per argument tuple; exposes
70
- the underlying `cache` map.
71
- - `withTimeout(promise, ms, message?)` — rejects with a `TimeoutError` if
72
- `promise` doesn't settle in time.
87
+ - `withTimeout(operation, ms, message?)` — rejects with a `TimeoutError` if
88
+ `operation` doesn't settle in time. Pass a function `(signal) => promise`
89
+ instead of a promise to also **cancel** the operation on timeout (e.g.
90
+ `withTimeout((signal) => fetch(url, { signal }), 5000)`).
91
+ - `memoize` — moved to [`function`](#function); still importable from
92
+ `async/memoize` (deprecated).
73
93
 
74
94
  ### `classes`
75
95
 
@@ -97,11 +117,19 @@ import { chunk } from "@forthtilliath/ts-kit/array/chunk";
97
117
  - `parseCsvLine(line, delimiter?)` — splits one CSV line into trimmed cells;
98
118
  quoted cells may contain the delimiter, `""` stands for a literal quote.
99
119
  Delimiter defaults to `","`.
100
- - `toCsv(rows, delimiter?)` — serializes rows to CSV, every cell quoted and
101
- escaped, rows joined with `\n`.
120
+ - `toCsv(rows, delimiter?)` — serializes whole rows to CSV, every cell quoted
121
+ and escaped, rows joined with `\n`. Delimiter defaults to `","`.
122
+ - `escapeCsvField(value, delimiter?)` — escapes a single field (RFC 4180),
123
+ quoting it only if it contains a `"`, the delimiter or a line break — to
124
+ build rows cell by cell. Delimiter defaults to `";"` (French-locale Excel).
125
+ - `formatCsvNumber(value, decimals?)` — formats a number with a comma decimal
126
+ separator (French-locale spreadsheets) instead of JS's dot.
102
127
  - `downloadCsv(filename, content)` — browser download as `text/csv`, with a
103
128
  UTF-8 BOM so Excel reads accented characters.
104
129
 
130
+ `escapeCsvField` and `formatCsvNumber` used to live in `string/` and
131
+ `number/`; those deep imports still work (deprecated).
132
+
105
133
  ### `date`
106
134
 
107
135
  - `startOfDay(date?)` — returns a new `Date` at local midnight.
@@ -111,13 +139,31 @@ import { chunk } from "@forthtilliath/ts-kit/array/chunk";
111
139
  - `getPeriodStartMs(period, now?)` — start timestamp (ms) for a
112
140
  `"today" | "7d" | "30d" | "all"` period filter, or `null` for `"all"`.
113
141
  `"today"` is the current calendar day, not a rolling 24h window.
142
+ - `dateInTimeZone(date, timeZone)` — the calendar date of an instant in an
143
+ IANA time zone, `{ year, month, day, iso }` (`month` from 1 to 12), whatever
144
+ the machine's own time zone.
145
+ - `todayInTimeZone(timeZone, now?)` — today's date in that time zone: on a
146
+ server running in UTC, `new Date()` is still "yesterday" in Paris just
147
+ after midnight.
148
+ - `daysUntil(date, timeZone, now?)` — calendar days between today and a date,
149
+ both taken in that time zone (`0` today, `1` tomorrow, `-1` yesterday);
150
+ unaffected by the time of day or daylight saving time.
114
151
 
115
152
  ### `files`
116
153
 
117
154
  - `downloadText(filename, text)` — triggers a browser download of a text file
118
155
  via a `data:` URI.
119
156
  - `downloadTextBlob(filename, text)` — same, but via a `Blob` + object URL
120
- (better for larger content).
157
+ (better for larger content). The object URL is revoked 40 s later, not
158
+ right after the click (which can cancel the download in Firefox/Safari).
159
+
160
+ ### `function`
161
+
162
+ - `memoize(fn, getKeyOrOptions?)` — caches `fn`'s results per argument tuple
163
+ (JSON-stringified by default); exposes the underlying `cache` map. Pass a
164
+ key function, or `{ getKey, maxSize }` to evict the least recently used
165
+ result beyond `maxSize`. A returned promise that rejects is evicted, so the
166
+ next call retries instead of failing forever.
121
167
  - `sanitizeFileName(name)` — replaces characters forbidden in file names
122
168
  (`\ / : * ? " < > |`) with hyphens, collapses whitespace and trims.
123
169
 
@@ -141,22 +187,21 @@ import { chunk } from "@forthtilliath/ts-kit/array/chunk";
141
187
  `bold`-aware segments), ready for a UI layer to render without a full
142
188
  Markdown dependency.
143
189
 
144
- ### `maths`
145
-
146
- - `sum(numbers)` — sum of an array of numbers.
147
- - `avg(numbers)` — average of an array of numbers.
148
-
149
190
  ### `number`
150
191
 
192
+ - `sum(numbers)` — sum of an array of numbers (`0` if empty).
193
+ - `avg(numbers)` — average of an array of numbers; `NaN` if empty (no
194
+ meaningful average of nothing, like lodash's `mean`).
151
195
  - `clamp(value, min, max)` — clamps a number between two bounds.
152
196
  - `round(value, decimals?)` — rounds to a given number of decimal places.
153
197
  - `formatBytes(bytes, decimals?)` — human-readable byte size ("1.5 MB").
154
198
  - `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
199
  - `padNumber(n, total, minWidth?)` — zero-pads `n` to the width of `total`
158
200
  (at least `minWidth`, default 2) so numbered file names sort correctly.
159
201
 
202
+ `sum` and `avg` used to live in `maths/`; those deep imports still work
203
+ (deprecated).
204
+
160
205
  ### `object`
161
206
 
162
207
  - `pick(obj, keys)` / `omit(obj, keys)` — keep or drop a set of keys.
@@ -174,15 +219,20 @@ import { chunk } from "@forthtilliath/ts-kit/array/chunk";
174
219
  suffix).
175
220
  - `slugify(str)` — URL-friendly slug (accents stripped, lower-cased,
176
221
  hyphenated).
177
- - `pluralize(count, singular, plural?)` — naive English pluralization, with an
178
- override for irregular words.
222
+ - `pluralize(count, singular, plural? | { plural?, locale? })` — picks the
223
+ singular or the plural form. Which counts take the singular follows the
224
+ locale's rules (`Intl.PluralRules`, `"en"` by default): only ±1 in
225
+ English, 0 and 1.5 too in French. Without an explicit plural form, a naive
226
+ suffix: English `s`/`es`/`ies`, French `s` (nothing after `s`/`x`/`z`),
227
+ `s` elsewhere.
228
+ - `createPluralize(locale)` — `pluralize` bound to a locale, declared once per
229
+ app: `const plural = createPluralize("fr"); plural(n, "est", "sont")`.
179
230
  - `normalizeForSearch(text)` — lowercases, trims, strips accents, and expands
180
231
  œ/æ ligatures (which `normalize("NFD")` alone doesn't decompose) — for
181
232
  accent/case-insensitive search matching.
182
- - `escapeCsvField(value)` — quotes and escapes a CSV field (RFC 4180) only if
183
- it contains a `"`, `;`, or newline.
184
- - `escapeHtml(text)` — basic HTML entity escaping (`&`, `<`, `>`, `"`) for
185
- inserting user text into an HTML template.
233
+ - `escapeHtml(text)` — HTML entity escaping (`&`, `<`, `>`, `"`, `'`) for
234
+ inserting user text into an HTML template, in text content or in an
235
+ attribute quoted with `"` or `'`.
186
236
 
187
237
  ### `version`
188
238
 
@@ -1,9 +1,11 @@
1
1
  /**
2
- * Recursively flattens a nested array.
2
+ * Recursively flattens a nested array, at any depth.
3
3
  *
4
- * @template T - The type of elements in the array.
5
- * @param {T[]} array - The array to flatten.
6
- * @return {T[]} The flattened array.
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[]): 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;;;;;;GAMG;AACH,wBAAgB,WAAW,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,GAAG,CAAC,EAAE,CAM9C"}
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
- * @template T - The type of elements in the array.
5
- * @param {T[]} array - The array to flatten.
6
- * @return {T[]} The flattened array.
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
- return array.reduce((acc, val) => Array.isArray(val) ? acc.concat(flattenDeep(val)) : acc.concat(val), []);
11
+ // 20 is the deepest level the built-in `FlatArray` type can unwind.
12
+ return array.flat(Infinity);
10
13
  }
@@ -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
+ }
@@ -1,19 +1,3 @@
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>;
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":"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"}
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"}
@@ -1,25 +1,4 @@
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
- }
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";
@@ -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
- /** Called with the error and the 1-based attempt number after each failure. */
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, if every attempt failed.
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(() => fetch("/api").then((r) => r.json()), {
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":"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"}
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"}
@@ -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, if every attempt failed.
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(() => fetch("/api").then((r) => r.json()), {
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
- let lastError;
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; attempt <= attempts; attempt++) {
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
- lastError = error;
29
+ const canRetry = attempt < maxAttempts && (shouldRetry?.(error, attempt) ?? true);
30
+ if (!canRetry)
31
+ throw error;
25
32
  onRetry?.(error, attempt);
26
- if (attempt < attempts) {
27
- await sleep(currentDelay);
28
- currentDelay *= backoffFactor;
29
- }
33
+ await sleep(Math.min(currentDelay, maxDelayMs), signal);
34
+ currentDelay *= backoffFactor;
30
35
  }
31
36
  }
32
- throw lastError;
33
37
  }
@@ -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":"AAAA;;;;;;GAMG;AACH,wBAAgB,KAAK,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAE/C"}
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"}
@@ -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
- export function sleep(ms) {
9
- return new Promise((resolve) => setTimeout(resolve, ms));
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 `promise` has not settled within
9
+ * Rejects with a {@link TimeoutError} if `operation` has not settled within
10
10
  * `ms` milliseconds.
11
11
  *
12
- * @param promise - The promise to race against the timeout.
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>(promise: Promise<T>, ms: number, message?: string): Promise<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;;;;;;;;;GASG;AACH,wBAAgB,WAAW,CAAC,CAAC,EAC3B,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC,EACnB,EAAE,EAAE,MAAM,EACV,OAAO,CAAC,EAAE,MAAM,GACf,OAAO,CAAC,CAAC,CAAC,CAgBZ"}
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 `promise` has not settled within
12
+ * Rejects with a {@link TimeoutError} if `operation` has not settled within
13
13
  * `ms` milliseconds.
14
14
  *
15
- * @param promise - The promise to race against the timeout.
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(promise, ms, message) {
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
- reject(new TimeoutError(message));
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
- }, (error) => {
30
- clearTimeout(timer);
31
- reject(error instanceof Error ? error : new Error(String(error)));
32
- });
54
+ }, fail);
33
55
  });
34
56
  }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Escapes a single CSV field per RFC 4180, quoting it only when needed: if it
3
+ * contains a double quote, the delimiter or a line break, it's wrapped in
4
+ * double quotes with any internal double quote doubled; otherwise it's
5
+ * returned as-is.
6
+ *
7
+ * Builds rows cell by cell. To serialize whole rows at once, see `toCsv`
8
+ * (which always quotes, and defaults to `,`).
9
+ *
10
+ * @param value - The raw field value.
11
+ * @param delimiter - The cell separator the field will be joined with.
12
+ * Defaults to `";"`, what French-locale Excel expects.
13
+ * @returns The value, quoted and escaped only if needed.
14
+ * @example
15
+ * escapeCsvField("Apple"); // => "Apple"
16
+ * escapeCsvField("a;b"); // => '"a;b"'
17
+ * escapeCsvField("a,b", ","); // => '"a,b"'
18
+ * escapeCsvField('He said "hi"'); // => '"He said ""hi"""'
19
+ */
20
+ export declare function escapeCsvField(value: string, delimiter?: string): string;
21
+ //# sourceMappingURL=escapeCsvField.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"escapeCsvField.d.ts","sourceRoot":"","sources":["../../src/csv/escapeCsvField.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,EAAE,SAAS,SAAM,GAAG,MAAM,CAQrE"}
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Escapes a single CSV field per RFC 4180, quoting it only when needed: if it
3
+ * contains a double quote, the delimiter or a line break, it's wrapped in
4
+ * double quotes with any internal double quote doubled; otherwise it's
5
+ * returned as-is.
6
+ *
7
+ * Builds rows cell by cell. To serialize whole rows at once, see `toCsv`
8
+ * (which always quotes, and defaults to `,`).
9
+ *
10
+ * @param value - The raw field value.
11
+ * @param delimiter - The cell separator the field will be joined with.
12
+ * Defaults to `";"`, what French-locale Excel expects.
13
+ * @returns The value, quoted and escaped only if needed.
14
+ * @example
15
+ * escapeCsvField("Apple"); // => "Apple"
16
+ * escapeCsvField("a;b"); // => '"a;b"'
17
+ * escapeCsvField("a,b", ","); // => '"a,b"'
18
+ * escapeCsvField('He said "hi"'); // => '"He said ""hi"""'
19
+ */
20
+ export function escapeCsvField(value, delimiter = ";") {
21
+ const needsQuoting = value.includes('"') ||
22
+ value.includes(delimiter) ||
23
+ value.includes("\n") ||
24
+ value.includes("\r");
25
+ if (!needsQuoting)
26
+ return value;
27
+ return `"${value.replace(/"/g, '""')}"`;
28
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Formats a number as a raw value (no unit suffix) for use in a spreadsheet
3
+ * (sum, average...), with a comma decimal separator as expected by
4
+ * French-locale spreadsheets instead of JS's dot.
5
+ *
6
+ * @param value - The number to format.
7
+ * @param decimals - Number of decimal places. Defaults to `1`.
8
+ * @returns The formatted number, comma as decimal separator.
9
+ * @example
10
+ * formatCsvNumber(1.5); // => "1,5"
11
+ * formatCsvNumber(12.345, 2); // => "12,35"
12
+ */
13
+ export declare function formatCsvNumber(value: number, decimals?: number): string;
14
+ //# sourceMappingURL=formatCsvNumber.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"formatCsvNumber.d.ts","sourceRoot":"","sources":["../../src/csv/formatCsvNumber.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,SAAI,GAAG,MAAM,CAEnE"}
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Formats a number as a raw value (no unit suffix) for use in a spreadsheet
3
+ * (sum, average...), with a comma decimal separator as expected by
4
+ * French-locale spreadsheets instead of JS's dot.
5
+ *
6
+ * @param value - The number to format.
7
+ * @param decimals - Number of decimal places. Defaults to `1`.
8
+ * @returns The formatted number, comma as decimal separator.
9
+ * @example
10
+ * formatCsvNumber(1.5); // => "1,5"
11
+ * formatCsvNumber(12.345, 2); // => "12,35"
12
+ */
13
+ export function formatCsvNumber(value, decimals = 1) {
14
+ return value.toFixed(decimals).replace(".", ",");
15
+ }
@@ -1,4 +1,6 @@
1
1
  export * from "./downloadCsv.js";
2
+ export * from "./escapeCsvField.js";
3
+ export * from "./formatCsvNumber.js";
2
4
  export * from "./parseCsvLine.js";
3
5
  export * from "./toCsv.js";
4
6
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/csv/index.ts"],"names":[],"mappings":"AAAA,cAAc,kBAAkB,CAAC;AACjC,cAAc,mBAAmB,CAAC;AAClC,cAAc,YAAY,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/csv/index.ts"],"names":[],"mappings":"AAAA,cAAc,kBAAkB,CAAC;AACjC,cAAc,qBAAqB,CAAC;AACpC,cAAc,sBAAsB,CAAC;AACrC,cAAc,mBAAmB,CAAC;AAClC,cAAc,YAAY,CAAC"}