@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
@@ -0,0 +1,77 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
2
+ import { retry } from './retry'
3
+
4
+ describe('retry', () => {
5
+ beforeEach(() => {
6
+ vi.useFakeTimers()
7
+ })
8
+
9
+ afterEach(() => {
10
+ vi.useRealTimers()
11
+ })
12
+
13
+ it('returns result when function succeeds on first attempt', async () => {
14
+ const successFn = vi.fn().mockResolvedValue('success')
15
+
16
+ const result = await retry(successFn)
17
+
18
+ expect(result).toBe('success')
19
+ expect(successFn).toHaveBeenCalledTimes(1)
20
+ })
21
+
22
+ it('returns result when function succeeds after failures', async () => {
23
+ const successFn = vi
24
+ .fn()
25
+ .mockRejectedValueOnce(new Error('fail 1'))
26
+ .mockRejectedValueOnce(new Error('fail 2'))
27
+ .mockResolvedValue('success')
28
+
29
+ const promise = retry(successFn)
30
+
31
+ await vi.advanceTimersByTimeAsync(3000)
32
+ const result = await promise
33
+
34
+ expect(result).toBe('success')
35
+ expect(successFn).toHaveBeenCalledTimes(3)
36
+ })
37
+
38
+ it('respects custom waitDuration setting', async () => {
39
+ const failFn = vi.fn().mockRejectedValueOnce(new Error('fail 1')).mockResolvedValue('success')
40
+
41
+ const promise = retry(failFn, { waitDuration: 2500 })
42
+
43
+ await vi.advanceTimersByTimeAsync(2000)
44
+ expect(failFn).toHaveBeenCalledTimes(1)
45
+
46
+ await vi.advanceTimersByTimeAsync(1000)
47
+ const result = await promise
48
+
49
+ expect(result).toBe('success')
50
+ expect(failFn).toHaveBeenCalledTimes(2)
51
+ })
52
+
53
+ it('retries only errors that match the filter', async () => {
54
+ class NetworkError extends Error {
55
+ constructor(message: string) {
56
+ super(message)
57
+ this.name = 'NetworkError'
58
+ }
59
+ }
60
+
61
+ const failFn = vi
62
+ .fn()
63
+ .mockRejectedValueOnce(new NetworkError('network fail'))
64
+ .mockRejectedValueOnce(new NetworkError('network fail 2'))
65
+ .mockResolvedValue('success')
66
+
67
+ const promise = retry(failFn, {
68
+ matchError: (error) => error instanceof NetworkError,
69
+ })
70
+
71
+ await vi.advanceTimersByTimeAsync(3000)
72
+ const result = await promise
73
+
74
+ expect(result).toBe('success')
75
+ expect(failFn).toHaveBeenCalledTimes(3)
76
+ })
77
+ })
package/src/lib/retry.ts CHANGED
@@ -1,6 +1,52 @@
1
1
  import { sleep } from './control'
2
2
 
3
- /** @internal */
3
+ /**
4
+ * Retries an async operation with configurable attempt count, wait duration, and error filtering.
5
+ * Executes the provided async function repeatedly until it succeeds or the maximum number of attempts is reached.
6
+ * Includes support for abort signals and custom error matching to determine which errors should trigger retries.
7
+ *
8
+ * @param fn - The async function to retry on failure
9
+ * @param options - Configuration options for retry behavior:
10
+ * - `attempts`: Maximum number of retry attempts (default: 3)
11
+ * - `waitDuration`: Milliseconds to wait between retry attempts (default: 1000)
12
+ * - `abortSignal`: Optional AbortSignal to cancel the retry operation
13
+ * - `matchError`: Optional function to determine if an error should trigger a retry
14
+ * @returns Promise that resolves with the function's return value on success
15
+ *
16
+ * @example
17
+ * ```ts
18
+ * // Basic retry with default settings (3 attempts, 1 second wait)
19
+ * const data = await retry(async () => {
20
+ * const response = await fetch('/api/data')
21
+ * if (!response.ok) throw new Error('Network error')
22
+ * return response.json()
23
+ * })
24
+ *
25
+ * // Custom retry configuration
26
+ * const result = await retry(
27
+ * async () => unreliableApiCall(),
28
+ * {
29
+ * attempts: 5,
30
+ * waitDuration: 2000,
31
+ * matchError: (error) => error instanceof NetworkError
32
+ * }
33
+ * )
34
+ *
35
+ * // With abort signal for cancellation
36
+ * const controller = new AbortController()
37
+ * setTimeout(() => controller.abort(), 10000) // Cancel after 10 seconds
38
+ *
39
+ * const data = await retry(
40
+ * async () => fetchData(),
41
+ * {
42
+ * attempts: 10,
43
+ * abortSignal: controller.signal
44
+ * }
45
+ * )
46
+ * ```
47
+ *
48
+ * @internal
49
+ */
4
50
  export async function retry<T>(
5
51
  fn: () => Promise<T>,
6
52
  {
@@ -0,0 +1,36 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { sortById } from './sort'
3
+
4
+ describe('sortById', () => {
5
+ it('sorts objects with string ids in ascending order', () => {
6
+ const items = [
7
+ { id: 'c', name: 'Charlie' },
8
+ { id: 'a', name: 'Alice' },
9
+ { id: 'b', name: 'Bob' },
10
+ ]
11
+
12
+ const sorted = items.sort(sortById)
13
+
14
+ expect(sorted).toEqual([
15
+ { id: 'a', name: 'Alice' },
16
+ { id: 'b', name: 'Bob' },
17
+ { id: 'c', name: 'Charlie' },
18
+ ])
19
+ })
20
+
21
+ it('sorts objects with numeric ids in ascending order', () => {
22
+ const items = [
23
+ { id: 3, label: 'three' },
24
+ { id: 1, label: 'one' },
25
+ { id: 2, label: 'two' },
26
+ ]
27
+
28
+ const sorted = items.sort(sortById)
29
+
30
+ expect(sorted).toEqual([
31
+ { id: 1, label: 'one' },
32
+ { id: 2, label: 'two' },
33
+ { id: 3, label: 'three' },
34
+ ])
35
+ })
36
+ })
package/src/lib/sort.ts CHANGED
@@ -1,4 +1,25 @@
1
- /** @public */
1
+ /**
2
+ * Compares two objects by their id property for use with Array.sort().
3
+ * Sorts objects in ascending order based on their id values.
4
+ *
5
+ * @param a - First object to compare
6
+ * @param b - Second object to compare
7
+ * @returns 1 if a.id \> b.id, -1 if a.id \<= b.id
8
+ *
9
+ * @example
10
+ * ```ts
11
+ * const items = [
12
+ * { id: 'c', name: 'Charlie' },
13
+ * { id: 'a', name: 'Alice' },
14
+ * { id: 'b', name: 'Bob' },
15
+ * ]
16
+ *
17
+ * const sorted = items.sort(sortById)
18
+ * // [{ id: 'a', name: 'Alice' }, { id: 'b', name: 'Bob' }, { id: 'c', name: 'Charlie' }]
19
+ * ```
20
+ *
21
+ * @public
22
+ */
2
23
  export function sortById<T extends { id: any }>(a: T, b: T) {
3
24
  return a.id > b.id ? 1 : -1
4
25
  }
@@ -0,0 +1,130 @@
1
+ /* eslint-disable no-restricted-syntax */
2
+
3
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
4
+ import {
5
+ clearLocalStorage,
6
+ clearSessionStorage,
7
+ deleteFromLocalStorage,
8
+ deleteFromSessionStorage,
9
+ getFromLocalStorage,
10
+ getFromSessionStorage,
11
+ setInLocalStorage,
12
+ setInSessionStorage,
13
+ } from './storage'
14
+
15
+ describe('storage', () => {
16
+ // Store original implementations
17
+ const originalLocalStorage = global.localStorage
18
+ const originalSessionStorage = global.sessionStorage
19
+
20
+ beforeEach(() => {
21
+ // Mock localStorage
22
+ const localStorageMock = {
23
+ getItem: vi.fn(),
24
+ setItem: vi.fn(),
25
+ removeItem: vi.fn(),
26
+ clear: vi.fn(),
27
+ }
28
+ global.localStorage = localStorageMock as any
29
+
30
+ // Mock sessionStorage
31
+ const sessionStorageMock = {
32
+ getItem: vi.fn(),
33
+ setItem: vi.fn(),
34
+ removeItem: vi.fn(),
35
+ clear: vi.fn(),
36
+ }
37
+ global.sessionStorage = sessionStorageMock as any
38
+ })
39
+
40
+ afterEach(() => {
41
+ // Restore original implementations
42
+ global.localStorage = originalLocalStorage
43
+ global.sessionStorage = originalSessionStorage
44
+ vi.clearAllMocks()
45
+ })
46
+
47
+ describe('getFromLocalStorage', () => {
48
+ it('should return null when localStorage.getItem throws an error', () => {
49
+ ;(localStorage.getItem as any).mockImplementation(() => {
50
+ throw new Error('Storage not available')
51
+ })
52
+
53
+ const result = getFromLocalStorage('test-key')
54
+
55
+ expect(result).toBe(null)
56
+ })
57
+ })
58
+
59
+ describe('setInLocalStorage', () => {
60
+ it('should not throw when localStorage.setItem throws an error', () => {
61
+ ;(localStorage.setItem as any).mockImplementation(() => {
62
+ throw new Error('Quota exceeded')
63
+ })
64
+
65
+ expect(() => setInLocalStorage('test-key', 'test-value')).not.toThrow()
66
+ })
67
+ })
68
+
69
+ describe('deleteFromLocalStorage', () => {
70
+ it('should not throw when localStorage.removeItem throws an error', () => {
71
+ ;(localStorage.removeItem as any).mockImplementation(() => {
72
+ throw new Error('Storage not available')
73
+ })
74
+
75
+ expect(() => deleteFromLocalStorage('test-key')).not.toThrow()
76
+ })
77
+ })
78
+
79
+ describe('clearLocalStorage', () => {
80
+ it('should not throw when localStorage.clear throws an error', () => {
81
+ ;(localStorage.clear as any).mockImplementation(() => {
82
+ throw new Error('Storage not available')
83
+ })
84
+
85
+ expect(() => clearLocalStorage()).not.toThrow()
86
+ })
87
+ })
88
+
89
+ describe('getFromSessionStorage', () => {
90
+ it('should return null when sessionStorage.getItem throws an error', () => {
91
+ ;(sessionStorage.getItem as any).mockImplementation(() => {
92
+ throw new Error('Storage not available')
93
+ })
94
+
95
+ const result = getFromSessionStorage('session-key')
96
+
97
+ expect(result).toBe(null)
98
+ })
99
+ })
100
+
101
+ describe('setInSessionStorage', () => {
102
+ it('should not throw when sessionStorage.setItem throws an error', () => {
103
+ ;(sessionStorage.setItem as any).mockImplementation(() => {
104
+ throw new Error('Quota exceeded')
105
+ })
106
+
107
+ expect(() => setInSessionStorage('session-key', 'session-value')).not.toThrow()
108
+ })
109
+ })
110
+
111
+ describe('deleteFromSessionStorage', () => {
112
+ it('should not throw when sessionStorage.removeItem throws an error', () => {
113
+ ;(sessionStorage.removeItem as any).mockImplementation(() => {
114
+ throw new Error('Storage not available')
115
+ })
116
+
117
+ expect(() => deleteFromSessionStorage('session-key')).not.toThrow()
118
+ })
119
+ })
120
+
121
+ describe('clearSessionStorage', () => {
122
+ it('should not throw when sessionStorage.clear throws an error', () => {
123
+ ;(sessionStorage.clear as any).mockImplementation(() => {
124
+ throw new Error('Storage not available')
125
+ })
126
+
127
+ expect(() => clearSessionStorage()).not.toThrow()
128
+ })
129
+ })
130
+ })
@@ -4,7 +4,14 @@
4
4
  * Get a value from local storage.
5
5
  *
6
6
  * @param key - The key to get.
7
- *
7
+ * @returns The stored value as a string, or null if not found or storage is unavailable.
8
+ * @example
9
+ * ```ts
10
+ * const userTheme = getFromLocalStorage('user-theme')
11
+ * if (userTheme) {
12
+ * console.log('Stored theme:', userTheme)
13
+ * }
14
+ * ```
8
15
  * @internal
9
16
  */
10
17
  export function getFromLocalStorage(key: string) {
@@ -20,7 +27,12 @@ export function getFromLocalStorage(key: string) {
20
27
  *
21
28
  * @param key - The key to set.
22
29
  * @param value - The value to set.
23
- *
30
+ * @returns void
31
+ * @example
32
+ * ```ts
33
+ * const preferences = { theme: 'dark', language: 'en' }
34
+ * setInLocalStorage('user-preferences', JSON.stringify(preferences))
35
+ * ```
24
36
  * @internal
25
37
  */
26
38
  export function setInLocalStorage(key: string, value: string) {
@@ -34,8 +46,13 @@ export function setInLocalStorage(key: string, value: string) {
34
46
  /**
35
47
  * Remove a value from local storage. Will not throw an error if localStorage is not available.
36
48
  *
37
- * @param key - The key to set.
38
- *
49
+ * @param key - The key to remove.
50
+ * @returns void
51
+ * @example
52
+ * ```ts
53
+ * deleteFromLocalStorage('user-preferences')
54
+ * // Value is now removed from localStorage
55
+ * ```
39
56
  * @internal
40
57
  */
41
58
  export function deleteFromLocalStorage(key: string) {
@@ -49,6 +66,12 @@ export function deleteFromLocalStorage(key: string) {
49
66
  /**
50
67
  * Clear all values from local storage. Will not throw an error if localStorage is not available.
51
68
  *
69
+ * @returns void
70
+ * @example
71
+ * ```ts
72
+ * clearLocalStorage()
73
+ * // All localStorage data is now cleared
74
+ * ```
52
75
  * @internal
53
76
  */
54
77
  export function clearLocalStorage() {
@@ -63,7 +86,14 @@ export function clearLocalStorage() {
63
86
  * Get a value from session storage.
64
87
  *
65
88
  * @param key - The key to get.
66
- *
89
+ * @returns The stored value as a string, or null if not found or storage is unavailable.
90
+ * @example
91
+ * ```ts
92
+ * const currentTool = getFromSessionStorage('current-tool')
93
+ * if (currentTool) {
94
+ * console.log('Active tool:', currentTool)
95
+ * }
96
+ * ```
67
97
  * @internal
68
98
  */
69
99
  export function getFromSessionStorage(key: string) {
@@ -79,7 +109,12 @@ export function getFromSessionStorage(key: string) {
79
109
  *
80
110
  * @param key - The key to set.
81
111
  * @param value - The value to set.
82
- *
112
+ * @returns void
113
+ * @example
114
+ * ```ts
115
+ * setInSessionStorage('current-tool', 'select')
116
+ * setInSessionStorage('temp-data', JSON.stringify({ x: 100, y: 200 }))
117
+ * ```
83
118
  * @internal
84
119
  */
85
120
  export function setInSessionStorage(key: string, value: string) {
@@ -93,8 +128,13 @@ export function setInSessionStorage(key: string, value: string) {
93
128
  /**
94
129
  * Remove a value from session storage. Will not throw an error if sessionStorage is not available.
95
130
  *
96
- * @param key - The key to set.
97
- *
131
+ * @param key - The key to remove.
132
+ * @returns void
133
+ * @example
134
+ * ```ts
135
+ * deleteFromSessionStorage('temp-data')
136
+ * // Value is now removed from sessionStorage
137
+ * ```
98
138
  * @internal
99
139
  */
100
140
  export function deleteFromSessionStorage(key: string) {
@@ -108,6 +148,12 @@ export function deleteFromSessionStorage(key: string) {
108
148
  /**
109
149
  * Clear all values from session storage. Will not throw an error if sessionStorage is not available.
110
150
  *
151
+ * @returns void
152
+ * @example
153
+ * ```ts
154
+ * clearSessionStorage()
155
+ * // All sessionStorage data is now cleared
156
+ * ```
111
157
  * @internal
112
158
  */
113
159
  export function clearSessionStorage() {
@@ -1,4 +1,23 @@
1
- /** @internal */
1
+ /**
2
+ * Creates an enum-like object from string values where each key maps to itself.
3
+ * Useful for creating string constant objects with type safety and autocompletion.
4
+ * @param values - The string values to create the enum from.
5
+ * @returns An object where each provided string is both the key and value.
6
+ * @example
7
+ * ```ts
8
+ * const Colors = stringEnum('red', 'green', 'blue')
9
+ * // Results in: { red: 'red', green: 'green', blue: 'blue' }
10
+ *
11
+ * // Type-safe usage
12
+ * function setColor(color: keyof typeof Colors) {
13
+ * console.log(`Setting color to ${Colors[color]}`)
14
+ * }
15
+ *
16
+ * setColor('red') // ✓ Valid
17
+ * setColor('yellow') // ✗ TypeScript error
18
+ * ```
19
+ * @internal
20
+ */
2
21
  export function stringEnum<T extends string>(...values: T[]): { [K in T]: K } {
3
22
  const obj = {} as { [K in T]: K }
4
23
  for (const value of values) {
@@ -52,10 +52,27 @@ function tick(isOnNextFrame = false) {
52
52
  }
53
53
 
54
54
  /**
55
- * Returns a throttled version of the function that will only be called max once per frame.
56
- * The target frame rate is 60fps.
57
- * @param fn - the fun to return a throttled version of
58
- * @returns
55
+ * Creates a throttled version of a function that executes at most once per frame (60fps).
56
+ * Subsequent calls within the same frame are ignored, ensuring smooth performance
57
+ * for high-frequency events like mouse movements or scroll events.
58
+ *
59
+ * @param fn - The function to throttle, optionally with a cancel method
60
+ * @returns A throttled function with an optional cancel method to remove pending calls
61
+ *
62
+ * @example
63
+ * ```ts
64
+ * const updateCanvas = fpsThrottle(() => {
65
+ * // This will run at most once per frame (~16.67ms)
66
+ * redrawCanvas()
67
+ * })
68
+ *
69
+ * // Call as often as you want - automatically throttled to 60fps
70
+ * document.addEventListener('mousemove', updateCanvas)
71
+ *
72
+ * // Cancel pending calls if needed
73
+ * updateCanvas.cancel?.()
74
+ * ```
75
+ *
59
76
  * @internal
60
77
  */
61
78
  export function fpsThrottle(fn: { (): void; cancel?(): void }): {
@@ -93,10 +110,31 @@ export function fpsThrottle(fn: { (): void; cancel?(): void }): {
93
110
  }
94
111
 
95
112
  /**
96
- * Calls the function on the next frame. The target frame rate is 60fps.
97
- * If the same fn is passed again before the next frame, it will still be called only once.
98
- * @param fn - the fun to call on the next frame
99
- * @returns a function that will cancel the call if called before the next frame
113
+ * Schedules a function to execute on the next animation frame, targeting 60fps.
114
+ * If the same function is passed multiple times before the frame executes,
115
+ * it will only be called once, effectively batching multiple calls.
116
+ *
117
+ * @param fn - The function to execute on the next frame
118
+ * @returns A cancel function that can prevent execution if called before the next frame
119
+ *
120
+ * @example
121
+ * ```ts
122
+ * const updateUI = throttleToNextFrame(() => {
123
+ * // Batches multiple calls into the next animation frame
124
+ * updateStatusBar()
125
+ * refreshToolbar()
126
+ * })
127
+ *
128
+ * // Multiple calls within the same frame are batched
129
+ * updateUI() // Will execute
130
+ * updateUI() // Ignored (same function already queued)
131
+ * updateUI() // Ignored (same function already queued)
132
+ *
133
+ * // Get cancel function to prevent execution
134
+ * const cancel = updateUI()
135
+ * cancel() // Prevents execution if called before next frame
136
+ * ```
137
+ *
100
138
  * @internal
101
139
  */
102
140
  export function throttleToNextFrame(fn: () => void): () => void {
@@ -0,0 +1,75 @@
1
+ import { describe, expect, it, vi } from 'vitest'
2
+ import { Timers } from './timers'
3
+
4
+ describe('Timers', () => {
5
+ it('tracks timers by context and disposes them correctly', () => {
6
+ const timers = new Timers()
7
+ const mockClearTimeout = vi.fn()
8
+ const mockClearInterval = vi.fn()
9
+ const mockCancelAnimationFrame = vi.fn()
10
+
11
+ // Mock only the clear functions since those are what we need to verify
12
+ vi.stubGlobal('setTimeout', vi.fn().mockReturnValueOnce(1).mockReturnValueOnce(2))
13
+ vi.stubGlobal('setInterval', vi.fn().mockReturnValue(3))
14
+ vi.stubGlobal('requestAnimationFrame', vi.fn().mockReturnValue(4))
15
+ vi.stubGlobal('clearTimeout', mockClearTimeout)
16
+ vi.stubGlobal('clearInterval', mockClearInterval)
17
+ vi.stubGlobal('cancelAnimationFrame', mockCancelAnimationFrame)
18
+
19
+ // Create timers in different contexts
20
+ timers.setTimeout('context1', () => {}, 1000)
21
+ timers.setTimeout('context1', () => {}, 2000)
22
+ timers.setInterval('context1', () => {}, 500)
23
+ timers.requestAnimationFrame('context2', () => {})
24
+
25
+ // Dispose one context
26
+ timers.dispose('context1')
27
+
28
+ // Should clear timers for context1 but not context2
29
+ expect(mockClearTimeout).toHaveBeenCalledWith(1)
30
+ expect(mockClearTimeout).toHaveBeenCalledWith(2)
31
+ expect(mockClearInterval).toHaveBeenCalledWith(3)
32
+ expect(mockCancelAnimationFrame).not.toHaveBeenCalled()
33
+
34
+ vi.unstubAllGlobals()
35
+ })
36
+
37
+ it('disposes all contexts with disposeAll', () => {
38
+ const timers = new Timers()
39
+ const mockClearTimeout = vi.fn()
40
+ const mockClearInterval = vi.fn()
41
+
42
+ vi.stubGlobal('setTimeout', vi.fn().mockReturnValueOnce(1).mockReturnValueOnce(2))
43
+ vi.stubGlobal('setInterval', vi.fn().mockReturnValue(3))
44
+ vi.stubGlobal('clearTimeout', mockClearTimeout)
45
+ vi.stubGlobal('clearInterval', mockClearInterval)
46
+
47
+ timers.setTimeout('context1', () => {}, 1000)
48
+ timers.setTimeout('context2', () => {}, 2000)
49
+ timers.setInterval('context1', () => {}, 500)
50
+
51
+ timers.disposeAll()
52
+
53
+ expect(mockClearTimeout).toHaveBeenCalledWith(1)
54
+ expect(mockClearTimeout).toHaveBeenCalledWith(2)
55
+ expect(mockClearInterval).toHaveBeenCalledWith(3)
56
+
57
+ vi.unstubAllGlobals()
58
+ })
59
+
60
+ it('provides context-bound methods via forContext', () => {
61
+ const timers = new Timers()
62
+ const mockClearTimeout = vi.fn()
63
+
64
+ vi.stubGlobal('setTimeout', vi.fn().mockReturnValue(1))
65
+ vi.stubGlobal('clearTimeout', mockClearTimeout)
66
+
67
+ const contextTimers = timers.forContext('test-context')
68
+ contextTimers.setTimeout(() => {}, 1000)
69
+ contextTimers.dispose()
70
+
71
+ expect(mockClearTimeout).toHaveBeenCalledWith(1)
72
+
73
+ vi.unstubAllGlobals()
74
+ })
75
+ })