@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.
Files changed (160) hide show
  1. package/dist-cjs/index.d.ts +1350 -80
  2. package/dist-cjs/index.js +5 -5
  3. package/dist-cjs/lib/ExecutionQueue.js +79 -0
  4. package/dist-cjs/lib/ExecutionQueue.js.map +2 -2
  5. package/dist-cjs/lib/PerformanceTracker.js +43 -0
  6. package/dist-cjs/lib/PerformanceTracker.js.map +2 -2
  7. package/dist-cjs/lib/array.js +3 -1
  8. package/dist-cjs/lib/array.js.map +2 -2
  9. package/dist-cjs/lib/bind.js.map +2 -2
  10. package/dist-cjs/lib/cache.js +27 -5
  11. package/dist-cjs/lib/cache.js.map +2 -2
  12. package/dist-cjs/lib/control.js +12 -0
  13. package/dist-cjs/lib/control.js.map +2 -2
  14. package/dist-cjs/lib/debounce.js.map +2 -2
  15. package/dist-cjs/lib/error.js.map +2 -2
  16. package/dist-cjs/lib/file.js +76 -11
  17. package/dist-cjs/lib/file.js.map +2 -2
  18. package/dist-cjs/lib/function.js.map +2 -2
  19. package/dist-cjs/lib/hash.js.map +2 -2
  20. package/dist-cjs/lib/id.js.map +2 -2
  21. package/dist-cjs/lib/iterable.js.map +2 -2
  22. package/dist-cjs/lib/json-value.js.map +1 -1
  23. package/dist-cjs/lib/media/apng.js.map +2 -2
  24. package/dist-cjs/lib/media/avif.js.map +2 -2
  25. package/dist-cjs/lib/media/gif.js.map +2 -2
  26. package/dist-cjs/lib/media/media.js +130 -4
  27. package/dist-cjs/lib/media/media.js.map +2 -2
  28. package/dist-cjs/lib/media/png.js +141 -0
  29. package/dist-cjs/lib/media/png.js.map +2 -2
  30. package/dist-cjs/lib/media/webp.js +1 -0
  31. package/dist-cjs/lib/media/webp.js.map +2 -2
  32. package/dist-cjs/lib/network.js.map +2 -2
  33. package/dist-cjs/lib/number.js.map +2 -2
  34. package/dist-cjs/lib/object.js +1 -1
  35. package/dist-cjs/lib/object.js.map +2 -2
  36. package/dist-cjs/lib/perf.js.map +2 -2
  37. package/dist-cjs/lib/reordering.js.map +2 -2
  38. package/dist-cjs/lib/retry.js.map +2 -2
  39. package/dist-cjs/lib/sort.js.map +2 -2
  40. package/dist-cjs/lib/storage.js.map +2 -2
  41. package/dist-cjs/lib/stringEnum.js.map +2 -2
  42. package/dist-cjs/lib/throttle.js.map +2 -2
  43. package/dist-cjs/lib/timers.js +103 -4
  44. package/dist-cjs/lib/timers.js.map +2 -2
  45. package/dist-cjs/lib/types.js.map +1 -1
  46. package/dist-cjs/lib/url.js.map +2 -2
  47. package/dist-cjs/lib/value.js.map +2 -2
  48. package/dist-cjs/lib/version.js.map +2 -2
  49. package/dist-cjs/lib/warn.js.map +2 -2
  50. package/dist-esm/index.d.mts +1350 -80
  51. package/dist-esm/index.mjs +1 -1
  52. package/dist-esm/lib/ExecutionQueue.mjs +79 -0
  53. package/dist-esm/lib/ExecutionQueue.mjs.map +2 -2
  54. package/dist-esm/lib/PerformanceTracker.mjs +43 -0
  55. package/dist-esm/lib/PerformanceTracker.mjs.map +2 -2
  56. package/dist-esm/lib/array.mjs +3 -1
  57. package/dist-esm/lib/array.mjs.map +2 -2
  58. package/dist-esm/lib/bind.mjs.map +2 -2
  59. package/dist-esm/lib/cache.mjs +27 -5
  60. package/dist-esm/lib/cache.mjs.map +2 -2
  61. package/dist-esm/lib/control.mjs +12 -0
  62. package/dist-esm/lib/control.mjs.map +2 -2
  63. package/dist-esm/lib/debounce.mjs.map +2 -2
  64. package/dist-esm/lib/error.mjs.map +2 -2
  65. package/dist-esm/lib/file.mjs +76 -11
  66. package/dist-esm/lib/file.mjs.map +2 -2
  67. package/dist-esm/lib/function.mjs.map +2 -2
  68. package/dist-esm/lib/hash.mjs.map +2 -2
  69. package/dist-esm/lib/id.mjs.map +2 -2
  70. package/dist-esm/lib/iterable.mjs.map +2 -2
  71. package/dist-esm/lib/media/apng.mjs.map +2 -2
  72. package/dist-esm/lib/media/avif.mjs.map +2 -2
  73. package/dist-esm/lib/media/gif.mjs.map +2 -2
  74. package/dist-esm/lib/media/media.mjs +130 -4
  75. package/dist-esm/lib/media/media.mjs.map +2 -2
  76. package/dist-esm/lib/media/png.mjs +141 -0
  77. package/dist-esm/lib/media/png.mjs.map +2 -2
  78. package/dist-esm/lib/media/webp.mjs +1 -0
  79. package/dist-esm/lib/media/webp.mjs.map +2 -2
  80. package/dist-esm/lib/network.mjs.map +2 -2
  81. package/dist-esm/lib/number.mjs.map +2 -2
  82. package/dist-esm/lib/object.mjs.map +2 -2
  83. package/dist-esm/lib/perf.mjs.map +2 -2
  84. package/dist-esm/lib/reordering.mjs.map +2 -2
  85. package/dist-esm/lib/retry.mjs.map +2 -2
  86. package/dist-esm/lib/sort.mjs.map +2 -2
  87. package/dist-esm/lib/storage.mjs.map +2 -2
  88. package/dist-esm/lib/stringEnum.mjs.map +2 -2
  89. package/dist-esm/lib/throttle.mjs.map +2 -2
  90. package/dist-esm/lib/timers.mjs +103 -4
  91. package/dist-esm/lib/timers.mjs.map +2 -2
  92. package/dist-esm/lib/url.mjs.map +2 -2
  93. package/dist-esm/lib/value.mjs.map +2 -2
  94. package/dist-esm/lib/version.mjs.map +2 -2
  95. package/dist-esm/lib/warn.mjs.map +2 -2
  96. package/package.json +1 -1
  97. package/src/lib/ExecutionQueue.test.ts +162 -20
  98. package/src/lib/ExecutionQueue.ts +110 -1
  99. package/src/lib/PerformanceTracker.test.ts +124 -0
  100. package/src/lib/PerformanceTracker.ts +63 -1
  101. package/src/lib/array.test.ts +263 -1
  102. package/src/lib/array.ts +183 -14
  103. package/src/lib/bind.test.ts +47 -0
  104. package/src/lib/bind.ts +69 -4
  105. package/src/lib/cache.test.ts +73 -0
  106. package/src/lib/cache.ts +47 -6
  107. package/src/lib/control.test.ts +50 -0
  108. package/src/lib/control.ts +198 -9
  109. package/src/lib/debounce.ts +28 -3
  110. package/src/lib/error.test.ts +60 -0
  111. package/src/lib/error.ts +27 -1
  112. package/src/lib/file.test.ts +49 -0
  113. package/src/lib/file.ts +117 -12
  114. package/src/lib/function.ts +11 -0
  115. package/src/lib/hash.test.ts +99 -0
  116. package/src/lib/hash.ts +69 -2
  117. package/src/lib/id.test.ts +32 -0
  118. package/src/lib/id.ts +53 -5
  119. package/src/lib/iterable.test.ts +25 -0
  120. package/src/lib/iterable.ts +4 -5
  121. package/src/lib/json-value.ts +71 -4
  122. package/src/lib/media/apng.test.ts +67 -0
  123. package/src/lib/media/apng.ts +38 -21
  124. package/src/lib/media/avif.test.ts +26 -0
  125. package/src/lib/media/avif.ts +34 -0
  126. package/src/lib/media/gif.test.ts +52 -0
  127. package/src/lib/media/gif.ts +25 -2
  128. package/src/lib/media/media.test.ts +58 -0
  129. package/src/lib/media/media.ts +220 -11
  130. package/src/lib/media/png.ts +162 -1
  131. package/src/lib/media/webp.test.ts +81 -0
  132. package/src/lib/media/webp.ts +33 -1
  133. package/src/lib/network.test.ts +38 -0
  134. package/src/lib/network.ts +6 -0
  135. package/src/lib/number.test.ts +74 -0
  136. package/src/lib/number.ts +29 -5
  137. package/src/lib/object.test.ts +236 -0
  138. package/src/lib/object.ts +194 -14
  139. package/src/lib/perf.ts +75 -3
  140. package/src/lib/reordering.test.ts +168 -0
  141. package/src/lib/reordering.ts +62 -4
  142. package/src/lib/retry.test.ts +77 -0
  143. package/src/lib/retry.ts +47 -1
  144. package/src/lib/sort.test.ts +36 -0
  145. package/src/lib/sort.ts +22 -1
  146. package/src/lib/storage.test.ts +130 -0
  147. package/src/lib/storage.tsx +54 -8
  148. package/src/lib/stringEnum.ts +20 -1
  149. package/src/lib/throttle.ts +46 -8
  150. package/src/lib/timers.test.ts +75 -0
  151. package/src/lib/timers.ts +124 -5
  152. package/src/lib/types.ts +126 -4
  153. package/src/lib/url.test.ts +44 -0
  154. package/src/lib/url.ts +40 -1
  155. package/src/lib/value.test.ts +102 -0
  156. package/src/lib/value.ts +67 -3
  157. package/src/lib/version.test.ts +494 -56
  158. package/src/lib/version.ts +36 -1
  159. package/src/lib/warn.test.ts +64 -0
  160. package/src/lib/warn.ts +43 -2
@@ -1,36 +1,160 @@
1
1
  import { omitFromStackTrace } from './function'
2
2
 
3
- /** @public */
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
- /** @public */
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
- /** @public */
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
- /** @public */
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
- /** @internal */
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
- /** @internal */
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
- /** @internal */
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
- /** @internal */
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
- /** @internal */
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))
@@ -1,10 +1,35 @@
1
1
  /**
2
- * Debounce a function.
2
+ * Create a debounced version of a function that delays execution until after a specified wait time.
3
3
  *
4
- * @example
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
- * const A = debounce(myFunction, 1000)
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
- /** @internal */
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
- * Helpers for files
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
- * @param url - The url of the file.
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 file to a base64 encoded data url.
92
+ * Convert a Blob to a base64 encoded data URL.
30
93
  *
31
- * @example
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 A = FileHelpers.toDataUrl(myImageFile)
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
- * @param file - The file as a blob.
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 file to a unicode text string.
124
+ * Convert a Blob to a unicode text string.
53
125
  *
54
- * @example
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 A = FileHelpers.fileToDataUrl(myTextFile)
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
- * @param file - The file as a blob.
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 {
@@ -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>(