@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
package/src/lib/timers.ts CHANGED
@@ -1,11 +1,40 @@
1
1
  /* eslint-disable no-restricted-properties */
2
2
 
3
- /** @public */
3
+ /**
4
+ * A utility class for managing timeouts, intervals, and animation frames with context-based organization and automatic cleanup.
5
+ * Helps prevent memory leaks by organizing timers into named contexts that can be cleared together.
6
+ * @example
7
+ * ```ts
8
+ * const timers = new Timers()
9
+ *
10
+ * // Set timers with context organization
11
+ * timers.setTimeout('ui', () => console.log('Auto save'), 5000)
12
+ * timers.setInterval('ui', () => console.log('Refresh'), 1000)
13
+ * timers.requestAnimationFrame('ui', () => console.log('Render'))
14
+ *
15
+ * // Clear all timers for a context
16
+ * timers.dispose('ui')
17
+ *
18
+ * // Or get context-bound functions
19
+ * const uiTimers = timers.forContext('ui')
20
+ * uiTimers.setTimeout(() => console.log('Contextual timeout'), 1000)
21
+ * ```
22
+ * @public
23
+ */
4
24
  export class Timers {
5
25
  private timeouts = new Map<string, number[]>()
6
26
  private intervals = new Map<string, number[]>()
7
27
  private rafs = new Map<string, number[]>()
8
28
 
29
+ /**
30
+ * Creates a new Timers instance with bound methods for safe callback usage.
31
+ * @example
32
+ * ```ts
33
+ * const timers = new Timers()
34
+ * // Methods are pre-bound, safe to use as callbacks
35
+ * element.addEventListener('click', timers.dispose)
36
+ * ```
37
+ */
9
38
  constructor() {
10
39
  this.setTimeout = this.setTimeout.bind(this)
11
40
  this.setInterval = this.setInterval.bind(this)
@@ -13,7 +42,21 @@ export class Timers {
13
42
  this.dispose = this.dispose.bind(this)
14
43
  }
15
44
 
16
- /** @public */
45
+ /**
46
+ * Creates a timeout that will be tracked under the specified context.
47
+ * @param contextId - The context identifier to group this timer under.
48
+ * @param handler - The function to execute when the timeout expires.
49
+ * @param timeout - The delay in milliseconds (default: 0).
50
+ * @param args - Additional arguments to pass to the handler.
51
+ * @returns The timer ID that can be used with clearTimeout.
52
+ * @example
53
+ * ```ts
54
+ * const timers = new Timers()
55
+ * const id = timers.setTimeout('autosave', () => save(), 5000)
56
+ * // Timer will be automatically cleared when 'autosave' context is disposed
57
+ * ```
58
+ * @public
59
+ */
17
60
  setTimeout(contextId: string, handler: TimerHandler, timeout?: number, ...args: any[]): number {
18
61
  const id = window.setTimeout(handler, timeout, args)
19
62
  const current = this.timeouts.get(contextId) ?? []
@@ -21,7 +64,21 @@ export class Timers {
21
64
  return id
22
65
  }
23
66
 
24
- /** @public */
67
+ /**
68
+ * Creates an interval that will be tracked under the specified context.
69
+ * @param contextId - The context identifier to group this timer under.
70
+ * @param handler - The function to execute repeatedly.
71
+ * @param timeout - The delay in milliseconds between executions (default: 0).
72
+ * @param args - Additional arguments to pass to the handler.
73
+ * @returns The interval ID that can be used with clearInterval.
74
+ * @example
75
+ * ```ts
76
+ * const timers = new Timers()
77
+ * const id = timers.setInterval('refresh', () => updateData(), 1000)
78
+ * // Interval will be automatically cleared when 'refresh' context is disposed
79
+ * ```
80
+ * @public
81
+ */
25
82
  setInterval(contextId: string, handler: TimerHandler, timeout?: number, ...args: any[]): number {
26
83
  const id = window.setInterval(handler, timeout, args)
27
84
  const current = this.intervals.get(contextId) ?? []
@@ -29,7 +86,19 @@ export class Timers {
29
86
  return id
30
87
  }
31
88
 
32
- /** @public */
89
+ /**
90
+ * Requests an animation frame that will be tracked under the specified context.
91
+ * @param contextId - The context identifier to group this animation frame under.
92
+ * @param callback - The function to execute on the next animation frame.
93
+ * @returns The request ID that can be used with cancelAnimationFrame.
94
+ * @example
95
+ * ```ts
96
+ * const timers = new Timers()
97
+ * const id = timers.requestAnimationFrame('render', () => draw())
98
+ * // Animation frame will be automatically cancelled when 'render' context is disposed
99
+ * ```
100
+ * @public
101
+ */
33
102
  requestAnimationFrame(contextId: string, callback: FrameRequestCallback): number {
34
103
  const id = window.requestAnimationFrame(callback)
35
104
  const current = this.rafs.get(contextId) ?? []
@@ -37,7 +106,22 @@ export class Timers {
37
106
  return id
38
107
  }
39
108
 
40
- /** @public */
109
+ /**
110
+ * Disposes of all timers associated with the specified context.
111
+ * Clears all timeouts, intervals, and animation frames for the given context ID.
112
+ * @param contextId - The context identifier whose timers should be cleared.
113
+ * @returns void
114
+ * @example
115
+ * ```ts
116
+ * const timers = new Timers()
117
+ * timers.setTimeout('ui', () => console.log('timeout'), 1000)
118
+ * timers.setInterval('ui', () => console.log('interval'), 500)
119
+ *
120
+ * // Clear all 'ui' context timers
121
+ * timers.dispose('ui')
122
+ * ```
123
+ * @public
124
+ */
41
125
  dispose(contextId: string) {
42
126
  this.timeouts.get(contextId)?.forEach((id) => clearTimeout(id))
43
127
  this.intervals.get(contextId)?.forEach((id) => clearInterval(id))
@@ -48,12 +132,47 @@ export class Timers {
48
132
  this.rafs.delete(contextId)
49
133
  }
50
134
 
135
+ /**
136
+ * Disposes of all timers across all contexts.
137
+ * Clears every timeout, interval, and animation frame managed by this instance.
138
+ * @returns void
139
+ * @example
140
+ * ```ts
141
+ * const timers = new Timers()
142
+ * timers.setTimeout('ui', () => console.log('ui'), 1000)
143
+ * timers.setTimeout('background', () => console.log('bg'), 2000)
144
+ *
145
+ * // Clear everything
146
+ * timers.disposeAll()
147
+ * ```
148
+ * @public
149
+ */
51
150
  disposeAll() {
52
151
  for (const contextId of this.timeouts.keys()) {
53
152
  this.dispose(contextId)
54
153
  }
55
154
  }
56
155
 
156
+ /**
157
+ * Returns an object with timer methods bound to a specific context.
158
+ * Convenient for getting context-specific timer functions without repeatedly passing the contextId.
159
+ * @param contextId - The context identifier to bind the returned methods to.
160
+ * @returns An object with setTimeout, setInterval, requestAnimationFrame, and dispose methods bound to the context.
161
+ * @example
162
+ * ```ts
163
+ * const timers = new Timers()
164
+ * const uiTimers = timers.forContext('ui')
165
+ *
166
+ * // These are equivalent to calling timers.setTimeout('ui', ...)
167
+ * uiTimers.setTimeout(() => console.log('timeout'), 1000)
168
+ * uiTimers.setInterval(() => console.log('interval'), 500)
169
+ * uiTimers.requestAnimationFrame(() => console.log('frame'))
170
+ *
171
+ * // Dispose only this context
172
+ * uiTimers.dispose()
173
+ * ```
174
+ * @public
175
+ */
57
176
  forContext(contextId: string) {
58
177
  return {
59
178
  setTimeout: (handler: TimerHandler, timeout?: number, ...args: any[]) =>
package/src/lib/types.ts CHANGED
@@ -1,15 +1,137 @@
1
- /** @public */
1
+ /**
2
+ * Makes all properties in a type and all nested properties optional recursively.
3
+ * This is useful for creating partial update objects where you only want to specify
4
+ * some deeply nested properties while leaving others unchanged.
5
+ *
6
+ * @example
7
+ * ```ts
8
+ * interface User {
9
+ * name: string
10
+ * settings: {
11
+ * theme: string
12
+ * notifications: {
13
+ * email: boolean
14
+ * push: boolean
15
+ * }
16
+ * }
17
+ * }
18
+ *
19
+ * type PartialUser = RecursivePartial<User>
20
+ * // Result: {
21
+ * // name?: string
22
+ * // settings?: {
23
+ * // theme?: string
24
+ * // notifications?: {
25
+ * // email?: boolean
26
+ * // push?: boolean
27
+ * // }
28
+ * // }
29
+ * // }
30
+ *
31
+ * const update: PartialUser = {
32
+ * settings: {
33
+ * notifications: {
34
+ * email: false
35
+ * }
36
+ * }
37
+ * }
38
+ * ```
39
+ *
40
+ * @public
41
+ */
2
42
  export type RecursivePartial<T> = {
3
43
  [P in keyof T]?: RecursivePartial<T[P]>
4
44
  }
5
45
 
6
- /** @public */
46
+ /**
47
+ * Expands a type definition to show its full structure in IDE tooltips and error messages.
48
+ * This utility type forces TypeScript to resolve and display the complete type structure
49
+ * instead of showing complex conditional types or intersections as-is.
50
+ *
51
+ * @example
52
+ * ```ts
53
+ * type User = { name: string }
54
+ * type WithId = { id: string }
55
+ * type UserWithId = User & WithId
56
+ *
57
+ * // Without Expand, IDE shows: User & WithId
58
+ * // With Expand, IDE shows: { name: string; id: string }
59
+ * type ExpandedUserWithId = Expand<UserWithId>
60
+ *
61
+ * // Useful for complex intersections
62
+ * type ComplexType = Expand<BaseType & Mixin1 & Mixin2>
63
+ * ```
64
+ *
65
+ * @public
66
+ */
7
67
  export type Expand<T> = T extends infer O ? { [K in keyof O]: O[K] } : never
8
68
 
9
- /** @internal */
69
+ /**
70
+ * Makes specified keys in a type required while keeping all other properties as-is.
71
+ * This is useful when you need to ensure certain optional properties are provided
72
+ * in specific contexts without affecting the entire type structure.
73
+ *
74
+ * @example
75
+ * ```ts
76
+ * interface Shape {
77
+ * id: string
78
+ * x?: number
79
+ * y?: number
80
+ * visible?: boolean
81
+ * }
82
+ *
83
+ * // Make position properties required
84
+ * type PositionedShape = Required<Shape, 'x' | 'y'>
85
+ * // Result: {
86
+ * // id: string
87
+ * // x: number // now required
88
+ * // y: number // now required
89
+ * // visible?: boolean
90
+ * // }
91
+ *
92
+ * const shape: PositionedShape = {
93
+ * id: 'rect1',
94
+ * x: 10, // must provide
95
+ * y: 20, // must provide
96
+ * // visible is still optional
97
+ * }
98
+ * ```
99
+ *
100
+ * @internal
101
+ */
10
102
  export type Required<T, K extends keyof T> = Expand<Omit<T, K> & { [P in K]-?: T[P] }>
11
103
 
12
- /** @public */
104
+ /**
105
+ * Automatically makes properties optional if their type includes `undefined`.
106
+ * This transforms properties like `prop: string | undefined` to `prop?: string | undefined`,
107
+ * making the API more ergonomic by not requiring explicit undefined assignments.
108
+ *
109
+ * @example
110
+ * ```ts
111
+ * interface RawConfig {
112
+ * name: string
113
+ * theme: string | undefined
114
+ * debug: boolean | undefined
115
+ * version: number
116
+ * }
117
+ *
118
+ * type Config = MakeUndefinedOptional<RawConfig>
119
+ * // Result: {
120
+ * // name: string
121
+ * // theme?: string | undefined // now optional
122
+ * // debug?: boolean | undefined // now optional
123
+ * // version: number
124
+ * // }
125
+ *
126
+ * const config: Config = {
127
+ * name: 'MyApp',
128
+ * version: 1
129
+ * // theme and debug can be omitted instead of explicitly set to undefined
130
+ * }
131
+ * ```
132
+ *
133
+ * @public
134
+ */
13
135
  export type MakeUndefinedOptional<T extends object> = Expand<
14
136
  {
15
137
  [P in { [K in keyof T]: undefined extends T[K] ? never : K }[keyof T]]: T[P]
@@ -0,0 +1,44 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { safeParseUrl } from './url'
3
+
4
+ describe('safeParseUrl', () => {
5
+ it('returns URL object for valid absolute URLs', () => {
6
+ const result = safeParseUrl('https://example.com/path')
7
+
8
+ expect(result).toBeInstanceOf(URL)
9
+ expect(result?.href).toBe('https://example.com/path')
10
+ })
11
+
12
+ it('resolves relative URLs with base URL', () => {
13
+ const result = safeParseUrl('/path', 'https://example.com')
14
+
15
+ expect(result).toBeInstanceOf(URL)
16
+ expect(result?.href).toBe('https://example.com/path')
17
+ })
18
+
19
+ it('accepts URL object as base URL', () => {
20
+ const baseUrl = new URL('https://example.com')
21
+ const result = safeParseUrl('/path', baseUrl)
22
+
23
+ expect(result).toBeInstanceOf(URL)
24
+ expect(result?.href).toBe('https://example.com/path')
25
+ })
26
+
27
+ it('returns undefined for invalid URLs', () => {
28
+ expect(safeParseUrl('')).toBeUndefined()
29
+ expect(safeParseUrl('not-a-url')).toBeUndefined()
30
+ expect(safeParseUrl('https://exam ple.com')).toBeUndefined()
31
+ })
32
+
33
+ it('returns undefined when relative URL has no base', () => {
34
+ const result = safeParseUrl('/relative/path')
35
+
36
+ expect(result).toBeUndefined()
37
+ })
38
+
39
+ it('returns undefined when base URL is invalid', () => {
40
+ const result = safeParseUrl('/path', 'not-a-base-url')
41
+
42
+ expect(result).toBeUndefined()
43
+ })
44
+ })
package/src/lib/url.ts CHANGED
@@ -1,4 +1,43 @@
1
- /** @public */
1
+ /**
2
+ * Safely parses a URL string without throwing exceptions on invalid input.
3
+ * Returns a URL object for valid URLs or undefined for invalid ones.
4
+ *
5
+ * @param url - The URL string to parse
6
+ * @param baseUrl - Optional base URL to resolve relative URLs against
7
+ * @returns A URL object if parsing succeeds, undefined if it fails
8
+ *
9
+ * @example
10
+ * ```ts
11
+ * // Valid absolute URL
12
+ * const url1 = safeParseUrl('https://example.com')
13
+ * if (url1) {
14
+ * console.log(`Valid URL: ${url1.href}`) // "Valid URL: https://example.com/"
15
+ * }
16
+ *
17
+ * // Invalid URL
18
+ * const url2 = safeParseUrl('not-a-url')
19
+ * console.log(url2) // undefined
20
+ *
21
+ * // Relative URL with base
22
+ * const url3 = safeParseUrl('/path', 'https://example.com')
23
+ * if (url3) {
24
+ * console.log(url3.href) // "https://example.com/path"
25
+ * }
26
+ *
27
+ * // Error handling
28
+ * function handleUserUrl(input: string) {
29
+ * const url = safeParseUrl(input)
30
+ * if (url) {
31
+ * return url
32
+ * } else {
33
+ * console.log('Invalid URL provided')
34
+ * return null
35
+ * }
36
+ * }
37
+ * ```
38
+ *
39
+ * @public
40
+ */
2
41
  export const safeParseUrl = (url: string, baseUrl?: string | URL) => {
3
42
  try {
4
43
  return new URL(url, baseUrl)
@@ -0,0 +1,102 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import {
3
+ isNativeStructuredClone,
4
+ STRUCTURED_CLONE_OBJECT_PROTOTYPE,
5
+ structuredClone,
6
+ } from './value'
7
+
8
+ describe('value utilities', () => {
9
+ describe('structuredClone', () => {
10
+ it('should create deep copies of objects', () => {
11
+ const original = { a: 1, b: { c: 2 } }
12
+ const copy = structuredClone(original)
13
+
14
+ expect(copy).toEqual(original)
15
+ expect(copy).not.toBe(original)
16
+ expect(copy.b).not.toBe(original.b)
17
+
18
+ // Modifying copy should not affect original
19
+ copy.b.c = 3
20
+ expect(original.b.c).toBe(2)
21
+ expect(copy.b.c).toBe(3)
22
+ })
23
+
24
+ it('should handle primitive values', () => {
25
+ expect(structuredClone(42)).toBe(42)
26
+ expect(structuredClone('hello')).toBe('hello')
27
+ expect(structuredClone(true)).toBe(true)
28
+ expect(structuredClone(null)).toBe(null)
29
+ expect(structuredClone(undefined)).toBe(undefined)
30
+ })
31
+
32
+ it('should handle arrays', () => {
33
+ const original = [1, [2, 3], { a: 4 }]
34
+ const copy = structuredClone(original)
35
+
36
+ expect(copy).toEqual(original)
37
+ expect(copy).not.toBe(original)
38
+ expect(copy[1]).not.toBe(original[1])
39
+ expect(copy[2]).not.toBe(original[2])
40
+
41
+ // Modifying copy should not affect original
42
+ ;(copy[1] as number[]).push(5)
43
+ ;(copy[2] as any).a = 99
44
+
45
+ expect(original[1]).toEqual([2, 3])
46
+ expect((original[2] as any).a).toBe(4)
47
+ })
48
+
49
+ it('should handle nested structures', () => {
50
+ const original = {
51
+ level1: {
52
+ level2: {
53
+ level3: {
54
+ value: 'deep',
55
+ },
56
+ },
57
+ },
58
+ }
59
+
60
+ const copy = structuredClone(original)
61
+
62
+ expect(copy).toEqual(original)
63
+ expect(copy.level1).not.toBe(original.level1)
64
+ expect(copy.level1.level2).not.toBe(original.level1.level2)
65
+ expect(copy.level1.level2.level3).not.toBe(original.level1.level2.level3)
66
+
67
+ copy.level1.level2.level3.value = 'modified'
68
+ expect(original.level1.level2.level3.value).toBe('deep')
69
+ })
70
+
71
+ it('should handle dates', () => {
72
+ const date = new Date('2023-01-01')
73
+ const copy = structuredClone(date)
74
+
75
+ expect(copy).toEqual(date)
76
+ expect(copy).not.toBe(date)
77
+ expect(copy instanceof Date).toBe(true)
78
+ })
79
+
80
+ it('should handle circular references if native structuredClone is available', () => {
81
+ if (isNativeStructuredClone) {
82
+ const obj: any = { a: 1 }
83
+ obj.self = obj
84
+
85
+ const copy = structuredClone(obj)
86
+
87
+ expect(copy.a).toBe(1)
88
+ expect(copy.self).toBe(copy)
89
+ expect(copy).not.toBe(obj)
90
+ }
91
+ })
92
+ })
93
+
94
+ describe('STRUCTURED_CLONE_OBJECT_PROTOTYPE', () => {
95
+ it('should be the prototype of objects created by structuredClone', () => {
96
+ const obj = {}
97
+ const cloned = structuredClone(obj)
98
+
99
+ expect(Object.getPrototypeOf(cloned)).toBe(STRUCTURED_CLONE_OBJECT_PROTOTYPE)
100
+ })
101
+ })
102
+ })
package/src/lib/value.ts CHANGED
@@ -2,6 +2,20 @@
2
2
  * Get whether a value is not undefined.
3
3
  *
4
4
  * @param value - The value to check.
5
+ * @returns True if the value is not undefined, with proper type narrowing.
6
+ * @example
7
+ * ```ts
8
+ * const maybeString: string | undefined = getValue()
9
+ *
10
+ * if (isDefined(maybeString)) {
11
+ * // TypeScript knows maybeString is string, not undefined
12
+ * console.log(maybeString.toUpperCase())
13
+ * }
14
+ *
15
+ * // Filter undefined values from arrays
16
+ * const values = [1, undefined, 2, undefined, 3]
17
+ * const definedValues = values.filter(isDefined) // [1, 2, 3]
18
+ * ```
5
19
  * @public
6
20
  */
7
21
  export function isDefined<T>(value: T): value is typeof value extends undefined ? never : T {
@@ -9,9 +23,23 @@ export function isDefined<T>(value: T): value is typeof value extends undefined
9
23
  }
10
24
 
11
25
  /**
12
- * Get whether a value is null
26
+ * Get whether a value is not null.
13
27
  *
14
28
  * @param value - The value to check.
29
+ * @returns True if the value is not null, with proper type narrowing.
30
+ * @example
31
+ * ```ts
32
+ * const maybeString: string | null = getValue()
33
+ *
34
+ * if (isNonNull(maybeString)) {
35
+ * // TypeScript knows maybeString is string, not null
36
+ * console.log(maybeString.length)
37
+ * }
38
+ *
39
+ * // Filter null values from arrays
40
+ * const values = ["a", null, "b", null, "c"]
41
+ * const nonNullValues = values.filter(isNonNull) // ["a", "b", "c"]
42
+ * ```
15
43
  * @public
16
44
  */
17
45
  export function isNonNull<T>(value: T): value is typeof value extends null ? never : T {
@@ -19,9 +47,23 @@ export function isNonNull<T>(value: T): value is typeof value extends null ? nev
19
47
  }
20
48
 
21
49
  /**
22
- * Get whether a value is nullish (null, undefined).
50
+ * Get whether a value is not nullish (not null and not undefined).
23
51
  *
24
52
  * @param value - The value to check.
53
+ * @returns True if the value is neither null nor undefined, with proper type narrowing.
54
+ * @example
55
+ * ```ts
56
+ * const maybeString: string | null | undefined = getValue()
57
+ *
58
+ * if (isNonNullish(maybeString)) {
59
+ * // TypeScript knows maybeString is string, not null or undefined
60
+ * console.log(maybeString.charAt(0))
61
+ * }
62
+ *
63
+ * // Filter nullish values from arrays
64
+ * const values = ["hello", null, "world", undefined, "!"]
65
+ * const cleanValues = values.filter(isNonNullish) // ["hello", "world", "!"]
66
+ * ```
25
67
  * @public
26
68
  */
27
69
  export function isNonNullish<T>(
@@ -52,15 +94,37 @@ const _structuredClone = getStructuredClone()
52
94
  * Create a deep copy of a value. Uses the structuredClone API if available, otherwise uses JSON.parse(JSON.stringify()).
53
95
  *
54
96
  * @param i - The value to clone.
55
- * @public */
97
+ * @returns A deep copy of the input value.
98
+ * @example
99
+ * ```ts
100
+ * const original = { a: 1, b: { c: 2 } }
101
+ * const copy = structuredClone(original)
102
+ *
103
+ * copy.b.c = 3
104
+ * console.log(original.b.c) // 2 (unchanged)
105
+ * console.log(copy.b.c) // 3
106
+ *
107
+ * // Works with complex objects
108
+ * const complexObject = {
109
+ * date: new Date(),
110
+ * array: [1, 2, 3],
111
+ * nested: { deep: { value: "test" } }
112
+ * }
113
+ * const cloned = structuredClone(complexObject)
114
+ * ```
115
+ * @public
116
+ */
56
117
  export const structuredClone = _structuredClone[0]
57
118
 
58
119
  /**
120
+ * Whether the current environment has native structuredClone support.
121
+ * @returns True if using native structuredClone, false if using JSON fallback.
59
122
  * @internal
60
123
  */
61
124
  export const isNativeStructuredClone = _structuredClone[1]
62
125
 
63
126
  /**
127
+ * The prototype object used by structuredClone for cloned objects.
64
128
  * When we patch structuredClone in jsdom for testing (see https://github.com/jsdom/jsdom/issues/3363),
65
129
  * the Object that is used as a prototype for the cloned object is not the same as the Object in
66
130
  * the code under test (that comes from jsdom's fake global context). This constant is used in