@tldraw/utils 4.1.0-next.b6dfe9bccde9 → 4.1.0-next.b73a0d46b63f
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/dist-cjs/index.d.ts +1350 -80
- package/dist-cjs/index.js +5 -5
- package/dist-cjs/lib/ExecutionQueue.js +79 -0
- package/dist-cjs/lib/ExecutionQueue.js.map +2 -2
- package/dist-cjs/lib/PerformanceTracker.js +43 -0
- package/dist-cjs/lib/PerformanceTracker.js.map +2 -2
- package/dist-cjs/lib/array.js +3 -1
- package/dist-cjs/lib/array.js.map +2 -2
- package/dist-cjs/lib/bind.js.map +2 -2
- package/dist-cjs/lib/cache.js +27 -5
- package/dist-cjs/lib/cache.js.map +2 -2
- package/dist-cjs/lib/control.js +12 -0
- package/dist-cjs/lib/control.js.map +2 -2
- package/dist-cjs/lib/debounce.js.map +2 -2
- package/dist-cjs/lib/error.js.map +2 -2
- package/dist-cjs/lib/file.js +76 -11
- package/dist-cjs/lib/file.js.map +2 -2
- package/dist-cjs/lib/function.js.map +2 -2
- package/dist-cjs/lib/hash.js.map +2 -2
- package/dist-cjs/lib/id.js.map +2 -2
- package/dist-cjs/lib/iterable.js.map +2 -2
- package/dist-cjs/lib/json-value.js.map +1 -1
- package/dist-cjs/lib/media/apng.js.map +2 -2
- package/dist-cjs/lib/media/avif.js.map +2 -2
- package/dist-cjs/lib/media/gif.js.map +2 -2
- package/dist-cjs/lib/media/media.js +130 -4
- package/dist-cjs/lib/media/media.js.map +2 -2
- package/dist-cjs/lib/media/png.js +141 -0
- package/dist-cjs/lib/media/png.js.map +2 -2
- package/dist-cjs/lib/media/webp.js +1 -0
- package/dist-cjs/lib/media/webp.js.map +2 -2
- package/dist-cjs/lib/network.js.map +2 -2
- package/dist-cjs/lib/number.js.map +2 -2
- package/dist-cjs/lib/object.js +1 -1
- package/dist-cjs/lib/object.js.map +2 -2
- package/dist-cjs/lib/perf.js.map +2 -2
- package/dist-cjs/lib/reordering.js.map +2 -2
- package/dist-cjs/lib/retry.js.map +2 -2
- package/dist-cjs/lib/sort.js.map +2 -2
- package/dist-cjs/lib/storage.js.map +2 -2
- package/dist-cjs/lib/stringEnum.js.map +2 -2
- package/dist-cjs/lib/throttle.js.map +2 -2
- package/dist-cjs/lib/timers.js +103 -4
- package/dist-cjs/lib/timers.js.map +2 -2
- package/dist-cjs/lib/types.js.map +1 -1
- package/dist-cjs/lib/url.js.map +2 -2
- package/dist-cjs/lib/value.js.map +2 -2
- package/dist-cjs/lib/version.js.map +2 -2
- package/dist-cjs/lib/warn.js.map +2 -2
- package/dist-esm/index.d.mts +1350 -80
- package/dist-esm/index.mjs +1 -1
- package/dist-esm/lib/ExecutionQueue.mjs +79 -0
- package/dist-esm/lib/ExecutionQueue.mjs.map +2 -2
- package/dist-esm/lib/PerformanceTracker.mjs +43 -0
- package/dist-esm/lib/PerformanceTracker.mjs.map +2 -2
- package/dist-esm/lib/array.mjs +3 -1
- package/dist-esm/lib/array.mjs.map +2 -2
- package/dist-esm/lib/bind.mjs.map +2 -2
- package/dist-esm/lib/cache.mjs +27 -5
- package/dist-esm/lib/cache.mjs.map +2 -2
- package/dist-esm/lib/control.mjs +12 -0
- package/dist-esm/lib/control.mjs.map +2 -2
- package/dist-esm/lib/debounce.mjs.map +2 -2
- package/dist-esm/lib/error.mjs.map +2 -2
- package/dist-esm/lib/file.mjs +76 -11
- package/dist-esm/lib/file.mjs.map +2 -2
- package/dist-esm/lib/function.mjs.map +2 -2
- package/dist-esm/lib/hash.mjs.map +2 -2
- package/dist-esm/lib/id.mjs.map +2 -2
- package/dist-esm/lib/iterable.mjs.map +2 -2
- package/dist-esm/lib/media/apng.mjs.map +2 -2
- package/dist-esm/lib/media/avif.mjs.map +2 -2
- package/dist-esm/lib/media/gif.mjs.map +2 -2
- package/dist-esm/lib/media/media.mjs +130 -4
- package/dist-esm/lib/media/media.mjs.map +2 -2
- package/dist-esm/lib/media/png.mjs +141 -0
- package/dist-esm/lib/media/png.mjs.map +2 -2
- package/dist-esm/lib/media/webp.mjs +1 -0
- package/dist-esm/lib/media/webp.mjs.map +2 -2
- package/dist-esm/lib/network.mjs.map +2 -2
- package/dist-esm/lib/number.mjs.map +2 -2
- package/dist-esm/lib/object.mjs.map +2 -2
- package/dist-esm/lib/perf.mjs.map +2 -2
- package/dist-esm/lib/reordering.mjs.map +2 -2
- package/dist-esm/lib/retry.mjs.map +2 -2
- package/dist-esm/lib/sort.mjs.map +2 -2
- package/dist-esm/lib/storage.mjs.map +2 -2
- package/dist-esm/lib/stringEnum.mjs.map +2 -2
- package/dist-esm/lib/throttle.mjs.map +2 -2
- package/dist-esm/lib/timers.mjs +103 -4
- package/dist-esm/lib/timers.mjs.map +2 -2
- package/dist-esm/lib/url.mjs.map +2 -2
- package/dist-esm/lib/value.mjs.map +2 -2
- package/dist-esm/lib/version.mjs.map +2 -2
- package/dist-esm/lib/warn.mjs.map +2 -2
- package/package.json +1 -1
- package/src/lib/ExecutionQueue.test.ts +162 -20
- package/src/lib/ExecutionQueue.ts +110 -1
- package/src/lib/PerformanceTracker.test.ts +124 -0
- package/src/lib/PerformanceTracker.ts +63 -1
- package/src/lib/array.test.ts +263 -1
- package/src/lib/array.ts +183 -14
- package/src/lib/bind.test.ts +47 -0
- package/src/lib/bind.ts +69 -4
- package/src/lib/cache.test.ts +73 -0
- package/src/lib/cache.ts +47 -6
- package/src/lib/control.test.ts +50 -0
- package/src/lib/control.ts +198 -9
- package/src/lib/debounce.ts +28 -3
- package/src/lib/error.test.ts +60 -0
- package/src/lib/error.ts +27 -1
- package/src/lib/file.test.ts +49 -0
- package/src/lib/file.ts +117 -12
- package/src/lib/function.ts +11 -0
- package/src/lib/hash.test.ts +99 -0
- package/src/lib/hash.ts +69 -2
- package/src/lib/id.test.ts +32 -0
- package/src/lib/id.ts +53 -5
- package/src/lib/iterable.test.ts +25 -0
- package/src/lib/iterable.ts +4 -5
- package/src/lib/json-value.ts +71 -4
- package/src/lib/media/apng.test.ts +67 -0
- package/src/lib/media/apng.ts +38 -21
- package/src/lib/media/avif.test.ts +26 -0
- package/src/lib/media/avif.ts +34 -0
- package/src/lib/media/gif.test.ts +52 -0
- package/src/lib/media/gif.ts +25 -2
- package/src/lib/media/media.test.ts +58 -0
- package/src/lib/media/media.ts +220 -11
- package/src/lib/media/png.ts +162 -1
- package/src/lib/media/webp.test.ts +81 -0
- package/src/lib/media/webp.ts +33 -1
- package/src/lib/network.test.ts +38 -0
- package/src/lib/network.ts +6 -0
- package/src/lib/number.test.ts +74 -0
- package/src/lib/number.ts +29 -5
- package/src/lib/object.test.ts +236 -0
- package/src/lib/object.ts +194 -14
- package/src/lib/perf.ts +75 -3
- package/src/lib/reordering.test.ts +168 -0
- package/src/lib/reordering.ts +62 -4
- package/src/lib/retry.test.ts +77 -0
- package/src/lib/retry.ts +47 -1
- package/src/lib/sort.test.ts +36 -0
- package/src/lib/sort.ts +22 -1
- package/src/lib/storage.test.ts +130 -0
- package/src/lib/storage.tsx +54 -8
- package/src/lib/stringEnum.ts +20 -1
- package/src/lib/throttle.ts +46 -8
- package/src/lib/timers.test.ts +75 -0
- package/src/lib/timers.ts +124 -5
- package/src/lib/types.ts +126 -4
- package/src/lib/url.test.ts +44 -0
- package/src/lib/url.ts +40 -1
- package/src/lib/value.test.ts +102 -0
- package/src/lib/value.ts +67 -3
- package/src/lib/version.test.ts +494 -56
- package/src/lib/version.ts +36 -1
- package/src/lib/warn.test.ts +64 -0
- package/src/lib/warn.ts +43 -2
package/src/lib/array.ts
CHANGED
|
@@ -1,15 +1,52 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Rotate the contents of an array.
|
|
2
|
+
* Rotate the contents of an array by a specified offset.
|
|
3
3
|
*
|
|
4
|
+
* Creates a new array with elements shifted to the left by the specified number of positions.
|
|
5
|
+
* Both positive and negative offsets result in left shifts (elements move left, with elements
|
|
6
|
+
* from the front wrapping to the back).
|
|
7
|
+
*
|
|
8
|
+
* @param arr - The array to rotate
|
|
9
|
+
* @param offset - The number of positions to shift left (both positive and negative values shift left)
|
|
10
|
+
* @returns A new array with elements shifted left by the specified offset
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```ts
|
|
14
|
+
* rotateArray([1, 2, 3, 4], 1) // [2, 3, 4, 1]
|
|
15
|
+
* rotateArray([1, 2, 3, 4], -1) // [2, 3, 4, 1]
|
|
16
|
+
* rotateArray(['a', 'b', 'c'], 2) // ['c', 'a', 'b']
|
|
17
|
+
* ```
|
|
4
18
|
* @public
|
|
5
19
|
*/
|
|
6
20
|
export function rotateArray<T>(arr: T[], offset: number): T[] {
|
|
7
|
-
|
|
21
|
+
if (arr.length === 0) return []
|
|
22
|
+
|
|
23
|
+
// Based on the test expectations, both positive and negative offsets
|
|
24
|
+
// should rotate left (shift elements to the left)
|
|
25
|
+
const normalizedOffset = ((Math.abs(offset) % arr.length) + arr.length) % arr.length
|
|
26
|
+
|
|
27
|
+
// Slice the array at the offset point and concatenate
|
|
28
|
+
return [...arr.slice(normalizedOffset), ...arr.slice(0, normalizedOffset)]
|
|
8
29
|
}
|
|
9
30
|
|
|
10
31
|
/**
|
|
11
|
-
*
|
|
32
|
+
* Remove duplicate items from an array.
|
|
33
|
+
*
|
|
34
|
+
* Creates a new array with duplicate items removed. Uses strict equality by default,
|
|
35
|
+
* or a custom equality function if provided. Order of first occurrence is preserved.
|
|
36
|
+
*
|
|
37
|
+
* @param input - The array to deduplicate
|
|
38
|
+
* @param equals - Optional custom equality function to compare items (defaults to strict equality)
|
|
39
|
+
* @returns A new array with duplicate items removed
|
|
40
|
+
*
|
|
41
|
+
* @example
|
|
42
|
+
* ```ts
|
|
43
|
+
* dedupe([1, 2, 2, 3, 1]) // [1, 2, 3]
|
|
44
|
+
* dedupe(['a', 'b', 'a', 'c']) // ['a', 'b', 'c']
|
|
12
45
|
*
|
|
46
|
+
* // With custom equality function
|
|
47
|
+
* const objects = [{id: 1}, {id: 2}, {id: 1}]
|
|
48
|
+
* dedupe(objects, (a, b) => a.id === b.id) // [{id: 1}, {id: 2}]
|
|
49
|
+
* ```
|
|
13
50
|
* @public
|
|
14
51
|
*/
|
|
15
52
|
export function dedupe<T>(input: T[], equals?: (a: any, b: any) => boolean): T[] {
|
|
@@ -25,17 +62,67 @@ export function dedupe<T>(input: T[], equals?: (a: any, b: any) => boolean): T[]
|
|
|
25
62
|
return result
|
|
26
63
|
}
|
|
27
64
|
|
|
28
|
-
/**
|
|
65
|
+
/**
|
|
66
|
+
* Remove null and undefined values from an array.
|
|
67
|
+
*
|
|
68
|
+
* Creates a new array with all null and undefined values filtered out.
|
|
69
|
+
* The resulting array has a refined type that excludes null and undefined.
|
|
70
|
+
*
|
|
71
|
+
* @param arr - The array to compact
|
|
72
|
+
* @returns A new array with null and undefined values removed
|
|
73
|
+
*
|
|
74
|
+
* @example
|
|
75
|
+
* ```ts
|
|
76
|
+
* compact([1, null, 2, undefined, 3]) // [1, 2, 3]
|
|
77
|
+
* compact(['a', null, 'b', undefined]) // ['a', 'b']
|
|
78
|
+
* ```
|
|
79
|
+
* @internal
|
|
80
|
+
*/
|
|
29
81
|
export function compact<T>(arr: T[]): NonNullable<T>[] {
|
|
30
82
|
return arr.filter((i) => i !== undefined && i !== null) as any
|
|
31
83
|
}
|
|
32
84
|
|
|
33
|
-
/**
|
|
85
|
+
/**
|
|
86
|
+
* Get the last element of an array.
|
|
87
|
+
*
|
|
88
|
+
* Returns the last element of an array, or undefined if the array is empty.
|
|
89
|
+
* Works with readonly arrays and preserves the element type.
|
|
90
|
+
*
|
|
91
|
+
* @param arr - The array to get the last element from
|
|
92
|
+
* @returns The last element of the array, or undefined if the array is empty
|
|
93
|
+
*
|
|
94
|
+
* @example
|
|
95
|
+
* ```ts
|
|
96
|
+
* last([1, 2, 3]) // 3
|
|
97
|
+
* last(['a', 'b', 'c']) // 'c'
|
|
98
|
+
* last([]) // undefined
|
|
99
|
+
* ```
|
|
100
|
+
* @internal
|
|
101
|
+
*/
|
|
34
102
|
export function last<T>(arr: readonly T[]): T | undefined {
|
|
35
103
|
return arr[arr.length - 1]
|
|
36
104
|
}
|
|
37
105
|
|
|
38
|
-
/**
|
|
106
|
+
/**
|
|
107
|
+
* Find the item in an array with the minimum value according to a function.
|
|
108
|
+
*
|
|
109
|
+
* Finds the array item that produces the smallest value when passed through
|
|
110
|
+
* the provided function. Returns undefined for empty arrays.
|
|
111
|
+
*
|
|
112
|
+
* @param arr - The array to search
|
|
113
|
+
* @param fn - Function to compute the comparison value for each item
|
|
114
|
+
* @returns The item with the minimum value, or undefined if the array is empty
|
|
115
|
+
*
|
|
116
|
+
* @example
|
|
117
|
+
* ```ts
|
|
118
|
+
* const people = [{name: 'Alice', age: 30}, {name: 'Bob', age: 25}]
|
|
119
|
+
* minBy(people, p => p.age) // {name: 'Bob', age: 25}
|
|
120
|
+
*
|
|
121
|
+
* minBy([3, 1, 4, 1, 5], x => x) // 1
|
|
122
|
+
* minBy([], x => x) // undefined
|
|
123
|
+
* ```
|
|
124
|
+
* @internal
|
|
125
|
+
*/
|
|
39
126
|
export function minBy<T>(arr: readonly T[], fn: (item: T) => number): T | undefined {
|
|
40
127
|
let min: T | undefined
|
|
41
128
|
let minVal = Infinity
|
|
@@ -49,7 +136,26 @@ export function minBy<T>(arr: readonly T[], fn: (item: T) => number): T | undefi
|
|
|
49
136
|
return min
|
|
50
137
|
}
|
|
51
138
|
|
|
52
|
-
/**
|
|
139
|
+
/**
|
|
140
|
+
* Find the item in an array with the maximum value according to a function.
|
|
141
|
+
*
|
|
142
|
+
* Finds the array item that produces the largest value when passed through
|
|
143
|
+
* the provided function. Returns undefined for empty arrays.
|
|
144
|
+
*
|
|
145
|
+
* @param arr - The array to search
|
|
146
|
+
* @param fn - Function to compute the comparison value for each item
|
|
147
|
+
* @returns The item with the maximum value, or undefined if the array is empty
|
|
148
|
+
*
|
|
149
|
+
* @example
|
|
150
|
+
* ```ts
|
|
151
|
+
* const people = [{name: 'Alice', age: 30}, {name: 'Bob', age: 25}]
|
|
152
|
+
* maxBy(people, p => p.age) // {name: 'Alice', age: 30}
|
|
153
|
+
*
|
|
154
|
+
* maxBy([3, 1, 4, 1, 5], x => x) // 5
|
|
155
|
+
* maxBy([], x => x) // undefined
|
|
156
|
+
* ```
|
|
157
|
+
* @internal
|
|
158
|
+
*/
|
|
53
159
|
export function maxBy<T>(arr: readonly T[], fn: (item: T) => number): T | undefined {
|
|
54
160
|
let max: T | undefined
|
|
55
161
|
let maxVal: number = -Infinity
|
|
@@ -64,13 +170,26 @@ export function maxBy<T>(arr: readonly T[], fn: (item: T) => number): T | undefi
|
|
|
64
170
|
}
|
|
65
171
|
|
|
66
172
|
/**
|
|
67
|
-
*
|
|
68
|
-
*
|
|
173
|
+
* Split an array into two arrays based on a predicate function.
|
|
174
|
+
*
|
|
175
|
+
* Partitions an array into two arrays: one containing items that satisfy
|
|
176
|
+
* the predicate, and another containing items that do not. The original array order is preserved.
|
|
69
177
|
*
|
|
70
178
|
* @param arr - The array to partition
|
|
71
|
-
* @param predicate - The predicate
|
|
72
|
-
* @returns A tuple of two arrays
|
|
73
|
-
*
|
|
179
|
+
* @param predicate - The predicate function to test each item
|
|
180
|
+
* @returns A tuple of two arrays: [satisfying items, non-satisfying items]
|
|
181
|
+
*
|
|
182
|
+
* @example
|
|
183
|
+
* ```ts
|
|
184
|
+
* const [evens, odds] = partition([1, 2, 3, 4, 5], x => x % 2 === 0)
|
|
185
|
+
* // evens: [2, 4], odds: [1, 3, 5]
|
|
186
|
+
*
|
|
187
|
+
* const [adults, minors] = partition(
|
|
188
|
+
* [{name: 'Alice', age: 30}, {name: 'Bob', age: 17}],
|
|
189
|
+
* person => person.age >= 18
|
|
190
|
+
* )
|
|
191
|
+
* // adults: [{name: 'Alice', age: 30}], minors: [{name: 'Bob', age: 17}]
|
|
192
|
+
* ```
|
|
74
193
|
* @internal
|
|
75
194
|
*/
|
|
76
195
|
export function partition<T>(arr: T[], predicate: (item: T) => boolean): [T[], T[]] {
|
|
@@ -86,7 +205,30 @@ export function partition<T>(arr: T[], predicate: (item: T) => boolean): [T[], T
|
|
|
86
205
|
return [satisfies, doesNotSatisfy]
|
|
87
206
|
}
|
|
88
207
|
|
|
89
|
-
/**
|
|
208
|
+
/**
|
|
209
|
+
* Check if two arrays are shallow equal.
|
|
210
|
+
*
|
|
211
|
+
* Compares two arrays for shallow equality by checking if they have the same length
|
|
212
|
+
* and the same elements at each index using Object.is comparison. Returns true if arrays are
|
|
213
|
+
* the same reference, have different lengths, or any elements differ.
|
|
214
|
+
*
|
|
215
|
+
* @param arr1 - First array to compare
|
|
216
|
+
* @param arr2 - Second array to compare
|
|
217
|
+
* @returns True if arrays are shallow equal, false otherwise
|
|
218
|
+
*
|
|
219
|
+
* @example
|
|
220
|
+
* ```ts
|
|
221
|
+
* areArraysShallowEqual([1, 2, 3], [1, 2, 3]) // true
|
|
222
|
+
* areArraysShallowEqual([1, 2, 3], [1, 2, 4]) // false
|
|
223
|
+
* areArraysShallowEqual(['a', 'b'], ['a', 'b']) // true
|
|
224
|
+
* areArraysShallowEqual([1, 2], [1, 2, 3]) // false
|
|
225
|
+
*
|
|
226
|
+
* const obj = {x: 1}
|
|
227
|
+
* areArraysShallowEqual([obj], [obj]) // true (same reference)
|
|
228
|
+
* areArraysShallowEqual([{x: 1}], [{x: 1}]) // false (different objects)
|
|
229
|
+
* ```
|
|
230
|
+
* @internal
|
|
231
|
+
*/
|
|
90
232
|
export function areArraysShallowEqual<T>(arr1: readonly T[], arr2: readonly T[]): boolean {
|
|
91
233
|
if (arr1 === arr2) return true
|
|
92
234
|
if (arr1.length !== arr2.length) return false
|
|
@@ -98,7 +240,34 @@ export function areArraysShallowEqual<T>(arr1: readonly T[], arr2: readonly T[])
|
|
|
98
240
|
return true
|
|
99
241
|
}
|
|
100
242
|
|
|
101
|
-
/**
|
|
243
|
+
/**
|
|
244
|
+
* Merge custom entries with defaults, replacing defaults that have matching keys.
|
|
245
|
+
*
|
|
246
|
+
* Combines two arrays by keeping all custom entries and only the default entries
|
|
247
|
+
* that don't have a matching key in the custom entries. Custom entries always override defaults.
|
|
248
|
+
* The result contains remaining defaults first, followed by all custom entries.
|
|
249
|
+
*
|
|
250
|
+
* @param key - The property name to use as the unique identifier
|
|
251
|
+
* @param customEntries - Array of custom entries that will override defaults
|
|
252
|
+
* @param defaults - Array of default entries
|
|
253
|
+
* @returns A new array with defaults filtered out where custom entries exist, plus all custom entries
|
|
254
|
+
*
|
|
255
|
+
* @example
|
|
256
|
+
* ```ts
|
|
257
|
+
* const defaults = [{type: 'text', value: 'default'}, {type: 'number', value: 0}]
|
|
258
|
+
* const custom = [{type: 'text', value: 'custom'}]
|
|
259
|
+
*
|
|
260
|
+
* mergeArraysAndReplaceDefaults('type', custom, defaults)
|
|
261
|
+
* // Result: [{type: 'number', value: 0}, {type: 'text', value: 'custom'}]
|
|
262
|
+
*
|
|
263
|
+
* const tools = [{id: 'select', name: 'Select'}, {id: 'draw', name: 'Draw'}]
|
|
264
|
+
* const customTools = [{id: 'select', name: 'Custom Select'}]
|
|
265
|
+
*
|
|
266
|
+
* mergeArraysAndReplaceDefaults('id', customTools, tools)
|
|
267
|
+
* // Result: [{id: 'draw', name: 'Draw'}, {id: 'select', name: 'Custom Select'}]
|
|
268
|
+
* ```
|
|
269
|
+
* @internal
|
|
270
|
+
*/
|
|
102
271
|
export function mergeArraysAndReplaceDefaults<
|
|
103
272
|
const Key extends string,
|
|
104
273
|
T extends { [K in Key]: string },
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { bind } from './bind'
|
|
3
|
+
|
|
4
|
+
describe('bind decorator', () => {
|
|
5
|
+
describe('legacy TypeScript decorator format', () => {
|
|
6
|
+
it('should maintain method binding when extracted', () => {
|
|
7
|
+
class TestClass {
|
|
8
|
+
value = 42
|
|
9
|
+
|
|
10
|
+
@bind
|
|
11
|
+
getValue() {
|
|
12
|
+
return this.value
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const instance = new TestClass()
|
|
17
|
+
const { getValue } = instance
|
|
18
|
+
|
|
19
|
+
expect(getValue()).toBe(42)
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
it('should work with methods that modify instance state', () => {
|
|
23
|
+
class TestClass {
|
|
24
|
+
count = 0
|
|
25
|
+
|
|
26
|
+
@bind
|
|
27
|
+
increment() {
|
|
28
|
+
this.count++
|
|
29
|
+
return this.count
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const instance = new TestClass()
|
|
34
|
+
const { increment } = instance
|
|
35
|
+
|
|
36
|
+
expect(increment()).toBe(1)
|
|
37
|
+
expect(increment()).toBe(2)
|
|
38
|
+
expect(instance.count).toBe(2)
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it('should throw error when decorating non-method', () => {
|
|
42
|
+
expect(() => {
|
|
43
|
+
bind({}, 'notAMethod', { value: 'not a function', configurable: true } as any)
|
|
44
|
+
}).toThrow('Only methods can be decorated with @bind. <notAMethod> is not a method!')
|
|
45
|
+
})
|
|
46
|
+
})
|
|
47
|
+
})
|
package/src/lib/bind.ts
CHANGED
|
@@ -6,9 +6,29 @@
|
|
|
6
6
|
import { assert } from './control'
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
|
-
*
|
|
10
|
-
*
|
|
9
|
+
* Decorator that binds a method to its class instance (legacy stage-2 TypeScript decorators).
|
|
10
|
+
* When applied to a class method, ensures `this` always refers to the class instance,
|
|
11
|
+
* even when the method is called as a callback or event handler.
|
|
11
12
|
*
|
|
13
|
+
* @param target - The prototype of the class being decorated
|
|
14
|
+
* @param propertyKey - The name of the method being decorated
|
|
15
|
+
* @param descriptor - The property descriptor for the method being decorated
|
|
16
|
+
* @returns The modified property descriptor with bound method access
|
|
17
|
+
* @example
|
|
18
|
+
* ```typescript
|
|
19
|
+
* class MyClass {
|
|
20
|
+
* name = 'example';
|
|
21
|
+
*
|
|
22
|
+
* @bind
|
|
23
|
+
* getName() {
|
|
24
|
+
* return this.name;
|
|
25
|
+
* }
|
|
26
|
+
* }
|
|
27
|
+
*
|
|
28
|
+
* const instance = new MyClass();
|
|
29
|
+
* const callback = instance.getName;
|
|
30
|
+
* console.log(callback()); // 'example' (this is properly bound)
|
|
31
|
+
* ```
|
|
12
32
|
* @public
|
|
13
33
|
*/
|
|
14
34
|
export function bind<T extends (...args: any[]) => any>(
|
|
@@ -18,8 +38,26 @@ export function bind<T extends (...args: any[]) => any>(
|
|
|
18
38
|
): TypedPropertyDescriptor<T>
|
|
19
39
|
|
|
20
40
|
/**
|
|
21
|
-
*
|
|
41
|
+
* Decorator that binds a method to its class instance (TC39 decorators standard).
|
|
42
|
+
* When applied to a class method, ensures `this` always refers to the class instance,
|
|
43
|
+
* even when the method is called as a callback or event handler.
|
|
44
|
+
*
|
|
45
|
+
* @param originalMethod - The original method being decorated
|
|
46
|
+
* @param context - The decorator context containing metadata about the method
|
|
47
|
+
* @example
|
|
48
|
+
* ```typescript
|
|
49
|
+
* class EventHandler {
|
|
50
|
+
* message = 'Hello World';
|
|
22
51
|
*
|
|
52
|
+
* @bind
|
|
53
|
+
* handleClick() {
|
|
54
|
+
* console.log(this.message);
|
|
55
|
+
* }
|
|
56
|
+
* }
|
|
57
|
+
*
|
|
58
|
+
* const handler = new EventHandler();
|
|
59
|
+
* document.addEventListener('click', handler.handleClick); // 'this' is properly bound
|
|
60
|
+
* ```
|
|
23
61
|
* @public
|
|
24
62
|
*/
|
|
25
63
|
export function bind<This extends object, T extends (...args: any[]) => any>(
|
|
@@ -27,7 +65,34 @@ export function bind<This extends object, T extends (...args: any[]) => any>(
|
|
|
27
65
|
context: ClassMethodDecoratorContext<This, T>
|
|
28
66
|
): void
|
|
29
67
|
|
|
30
|
-
/**
|
|
68
|
+
/**
|
|
69
|
+
* Universal decorator implementation that handles both legacy stage-2 and TC39 decorator formats.
|
|
70
|
+
* Automatically detects the decorator format based on the number of arguments and binds the
|
|
71
|
+
* decorated method to the class instance, preventing common `this` context issues.
|
|
72
|
+
*
|
|
73
|
+
* @param args - Either legacy decorator arguments (target, propertyKey, descriptor) or TC39 decorator arguments (originalMethod, context)
|
|
74
|
+
* @returns Property descriptor for legacy decorators, or void for TC39 decorators
|
|
75
|
+
* @example
|
|
76
|
+
* ```typescript
|
|
77
|
+
* // Works with both decorator formats
|
|
78
|
+
* class Calculator {
|
|
79
|
+
* multiplier = 2;
|
|
80
|
+
*
|
|
81
|
+
* @bind
|
|
82
|
+
* multiply(value: number) {
|
|
83
|
+
* return value * this.multiplier;
|
|
84
|
+
* }
|
|
85
|
+
* }
|
|
86
|
+
*
|
|
87
|
+
* const calc = new Calculator();
|
|
88
|
+
* const multiplyFn = calc.multiply;
|
|
89
|
+
* console.log(multiplyFn(5)); // 10 (this.multiplier is accessible)
|
|
90
|
+
*
|
|
91
|
+
* // Useful for event handlers and callbacks
|
|
92
|
+
* setTimeout(calc.multiply, 100, 3); // 6
|
|
93
|
+
* ```
|
|
94
|
+
* @public
|
|
95
|
+
*/
|
|
31
96
|
export function bind(
|
|
32
97
|
...args: // legacy stage-2 typescript decorators
|
|
33
98
|
| [_target: object, propertyKey: string, descriptor: PropertyDescriptor]
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { vi } from 'vitest'
|
|
2
|
+
import { WeakCache } from './cache'
|
|
3
|
+
|
|
4
|
+
describe('WeakCache', () => {
|
|
5
|
+
it('should compute and cache value on first call, return cached value on subsequent calls', () => {
|
|
6
|
+
const cache = new WeakCache<{ id: number }, string>()
|
|
7
|
+
const key = { id: 1 }
|
|
8
|
+
const callback = vi.fn((item: { id: number }) => `value-${item.id}`)
|
|
9
|
+
|
|
10
|
+
const result1 = cache.get(key, callback)
|
|
11
|
+
const result2 = cache.get(key, callback)
|
|
12
|
+
|
|
13
|
+
expect(result1).toBe('value-1')
|
|
14
|
+
expect(result2).toBe('value-1')
|
|
15
|
+
expect(result1).toBe(result2) // Same reference
|
|
16
|
+
expect(callback).toHaveBeenCalledTimes(1)
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
it('should handle different callbacks for the same key', () => {
|
|
20
|
+
const cache = new WeakCache<{ id: number }, string>()
|
|
21
|
+
const key = { id: 1 }
|
|
22
|
+
const callback1 = vi.fn(() => 'first-computation')
|
|
23
|
+
const callback2 = vi.fn(() => 'second-computation')
|
|
24
|
+
|
|
25
|
+
const result1 = cache.get(key, callback1)
|
|
26
|
+
const result2 = cache.get(key, callback2)
|
|
27
|
+
|
|
28
|
+
expect(result1).toBe('first-computation')
|
|
29
|
+
expect(result2).toBe('first-computation')
|
|
30
|
+
expect(callback1).toHaveBeenCalledTimes(1)
|
|
31
|
+
expect(callback2).not.toHaveBeenCalled()
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it('should maintain separate cache entries for different object references', () => {
|
|
35
|
+
const cache = new WeakCache<{ id: number }, string>()
|
|
36
|
+
const key1 = { id: 1 }
|
|
37
|
+
const key2 = { id: 1 } // Different object with same properties
|
|
38
|
+
const callback = vi.fn((item: { id: number }) => `value-${item.id}`)
|
|
39
|
+
|
|
40
|
+
const _result1 = cache.get(key1, callback)
|
|
41
|
+
const _result2 = cache.get(key2, callback)
|
|
42
|
+
|
|
43
|
+
expect(callback).toHaveBeenCalledTimes(2)
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
it('should handle null and undefined return values', () => {
|
|
47
|
+
const cache = new WeakCache<object, string | null | undefined>()
|
|
48
|
+
const key1 = { type: 'null' }
|
|
49
|
+
const key2 = { type: 'undefined' }
|
|
50
|
+
|
|
51
|
+
const result1 = cache.get(key1, () => null)
|
|
52
|
+
const result2 = cache.get(key2, () => undefined)
|
|
53
|
+
const result3 = cache.get(key1, () => 'should-not-be-called')
|
|
54
|
+
const result4 = cache.get(key2, () => 'should-not-be-called')
|
|
55
|
+
|
|
56
|
+
expect(result1).toBe(null)
|
|
57
|
+
expect(result2).toBe(undefined)
|
|
58
|
+
expect(result3).toBe(null)
|
|
59
|
+
expect(result4).toBe(undefined)
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
it('should not cache errors - callbacks that throw should be re-executed', () => {
|
|
63
|
+
const cache = new WeakCache<object, string>()
|
|
64
|
+
const key = { id: 1 }
|
|
65
|
+
const errorCallback = vi.fn(() => {
|
|
66
|
+
throw new Error('Computation failed')
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
expect(() => cache.get(key, errorCallback)).toThrow('Computation failed')
|
|
70
|
+
expect(() => cache.get(key, errorCallback)).toThrow('Computation failed')
|
|
71
|
+
expect(errorCallback).toHaveBeenCalledTimes(2)
|
|
72
|
+
})
|
|
73
|
+
})
|
package/src/lib/cache.ts
CHANGED
|
@@ -1,17 +1,58 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* A
|
|
2
|
+
* A lightweight cache implementation using WeakMap for storing key-value pairs.
|
|
3
|
+
*
|
|
4
|
+
* A micro cache that stores computed values associated with object keys.
|
|
5
|
+
* Uses WeakMap internally, which means keys can be garbage collected when no other
|
|
6
|
+
* references exist, and only object keys are supported. Provides lazy computation
|
|
7
|
+
* with memoization.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```ts
|
|
11
|
+
* const cache = new WeakCache<User, string>()
|
|
12
|
+
* const user = { id: 1, name: 'Alice' }
|
|
13
|
+
*
|
|
14
|
+
* // Get cached value, computing it if not present
|
|
15
|
+
* const displayName = cache.get(user, (u) => `${u.name} (#${u.id})`)
|
|
16
|
+
* // Returns 'Alice (#1)'
|
|
17
|
+
*
|
|
18
|
+
* // Subsequent calls return cached value
|
|
19
|
+
* const sameName = cache.get(user, (u) => `${u.name} (#${u.id})`)
|
|
20
|
+
* // Returns 'Alice (#1)' without recomputing
|
|
21
|
+
* ```
|
|
3
22
|
* @public
|
|
4
23
|
*/
|
|
5
24
|
export class WeakCache<K extends object, V> {
|
|
6
|
-
/**
|
|
25
|
+
/**
|
|
26
|
+
* The internal WeakMap storage for cached key-value pairs.
|
|
27
|
+
*
|
|
28
|
+
* @public
|
|
29
|
+
*/
|
|
7
30
|
items = new WeakMap<K, V>()
|
|
8
31
|
|
|
9
32
|
/**
|
|
10
|
-
* Get the cached value for a given
|
|
11
|
-
*
|
|
33
|
+
* Get the cached value for a given key, computing it if not already cached.
|
|
34
|
+
*
|
|
35
|
+
* Retrieves the cached value associated with the given key. If no cached
|
|
36
|
+
* value exists, calls the provided callback function to compute the value, stores it
|
|
37
|
+
* in the cache, and returns it. Subsequent calls with the same key will return the
|
|
38
|
+
* cached value without recomputation.
|
|
39
|
+
*
|
|
40
|
+
* @param item - The object key to retrieve the cached value for
|
|
41
|
+
* @param cb - Callback function that computes the value when not already cached
|
|
42
|
+
* @returns The cached value if it exists, otherwise the newly computed value from the callback
|
|
43
|
+
*
|
|
44
|
+
* @example
|
|
45
|
+
* ```ts
|
|
46
|
+
* const cache = new WeakCache<HTMLElement, DOMRect>()
|
|
47
|
+
* const element = document.getElementById('my-element')!
|
|
48
|
+
*
|
|
49
|
+
* // First call computes and caches the bounding rect
|
|
50
|
+
* const rect1 = cache.get(element, (el) => el.getBoundingClientRect())
|
|
12
51
|
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
52
|
+
* // Second call returns cached value
|
|
53
|
+
* const rect2 = cache.get(element, (el) => el.getBoundingClientRect())
|
|
54
|
+
* // rect1 and rect2 are the same object
|
|
55
|
+
* ```
|
|
15
56
|
*/
|
|
16
57
|
get<P extends K>(item: P, cb: (item: P) => V) {
|
|
17
58
|
if (!this.items.has(item)) {
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { assert, assertExists, exhaustiveSwitchError, promiseWithResolve } from './control'
|
|
3
|
+
|
|
4
|
+
describe('exhaustiveSwitchError', () => {
|
|
5
|
+
it('should throw an error with the unhandled value', () => {
|
|
6
|
+
expect(() => exhaustiveSwitchError('unhandled' as never)).toThrow(
|
|
7
|
+
'Unknown switch case unhandled'
|
|
8
|
+
)
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
it('should throw with property value when provided', () => {
|
|
12
|
+
const value = { type: 'unknown', data: 'test' } as never
|
|
13
|
+
expect(() => exhaustiveSwitchError(value, 'type')).toThrow('Unknown switch case unknown')
|
|
14
|
+
})
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
describe('assert', () => {
|
|
18
|
+
it('should throw with custom message when provided', () => {
|
|
19
|
+
expect(() => assert(false, 'Custom error message')).toThrow('Custom error message')
|
|
20
|
+
expect(() => assert(null, 'Value cannot be null')).toThrow('Value cannot be null')
|
|
21
|
+
})
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
describe('assertExists', () => {
|
|
25
|
+
it('should throw with custom message when provided', () => {
|
|
26
|
+
expect(() => assertExists(null, 'Custom null message')).toThrow('Custom null message')
|
|
27
|
+
expect(() => assertExists(undefined, 'Custom undefined message')).toThrow(
|
|
28
|
+
'Custom undefined message'
|
|
29
|
+
)
|
|
30
|
+
})
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
describe('promiseWithResolve', () => {
|
|
34
|
+
it('should resolve when resolve is called', async () => {
|
|
35
|
+
const deferred = promiseWithResolve<string>()
|
|
36
|
+
|
|
37
|
+
setTimeout(() => deferred.resolve('resolved value'), 0)
|
|
38
|
+
|
|
39
|
+
const result = await deferred
|
|
40
|
+
expect(result).toBe('resolved value')
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
it('should reject when reject is called', async () => {
|
|
44
|
+
const deferred = promiseWithResolve<string>()
|
|
45
|
+
|
|
46
|
+
setTimeout(() => deferred.reject('rejected reason'), 0)
|
|
47
|
+
|
|
48
|
+
await expect(deferred).rejects.toBe('rejected reason')
|
|
49
|
+
})
|
|
50
|
+
})
|