@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
|
@@ -1,12 +1,75 @@
|
|
|
1
1
|
import { sleep } from './control'
|
|
2
2
|
|
|
3
|
-
/**
|
|
3
|
+
/**
|
|
4
|
+
* A queue that executes tasks sequentially with optional delay between tasks.
|
|
5
|
+
*
|
|
6
|
+
* ExecutionQueue ensures that tasks are executed one at a time in the order they were added,
|
|
7
|
+
* with an optional timeout delay between each task execution. This is useful for rate limiting,
|
|
8
|
+
* preventing race conditions, or controlling the flow of asynchronous operations.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```ts
|
|
12
|
+
* // Create a queue with 100ms delay between tasks
|
|
13
|
+
* const queue = new ExecutionQueue(100)
|
|
14
|
+
*
|
|
15
|
+
* // Add tasks to the queue
|
|
16
|
+
* const result1 = await queue.push(() => fetch('/api/data'))
|
|
17
|
+
* const result2 = await queue.push(async () => {
|
|
18
|
+
* const data = await processData()
|
|
19
|
+
* return data
|
|
20
|
+
* })
|
|
21
|
+
*
|
|
22
|
+
* // Check if queue is empty
|
|
23
|
+
* if (queue.isEmpty()) {
|
|
24
|
+
* console.log('All tasks completed')
|
|
25
|
+
* }
|
|
26
|
+
*
|
|
27
|
+
* // Clean up
|
|
28
|
+
* queue.close()
|
|
29
|
+
* ```
|
|
30
|
+
*
|
|
31
|
+
* @internal
|
|
32
|
+
*/
|
|
4
33
|
export class ExecutionQueue {
|
|
5
34
|
private queue: (() => Promise<any>)[] = []
|
|
6
35
|
private running = false
|
|
7
36
|
|
|
37
|
+
/**
|
|
38
|
+
* Creates a new ExecutionQueue.
|
|
39
|
+
*
|
|
40
|
+
* Creates a new execution queue that will process tasks sequentially.
|
|
41
|
+
* If a timeout is provided, there will be a delay between each task execution,
|
|
42
|
+
* which is useful for rate limiting or controlling execution flow.
|
|
43
|
+
*
|
|
44
|
+
* timeout - Optional delay in milliseconds between task executions
|
|
45
|
+
* @example
|
|
46
|
+
* ```ts
|
|
47
|
+
* // Create queue without delay
|
|
48
|
+
* const fastQueue = new ExecutionQueue()
|
|
49
|
+
*
|
|
50
|
+
* // Create queue with 500ms delay between tasks
|
|
51
|
+
* const slowQueue = new ExecutionQueue(500)
|
|
52
|
+
* ```
|
|
53
|
+
*/
|
|
8
54
|
constructor(private readonly timeout?: number) {}
|
|
9
55
|
|
|
56
|
+
/**
|
|
57
|
+
* Checks if the queue is empty and not currently running a task.
|
|
58
|
+
*
|
|
59
|
+
* Determines whether the execution queue has completed all tasks and is idle.
|
|
60
|
+
* Returns true only when there are no pending tasks in the queue AND no task is currently being executed.
|
|
61
|
+
*
|
|
62
|
+
* @returns True if the queue has no pending tasks and is not currently executing
|
|
63
|
+
* @example
|
|
64
|
+
* ```ts
|
|
65
|
+
* const queue = new ExecutionQueue()
|
|
66
|
+
*
|
|
67
|
+
* console.log(queue.isEmpty()) // true - queue is empty
|
|
68
|
+
*
|
|
69
|
+
* queue.push(() => console.log('task'))
|
|
70
|
+
* console.log(queue.isEmpty()) // false - task is running/pending
|
|
71
|
+
* ```
|
|
72
|
+
*/
|
|
10
73
|
isEmpty() {
|
|
11
74
|
return this.queue.length === 0 && !this.running
|
|
12
75
|
}
|
|
@@ -25,10 +88,34 @@ export class ExecutionQueue {
|
|
|
25
88
|
} finally {
|
|
26
89
|
// this try/finally should not be needed because the tasks don't throw
|
|
27
90
|
// but better safe than sorry
|
|
91
|
+
// console.log('\n\n\nrunning false\n\n\n')
|
|
28
92
|
this.running = false
|
|
29
93
|
}
|
|
30
94
|
}
|
|
31
95
|
|
|
96
|
+
/**
|
|
97
|
+
* Adds a task to the queue and returns a promise that resolves with the task's result.
|
|
98
|
+
*
|
|
99
|
+
* Enqueues a task for sequential execution. The task will be executed after all
|
|
100
|
+
* previously queued tasks have completed. If a timeout was specified in the constructor,
|
|
101
|
+
* there will be a delay between this task and the next one.
|
|
102
|
+
*
|
|
103
|
+
* @param task - The function to execute (can be sync or async)
|
|
104
|
+
* @returns Promise that resolves with the task's return value
|
|
105
|
+
* @example
|
|
106
|
+
* ```ts
|
|
107
|
+
* const queue = new ExecutionQueue(100)
|
|
108
|
+
*
|
|
109
|
+
* // Add async task
|
|
110
|
+
* const result = await queue.push(async () => {
|
|
111
|
+
* const response = await fetch('/api/data')
|
|
112
|
+
* return response.json()
|
|
113
|
+
* })
|
|
114
|
+
*
|
|
115
|
+
* // Add sync task
|
|
116
|
+
* const number = await queue.push(() => 42)
|
|
117
|
+
* ```
|
|
118
|
+
*/
|
|
32
119
|
async push<T>(task: () => T): Promise<Awaited<T>> {
|
|
33
120
|
return new Promise<Awaited<T>>((resolve, reject) => {
|
|
34
121
|
this.queue.push(() => Promise.resolve(task()).then(resolve).catch(reject))
|
|
@@ -36,6 +123,28 @@ export class ExecutionQueue {
|
|
|
36
123
|
})
|
|
37
124
|
}
|
|
38
125
|
|
|
126
|
+
/**
|
|
127
|
+
* Clears all pending tasks from the queue.
|
|
128
|
+
*
|
|
129
|
+
* Immediately removes all pending tasks from the queue. Any currently
|
|
130
|
+
* running task will complete normally, but no additional tasks will be executed.
|
|
131
|
+
* This method does not wait for the current task to finish.
|
|
132
|
+
*
|
|
133
|
+
* @returns void
|
|
134
|
+
* @example
|
|
135
|
+
* ```ts
|
|
136
|
+
* const queue = new ExecutionQueue()
|
|
137
|
+
*
|
|
138
|
+
* // Add several tasks
|
|
139
|
+
* queue.push(() => console.log('task 1'))
|
|
140
|
+
* queue.push(() => console.log('task 2'))
|
|
141
|
+
* queue.push(() => console.log('task 3'))
|
|
142
|
+
*
|
|
143
|
+
* // Clear all pending tasks
|
|
144
|
+
* queue.close()
|
|
145
|
+
* // Only 'task 1' will execute if it was already running
|
|
146
|
+
* ```
|
|
147
|
+
*/
|
|
39
148
|
close() {
|
|
40
149
|
this.queue = []
|
|
41
150
|
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
2
|
+
import { PerformanceTracker } from './PerformanceTracker'
|
|
3
|
+
import { PERFORMANCE_COLORS, PERFORMANCE_PREFIX_COLOR } from './perf'
|
|
4
|
+
|
|
5
|
+
describe('PerformanceTracker', () => {
|
|
6
|
+
let tracker: PerformanceTracker
|
|
7
|
+
let mockPerformanceNow: ReturnType<typeof vi.fn>
|
|
8
|
+
let mockRequestAnimationFrame: ReturnType<typeof vi.fn>
|
|
9
|
+
let mockCancelAnimationFrame: ReturnType<typeof vi.fn>
|
|
10
|
+
let mockConsoleDebug: ReturnType<typeof vi.fn>
|
|
11
|
+
let frameId = 1
|
|
12
|
+
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
tracker = new PerformanceTracker()
|
|
15
|
+
|
|
16
|
+
// Mock performance.now
|
|
17
|
+
mockPerformanceNow = vi.fn()
|
|
18
|
+
vi.stubGlobal('performance', { now: mockPerformanceNow })
|
|
19
|
+
|
|
20
|
+
// Mock requestAnimationFrame and cancelAnimationFrame
|
|
21
|
+
mockRequestAnimationFrame = vi.fn().mockImplementation(() => frameId++)
|
|
22
|
+
mockCancelAnimationFrame = vi.fn()
|
|
23
|
+
vi.stubGlobal('requestAnimationFrame', mockRequestAnimationFrame)
|
|
24
|
+
vi.stubGlobal('cancelAnimationFrame', mockCancelAnimationFrame)
|
|
25
|
+
|
|
26
|
+
// Mock console.debug
|
|
27
|
+
mockConsoleDebug = vi.fn()
|
|
28
|
+
vi.spyOn(console, 'debug').mockImplementation(mockConsoleDebug)
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
afterEach(() => {
|
|
32
|
+
vi.unstubAllGlobals()
|
|
33
|
+
vi.restoreAllMocks()
|
|
34
|
+
frameId = 1
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it('tracks start/stop state correctly', () => {
|
|
38
|
+
expect(tracker.isStarted()).toBe(false)
|
|
39
|
+
|
|
40
|
+
mockPerformanceNow.mockReturnValue(100)
|
|
41
|
+
tracker.start('test')
|
|
42
|
+
expect(tracker.isStarted()).toBe(true)
|
|
43
|
+
|
|
44
|
+
mockPerformanceNow.mockReturnValue(200)
|
|
45
|
+
tracker.stop()
|
|
46
|
+
expect(tracker.isStarted()).toBe(false)
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it('calculates and logs FPS correctly', () => {
|
|
50
|
+
// Setup: 1 second duration with 60 frames
|
|
51
|
+
mockPerformanceNow.mockReturnValueOnce(0).mockReturnValueOnce(1000)
|
|
52
|
+
|
|
53
|
+
tracker.start('render')
|
|
54
|
+
const recordFrame = mockRequestAnimationFrame.mock.calls[0][0]
|
|
55
|
+
|
|
56
|
+
// Simulate 60 frames
|
|
57
|
+
for (let i = 0; i < 60; i++) {
|
|
58
|
+
recordFrame()
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
tracker.stop()
|
|
62
|
+
|
|
63
|
+
expect(mockConsoleDebug).toHaveBeenCalledWith(
|
|
64
|
+
'%cPerf%c Render %c60%c fps',
|
|
65
|
+
`color: white; background: ${PERFORMANCE_PREFIX_COLOR};padding: 2px;border-radius: 3px;`,
|
|
66
|
+
'font-weight: normal',
|
|
67
|
+
`font-weight: bold; padding: 2px; background: ${PERFORMANCE_COLORS.Good};color: white;`,
|
|
68
|
+
'font-weight: normal'
|
|
69
|
+
)
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
it('resets frame count between tracking sessions', () => {
|
|
73
|
+
mockPerformanceNow.mockReturnValueOnce(100).mockReturnValueOnce(200)
|
|
74
|
+
|
|
75
|
+
// First session with frames
|
|
76
|
+
tracker.start('test')
|
|
77
|
+
const recordFrame = mockRequestAnimationFrame.mock.calls[0][0]
|
|
78
|
+
recordFrame() // simulate frame
|
|
79
|
+
recordFrame() // simulate frame
|
|
80
|
+
tracker.stop()
|
|
81
|
+
|
|
82
|
+
// Second session should reset frame count
|
|
83
|
+
mockPerformanceNow.mockReturnValueOnce(300).mockReturnValueOnce(400)
|
|
84
|
+
tracker.start('test2')
|
|
85
|
+
tracker.stop()
|
|
86
|
+
|
|
87
|
+
// Should show 0 fps for second session (0 frames in 0.1 seconds)
|
|
88
|
+
expect(mockConsoleDebug).toHaveBeenLastCalledWith(
|
|
89
|
+
'%cPerf%c Test2 %c0%c fps',
|
|
90
|
+
expect.any(String),
|
|
91
|
+
'font-weight: normal',
|
|
92
|
+
expect.any(String),
|
|
93
|
+
'font-weight: normal'
|
|
94
|
+
)
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
it('records frames only while tracking is active', () => {
|
|
98
|
+
mockPerformanceNow.mockReturnValue(100)
|
|
99
|
+
|
|
100
|
+
tracker.start('test')
|
|
101
|
+
const recordFrame = mockRequestAnimationFrame.mock.calls[0][0]
|
|
102
|
+
|
|
103
|
+
tracker.stop()
|
|
104
|
+
mockRequestAnimationFrame.mockClear()
|
|
105
|
+
|
|
106
|
+
// Frame recording should stop after stop() is called
|
|
107
|
+
recordFrame()
|
|
108
|
+
expect(mockRequestAnimationFrame).not.toHaveBeenCalled()
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
it('handles zero duration gracefully', () => {
|
|
112
|
+
// Zero duration should not crash
|
|
113
|
+
mockPerformanceNow.mockReturnValue(100)
|
|
114
|
+
tracker.start('instant')
|
|
115
|
+
tracker.stop()
|
|
116
|
+
expect(mockConsoleDebug).toHaveBeenCalledWith(
|
|
117
|
+
'%cPerf%c Instant %c0%c fps',
|
|
118
|
+
expect.any(String),
|
|
119
|
+
'font-weight: normal',
|
|
120
|
+
expect.any(String),
|
|
121
|
+
'font-weight: normal'
|
|
122
|
+
)
|
|
123
|
+
})
|
|
124
|
+
})
|
|
@@ -1,6 +1,25 @@
|
|
|
1
1
|
import { PERFORMANCE_COLORS, PERFORMANCE_PREFIX_COLOR } from './perf'
|
|
2
2
|
|
|
3
|
-
/**
|
|
3
|
+
/**
|
|
4
|
+
* A utility class for measuring and tracking frame rate performance during operations.
|
|
5
|
+
* Provides visual feedback in the browser console with color-coded FPS indicators.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```ts
|
|
9
|
+
* const tracker = new PerformanceTracker()
|
|
10
|
+
*
|
|
11
|
+
* tracker.start('render')
|
|
12
|
+
* renderShapes()
|
|
13
|
+
* tracker.stop() // Logs performance info to console
|
|
14
|
+
*
|
|
15
|
+
* // Check if tracking is active
|
|
16
|
+
* if (tracker.isStarted()) {
|
|
17
|
+
* console.log('Still tracking performance')
|
|
18
|
+
* }
|
|
19
|
+
* ```
|
|
20
|
+
*
|
|
21
|
+
* @public
|
|
22
|
+
*/
|
|
4
23
|
export class PerformanceTracker {
|
|
5
24
|
private startTime = 0
|
|
6
25
|
private name = ''
|
|
@@ -8,6 +27,10 @@ export class PerformanceTracker {
|
|
|
8
27
|
private started = false
|
|
9
28
|
private frame: number | null = null
|
|
10
29
|
|
|
30
|
+
/**
|
|
31
|
+
* Records animation frames to calculate frame rate.
|
|
32
|
+
* Called automatically during performance tracking.
|
|
33
|
+
*/
|
|
11
34
|
// eslint-disable-next-line local/prefer-class-methods
|
|
12
35
|
recordFrame = () => {
|
|
13
36
|
this.frames++
|
|
@@ -16,6 +39,18 @@ export class PerformanceTracker {
|
|
|
16
39
|
this.frame = requestAnimationFrame(this.recordFrame)
|
|
17
40
|
}
|
|
18
41
|
|
|
42
|
+
/**
|
|
43
|
+
* Starts performance tracking for a named operation.
|
|
44
|
+
*
|
|
45
|
+
* @param name - A descriptive name for the operation being tracked
|
|
46
|
+
*
|
|
47
|
+
* @example
|
|
48
|
+
* ```ts
|
|
49
|
+
* tracker.start('canvas-render')
|
|
50
|
+
* // ... perform rendering operations
|
|
51
|
+
* tracker.stop()
|
|
52
|
+
* ```
|
|
53
|
+
*/
|
|
19
54
|
start(name: string) {
|
|
20
55
|
this.name = name
|
|
21
56
|
this.frames = 0
|
|
@@ -26,6 +61,21 @@ export class PerformanceTracker {
|
|
|
26
61
|
this.startTime = performance.now()
|
|
27
62
|
}
|
|
28
63
|
|
|
64
|
+
/**
|
|
65
|
+
* Stops performance tracking and logs results to the console.
|
|
66
|
+
*
|
|
67
|
+
* Displays the operation name, frame rate, and uses color coding:
|
|
68
|
+
* - Green background: \> 55 FPS (good performance)
|
|
69
|
+
* - Yellow background: 30-55 FPS (moderate performance)
|
|
70
|
+
* - Red background: \< 30 FPS (poor performance)
|
|
71
|
+
*
|
|
72
|
+
* @example
|
|
73
|
+
* ```ts
|
|
74
|
+
* tracker.start('interaction')
|
|
75
|
+
* handleUserInteraction()
|
|
76
|
+
* tracker.stop() // Logs: "Perf Interaction 60 fps"
|
|
77
|
+
* ```
|
|
78
|
+
*/
|
|
29
79
|
stop() {
|
|
30
80
|
this.started = false
|
|
31
81
|
if (this.frame !== null) cancelAnimationFrame(this.frame)
|
|
@@ -49,6 +99,18 @@ export class PerformanceTracker {
|
|
|
49
99
|
)
|
|
50
100
|
}
|
|
51
101
|
|
|
102
|
+
/**
|
|
103
|
+
* Checks whether performance tracking is currently active.
|
|
104
|
+
*
|
|
105
|
+
* @returns True if tracking is in progress, false otherwise
|
|
106
|
+
*
|
|
107
|
+
* @example
|
|
108
|
+
* ```ts
|
|
109
|
+
* if (!tracker.isStarted()) {
|
|
110
|
+
* tracker.start('new-operation')
|
|
111
|
+
* }
|
|
112
|
+
* ```
|
|
113
|
+
*/
|
|
52
114
|
isStarted() {
|
|
53
115
|
return this.started
|
|
54
116
|
}
|
package/src/lib/array.test.ts
CHANGED
|
@@ -1,4 +1,266 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
areArraysShallowEqual,
|
|
3
|
+
compact,
|
|
4
|
+
dedupe,
|
|
5
|
+
last,
|
|
6
|
+
maxBy,
|
|
7
|
+
mergeArraysAndReplaceDefaults,
|
|
8
|
+
minBy,
|
|
9
|
+
partition,
|
|
10
|
+
rotateArray,
|
|
11
|
+
} from './array'
|
|
12
|
+
|
|
13
|
+
describe('rotateArray', () => {
|
|
14
|
+
test('should rotate array to the left with positive offset', () => {
|
|
15
|
+
// Based on JSDoc examples, this is the expected behavior
|
|
16
|
+
expect(rotateArray([1, 2, 3, 4], 1)).toEqual([2, 3, 4, 1])
|
|
17
|
+
expect(rotateArray([1, 2, 3, 4], 2)).toEqual([3, 4, 1, 2])
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
test('should rotate array to the left with negative offset', () => {
|
|
21
|
+
// Based on JSDoc examples, this is the expected behavior
|
|
22
|
+
expect(rotateArray([1, 2, 3, 4], -1)).toEqual([2, 3, 4, 1])
|
|
23
|
+
expect(rotateArray([1, 2, 3, 4], -2)).toEqual([3, 4, 1, 2])
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
it('should handle zero offset', () => {
|
|
27
|
+
expect(rotateArray([1, 2, 3, 4], 0)).toEqual([1, 2, 3, 4])
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
test('should handle offset larger than array length', () => {
|
|
31
|
+
// Based on understanding of rotation: offset 5 on length 3 should be same as offset 2
|
|
32
|
+
expect(rotateArray([1, 2, 3], 5)).toEqual([3, 1, 2])
|
|
33
|
+
// offset -5 on length 3 should be same as offset -2 which should be same as offset 2
|
|
34
|
+
expect(rotateArray([1, 2, 3], -5)).toEqual([3, 1, 2])
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it('should handle empty array', () => {
|
|
38
|
+
expect(rotateArray([], 1)).toEqual([])
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
test('should work with different types', () => {
|
|
42
|
+
// Based on JSDoc examples, this is the expected behavior
|
|
43
|
+
expect(rotateArray(['a', 'b', 'c'], 2)).toEqual(['c', 'a', 'b'])
|
|
44
|
+
})
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
describe('dedupe', () => {
|
|
48
|
+
it('should remove duplicate primitives', () => {
|
|
49
|
+
expect(dedupe([1, 2, 2, 3, 1])).toEqual([1, 2, 3])
|
|
50
|
+
expect(dedupe(['a', 'b', 'a', 'c'])).toEqual(['a', 'b', 'c'])
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
it('should preserve order of first occurrence', () => {
|
|
54
|
+
expect(dedupe([3, 1, 2, 1, 3])).toEqual([3, 1, 2])
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
it('should handle empty array', () => {
|
|
58
|
+
expect(dedupe([])).toEqual([])
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
it('should handle array with no duplicates', () => {
|
|
62
|
+
expect(dedupe([1, 2, 3])).toEqual([1, 2, 3])
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
it('should use custom equality function', () => {
|
|
66
|
+
const objects = [
|
|
67
|
+
{ id: 1, name: 'a' },
|
|
68
|
+
{ id: 2, name: 'b' },
|
|
69
|
+
{ id: 1, name: 'c' },
|
|
70
|
+
]
|
|
71
|
+
expect(dedupe(objects, (a, b) => a.id === b.id)).toEqual([
|
|
72
|
+
{ id: 1, name: 'a' },
|
|
73
|
+
{ id: 2, name: 'b' },
|
|
74
|
+
])
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
it('should handle objects without custom equality', () => {
|
|
78
|
+
const obj1 = { id: 1 }
|
|
79
|
+
const obj2 = { id: 2 }
|
|
80
|
+
expect(dedupe([obj1, obj2, obj1])).toEqual([obj1, obj2])
|
|
81
|
+
})
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
describe('compact', () => {
|
|
85
|
+
it('should remove null and undefined values', () => {
|
|
86
|
+
expect(compact([1, null, 2, undefined, 3])).toEqual([1, 2, 3])
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
it('should preserve falsy values that are not null/undefined', () => {
|
|
90
|
+
expect(compact([0, false, '', null, undefined, 'hello'])).toEqual([0, false, '', 'hello'])
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
it('should handle empty array', () => {
|
|
94
|
+
expect(compact([])).toEqual([])
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
it('should handle array with only null/undefined', () => {
|
|
98
|
+
expect(compact([null, undefined, null])).toEqual([])
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
it('should handle array with no null/undefined', () => {
|
|
102
|
+
expect(compact([1, 2, 3])).toEqual([1, 2, 3])
|
|
103
|
+
})
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
describe('last', () => {
|
|
107
|
+
it('should return last element of array', () => {
|
|
108
|
+
expect(last([1, 2, 3])).toBe(3)
|
|
109
|
+
expect(last(['a', 'b', 'c'])).toBe('c')
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
it('should return undefined for empty array', () => {
|
|
113
|
+
expect(last([])).toBeUndefined()
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
it('should work with single element array', () => {
|
|
117
|
+
expect(last([42])).toBe(42)
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
it('should work with readonly arrays', () => {
|
|
121
|
+
const readonlyArr: readonly number[] = [1, 2, 3]
|
|
122
|
+
expect(last(readonlyArr)).toBe(3)
|
|
123
|
+
})
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
describe('minBy', () => {
|
|
127
|
+
it('should find item with minimum value', () => {
|
|
128
|
+
const people = [
|
|
129
|
+
{ name: 'Alice', age: 30 },
|
|
130
|
+
{ name: 'Bob', age: 25 },
|
|
131
|
+
{ name: 'Charlie', age: 35 },
|
|
132
|
+
]
|
|
133
|
+
expect(minBy(people, (p) => p.age)).toEqual({ name: 'Bob', age: 25 })
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
it('should work with numbers', () => {
|
|
137
|
+
expect(minBy([3, 1, 4, 1, 5], (x) => x)).toBe(1)
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
it('should return undefined for empty array', () => {
|
|
141
|
+
expect(minBy([], (x) => x)).toBeUndefined()
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
it('should handle ties by returning first occurrence', () => {
|
|
145
|
+
const items = [{ val: 5 }, { val: 3 }, { val: 3 }, { val: 7 }]
|
|
146
|
+
expect(minBy(items, (x) => x.val)).toEqual({ val: 3 })
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
it('should work with negative values', () => {
|
|
150
|
+
expect(minBy([-1, -5, -3], (x) => x)).toBe(-5)
|
|
151
|
+
})
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
describe('maxBy', () => {
|
|
155
|
+
it('should find item with maximum value', () => {
|
|
156
|
+
const people = [
|
|
157
|
+
{ name: 'Alice', age: 30 },
|
|
158
|
+
{ name: 'Bob', age: 25 },
|
|
159
|
+
{ name: 'Charlie', age: 35 },
|
|
160
|
+
]
|
|
161
|
+
expect(maxBy(people, (p) => p.age)).toEqual({ name: 'Charlie', age: 35 })
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
it('should work with numbers', () => {
|
|
165
|
+
expect(maxBy([3, 1, 4, 1, 5], (x) => x)).toBe(5)
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
it('should return undefined for empty array', () => {
|
|
169
|
+
expect(maxBy([], (x) => x)).toBeUndefined()
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
it('should handle ties by returning first occurrence', () => {
|
|
173
|
+
const items = [{ val: 5 }, { val: 7 }, { val: 7 }, { val: 3 }]
|
|
174
|
+
expect(maxBy(items, (x) => x.val)).toEqual({ val: 7 })
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
it('should work with negative values', () => {
|
|
178
|
+
expect(maxBy([-1, -5, -3], (x) => x)).toBe(-1)
|
|
179
|
+
})
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
describe('partition', () => {
|
|
183
|
+
it('should split array based on predicate', () => {
|
|
184
|
+
const [evens, odds] = partition([1, 2, 3, 4, 5], (x) => x % 2 === 0)
|
|
185
|
+
expect(evens).toEqual([2, 4])
|
|
186
|
+
expect(odds).toEqual([1, 3, 5])
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
it('should preserve order within partitions', () => {
|
|
190
|
+
const [adults, minors] = partition(
|
|
191
|
+
[
|
|
192
|
+
{ name: 'Alice', age: 30 },
|
|
193
|
+
{ name: 'Bob', age: 17 },
|
|
194
|
+
{ name: 'Charlie', age: 25 },
|
|
195
|
+
],
|
|
196
|
+
(person) => person.age >= 18
|
|
197
|
+
)
|
|
198
|
+
expect(adults).toEqual([
|
|
199
|
+
{ name: 'Alice', age: 30 },
|
|
200
|
+
{ name: 'Charlie', age: 25 },
|
|
201
|
+
])
|
|
202
|
+
expect(minors).toEqual([{ name: 'Bob', age: 17 }])
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
it('should handle empty array', () => {
|
|
206
|
+
const [satisfies, doesNotSatisfy] = partition([], (x) => x > 0)
|
|
207
|
+
expect(satisfies).toEqual([])
|
|
208
|
+
expect(doesNotSatisfy).toEqual([])
|
|
209
|
+
})
|
|
210
|
+
|
|
211
|
+
it('should handle all items satisfying predicate', () => {
|
|
212
|
+
const [satisfies, doesNotSatisfy] = partition([2, 4, 6], (x) => x % 2 === 0)
|
|
213
|
+
expect(satisfies).toEqual([2, 4, 6])
|
|
214
|
+
expect(doesNotSatisfy).toEqual([])
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
it('should handle no items satisfying predicate', () => {
|
|
218
|
+
const [satisfies, doesNotSatisfy] = partition([1, 3, 5], (x) => x % 2 === 0)
|
|
219
|
+
expect(satisfies).toEqual([])
|
|
220
|
+
expect(doesNotSatisfy).toEqual([1, 3, 5])
|
|
221
|
+
})
|
|
222
|
+
})
|
|
223
|
+
|
|
224
|
+
describe('areArraysShallowEqual', () => {
|
|
225
|
+
it('should return true for identical arrays', () => {
|
|
226
|
+
expect(areArraysShallowEqual([1, 2, 3], [1, 2, 3])).toBe(true)
|
|
227
|
+
expect(areArraysShallowEqual(['a', 'b'], ['a', 'b'])).toBe(true)
|
|
228
|
+
})
|
|
229
|
+
|
|
230
|
+
it('should return true for same reference', () => {
|
|
231
|
+
const arr = [1, 2, 3]
|
|
232
|
+
expect(areArraysShallowEqual(arr, arr)).toBe(true)
|
|
233
|
+
})
|
|
234
|
+
|
|
235
|
+
it('should return false for different lengths', () => {
|
|
236
|
+
expect(areArraysShallowEqual([1, 2], [1, 2, 3])).toBe(false)
|
|
237
|
+
})
|
|
238
|
+
|
|
239
|
+
it('should return false for different elements', () => {
|
|
240
|
+
expect(areArraysShallowEqual([1, 2, 3], [1, 2, 4])).toBe(false)
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
it('should return true for empty arrays', () => {
|
|
244
|
+
expect(areArraysShallowEqual([], [])).toBe(true)
|
|
245
|
+
})
|
|
246
|
+
|
|
247
|
+
it('should use Object.is for comparison', () => {
|
|
248
|
+
expect(areArraysShallowEqual([NaN], [NaN])).toBe(true)
|
|
249
|
+
expect(areArraysShallowEqual([0], [-0])).toBe(false)
|
|
250
|
+
})
|
|
251
|
+
|
|
252
|
+
it('should work with object references', () => {
|
|
253
|
+
const obj = { x: 1 }
|
|
254
|
+
expect(areArraysShallowEqual([obj], [obj])).toBe(true)
|
|
255
|
+
expect(areArraysShallowEqual([{ x: 1 }], [{ x: 1 }])).toBe(false)
|
|
256
|
+
})
|
|
257
|
+
|
|
258
|
+
it('should work with readonly arrays', () => {
|
|
259
|
+
const arr1: readonly number[] = [1, 2, 3]
|
|
260
|
+
const arr2: readonly number[] = [1, 2, 3]
|
|
261
|
+
expect(areArraysShallowEqual(arr1, arr2)).toBe(true)
|
|
262
|
+
})
|
|
263
|
+
})
|
|
2
264
|
|
|
3
265
|
describe('mergeArraysAndReplaceDefaults', () => {
|
|
4
266
|
it('should merge custom entries with defaults, allowing custom entries to override defaults', () => {
|