@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
|
@@ -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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
+
})
|
package/src/lib/storage.tsx
CHANGED
|
@@ -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
|
|
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
|
|
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() {
|
package/src/lib/stringEnum.ts
CHANGED
|
@@ -1,4 +1,23 @@
|
|
|
1
|
-
/**
|
|
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) {
|
package/src/lib/throttle.ts
CHANGED
|
@@ -52,10 +52,27 @@ function tick(isOnNextFrame = false) {
|
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
/**
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
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
|
-
*
|
|
97
|
-
* If the same
|
|
98
|
-
*
|
|
99
|
-
*
|
|
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
|
+
})
|