@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.
Files changed (128) hide show
  1. package/README.md +104 -19
  2. package/dist/array/getMostRecentIds.d.ts +21 -0
  3. package/dist/array/getMostRecentIds.d.ts.map +1 -0
  4. package/dist/array/getMostRecentIds.js +30 -0
  5. package/dist/array/index.d.ts +3 -0
  6. package/dist/array/index.d.ts.map +1 -1
  7. package/dist/array/index.js +3 -0
  8. package/dist/array/nextInCycle.d.ts +15 -0
  9. package/dist/array/nextInCycle.d.ts.map +1 -0
  10. package/dist/array/nextInCycle.js +19 -0
  11. package/dist/array/rankByNameMatch.d.ts +19 -0
  12. package/dist/array/rankByNameMatch.d.ts.map +1 -0
  13. package/dist/array/rankByNameMatch.js +45 -0
  14. package/dist/async/debounce.d.ts +20 -0
  15. package/dist/async/debounce.d.ts.map +1 -0
  16. package/dist/async/debounce.js +44 -0
  17. package/dist/async/index.d.ts +7 -0
  18. package/dist/async/index.d.ts.map +1 -0
  19. package/dist/async/index.js +6 -0
  20. package/dist/async/memoize.d.ts +19 -0
  21. package/dist/async/memoize.d.ts.map +1 -0
  22. package/dist/async/memoize.js +25 -0
  23. package/dist/async/retry.d.ts +25 -0
  24. package/dist/async/retry.d.ts.map +1 -0
  25. package/dist/async/retry.js +33 -0
  26. package/dist/async/sleep.d.ts +9 -0
  27. package/dist/async/sleep.d.ts.map +1 -0
  28. package/dist/async/sleep.js +10 -0
  29. package/dist/async/throttle.d.ts +20 -0
  30. package/dist/async/throttle.d.ts.map +1 -0
  31. package/dist/async/throttle.js +43 -0
  32. package/dist/async/withTimeout.d.ts +19 -0
  33. package/dist/async/withTimeout.d.ts.map +1 -0
  34. package/dist/async/withTimeout.js +34 -0
  35. package/dist/classes/array.d.ts +88 -0
  36. package/dist/classes/array.d.ts.map +1 -1
  37. package/dist/classes/array.js +74 -0
  38. package/dist/classes/index.d.ts +2 -0
  39. package/dist/classes/index.d.ts.map +1 -0
  40. package/dist/classes/index.js +1 -0
  41. package/dist/date/formatRelativeTime.d.ts +13 -0
  42. package/dist/date/formatRelativeTime.d.ts.map +1 -0
  43. package/dist/date/formatRelativeTime.js +30 -0
  44. package/dist/date/getPeriodStartMs.d.ts +16 -0
  45. package/dist/date/getPeriodStartMs.d.ts.map +1 -0
  46. package/dist/date/getPeriodStartMs.js +24 -0
  47. package/dist/date/index.d.ts +5 -0
  48. package/dist/date/index.d.ts.map +1 -0
  49. package/dist/date/index.js +4 -0
  50. package/dist/date/isSameDay.d.ts +11 -0
  51. package/dist/date/isSameDay.d.ts.map +1 -0
  52. package/dist/date/isSameDay.js +14 -0
  53. package/dist/date/startOfDay.d.ts +10 -0
  54. package/dist/date/startOfDay.d.ts.map +1 -0
  55. package/dist/date/startOfDay.js +13 -0
  56. package/dist/files/index.d.ts +2 -0
  57. package/dist/files/index.d.ts.map +1 -0
  58. package/dist/files/index.js +1 -0
  59. package/dist/index.d.ts +10 -1
  60. package/dist/index.d.ts.map +1 -1
  61. package/dist/index.js +15 -1
  62. package/dist/number/clamp.d.ts +13 -0
  63. package/dist/number/clamp.d.ts.map +1 -0
  64. package/dist/number/clamp.js +14 -0
  65. package/dist/number/formatBytes.d.ts +12 -0
  66. package/dist/number/formatBytes.d.ts.map +1 -0
  67. package/dist/number/formatBytes.js +19 -0
  68. package/dist/number/formatCsvNumber.d.ts +14 -0
  69. package/dist/number/formatCsvNumber.d.ts.map +1 -0
  70. package/dist/number/formatCsvNumber.js +15 -0
  71. package/dist/number/formatDuration.d.ts +12 -0
  72. package/dist/number/formatDuration.d.ts.map +1 -0
  73. package/dist/number/formatDuration.js +29 -0
  74. package/dist/number/index.d.ts +6 -0
  75. package/dist/number/index.d.ts.map +1 -0
  76. package/dist/number/index.js +5 -0
  77. package/dist/number/round.d.ts +12 -0
  78. package/dist/number/round.d.ts.map +1 -0
  79. package/dist/number/round.js +14 -0
  80. package/dist/object/deepClone.d.ts +12 -0
  81. package/dist/object/deepClone.d.ts.map +1 -0
  82. package/dist/object/deepClone.js +16 -0
  83. package/dist/object/deepEqual.d.ts +12 -0
  84. package/dist/object/deepEqual.d.ts.map +1 -0
  85. package/dist/object/deepEqual.js +31 -0
  86. package/dist/object/groupBy.d.ts +12 -0
  87. package/dist/object/groupBy.d.ts.map +1 -0
  88. package/dist/object/groupBy.js +18 -0
  89. package/dist/object/index.d.ts +8 -0
  90. package/dist/object/index.d.ts.map +1 -0
  91. package/dist/object/index.js +7 -0
  92. package/dist/object/mapValues.d.ts +11 -0
  93. package/dist/object/mapValues.d.ts.map +1 -0
  94. package/dist/object/mapValues.js +16 -0
  95. package/dist/object/omit.d.ts +11 -0
  96. package/dist/object/omit.d.ts.map +1 -0
  97. package/dist/object/omit.js +16 -0
  98. package/dist/object/pick.d.ts +11 -0
  99. package/dist/object/pick.d.ts.map +1 -0
  100. package/dist/object/pick.js +18 -0
  101. package/dist/object/uniqueBy.d.ts +13 -0
  102. package/dist/object/uniqueBy.d.ts.map +1 -0
  103. package/dist/object/uniqueBy.js +23 -0
  104. package/dist/string/capitalize.d.ts +11 -0
  105. package/dist/string/capitalize.d.ts.map +1 -0
  106. package/dist/string/capitalize.js +14 -0
  107. package/dist/string/escapeCsvField.d.ts +14 -0
  108. package/dist/string/escapeCsvField.d.ts.map +1 -0
  109. package/dist/string/escapeCsvField.js +17 -0
  110. package/dist/string/escapeHtml.d.ts +12 -0
  111. package/dist/string/escapeHtml.d.ts.map +1 -0
  112. package/dist/string/escapeHtml.js +17 -0
  113. package/dist/string/index.d.ts +8 -0
  114. package/dist/string/index.d.ts.map +1 -0
  115. package/dist/string/index.js +7 -0
  116. package/dist/string/normalizeForSearch.d.ts +12 -0
  117. package/dist/string/normalizeForSearch.d.ts.map +1 -0
  118. package/dist/string/normalizeForSearch.js +25 -0
  119. package/dist/string/pluralize.d.ts +17 -0
  120. package/dist/string/pluralize.d.ts.map +1 -0
  121. package/dist/string/pluralize.js +26 -0
  122. package/dist/string/slugify.d.ts +12 -0
  123. package/dist/string/slugify.d.ts.map +1 -0
  124. package/dist/string/slugify.js +18 -0
  125. package/dist/string/truncate.d.ts +13 -0
  126. package/dist/string/truncate.d.ts.map +1 -0
  127. package/dist/string/truncate.js +17 -0
  128. 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, maths, browser file
4
- downloads, and a couple of `Array` extensions with better type-narrowing.
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
- Each module is exported individually — import the file you need directly
25
- rather than a single barrel:
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 whose `includes()` narrows the type of the
47
- searched value when it's found (unlike the native `Array.prototype.includes`,
48
- which only narrows the _array's_ element type).
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
+ }
@@ -1,3 +1,6 @@
1
1
  export * from "./chunk.js";
2
2
  export * from "./flattenDeep.js";
3
+ export * from "./getMostRecentIds.js";
4
+ export * from "./nextInCycle.js";
5
+ export * from "./rankByNameMatch.js";
3
6
  //# 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,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"}
@@ -1,2 +1,5 @@
1
1
  export * from "./chunk.js";
2
2
  export * from "./flattenDeep.js";
3
+ export * from "./getMostRecentIds.js";
4
+ export * from "./nextInCycle.js";
5
+ export * from "./rankByNameMatch.js";
@@ -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,7 @@
1
+ export * from "./debounce.js";
2
+ export * from "./memoize.js";
3
+ export * from "./retry.js";
4
+ export * from "./sleep.js";
5
+ export * from "./throttle.js";
6
+ export * from "./withTimeout.js";
7
+ //# sourceMappingURL=index.d.ts.map
@@ -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,6 @@
1
+ export * from "./debounce.js";
2
+ export * from "./memoize.js";
3
+ export * from "./retry.js";
4
+ export * from "./sleep.js";
5
+ export * from "./throttle.js";
6
+ export * from "./withTimeout.js";
@@ -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,9 @@
1
+ /**
2
+ * Resolves after the given number of milliseconds.
3
+ *
4
+ * @param ms - The delay in milliseconds.
5
+ * @example
6
+ * await sleep(1000); // waits 1 second
7
+ */
8
+ export declare function sleep(ms: number): Promise<void>;
9
+ //# sourceMappingURL=sleep.d.ts.map
@@ -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,10 @@
1
+ /**
2
+ * Resolves after the given number of milliseconds.
3
+ *
4
+ * @param ms - The delay in milliseconds.
5
+ * @example
6
+ * await sleep(1000); // waits 1 second
7
+ */
8
+ export function sleep(ms) {
9
+ return new Promise((resolve) => setTimeout(resolve, ms));
10
+ }
@@ -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
+ }