@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/control.ts
CHANGED
|
@@ -1,36 +1,160 @@
|
|
|
1
1
|
import { omitFromStackTrace } from './function'
|
|
2
2
|
|
|
3
|
-
/**
|
|
3
|
+
/**
|
|
4
|
+
* Represents a successful result containing a value.
|
|
5
|
+
*
|
|
6
|
+
* Interface for the success case of a Result type, containing the computed value.
|
|
7
|
+
* Used in conjunction with ErrorResult to create a discriminated union for error handling.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```ts
|
|
11
|
+
* const success: OkResult<string> = { ok: true, value: 'Hello World' }
|
|
12
|
+
* if (success.ok) {
|
|
13
|
+
* console.log(success.value) // 'Hello World'
|
|
14
|
+
* }
|
|
15
|
+
* ```
|
|
16
|
+
* @public
|
|
17
|
+
*/
|
|
4
18
|
export interface OkResult<T> {
|
|
5
19
|
readonly ok: true
|
|
6
20
|
readonly value: T
|
|
7
21
|
}
|
|
8
|
-
/**
|
|
22
|
+
/**
|
|
23
|
+
* Represents a failed result containing an error.
|
|
24
|
+
*
|
|
25
|
+
* Interface for the error case of a Result type, containing the error information.
|
|
26
|
+
* Used in conjunction with OkResult to create a discriminated union for error handling.
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* ```ts
|
|
30
|
+
* const failure: ErrorResult<string> = { ok: false, error: 'Something went wrong' }
|
|
31
|
+
* if (!failure.ok) {
|
|
32
|
+
* console.error(failure.error) // 'Something went wrong'
|
|
33
|
+
* }
|
|
34
|
+
* ```
|
|
35
|
+
* @public
|
|
36
|
+
*/
|
|
9
37
|
export interface ErrorResult<E> {
|
|
10
38
|
readonly ok: false
|
|
11
39
|
readonly error: E
|
|
12
40
|
}
|
|
13
|
-
/**
|
|
41
|
+
/**
|
|
42
|
+
* A discriminated union type for handling success and error cases.
|
|
43
|
+
*
|
|
44
|
+
* Represents either a successful result with a value or a failed result with an error.
|
|
45
|
+
* This pattern provides type-safe error handling without throwing exceptions. The 'ok' property
|
|
46
|
+
* serves as the discriminant for type narrowing.
|
|
47
|
+
*
|
|
48
|
+
* @example
|
|
49
|
+
* ```ts
|
|
50
|
+
* function divide(a: number, b: number): Result<number, string> {
|
|
51
|
+
* if (b === 0) {
|
|
52
|
+
* return Result.err('Division by zero')
|
|
53
|
+
* }
|
|
54
|
+
* return Result.ok(a / b)
|
|
55
|
+
* }
|
|
56
|
+
*
|
|
57
|
+
* const result = divide(10, 2)
|
|
58
|
+
* if (result.ok) {
|
|
59
|
+
* console.log(`Result: ${result.value}`) // Result: 5
|
|
60
|
+
* } else {
|
|
61
|
+
* console.error(`Error: ${result.error}`)
|
|
62
|
+
* }
|
|
63
|
+
* ```
|
|
64
|
+
* @public
|
|
65
|
+
*/
|
|
14
66
|
export type Result<T, E> = OkResult<T> | ErrorResult<E>
|
|
15
67
|
|
|
16
|
-
/**
|
|
68
|
+
/**
|
|
69
|
+
* Utility object for creating Result instances.
|
|
70
|
+
*
|
|
71
|
+
* Provides factory methods for creating OkResult and ErrorResult instances.
|
|
72
|
+
* This is the preferred way to construct Result values for consistent structure.
|
|
73
|
+
*
|
|
74
|
+
* @example
|
|
75
|
+
* ```ts
|
|
76
|
+
* // Create success result
|
|
77
|
+
* const success = Result.ok(42)
|
|
78
|
+
* // success: OkResult<number> = { ok: true, value: 42 }
|
|
79
|
+
*
|
|
80
|
+
* // Create error result
|
|
81
|
+
* const failure = Result.err('Invalid input')
|
|
82
|
+
* // failure: ErrorResult<string> = { ok: false, error: 'Invalid input' }
|
|
83
|
+
* ```
|
|
84
|
+
* @public
|
|
85
|
+
*/
|
|
17
86
|
export const Result = {
|
|
87
|
+
/**
|
|
88
|
+
* Create a successful result containing a value.
|
|
89
|
+
*
|
|
90
|
+
* @param value - The success value to wrap
|
|
91
|
+
* @returns An OkResult containing the value
|
|
92
|
+
*/
|
|
18
93
|
ok<T>(value: T): OkResult<T> {
|
|
19
94
|
return { ok: true, value }
|
|
20
95
|
},
|
|
96
|
+
/**
|
|
97
|
+
* Create a failed result containing an error.
|
|
98
|
+
*
|
|
99
|
+
* @param error - The error value to wrap
|
|
100
|
+
* @returns An ErrorResult containing the error
|
|
101
|
+
*/
|
|
21
102
|
err<E>(error: E): ErrorResult<E> {
|
|
22
103
|
return { ok: false, error }
|
|
23
104
|
},
|
|
24
105
|
}
|
|
25
106
|
|
|
26
|
-
/**
|
|
107
|
+
/**
|
|
108
|
+
* Throws an error for unhandled switch cases in exhaustive switch statements.
|
|
109
|
+
*
|
|
110
|
+
* Utility function to ensure exhaustive handling of discriminated unions in switch
|
|
111
|
+
* statements. When called, it indicates a programming error where a case was not handled.
|
|
112
|
+
* The TypeScript 'never' type ensures this function is only reachable if all cases aren't covered.
|
|
113
|
+
*
|
|
114
|
+
* @param value - The unhandled value (typed as 'never' for exhaustiveness checking)
|
|
115
|
+
* @param property - Optional property name to extract from the value for better error messages
|
|
116
|
+
* @returns Never returns (always throws)
|
|
117
|
+
*
|
|
118
|
+
* @example
|
|
119
|
+
* ```ts
|
|
120
|
+
* type Shape = 'circle' | 'square' | 'triangle'
|
|
121
|
+
*
|
|
122
|
+
* function getArea(shape: Shape): number {
|
|
123
|
+
* switch (shape) {
|
|
124
|
+
* case 'circle': return Math.PI * 5 * 5
|
|
125
|
+
* case 'square': return 10 * 10
|
|
126
|
+
* case 'triangle': return 0.5 * 10 * 8
|
|
127
|
+
* default: return exhaustiveSwitchError(shape)
|
|
128
|
+
* }
|
|
129
|
+
* }
|
|
130
|
+
* ```
|
|
131
|
+
* @internal
|
|
132
|
+
*/
|
|
27
133
|
export function exhaustiveSwitchError(value: never, property?: string): never {
|
|
28
134
|
const debugValue =
|
|
29
135
|
property && value && typeof value === 'object' && property in value ? value[property] : value
|
|
30
136
|
throw new Error(`Unknown switch case ${debugValue}`)
|
|
31
137
|
}
|
|
32
138
|
|
|
33
|
-
/**
|
|
139
|
+
/**
|
|
140
|
+
* Assert that a value is truthy, throwing an error if it's not.
|
|
141
|
+
*
|
|
142
|
+
* TypeScript assertion function that throws an error if the provided value is falsy.
|
|
143
|
+
* After this function executes successfully, TypeScript narrows the type to exclude falsy values.
|
|
144
|
+
* Stack trace is omitted from the error for cleaner debugging.
|
|
145
|
+
*
|
|
146
|
+
* @param value - The value to assert as truthy
|
|
147
|
+
* @param message - Optional custom error message
|
|
148
|
+
*
|
|
149
|
+
* @example
|
|
150
|
+
* ```ts
|
|
151
|
+
* const user = getUser() // User | null
|
|
152
|
+
* assert(user, 'User must be logged in')
|
|
153
|
+
* // TypeScript now knows user is non-null
|
|
154
|
+
* console.log(user.name) // Safe to access properties
|
|
155
|
+
* ```
|
|
156
|
+
* @internal
|
|
157
|
+
*/
|
|
34
158
|
export const assert: (value: unknown, message?: string) => asserts value = omitFromStackTrace(
|
|
35
159
|
(value, message) => {
|
|
36
160
|
if (!value) {
|
|
@@ -39,7 +163,25 @@ export const assert: (value: unknown, message?: string) => asserts value = omitF
|
|
|
39
163
|
}
|
|
40
164
|
)
|
|
41
165
|
|
|
42
|
-
/**
|
|
166
|
+
/**
|
|
167
|
+
* Assert that a value is not null or undefined.
|
|
168
|
+
*
|
|
169
|
+
* Throws an error if the value is null or undefined, otherwise returns the value
|
|
170
|
+
* with a refined type that excludes null and undefined. Stack trace is omitted for cleaner debugging.
|
|
171
|
+
*
|
|
172
|
+
* @param value - The value to check for null/undefined
|
|
173
|
+
* @param message - Optional custom error message
|
|
174
|
+
* @returns The value with null and undefined excluded from the type
|
|
175
|
+
*
|
|
176
|
+
* @example
|
|
177
|
+
* ```ts
|
|
178
|
+
* const element = document.getElementById('my-id') // HTMLElement | null
|
|
179
|
+
* const safeElement = assertExists(element, 'Element not found')
|
|
180
|
+
* // TypeScript now knows safeElement is HTMLElement (not null)
|
|
181
|
+
* safeElement.addEventListener('click', handler) // Safe to call methods
|
|
182
|
+
* ```
|
|
183
|
+
* @internal
|
|
184
|
+
*/
|
|
43
185
|
export const assertExists = omitFromStackTrace(<T>(value: T, message?: string): NonNullable<T> => {
|
|
44
186
|
// note that value == null is equivalent to value === null || value === undefined
|
|
45
187
|
if (value == null) {
|
|
@@ -48,7 +190,30 @@ export const assertExists = omitFromStackTrace(<T>(value: T, message?: string):
|
|
|
48
190
|
return value as NonNullable<T>
|
|
49
191
|
})
|
|
50
192
|
|
|
51
|
-
/**
|
|
193
|
+
/**
|
|
194
|
+
* Create a Promise with externally accessible resolve and reject functions.
|
|
195
|
+
*
|
|
196
|
+
* Creates a Promise along with its resolve and reject functions exposed as
|
|
197
|
+
* properties on the returned object. This allows external code to control when the
|
|
198
|
+
* Promise resolves or rejects, useful for coordination between async operations.
|
|
199
|
+
*
|
|
200
|
+
* @returns A Promise object with additional resolve and reject methods
|
|
201
|
+
*
|
|
202
|
+
* @example
|
|
203
|
+
* ```ts
|
|
204
|
+
* const deferred = promiseWithResolve<string>()
|
|
205
|
+
*
|
|
206
|
+
* // Set up the promise consumer
|
|
207
|
+
* deferred.then(value => console.log(`Resolved: ${value}`))
|
|
208
|
+
* deferred.catch(error => console.error(`Rejected: ${error}`))
|
|
209
|
+
*
|
|
210
|
+
* // Later, resolve from external code
|
|
211
|
+
* setTimeout(() => {
|
|
212
|
+
* deferred.resolve('Hello World')
|
|
213
|
+
* }, 1000)
|
|
214
|
+
* ```
|
|
215
|
+
* @internal
|
|
216
|
+
*/
|
|
52
217
|
export function promiseWithResolve<T>(): Promise<T> & {
|
|
53
218
|
resolve(value: T): void
|
|
54
219
|
reject(reason?: any): void
|
|
@@ -65,7 +230,31 @@ export function promiseWithResolve<T>(): Promise<T> & {
|
|
|
65
230
|
})
|
|
66
231
|
}
|
|
67
232
|
|
|
68
|
-
/**
|
|
233
|
+
/**
|
|
234
|
+
* Create a Promise that resolves after a specified delay.
|
|
235
|
+
*
|
|
236
|
+
* Utility function for introducing delays in async code. Returns a Promise
|
|
237
|
+
* that resolves with undefined after the specified number of milliseconds. Useful for
|
|
238
|
+
* implementing timeouts, rate limiting, or adding delays in testing scenarios.
|
|
239
|
+
*
|
|
240
|
+
* @param ms - The delay in milliseconds
|
|
241
|
+
* @returns A Promise that resolves after the specified delay
|
|
242
|
+
*
|
|
243
|
+
* @example
|
|
244
|
+
* ```ts
|
|
245
|
+
* async function delayedOperation() {
|
|
246
|
+
* console.log('Starting...')
|
|
247
|
+
* await sleep(1000) // Wait 1 second
|
|
248
|
+
* console.log('Done!')
|
|
249
|
+
* }
|
|
250
|
+
*
|
|
251
|
+
* // Can also be used with .then()
|
|
252
|
+
* sleep(500).then(() => {
|
|
253
|
+
* console.log('Half second has passed')
|
|
254
|
+
* })
|
|
255
|
+
* ```
|
|
256
|
+
* @internal
|
|
257
|
+
*/
|
|
69
258
|
export function sleep(ms: number): Promise<void> {
|
|
70
259
|
// eslint-disable-next-line no-restricted-globals
|
|
71
260
|
return new Promise((resolve) => setTimeout(resolve, ms))
|
package/src/lib/debounce.ts
CHANGED
|
@@ -1,10 +1,35 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Create a debounced version of a function that delays execution until after a specified wait time.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* Debouncing ensures that a function is only executed once after a specified delay,
|
|
5
|
+
* even if called multiple times in rapid succession. Each new call resets the timer. The debounced
|
|
6
|
+
* function returns a Promise that resolves with the result of the original function. Includes a
|
|
7
|
+
* cancel method to prevent execution if needed.
|
|
8
|
+
*
|
|
9
|
+
* @param callback - The function to debounce (can be sync or async)
|
|
10
|
+
* @param wait - The delay in milliseconds before executing the function
|
|
11
|
+
* @returns A debounced function that returns a Promise and includes a cancel method
|
|
5
12
|
*
|
|
13
|
+
* @example
|
|
6
14
|
* ```ts
|
|
7
|
-
*
|
|
15
|
+
* // Debounce a search function
|
|
16
|
+
* const searchAPI = (query: string) => fetch(`/search?q=${query}`)
|
|
17
|
+
* const debouncedSearch = debounce(searchAPI, 300)
|
|
18
|
+
*
|
|
19
|
+
* // Multiple rapid calls will only execute the last one after 300ms
|
|
20
|
+
* debouncedSearch('react').then(result => console.log(result))
|
|
21
|
+
* debouncedSearch('react hooks') // This cancels the previous call
|
|
22
|
+
* debouncedSearch('react typescript') // Only this will execute
|
|
23
|
+
*
|
|
24
|
+
* // Cancel pending execution
|
|
25
|
+
* debouncedSearch.cancel()
|
|
26
|
+
*
|
|
27
|
+
* // With async/await
|
|
28
|
+
* const saveData = debounce(async (data: any) => {
|
|
29
|
+
* return await api.save(data)
|
|
30
|
+
* }, 1000)
|
|
31
|
+
*
|
|
32
|
+
* const result = await saveData({name: 'John'})
|
|
8
33
|
* ```
|
|
9
34
|
*
|
|
10
35
|
* @public
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { annotateError, getErrorAnnotations } from './error'
|
|
3
|
+
|
|
4
|
+
describe('annotateError', () => {
|
|
5
|
+
it('should annotate an Error object with tags and extras', () => {
|
|
6
|
+
const error = new Error('test error')
|
|
7
|
+
annotateError(error, {
|
|
8
|
+
tags: { userId: '123', operation: 'save' },
|
|
9
|
+
extras: { timestamp: 1234567890, data: { key: 'value' } },
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
const annotations = getErrorAnnotations(error)
|
|
13
|
+
expect(annotations.tags.userId).toBe('123')
|
|
14
|
+
expect(annotations.tags.operation).toBe('save')
|
|
15
|
+
expect(annotations.extras.timestamp).toBe(1234567890)
|
|
16
|
+
expect(annotations.extras.data).toEqual({ key: 'value' })
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
it('should merge annotations from multiple calls', () => {
|
|
20
|
+
const error = new Error('test error')
|
|
21
|
+
|
|
22
|
+
annotateError(error, {
|
|
23
|
+
tags: { userId: '123', operation: 'save' },
|
|
24
|
+
extras: { timestamp: 1234567890 },
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
annotateError(error, {
|
|
28
|
+
tags: { userId: '456', component: 'auth' }, // userId should be overwritten
|
|
29
|
+
extras: { requestId: 'req456' },
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
const annotations = getErrorAnnotations(error)
|
|
33
|
+
expect(annotations.tags).toEqual({
|
|
34
|
+
userId: '456', // overwritten
|
|
35
|
+
operation: 'save', // preserved
|
|
36
|
+
component: 'auth', // new
|
|
37
|
+
})
|
|
38
|
+
expect(annotations.extras).toEqual({
|
|
39
|
+
timestamp: 1234567890, // preserved
|
|
40
|
+
requestId: 'req456', // new
|
|
41
|
+
})
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it('should handle non-object inputs gracefully', () => {
|
|
45
|
+
expect(() => annotateError(null, { tags: { test: 'value' } })).not.toThrow()
|
|
46
|
+
expect(() => annotateError('string error', { tags: { test: 'value' } })).not.toThrow()
|
|
47
|
+
})
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
describe('getErrorAnnotations', () => {
|
|
51
|
+
it('should return empty annotations for unannotated errors', () => {
|
|
52
|
+
const error = new Error('test error')
|
|
53
|
+
const annotations = getErrorAnnotations(error)
|
|
54
|
+
|
|
55
|
+
expect(annotations).toEqual({
|
|
56
|
+
tags: {},
|
|
57
|
+
extras: {},
|
|
58
|
+
})
|
|
59
|
+
})
|
|
60
|
+
})
|
package/src/lib/error.ts
CHANGED
|
@@ -10,6 +10,18 @@ const annotationsByError = new WeakMap<object, ErrorAnnotations>()
|
|
|
10
10
|
* Annotate an error with tags and additional data. Annotations won't overwrite existing ones.
|
|
11
11
|
* Retrieve them with `getErrorAnnotations`.
|
|
12
12
|
*
|
|
13
|
+
* @param error - The error object to annotate
|
|
14
|
+
* @param annotations - Partial annotations to add (tags and/or extras)
|
|
15
|
+
* @returns void
|
|
16
|
+
* @example
|
|
17
|
+
* ```ts
|
|
18
|
+
* const error = new Error('Something went wrong')
|
|
19
|
+
* annotateError(error, {
|
|
20
|
+
* tags: { userId: '123', operation: 'save' },
|
|
21
|
+
* extras: { timestamp: Date.now() }
|
|
22
|
+
* })
|
|
23
|
+
* ```
|
|
24
|
+
*
|
|
13
25
|
* @internal
|
|
14
26
|
*/
|
|
15
27
|
export function annotateError(error: unknown, annotations: Partial<ErrorAnnotations>) {
|
|
@@ -35,7 +47,21 @@ export function annotateError(error: unknown, annotations: Partial<ErrorAnnotati
|
|
|
35
47
|
}
|
|
36
48
|
}
|
|
37
49
|
|
|
38
|
-
/**
|
|
50
|
+
/**
|
|
51
|
+
* Retrieve annotations that have been added to an error object.
|
|
52
|
+
*
|
|
53
|
+
* @param error - The error object to get annotations from
|
|
54
|
+
* @returns The error annotations (tags and extras) or empty objects if none exist
|
|
55
|
+
* @example
|
|
56
|
+
* ```ts
|
|
57
|
+
* const error = new Error('Something went wrong')
|
|
58
|
+
* annotateError(error, { tags: { userId: '123' } })
|
|
59
|
+
* const annotations = getErrorAnnotations(error)
|
|
60
|
+
* console.log(annotations.tags.userId) // '123'
|
|
61
|
+
* ```
|
|
62
|
+
*
|
|
63
|
+
* @internal
|
|
64
|
+
*/
|
|
39
65
|
export function getErrorAnnotations(error: Error): ErrorAnnotations {
|
|
40
66
|
return annotationsByError.get(error) ?? { tags: {}, extras: {} }
|
|
41
67
|
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { FileHelpers } from './file'
|
|
3
|
+
|
|
4
|
+
describe('FileHelpers', () => {
|
|
5
|
+
describe('urlToDataUrl', () => {
|
|
6
|
+
it('should return data URL unchanged if already a data URL', async () => {
|
|
7
|
+
const dataUrl = 'data:text/plain;base64,SGVsbG8gV29ybGQ='
|
|
8
|
+
const result = await FileHelpers.urlToDataUrl(dataUrl)
|
|
9
|
+
|
|
10
|
+
expect(result).toBe(dataUrl)
|
|
11
|
+
})
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
describe('rewriteMimeType', () => {
|
|
15
|
+
it('should return the same blob if MIME type matches', () => {
|
|
16
|
+
const blob = new Blob(['content'], { type: 'text/plain' })
|
|
17
|
+
const result = FileHelpers.rewriteMimeType(blob, 'text/plain')
|
|
18
|
+
|
|
19
|
+
expect(result).toBe(blob)
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
it('should create new blob with different MIME type', () => {
|
|
23
|
+
const blob = new Blob(['content'], { type: 'text/plain' })
|
|
24
|
+
const result = FileHelpers.rewriteMimeType(blob, 'application/json')
|
|
25
|
+
|
|
26
|
+
expect(result).not.toBe(blob)
|
|
27
|
+
expect(result.type).toBe('application/json')
|
|
28
|
+
expect(result.size).toBe(blob.size)
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
it('should return the same file if MIME type matches', () => {
|
|
32
|
+
const file = new File(['content'], 'test.txt', { type: 'text/plain' })
|
|
33
|
+
const result = FileHelpers.rewriteMimeType(file, 'text/plain')
|
|
34
|
+
|
|
35
|
+
expect(result).toBe(file)
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
it('should create new file with different MIME type and preserve name', () => {
|
|
39
|
+
const file = new File(['content'], 'test.txt', { type: 'text/plain' })
|
|
40
|
+
const result = FileHelpers.rewriteMimeType(file, 'application/json') as File
|
|
41
|
+
|
|
42
|
+
expect(result).not.toBe(file)
|
|
43
|
+
expect(result.type).toBe('application/json')
|
|
44
|
+
expect(result.name).toBe('test.txt')
|
|
45
|
+
expect(result.size).toBe(file.size)
|
|
46
|
+
expect(result).toBeInstanceOf(File)
|
|
47
|
+
})
|
|
48
|
+
})
|
|
49
|
+
})
|
package/src/lib/file.ts
CHANGED
|
@@ -1,24 +1,87 @@
|
|
|
1
1
|
import { fetch } from './network'
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
4
|
+
* Utility class providing helper methods for file and blob operations.
|
|
5
|
+
*
|
|
6
|
+
* FileHelpers contains static methods for common file operations including
|
|
7
|
+
* URL fetching, format conversion, and MIME type manipulation. All methods work with
|
|
8
|
+
* web APIs like fetch, FileReader, and Blob/File objects.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```ts
|
|
12
|
+
* // Fetch and convert a remote image to data URL
|
|
13
|
+
* const dataUrl = await FileHelpers.urlToDataUrl('https://example.com/image.png')
|
|
14
|
+
*
|
|
15
|
+
* // Convert user-selected file to text
|
|
16
|
+
* const text = await FileHelpers.blobToText(userFile)
|
|
17
|
+
*
|
|
18
|
+
* // Change file MIME type
|
|
19
|
+
* const newFile = FileHelpers.rewriteMimeType(originalFile, 'application/json')
|
|
20
|
+
* ```
|
|
5
21
|
*
|
|
6
22
|
* @public
|
|
7
23
|
*/
|
|
8
24
|
export class FileHelpers {
|
|
9
25
|
/**
|
|
10
|
-
*
|
|
26
|
+
* Converts a URL to an ArrayBuffer by fetching the resource.
|
|
27
|
+
*
|
|
28
|
+
* Fetches the resource at the given URL and returns its content as an ArrayBuffer.
|
|
29
|
+
* This is useful for loading binary data like images, videos, or other file types.
|
|
30
|
+
*
|
|
31
|
+
* @param url - The URL of the file to fetch
|
|
32
|
+
* @returns Promise that resolves to the file content as an ArrayBuffer
|
|
33
|
+
* @example
|
|
34
|
+
* ```ts
|
|
35
|
+
* const buffer = await FileHelpers.urlToArrayBuffer('https://example.com/image.png')
|
|
36
|
+
* console.log(buffer.byteLength) // Size of the file in bytes
|
|
37
|
+
* ```
|
|
38
|
+
* @public
|
|
11
39
|
*/
|
|
12
40
|
static async urlToArrayBuffer(url: string) {
|
|
13
41
|
const response = await fetch(url)
|
|
14
42
|
return await response.arrayBuffer()
|
|
15
43
|
}
|
|
16
44
|
|
|
45
|
+
/**
|
|
46
|
+
* Converts a URL to a Blob by fetching the resource.
|
|
47
|
+
*
|
|
48
|
+
* Fetches the resource at the given URL and returns its content as a Blob object.
|
|
49
|
+
* Blobs are useful for handling file data in web applications.
|
|
50
|
+
*
|
|
51
|
+
* @param url - The URL of the file to fetch
|
|
52
|
+
* @returns Promise that resolves to the file content as a Blob
|
|
53
|
+
* @example
|
|
54
|
+
* ```ts
|
|
55
|
+
* const blob = await FileHelpers.urlToBlob('https://example.com/document.pdf')
|
|
56
|
+
* console.log(blob.type) // 'application/pdf'
|
|
57
|
+
* console.log(blob.size) // Size in bytes
|
|
58
|
+
* ```
|
|
59
|
+
* @public
|
|
60
|
+
*/
|
|
17
61
|
static async urlToBlob(url: string) {
|
|
18
62
|
const response = await fetch(url)
|
|
19
63
|
return await response.blob()
|
|
20
64
|
}
|
|
21
65
|
|
|
66
|
+
/**
|
|
67
|
+
* Converts a URL to a data URL by fetching the resource.
|
|
68
|
+
*
|
|
69
|
+
* Fetches the resource at the given URL and converts it to a base64-encoded data URL.
|
|
70
|
+
* If the URL is already a data URL, it returns the URL unchanged. This is useful for embedding
|
|
71
|
+
* resources directly in HTML or CSS.
|
|
72
|
+
*
|
|
73
|
+
* @param url - The URL of the file to convert, or an existing data URL
|
|
74
|
+
* @returns Promise that resolves to a data URL string
|
|
75
|
+
* @example
|
|
76
|
+
* ```ts
|
|
77
|
+
* const dataUrl = await FileHelpers.urlToDataUrl('https://example.com/image.jpg')
|
|
78
|
+
* // Returns: 'data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEA...'
|
|
79
|
+
*
|
|
80
|
+
* const existing = await FileHelpers.urlToDataUrl('data:text/plain;base64,SGVsbG8=')
|
|
81
|
+
* // Returns the same data URL unchanged
|
|
82
|
+
* ```
|
|
83
|
+
* @public
|
|
84
|
+
*/
|
|
22
85
|
static async urlToDataUrl(url: string) {
|
|
23
86
|
if (url.startsWith('data:')) return url
|
|
24
87
|
const blob = await FileHelpers.urlToBlob(url)
|
|
@@ -26,15 +89,24 @@ export class FileHelpers {
|
|
|
26
89
|
}
|
|
27
90
|
|
|
28
91
|
/**
|
|
29
|
-
* Convert a
|
|
92
|
+
* Convert a Blob to a base64 encoded data URL.
|
|
30
93
|
*
|
|
31
|
-
*
|
|
94
|
+
* Converts a Blob object to a base64-encoded data URL using the FileReader API.
|
|
95
|
+
* This is useful for displaying images or embedding file content directly in HTML.
|
|
32
96
|
*
|
|
97
|
+
* @param file - The Blob object to convert
|
|
98
|
+
* @returns Promise that resolves to a base64-encoded data URL string
|
|
99
|
+
* @example
|
|
33
100
|
* ```ts
|
|
34
|
-
* const
|
|
35
|
-
*
|
|
101
|
+
* const blob = new Blob(['Hello World'], { type: 'text/plain' })
|
|
102
|
+
* const dataUrl = await FileHelpers.blobToDataUrl(blob)
|
|
103
|
+
* // Returns: 'data:text/plain;base64,SGVsbG8gV29ybGQ='
|
|
36
104
|
*
|
|
37
|
-
*
|
|
105
|
+
* // With an image file
|
|
106
|
+
* const imageDataUrl = await FileHelpers.blobToDataUrl(myImageFile)
|
|
107
|
+
* // Can be used directly in img src attribute
|
|
108
|
+
* ```
|
|
109
|
+
* @public
|
|
38
110
|
*/
|
|
39
111
|
static async blobToDataUrl(file: Blob): Promise<string> {
|
|
40
112
|
return await new Promise((resolve, reject) => {
|
|
@@ -49,15 +121,24 @@ export class FileHelpers {
|
|
|
49
121
|
}
|
|
50
122
|
|
|
51
123
|
/**
|
|
52
|
-
* Convert a
|
|
124
|
+
* Convert a Blob to a unicode text string.
|
|
53
125
|
*
|
|
54
|
-
*
|
|
126
|
+
* Reads the content of a Blob object as a UTF-8 text string using the FileReader API.
|
|
127
|
+
* This is useful for reading text files or extracting text content from blobs.
|
|
55
128
|
*
|
|
129
|
+
* @param file - The Blob object to convert to text
|
|
130
|
+
* @returns Promise that resolves to the text content as a string
|
|
131
|
+
* @example
|
|
56
132
|
* ```ts
|
|
57
|
-
* const
|
|
58
|
-
*
|
|
133
|
+
* const textBlob = new Blob(['Hello World'], { type: 'text/plain' })
|
|
134
|
+
* const text = await FileHelpers.blobToText(textBlob)
|
|
135
|
+
* console.log(text) // 'Hello World'
|
|
59
136
|
*
|
|
60
|
-
*
|
|
137
|
+
* // With a text file from user input
|
|
138
|
+
* const content = await FileHelpers.blobToText(myTextFile)
|
|
139
|
+
* console.log(content) // File content as string
|
|
140
|
+
* ```
|
|
141
|
+
* @public
|
|
61
142
|
*/
|
|
62
143
|
static async blobToText(file: Blob): Promise<string> {
|
|
63
144
|
return await new Promise((resolve, reject) => {
|
|
@@ -71,6 +152,30 @@ export class FileHelpers {
|
|
|
71
152
|
})
|
|
72
153
|
}
|
|
73
154
|
|
|
155
|
+
/**
|
|
156
|
+
* Creates a new Blob or File with a different MIME type.
|
|
157
|
+
*
|
|
158
|
+
* Creates a copy of the given Blob or File with a new MIME type while preserving
|
|
159
|
+
* all other properties. If the current MIME type already matches the new one, returns the
|
|
160
|
+
* original object unchanged. For File objects, preserves the filename.
|
|
161
|
+
*
|
|
162
|
+
* @param blob - The Blob or File object to modify
|
|
163
|
+
* @param newMimeType - The new MIME type to assign
|
|
164
|
+
* @returns A new Blob or File with the updated MIME type
|
|
165
|
+
* @example
|
|
166
|
+
* ```ts
|
|
167
|
+
* // Change a generic blob to a specific image type
|
|
168
|
+
* const blob = new Blob([imageData])
|
|
169
|
+
* const imageBlob = FileHelpers.rewriteMimeType(blob, 'image/png')
|
|
170
|
+
*
|
|
171
|
+
* // Change a file's MIME type while preserving filename
|
|
172
|
+
* const file = new File([data], 'document.txt', { type: 'text/plain' })
|
|
173
|
+
* const jsonFile = FileHelpers.rewriteMimeType(file, 'application/json')
|
|
174
|
+
* console.log(jsonFile.name) // 'document.txt' (preserved)
|
|
175
|
+
* console.log(jsonFile.type) // 'application/json' (updated)
|
|
176
|
+
* ```
|
|
177
|
+
* @public
|
|
178
|
+
*/
|
|
74
179
|
static rewriteMimeType(blob: Blob, newMimeType: string): Blob
|
|
75
180
|
static rewriteMimeType(blob: File, newMimeType: string): File
|
|
76
181
|
static rewriteMimeType(blob: Blob | File, newMimeType: string): Blob | File {
|
package/src/lib/function.ts
CHANGED
|
@@ -6,6 +6,17 @@
|
|
|
6
6
|
*
|
|
7
7
|
* Only works in platforms that support `Error.captureStackTrace` (ie v8).
|
|
8
8
|
*
|
|
9
|
+
* @param fn - The function to wrap and exclude from stack traces
|
|
10
|
+
* @returns A wrapped version of the function that omits itself from error stack traces
|
|
11
|
+
* @example
|
|
12
|
+
* ```ts
|
|
13
|
+
* const assertPositive = omitFromStackTrace((value: number) => {
|
|
14
|
+
* if (value <= 0) throw new Error('Value must be positive')
|
|
15
|
+
* return value
|
|
16
|
+
* })
|
|
17
|
+
*
|
|
18
|
+
* assertPositive(-1) // Error stack trace will point to this line, not inside assertPositive
|
|
19
|
+
* ```
|
|
9
20
|
* @internal
|
|
10
21
|
*/
|
|
11
22
|
export function omitFromStackTrace<Args extends Array<unknown>, Return>(
|