@tldraw/utils 5.2.0-next.355d68cf24e8 → 5.2.0-next.547772a8f51a
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/DOCS.md +930 -0
- package/README.md +9 -1
- package/dist-cjs/index.js +1 -1
- package/dist-esm/index.mjs +1 -1
- package/package.json +4 -3
- package/src/lib/PerformanceTracker.test.ts +3 -4
- package/src/lib/version.test.ts +1 -1
package/DOCS.md
ADDED
|
@@ -0,0 +1,930 @@
|
|
|
1
|
+
# @tldraw/utils
|
|
2
|
+
|
|
3
|
+
The `@tldraw/utils` package provides foundational utility functions that power the tldraw SDK. It contains pure, reusable helper functions for common programming tasks including array manipulation, object operations, error handling, performance optimization, and media processing.
|
|
4
|
+
|
|
5
|
+
## 1. Introduction
|
|
6
|
+
|
|
7
|
+
The utils package serves as the foundation of the tldraw ecosystem, providing battle-tested utilities that other tldraw packages depend on. Every function is designed with type safety, performance, and cross-platform compatibility in mind.
|
|
8
|
+
|
|
9
|
+
You import utilities directly as named exports:
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
import { dedupe, rotateArray, partition } from '@tldraw/utils'
|
|
13
|
+
import { ExecutionQueue, Result, assert } from '@tldraw/utils'
|
|
14
|
+
import { WeakCache, MediaHelpers } from '@tldraw/utils'
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
The package is completely self-contained with no dependencies on other `@tldraw/*` packages, making it safe to use in any JavaScript environment.
|
|
18
|
+
|
|
19
|
+
> Important: Many utilities in this package are marked as `@internal` in the source code. These are implementation details used within the tldraw ecosystem. While they're exported for internal tldraw packages to use, they may change without notice in minor versions. Focus on the `@public` APIs for stable external usage.
|
|
20
|
+
|
|
21
|
+
## 2. Core Utilities
|
|
22
|
+
|
|
23
|
+
### Array Operations: Transforming Collections
|
|
24
|
+
|
|
25
|
+
Arrays are everywhere in tldraw - from managing shapes and tools to handling selections and ordering. The array utilities provide type-safe operations that preserve your data's integrity.
|
|
26
|
+
|
|
27
|
+
#### Deduplication and Uniqueness
|
|
28
|
+
|
|
29
|
+
You deduplicate arrays using the `dedupe` function:
|
|
30
|
+
|
|
31
|
+
```ts
|
|
32
|
+
import { dedupe } from '@tldraw/utils'
|
|
33
|
+
|
|
34
|
+
const shapes = [
|
|
35
|
+
{ id: 'a', type: 'rect' },
|
|
36
|
+
{ id: 'b', type: 'circle' },
|
|
37
|
+
{ id: 'a', type: 'rect' }, // duplicate
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
const uniqueShapes = dedupe(shapes, (a, b) => a.id === b.id)
|
|
41
|
+
console.log(uniqueShapes) // [{ id: 'a', type: 'rect' }, { id: 'b', type: 'circle' }]
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
For simple value arrays, you can omit the equality function:
|
|
45
|
+
|
|
46
|
+
```ts
|
|
47
|
+
const ids = ['a', 'b', 'c', 'a', 'b']
|
|
48
|
+
const uniqueIds = dedupe(ids)
|
|
49
|
+
console.log(uniqueIds) // ['a', 'b', 'c']
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
#### Rotating and Reordering
|
|
53
|
+
|
|
54
|
+
You can rotate array contents with `rotateArray`:
|
|
55
|
+
|
|
56
|
+
```ts
|
|
57
|
+
import { rotateArray } from '@tldraw/utils'
|
|
58
|
+
|
|
59
|
+
const tools = ['select', 'draw', 'eraser', 'text']
|
|
60
|
+
const rotated = rotateArray(tools, 1)
|
|
61
|
+
console.log(rotated) // ['text', 'select', 'draw', 'eraser']
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
This is particularly useful for cycling through tools or shifting z-order arrangements.
|
|
65
|
+
|
|
66
|
+
#### Splitting Collections
|
|
67
|
+
|
|
68
|
+
You partition arrays based on conditions using `partition`:
|
|
69
|
+
|
|
70
|
+
```ts
|
|
71
|
+
import { partition } from '@tldraw/utils'
|
|
72
|
+
|
|
73
|
+
const shapes = [
|
|
74
|
+
{ id: 'a', selected: true },
|
|
75
|
+
{ id: 'b', selected: false },
|
|
76
|
+
{ id: 'c', selected: true },
|
|
77
|
+
]
|
|
78
|
+
|
|
79
|
+
const [selected, unselected] = partition(shapes, (shape) => shape.selected)
|
|
80
|
+
console.log(selected) // [{ id: 'a', selected: true }, { id: 'c', selected: true }]
|
|
81
|
+
console.log(unselected) // [{ id: 'b', selected: false }]
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
> Note: `partition` is marked as `@internal` in the source code but is exported for use. It may change without notice in minor versions.
|
|
85
|
+
|
|
86
|
+
### Error Handling: The Result Pattern
|
|
87
|
+
|
|
88
|
+
Instead of throwing exceptions, tldraw uses the `Result` pattern for predictable error handling. This approach makes errors explicit and prevents unexpected crashes.
|
|
89
|
+
|
|
90
|
+
#### Creating Results
|
|
91
|
+
|
|
92
|
+
You create successful results with `Result.ok()` and errors with `Result.err()`:
|
|
93
|
+
|
|
94
|
+
```ts
|
|
95
|
+
import { Result } from '@tldraw/utils'
|
|
96
|
+
|
|
97
|
+
function parseShape(data: unknown): Result<Shape, string> {
|
|
98
|
+
if (typeof data !== 'object' || data === null) {
|
|
99
|
+
return Result.err('Invalid data: not an object')
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Type checking logic...
|
|
103
|
+
return Result.ok(validShape)
|
|
104
|
+
}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
#### Handling Results
|
|
108
|
+
|
|
109
|
+
You check results using the `ok` property:
|
|
110
|
+
|
|
111
|
+
```ts
|
|
112
|
+
const result = parseShape(unknownData)
|
|
113
|
+
|
|
114
|
+
if (result.ok) {
|
|
115
|
+
// TypeScript knows result.value is a Shape
|
|
116
|
+
console.log(`Shape type: ${result.value.type}`)
|
|
117
|
+
} else {
|
|
118
|
+
// TypeScript knows result.error is a string
|
|
119
|
+
console.error(`Parse failed: ${result.error}`)
|
|
120
|
+
}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
#### Chaining Operations
|
|
124
|
+
|
|
125
|
+
Results compose well for sequential operations:
|
|
126
|
+
|
|
127
|
+
```ts
|
|
128
|
+
function validateAndCreateShape(data: unknown): Result<ProcessedShape, string> {
|
|
129
|
+
const parseResult = parseShape(data)
|
|
130
|
+
if (!parseResult.ok) {
|
|
131
|
+
return parseResult // Pass through the error
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const processResult = processShape(parseResult.value)
|
|
135
|
+
return processResult
|
|
136
|
+
}
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
### Assertions: Runtime Type Checking
|
|
140
|
+
|
|
141
|
+
When you need to verify assumptions at runtime, use the assertion functions. These provide both runtime safety and TypeScript type narrowing.
|
|
142
|
+
|
|
143
|
+
#### Basic Assertions
|
|
144
|
+
|
|
145
|
+
The `assert` function throws if a condition is false:
|
|
146
|
+
|
|
147
|
+
```ts
|
|
148
|
+
import { assert } from '@tldraw/utils'
|
|
149
|
+
|
|
150
|
+
function processShape(shape: unknown) {
|
|
151
|
+
assert(shape && typeof shape === 'object', 'Shape must be an object')
|
|
152
|
+
// TypeScript now knows shape is object & not null
|
|
153
|
+
|
|
154
|
+
assert('type' in shape, 'Shape must have a type property')
|
|
155
|
+
// TypeScript knows shape has a type property
|
|
156
|
+
}
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
#### Existence Checking
|
|
160
|
+
|
|
161
|
+
Use `assertExists` to verify values aren't null or undefined:
|
|
162
|
+
|
|
163
|
+
```ts
|
|
164
|
+
import { assertExists } from '@tldraw/utils'
|
|
165
|
+
|
|
166
|
+
function findShapeById(id: string): Shape {
|
|
167
|
+
const shape = shapes.find((s) => s.id === id)
|
|
168
|
+
assertExists(shape, `Shape with id ${id} not found`)
|
|
169
|
+
// TypeScript knows shape is not undefined
|
|
170
|
+
return shape
|
|
171
|
+
}
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
> Tip: Assertions are removed from production builds in most bundlers, but the type narrowing still helps during development.
|
|
175
|
+
|
|
176
|
+
## 3. Advanced Features
|
|
177
|
+
|
|
178
|
+
### ExecutionQueue: Sequential Task Processing
|
|
179
|
+
|
|
180
|
+
When you need to ensure operations happen in order, use `ExecutionQueue`. This is particularly important for database writes, file operations, or any sequence where order matters.
|
|
181
|
+
|
|
182
|
+
#### Basic Queue Usage
|
|
183
|
+
|
|
184
|
+
You create a queue and push tasks to it:
|
|
185
|
+
|
|
186
|
+
```ts
|
|
187
|
+
import { ExecutionQueue } from '@tldraw/utils'
|
|
188
|
+
|
|
189
|
+
const saveQueue = new ExecutionQueue()
|
|
190
|
+
|
|
191
|
+
// These will execute in order, not parallel
|
|
192
|
+
const save1 = saveQueue.push(() => saveToDatabase(data1))
|
|
193
|
+
const save2 = saveQueue.push(() => saveToDatabase(data2))
|
|
194
|
+
const save3 = saveQueue.push(() => saveToDatabase(data3))
|
|
195
|
+
|
|
196
|
+
// All saves complete in order
|
|
197
|
+
await Promise.all([save1, save2, save3])
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
#### Task Timing and Cleanup
|
|
201
|
+
|
|
202
|
+
You can add a timeout between tasks and clean up when needed:
|
|
203
|
+
|
|
204
|
+
```ts
|
|
205
|
+
// 500ms timeout between tasks
|
|
206
|
+
const queue = new ExecutionQueue(500)
|
|
207
|
+
|
|
208
|
+
// Queue some operations
|
|
209
|
+
await queue.push(() => heavyComputation1())
|
|
210
|
+
// 500ms delay automatically added
|
|
211
|
+
await queue.push(() => heavyComputation2())
|
|
212
|
+
|
|
213
|
+
// Clean up when done
|
|
214
|
+
queue.close()
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
> Note: ExecutionQueue ensures sequential execution even if you don't await individual tasks immediately.
|
|
218
|
+
|
|
219
|
+
### WeakCache: Memory-Efficient Caching
|
|
220
|
+
|
|
221
|
+
When you need to cache expensive computations tied to object lifecycles, `WeakCache` automatically cleans up when objects are garbage collected.
|
|
222
|
+
|
|
223
|
+
#### Caching Expensive Computations
|
|
224
|
+
|
|
225
|
+
You cache results tied to specific objects:
|
|
226
|
+
|
|
227
|
+
```ts
|
|
228
|
+
import { WeakCache } from '@tldraw/utils'
|
|
229
|
+
|
|
230
|
+
const boundingBoxCache = new WeakCache<Shape, BoundingBox>()
|
|
231
|
+
|
|
232
|
+
function getBoundingBox(shape: Shape): BoundingBox {
|
|
233
|
+
return boundingBoxCache.get(shape, (s) => computeBoundingBox(s))
|
|
234
|
+
// Expensive computation only runs once per shape
|
|
235
|
+
}
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
Each time you call `getBoundingBox` with the same shape object, it returns the cached result. When the shape object is garbage collected, the cache entry is automatically cleaned up by the underlying WeakMap.
|
|
239
|
+
|
|
240
|
+
#### Multiple Cache Layers
|
|
241
|
+
|
|
242
|
+
You can create specialized caches for different computations:
|
|
243
|
+
|
|
244
|
+
```ts
|
|
245
|
+
const geometryCache = new WeakCache<Shape, Geometry>()
|
|
246
|
+
const selectionCache = new WeakCache<Shape, SelectionBounds>()
|
|
247
|
+
|
|
248
|
+
function getGeometry(shape: Shape): Geometry {
|
|
249
|
+
return geometryCache.get(shape, computeGeometry)
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function getSelectionBounds(shape: Shape): SelectionBounds {
|
|
253
|
+
return selectionCache.get(shape, computeSelectionBounds)
|
|
254
|
+
}
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
> Tip: WeakCache is perfect for any computation where the result depends only on the input object and the object reference acts as a natural cache key.
|
|
258
|
+
|
|
259
|
+
### IndexKey System: Fractional Ordering
|
|
260
|
+
|
|
261
|
+
The tldraw editor needs to maintain stable ordering of shapes, even when inserting items between existing ones. The IndexKey system provides fractional indexing for this purpose.
|
|
262
|
+
|
|
263
|
+
#### Understanding IndexKeys
|
|
264
|
+
|
|
265
|
+
An `IndexKey` is a special string that maintains lexicographic order:
|
|
266
|
+
|
|
267
|
+
```ts
|
|
268
|
+
import { ZERO_INDEX_KEY, getIndexBetween, getIndexAbove } from '@tldraw/utils'
|
|
269
|
+
|
|
270
|
+
// Start with the zero index
|
|
271
|
+
let firstIndex = ZERO_INDEX_KEY // 'a0'
|
|
272
|
+
|
|
273
|
+
// Get an index above it
|
|
274
|
+
let secondIndex = getIndexAbove(firstIndex) // 'a1'
|
|
275
|
+
|
|
276
|
+
// Insert between them
|
|
277
|
+
let middleIndex = getIndexBetween(firstIndex, secondIndex) // 'a0V'
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
#### Maintaining Shape Order
|
|
281
|
+
|
|
282
|
+
When you need to reorder shapes, use the IndexKey system:
|
|
283
|
+
|
|
284
|
+
```ts
|
|
285
|
+
import { getIndicesBetween, getIndicesAbove, getIndicesBelow, sortByIndex } from '@tldraw/utils'
|
|
286
|
+
|
|
287
|
+
// Insert multiple shapes between two existing ones
|
|
288
|
+
const newIndices = getIndicesBetween(belowShape?.index ?? null, aboveShape?.index ?? null, 3)
|
|
289
|
+
|
|
290
|
+
// Get multiple indices above a shape
|
|
291
|
+
const indicesAbove = getIndicesAbove(lastShape?.index ?? null, 3)
|
|
292
|
+
|
|
293
|
+
// Get multiple indices below a shape
|
|
294
|
+
const indicesBelow = getIndicesBelow(firstShape?.index ?? null, 3)
|
|
295
|
+
|
|
296
|
+
const newShapes = shapeData.map((data, i) => ({
|
|
297
|
+
...data,
|
|
298
|
+
index: newIndices[i],
|
|
299
|
+
}))
|
|
300
|
+
|
|
301
|
+
// Sort all shapes by their indices
|
|
302
|
+
const sortedShapes = allShapes.sort(sortByIndex)
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
You can also generate a sequence of indices starting from a specific point:
|
|
306
|
+
|
|
307
|
+
```ts
|
|
308
|
+
import { getIndices } from '@tldraw/utils'
|
|
309
|
+
|
|
310
|
+
// Generate 5 indices starting from 'a1' (returns start + n indices)
|
|
311
|
+
const indices = getIndices(5, 'a1') // ['a1', 'a2', 'a3', 'a4', 'a5', 'a6']
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
The IndexKey system ensures that insertion operations always succeed, even with complex reordering scenarios.
|
|
315
|
+
|
|
316
|
+
### Media Helpers: File Processing
|
|
317
|
+
|
|
318
|
+
Working with images and videos requires careful handling of formats, dimensions, and browser compatibility. The MediaHelpers provide robust utilities for common media operations.
|
|
319
|
+
|
|
320
|
+
The package also exports constants for supported media types:
|
|
321
|
+
|
|
322
|
+
```ts
|
|
323
|
+
import {
|
|
324
|
+
DEFAULT_SUPPORTED_IMAGE_TYPES,
|
|
325
|
+
DEFAULT_SUPPORT_VIDEO_TYPES,
|
|
326
|
+
DEFAULT_SUPPORTED_MEDIA_TYPES,
|
|
327
|
+
DEFAULT_SUPPORTED_MEDIA_TYPE_LIST,
|
|
328
|
+
} from '@tldraw/utils'
|
|
329
|
+
|
|
330
|
+
console.log(DEFAULT_SUPPORTED_IMAGE_TYPES)
|
|
331
|
+
// ['image/jpeg', 'image/png', 'image/webp', 'image/svg+xml', 'image/gif', 'image/apng', 'image/avif']
|
|
332
|
+
|
|
333
|
+
console.log(DEFAULT_SUPPORT_VIDEO_TYPES)
|
|
334
|
+
// ['video/mp4', 'video/webm', 'video/quicktime']
|
|
335
|
+
|
|
336
|
+
console.log(DEFAULT_SUPPORTED_MEDIA_TYPE_LIST)
|
|
337
|
+
// Comma-separated string of all supported types
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
#### Image Processing
|
|
341
|
+
|
|
342
|
+
You can get image dimensions and metadata:
|
|
343
|
+
|
|
344
|
+
```ts
|
|
345
|
+
import { MediaHelpers } from '@tldraw/utils'
|
|
346
|
+
|
|
347
|
+
// Get image dimensions from a Blob
|
|
348
|
+
const { w, h } = await MediaHelpers.getImageSize(imageFile)
|
|
349
|
+
console.log(`Image is ${w}x${h} pixels`)
|
|
350
|
+
|
|
351
|
+
// Load image from URL and get dimensions together
|
|
352
|
+
const { image, w: width, h: height } = await MediaHelpers.getImageAndDimensions(imageUrl)
|
|
353
|
+
// Use the loaded image element and dimensions
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
#### Format Detection
|
|
357
|
+
|
|
358
|
+
Check media formats and capabilities:
|
|
359
|
+
|
|
360
|
+
```ts
|
|
361
|
+
const isImage = MediaHelpers.isImageType('image/png') // true
|
|
362
|
+
const isStatic = MediaHelpers.isStaticImageType('image/gif') // false
|
|
363
|
+
const isAnimated = MediaHelpers.isAnimatedImageType('image/gif') // true
|
|
364
|
+
const isVector = MediaHelpers.isVectorImageType('image/svg+xml') // true
|
|
365
|
+
|
|
366
|
+
// Check if a specific file is animated
|
|
367
|
+
const animated = await MediaHelpers.isAnimated(gifFile)
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
#### Video Operations
|
|
371
|
+
|
|
372
|
+
Process video files and extract frames:
|
|
373
|
+
|
|
374
|
+
```ts
|
|
375
|
+
// Get video dimensions from a Blob
|
|
376
|
+
const { w, h } = await MediaHelpers.getVideoSize(videoFile)
|
|
377
|
+
|
|
378
|
+
// Load a video from URL
|
|
379
|
+
const videoElement = await MediaHelpers.loadVideo(videoUrl)
|
|
380
|
+
|
|
381
|
+
// Extract a frame as a data URL from loaded video element
|
|
382
|
+
const frameDataUrl = await MediaHelpers.getVideoFrameAsDataUrl(videoElement, 0)
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
## 4. Performance and Optimization
|
|
386
|
+
|
|
387
|
+
### Throttling and Debouncing
|
|
388
|
+
|
|
389
|
+
High-frequency events like mouse moves and resize events need careful handling to maintain smooth performance.
|
|
390
|
+
|
|
391
|
+
#### Frame-Rate Throttling
|
|
392
|
+
|
|
393
|
+
Use `fpsThrottle` for smooth 60fps updates:
|
|
394
|
+
|
|
395
|
+
```ts
|
|
396
|
+
import { fpsThrottle } from '@tldraw/utils'
|
|
397
|
+
|
|
398
|
+
const updateCanvas = fpsThrottle(() => {
|
|
399
|
+
// This will run at most once per frame (16.67ms)
|
|
400
|
+
redrawCanvas()
|
|
401
|
+
})
|
|
402
|
+
|
|
403
|
+
// Call as often as you want - it's automatically throttled
|
|
404
|
+
document.addEventListener('mousemove', updateCanvas)
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
#### Next-Frame Throttling
|
|
408
|
+
|
|
409
|
+
For less critical updates, use `throttleToNextFrame`:
|
|
410
|
+
|
|
411
|
+
```ts
|
|
412
|
+
import { throttleToNextFrame } from '@tldraw/utils'
|
|
413
|
+
|
|
414
|
+
const updateUI = throttleToNextFrame(() => {
|
|
415
|
+
// Batches multiple calls into the next animation frame
|
|
416
|
+
updateStatusBar()
|
|
417
|
+
})
|
|
418
|
+
|
|
419
|
+
// Returns a cancel function
|
|
420
|
+
const cancel = updateUI()
|
|
421
|
+
// Call cancel() to prevent execution if needed
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
> Note: `throttleToNextFrame` batches multiple calls to the same function and executes it only once on the next frame.
|
|
425
|
+
|
|
426
|
+
#### Debouncing User Input
|
|
427
|
+
|
|
428
|
+
Use `debounce` for operations that should only happen after user input stops:
|
|
429
|
+
|
|
430
|
+
```ts
|
|
431
|
+
import { debounce } from '@tldraw/utils'
|
|
432
|
+
|
|
433
|
+
const saveDocument = debounce(async () => {
|
|
434
|
+
await saveToServer(document)
|
|
435
|
+
console.log('Document saved!')
|
|
436
|
+
}, 1000)
|
|
437
|
+
|
|
438
|
+
// Call whenever document changes
|
|
439
|
+
document.addEventListener('input', saveDocument)
|
|
440
|
+
// Only saves 1 second after user stops typing
|
|
441
|
+
```
|
|
442
|
+
|
|
443
|
+
The debounced function returns a promise and includes a `cancel` method:
|
|
444
|
+
|
|
445
|
+
```ts
|
|
446
|
+
const debouncedSave = debounce(saveToServer, 1000)
|
|
447
|
+
|
|
448
|
+
// Start a save operation
|
|
449
|
+
const savePromise = debouncedSave()
|
|
450
|
+
|
|
451
|
+
// Cancel if needed
|
|
452
|
+
debouncedSave.cancel()
|
|
453
|
+
```
|
|
454
|
+
|
|
455
|
+
### Performance Measurement
|
|
456
|
+
|
|
457
|
+
Understanding where time is spent helps optimize tldraw applications.
|
|
458
|
+
|
|
459
|
+
#### Performance Tracking
|
|
460
|
+
|
|
461
|
+
Use `PerformanceTracker` for detailed timing analysis:
|
|
462
|
+
|
|
463
|
+
```ts
|
|
464
|
+
import { PerformanceTracker } from '@tldraw/utils'
|
|
465
|
+
|
|
466
|
+
const tracker = new PerformanceTracker()
|
|
467
|
+
|
|
468
|
+
tracker.start('render')
|
|
469
|
+
renderShapes()
|
|
470
|
+
tracker.stop()
|
|
471
|
+
|
|
472
|
+
tracker.start('interaction')
|
|
473
|
+
handleUserInteraction()
|
|
474
|
+
tracker.stop()
|
|
475
|
+
```
|
|
476
|
+
|
|
477
|
+
> Tip: Performance measurements integrate with browser DevTools Performance tab for detailed analysis.
|
|
478
|
+
|
|
479
|
+
### Mathematical Operations
|
|
480
|
+
|
|
481
|
+
The utils package includes mathematical helpers for interpolation and deterministic randomness.
|
|
482
|
+
|
|
483
|
+
#### Linear Interpolation
|
|
484
|
+
|
|
485
|
+
Use `lerp` and `invLerp` for smooth transitions:
|
|
486
|
+
|
|
487
|
+
```ts
|
|
488
|
+
import { lerp, invLerp } from '@tldraw/utils'
|
|
489
|
+
|
|
490
|
+
// Linear interpolate between two values
|
|
491
|
+
const interpolated = lerp(0, 100, 0.5) // 50
|
|
492
|
+
|
|
493
|
+
// Inverse interpolation - find t given result
|
|
494
|
+
const t = invLerp(0, 100, 25) // 0.25
|
|
495
|
+
```
|
|
496
|
+
|
|
497
|
+
#### Value Mapping
|
|
498
|
+
|
|
499
|
+
Use `modulate` to map values between different ranges:
|
|
500
|
+
|
|
501
|
+
```ts
|
|
502
|
+
import { modulate } from '@tldraw/utils'
|
|
503
|
+
|
|
504
|
+
// Map a value from one range to another
|
|
505
|
+
const result = modulate(5, [0, 10], [0, 100]) // 50
|
|
506
|
+
|
|
507
|
+
// With clamping to prevent out-of-bounds results
|
|
508
|
+
const clamped = modulate(15, [0, 10], [0, 100], true) // 100 (clamped)
|
|
509
|
+
```
|
|
510
|
+
|
|
511
|
+
#### Deterministic Random Numbers
|
|
512
|
+
|
|
513
|
+
Use `rng` for repeatable pseudo-random sequences:
|
|
514
|
+
|
|
515
|
+
```ts
|
|
516
|
+
import { rng } from '@tldraw/utils'
|
|
517
|
+
|
|
518
|
+
// Create a seeded random number generator
|
|
519
|
+
const random = rng('my-seed')
|
|
520
|
+
|
|
521
|
+
const num1 = random() // Always the same for this seed
|
|
522
|
+
const num2 = random() // Next number in sequence
|
|
523
|
+
|
|
524
|
+
// Different seed produces different sequence
|
|
525
|
+
const otherRandom = rng('other-seed')
|
|
526
|
+
const different = otherRandom() // Different value
|
|
527
|
+
```
|
|
528
|
+
|
|
529
|
+
> Tip: The `rng` function returns values between -1 and 1, making it useful for generating consistent random variations. You can normalize to other ranges as needed.
|
|
530
|
+
|
|
531
|
+
## 5. Cross-Platform Compatibility
|
|
532
|
+
|
|
533
|
+
### Storage Operations
|
|
534
|
+
|
|
535
|
+
Browser storage operations need careful error handling for quota limits and privacy modes.
|
|
536
|
+
|
|
537
|
+
#### LocalStorage with Error Handling
|
|
538
|
+
|
|
539
|
+
The storage utilities handle errors gracefully (note that these functions are marked as `@internal` but are exported for use):
|
|
540
|
+
|
|
541
|
+
```ts
|
|
542
|
+
import { getFromLocalStorage, setInLocalStorage, clearLocalStorage } from '@tldraw/utils'
|
|
543
|
+
|
|
544
|
+
// These handle quota exceeded errors and privacy mode
|
|
545
|
+
setInLocalStorage('user-preferences', JSON.stringify(preferences))
|
|
546
|
+
const saved = getFromLocalStorage('user-preferences')
|
|
547
|
+
|
|
548
|
+
// Clear all data when needed
|
|
549
|
+
clearLocalStorage()
|
|
550
|
+
```
|
|
551
|
+
|
|
552
|
+
#### SessionStorage Operations
|
|
553
|
+
|
|
554
|
+
Session storage works identically:
|
|
555
|
+
|
|
556
|
+
```ts
|
|
557
|
+
import { getFromSessionStorage, setInSessionStorage, clearSessionStorage } from '@tldraw/utils'
|
|
558
|
+
|
|
559
|
+
// Temporary data for the current session
|
|
560
|
+
setInSessionStorage('current-tool', 'select')
|
|
561
|
+
const currentTool = getFromSessionStorage('current-tool')
|
|
562
|
+
|
|
563
|
+
// Clear all session data when needed
|
|
564
|
+
clearSessionStorage()
|
|
565
|
+
```
|
|
566
|
+
|
|
567
|
+
### File Operations
|
|
568
|
+
|
|
569
|
+
The `FileHelpers` class provides utilities for working with files and data conversion.
|
|
570
|
+
|
|
571
|
+
#### Data URL Conversion
|
|
572
|
+
|
|
573
|
+
Convert between different file formats:
|
|
574
|
+
|
|
575
|
+
```ts
|
|
576
|
+
import { FileHelpers } from '@tldraw/utils'
|
|
577
|
+
|
|
578
|
+
// Convert blob to data URL
|
|
579
|
+
const dataUrl = await FileHelpers.blobToDataUrl(imageBlob)
|
|
580
|
+
console.log(dataUrl) // "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAA..."
|
|
581
|
+
|
|
582
|
+
// Convert blob to text
|
|
583
|
+
const textContent = await FileHelpers.blobToText(textBlob)
|
|
584
|
+
|
|
585
|
+
// Fetch URL and convert to different formats
|
|
586
|
+
const buffer = await FileHelpers.urlToArrayBuffer('https://example.com/image.png')
|
|
587
|
+
const blob = await FileHelpers.urlToBlob('https://example.com/data.json')
|
|
588
|
+
const urlAsDataUrl = await FileHelpers.urlToDataUrl('https://example.com/image.svg')
|
|
589
|
+
```
|
|
590
|
+
|
|
591
|
+
#### MIME Type Management
|
|
592
|
+
|
|
593
|
+
Modify file MIME types while preserving content:
|
|
594
|
+
|
|
595
|
+
```ts
|
|
596
|
+
// Change MIME type of a Blob
|
|
597
|
+
const newBlob = FileHelpers.rewriteMimeType(originalBlob, 'image/webp')
|
|
598
|
+
|
|
599
|
+
// Change MIME type of a File (preserves filename)
|
|
600
|
+
const newFile = FileHelpers.rewriteMimeType(originalFile, 'application/json')
|
|
601
|
+
```
|
|
602
|
+
|
|
603
|
+
### URL Processing
|
|
604
|
+
|
|
605
|
+
Parsing URLs from user input requires careful validation:
|
|
606
|
+
|
|
607
|
+
```ts
|
|
608
|
+
import { safeParseUrl } from '@tldraw/utils'
|
|
609
|
+
|
|
610
|
+
function handleUserUrl(input: string) {
|
|
611
|
+
const url = safeParseUrl(input)
|
|
612
|
+
if (url) {
|
|
613
|
+
console.log(`Valid URL: ${url.href}`)
|
|
614
|
+
return url
|
|
615
|
+
} else {
|
|
616
|
+
console.log('Invalid URL provided')
|
|
617
|
+
return null
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
```
|
|
621
|
+
|
|
622
|
+
> Note: `safeParseUrl` returns `undefined` for invalid URLs instead of throwing exceptions.
|
|
623
|
+
|
|
624
|
+
## 6. Debugging and Development
|
|
625
|
+
|
|
626
|
+
### Timer Management
|
|
627
|
+
|
|
628
|
+
The `Timers` class helps manage timeouts and intervals with automatic cleanup:
|
|
629
|
+
|
|
630
|
+
```ts
|
|
631
|
+
import { Timers } from '@tldraw/utils'
|
|
632
|
+
|
|
633
|
+
class MyComponent {
|
|
634
|
+
private timers = new Timers()
|
|
635
|
+
|
|
636
|
+
startPeriodicUpdate() {
|
|
637
|
+
// Set timers with context IDs for organization
|
|
638
|
+
this.timers.setTimeout('component', () => this.autoSave(), 5000)
|
|
639
|
+
this.timers.setInterval('component', () => this.refresh(), 1000)
|
|
640
|
+
this.timers.requestAnimationFrame('component', () => this.render())
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
cleanup() {
|
|
644
|
+
// Clears all timers for this context
|
|
645
|
+
this.timers.dispose('component')
|
|
646
|
+
// Or dispose all contexts
|
|
647
|
+
this.timers.disposeAll()
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
// You can also get context-bound timer functions
|
|
651
|
+
getContextTimers() {
|
|
652
|
+
return this.timers.forContext('component')
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
```
|
|
656
|
+
|
|
657
|
+
### Error Annotation
|
|
658
|
+
|
|
659
|
+
You can add debugging context to errors:
|
|
660
|
+
|
|
661
|
+
```ts
|
|
662
|
+
import { annotateError, getErrorAnnotations } from '@tldraw/utils'
|
|
663
|
+
|
|
664
|
+
try {
|
|
665
|
+
performRiskyOperation()
|
|
666
|
+
} catch (error) {
|
|
667
|
+
annotateError(error, {
|
|
668
|
+
tags: { operation: 'shape-creation' },
|
|
669
|
+
extras: { shapeId: 'shape-123' },
|
|
670
|
+
})
|
|
671
|
+
|
|
672
|
+
// Later, retrieve the context
|
|
673
|
+
const annotations = getErrorAnnotations(error)
|
|
674
|
+
console.log('Error context:', annotations)
|
|
675
|
+
|
|
676
|
+
throw error // Re-throw with added context
|
|
677
|
+
}
|
|
678
|
+
```
|
|
679
|
+
|
|
680
|
+
### Utility Functions
|
|
681
|
+
|
|
682
|
+
Several utility functions provide common functionality:
|
|
683
|
+
|
|
684
|
+
#### Unique ID Generation
|
|
685
|
+
|
|
686
|
+
Generate unique identifiers for objects:
|
|
687
|
+
|
|
688
|
+
```ts
|
|
689
|
+
import { uniqueId, mockUniqueId, restoreUniqueId } from '@tldraw/utils'
|
|
690
|
+
|
|
691
|
+
// Generate a unique ID
|
|
692
|
+
const id = uniqueId() // 'VxhUYo3k8GsLmWkjhGq9e'
|
|
693
|
+
|
|
694
|
+
// Mock IDs for testing (returns predictable sequence)
|
|
695
|
+
mockUniqueId(() => 'mock-id-0')
|
|
696
|
+
const testId1 = uniqueId() // 'mock-id-0'
|
|
697
|
+
const testId2 = uniqueId() // 'mock-id-0'
|
|
698
|
+
|
|
699
|
+
// Restore normal ID generation
|
|
700
|
+
restoreUniqueId()
|
|
701
|
+
```
|
|
702
|
+
|
|
703
|
+
#### Content Hashing
|
|
704
|
+
|
|
705
|
+
Generate consistent hashes for deduplication and caching:
|
|
706
|
+
|
|
707
|
+
```ts
|
|
708
|
+
import { getHashForString, getHashForObject, getHashForBuffer, lns } from '@tldraw/utils'
|
|
709
|
+
|
|
710
|
+
// Hash a string
|
|
711
|
+
const stringHash = getHashForString('hello world') // '1794106052'
|
|
712
|
+
|
|
713
|
+
// Hash an object (uses JSON.stringify internally)
|
|
714
|
+
const objectHash = getHashForObject({ name: 'Alice', age: 30 })
|
|
715
|
+
|
|
716
|
+
// Hash binary data
|
|
717
|
+
const buffer = new ArrayBuffer(8)
|
|
718
|
+
const bufferHash = getHashForBuffer(buffer)
|
|
719
|
+
|
|
720
|
+
// Locale-normalized string for consistent hashing across cultures
|
|
721
|
+
const normalized = lns('Café') // Handles unicode normalization
|
|
722
|
+
```
|
|
723
|
+
|
|
724
|
+
#### Sorting Utilities
|
|
725
|
+
|
|
726
|
+
Sort objects by common properties:
|
|
727
|
+
|
|
728
|
+
```ts
|
|
729
|
+
import { sortById } from '@tldraw/utils'
|
|
730
|
+
|
|
731
|
+
const items = [
|
|
732
|
+
{ id: 'c', name: 'Charlie' },
|
|
733
|
+
{ id: 'a', name: 'Alice' },
|
|
734
|
+
{ id: 'b', name: 'Bob' },
|
|
735
|
+
]
|
|
736
|
+
|
|
737
|
+
const sorted = items.sort(sortById)
|
|
738
|
+
// [{ id: 'a', name: 'Alice' }, { id: 'b', name: 'Bob' }, { id: 'c', name: 'Charlie' }]
|
|
739
|
+
```
|
|
740
|
+
|
|
741
|
+
#### Collection Utilities
|
|
742
|
+
|
|
743
|
+
Extract values from iterables:
|
|
744
|
+
|
|
745
|
+
```ts
|
|
746
|
+
import { getFirstFromIterable } from '@tldraw/utils'
|
|
747
|
+
|
|
748
|
+
const set = new Set([1, 2, 3])
|
|
749
|
+
const first = getFirstFromIterable(set)
|
|
750
|
+
|
|
751
|
+
const map = new Map([
|
|
752
|
+
['a', 1],
|
|
753
|
+
['b', 2],
|
|
754
|
+
])
|
|
755
|
+
const firstValue = getFirstFromIterable(map)
|
|
756
|
+
```
|
|
757
|
+
|
|
758
|
+
#### Method Binding Decorator
|
|
759
|
+
|
|
760
|
+
The `@bind` decorator ensures methods are properly bound to their class instance:
|
|
761
|
+
|
|
762
|
+
```ts
|
|
763
|
+
import { bind } from '@tldraw/utils'
|
|
764
|
+
|
|
765
|
+
class EventHandler {
|
|
766
|
+
name = 'MyHandler'
|
|
767
|
+
|
|
768
|
+
@bind
|
|
769
|
+
handleClick(event: MouseEvent) {
|
|
770
|
+
console.log(`${this.name} handled click`) // 'this' is always correct
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
const handler = new EventHandler()
|
|
775
|
+
// Safe to use as callback - 'this' binding preserved
|
|
776
|
+
element.addEventListener('click', handler.handleClick)
|
|
777
|
+
```
|
|
778
|
+
|
|
779
|
+
### Development Helpers
|
|
780
|
+
|
|
781
|
+
Some utilities are particularly helpful during development:
|
|
782
|
+
|
|
783
|
+
```ts
|
|
784
|
+
import { warnOnce, exhaustiveSwitchError } from '@tldraw/utils'
|
|
785
|
+
|
|
786
|
+
// Warn about deprecated usage, but only once
|
|
787
|
+
function oldFunction() {
|
|
788
|
+
warnOnce('oldFunction is deprecated, use newFunction instead')
|
|
789
|
+
// Continue with implementation...
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
// Ensure switch statements are exhaustive
|
|
793
|
+
function handleShapeType(shape: Shape) {
|
|
794
|
+
switch (shape.type) {
|
|
795
|
+
case 'rect':
|
|
796
|
+
return handleRect(shape)
|
|
797
|
+
case 'circle':
|
|
798
|
+
return handleCircle(shape)
|
|
799
|
+
default:
|
|
800
|
+
// TypeScript error if new shape types are added but not handled
|
|
801
|
+
throw exhaustiveSwitchError(shape)
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
```
|
|
805
|
+
|
|
806
|
+
### Value Validation
|
|
807
|
+
|
|
808
|
+
Type guards provide runtime checking with TypeScript integration:
|
|
809
|
+
|
|
810
|
+
```ts
|
|
811
|
+
import { isDefined, isNonNull, isNonNullish } from '@tldraw/utils'
|
|
812
|
+
|
|
813
|
+
function processUserInput(data: unknown) {
|
|
814
|
+
if (isDefined(data)) {
|
|
815
|
+
// TypeScript knows data is not undefined
|
|
816
|
+
console.log('Data provided:', data)
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
if (isNonNullish(data)) {
|
|
820
|
+
// TypeScript knows data is not null or undefined
|
|
821
|
+
return processData(data)
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
throw new Error('Invalid input data')
|
|
825
|
+
}
|
|
826
|
+
```
|
|
827
|
+
|
|
828
|
+
## 7. Integration Patterns
|
|
829
|
+
|
|
830
|
+
### Using Utils in Custom Shapes
|
|
831
|
+
|
|
832
|
+
When creating custom shapes, utils provide essential building blocks:
|
|
833
|
+
|
|
834
|
+
```ts
|
|
835
|
+
import { WeakCache, Result, assert, getIndexBetween } from '@tldraw/utils'
|
|
836
|
+
|
|
837
|
+
class CustomShapeUtil extends BaseBoxShapeUtil<CustomShape> {
|
|
838
|
+
private geometryCache = new WeakCache<CustomShape, Geometry>()
|
|
839
|
+
|
|
840
|
+
getGeometry(shape: CustomShape): Geometry {
|
|
841
|
+
return this.geometryCache.get(shape, (s) => {
|
|
842
|
+
return this.computeComplexGeometry(s)
|
|
843
|
+
})
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
canReceiveNewChildIndex(shape: CustomShape, droppingShape: Shape): boolean {
|
|
847
|
+
// Use Result pattern for complex validation
|
|
848
|
+
const validation = this.validateChildShape(droppingShape)
|
|
849
|
+
return validation.ok
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
private validateChildShape(shape: Shape): Result<true, string> {
|
|
853
|
+
if (!this.isCompatibleChild(shape)) {
|
|
854
|
+
return Result.err(`${shape.type} cannot be a child of CustomShape`)
|
|
855
|
+
}
|
|
856
|
+
return Result.ok(true)
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
```
|
|
860
|
+
|
|
861
|
+
### Custom Tool Development
|
|
862
|
+
|
|
863
|
+
Tools benefit from utils for state management and performance:
|
|
864
|
+
|
|
865
|
+
```ts
|
|
866
|
+
import { debounce, throttleToNextFrame, ExecutionQueue, partition } from '@tldraw/utils'
|
|
867
|
+
|
|
868
|
+
class CustomTool extends StateNode {
|
|
869
|
+
private updateQueue = new ExecutionQueue(16) // 60fps limit
|
|
870
|
+
private debouncedSave = debounce(() => this.saveToolState(), 1000)
|
|
871
|
+
|
|
872
|
+
onPointerMove = throttleToNextFrame((info: TLPointerEventInfo) => {
|
|
873
|
+
this.updateQueue.push(() => this.handleMove(info))
|
|
874
|
+
this.debouncedSave()
|
|
875
|
+
})
|
|
876
|
+
|
|
877
|
+
private handleMove(info: TLPointerEventInfo) {
|
|
878
|
+
const shapes = this.editor.getCurrentPageShapes()
|
|
879
|
+
const [movingShapes, staticShapes] = partition(shapes, (shape) => this.isShapeMoving(shape))
|
|
880
|
+
|
|
881
|
+
// Update only the shapes that need it
|
|
882
|
+
this.updateMovingShapes(movingShapes)
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
```
|
|
886
|
+
|
|
887
|
+
### Error Handling in Applications
|
|
888
|
+
|
|
889
|
+
Robust applications use Result patterns throughout:
|
|
890
|
+
|
|
891
|
+
```ts
|
|
892
|
+
import { Result, assertExists } from '@tldraw/utils'
|
|
893
|
+
|
|
894
|
+
class DocumentManager {
|
|
895
|
+
async loadDocument(id: string): Promise<Result<Document, string>> {
|
|
896
|
+
try {
|
|
897
|
+
const data = await this.storage.load(id)
|
|
898
|
+
assertExists(data, `Document ${id} not found`)
|
|
899
|
+
|
|
900
|
+
const parseResult = this.parseDocument(data)
|
|
901
|
+
if (!parseResult.ok) {
|
|
902
|
+
return Result.err(`Failed to parse document: ${parseResult.error}`)
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
return Result.ok(parseResult.value)
|
|
906
|
+
} catch (error) {
|
|
907
|
+
return Result.err(`Storage error: ${error.message}`)
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
private parseDocument(data: unknown): Result<Document, string> {
|
|
912
|
+
// Detailed parsing with Result pattern...
|
|
913
|
+
return Result.ok(validDocument)
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
```
|
|
917
|
+
|
|
918
|
+
## Key Benefits
|
|
919
|
+
|
|
920
|
+
The `@tldraw/utils` package provides:
|
|
921
|
+
|
|
922
|
+
**Type Safety**: Every utility maintains and enhances TypeScript's type information, preventing runtime errors and improving developer experience.
|
|
923
|
+
|
|
924
|
+
**Performance**: Optimized implementations with caching, throttling, and memory management prevent performance bottlenecks in complex applications.
|
|
925
|
+
|
|
926
|
+
**Reliability**: Comprehensive error handling with Result patterns and assertions creates predictable, debuggable applications.
|
|
927
|
+
|
|
928
|
+
**Cross-Platform**: Consistent behavior across browsers, Node.js, and other JavaScript environments with appropriate polyfills and fallbacks.
|
|
929
|
+
|
|
930
|
+
These utilities form the foundation that makes tldraw's complex canvas operations feel smooth and reliable. Whether you're building custom shapes, tools, or integrating tldraw into larger applications, these utilities provide the building blocks for professional-grade functionality.
|
package/README.md
CHANGED
|
@@ -2,9 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
Utility functions used by tldraw.
|
|
4
4
|
|
|
5
|
+
## Documentation
|
|
6
|
+
|
|
7
|
+
Documentation for the most recent release can be found on [tldraw.dev/docs](https://tldraw.dev/docs), including [reference docs](https://tldraw.dev/reference/editor/Editor). Our release notes can be found [here](https://tldraw.dev/releases).
|
|
8
|
+
|
|
9
|
+
For more agent-friendly docs, see our [LLMs.txt](https://tldraw.dev/llms.txt).
|
|
10
|
+
|
|
11
|
+
A `DOCS.md` file is included alongside this README in the published package, with detailed API documentation and usage examples.
|
|
12
|
+
|
|
5
13
|
## Contribution
|
|
6
14
|
|
|
7
|
-
|
|
15
|
+
Found a bug? Please [submit an issue](https://github.com/tldraw/tldraw/issues/new).
|
|
8
16
|
|
|
9
17
|
## License
|
|
10
18
|
|
package/dist-cjs/index.js
CHANGED
|
@@ -171,7 +171,7 @@ var import_version2 = require("./lib/version");
|
|
|
171
171
|
var import_warn = require("./lib/warn");
|
|
172
172
|
(0, import_version.registerTldrawLibraryVersion)(
|
|
173
173
|
"@tldraw/utils",
|
|
174
|
-
"5.2.0-next.
|
|
174
|
+
"5.2.0-next.547772a8f51a",
|
|
175
175
|
"cjs"
|
|
176
176
|
);
|
|
177
177
|
//# sourceMappingURL=index.js.map
|
package/dist-esm/index.mjs
CHANGED
|
@@ -102,7 +102,7 @@ import { registerTldrawLibraryVersion as registerTldrawLibraryVersion2 } from ".
|
|
|
102
102
|
import { warnDeprecatedGetter, warnOnce } from "./lib/warn.mjs";
|
|
103
103
|
registerTldrawLibraryVersion(
|
|
104
104
|
"@tldraw/utils",
|
|
105
|
-
"5.2.0-next.
|
|
105
|
+
"5.2.0-next.547772a8f51a",
|
|
106
106
|
"esm"
|
|
107
107
|
);
|
|
108
108
|
export {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tldraw/utils",
|
|
3
3
|
"description": "tldraw infinite canvas SDK (private utilities).",
|
|
4
|
-
"version": "5.2.0-next.
|
|
4
|
+
"version": "5.2.0-next.547772a8f51a",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "tldraw Inc.",
|
|
7
7
|
"email": "hello@tldraw.com"
|
|
@@ -29,7 +29,8 @@
|
|
|
29
29
|
"files": [
|
|
30
30
|
"dist-esm",
|
|
31
31
|
"dist-cjs",
|
|
32
|
-
"src"
|
|
32
|
+
"src",
|
|
33
|
+
"DOCS.md"
|
|
33
34
|
],
|
|
34
35
|
"scripts": {
|
|
35
36
|
"test-ci": "yarn run -T vitest run --passWithNoTests",
|
|
@@ -55,7 +56,7 @@
|
|
|
55
56
|
"@types/lodash.throttle": "^4.1.9",
|
|
56
57
|
"@types/lodash.uniq": "^4.5.9",
|
|
57
58
|
"lazyrepo": "0.0.0-alpha.27",
|
|
58
|
-
"vitest": "^
|
|
59
|
+
"vitest": "^4.1.7"
|
|
59
60
|
},
|
|
60
61
|
"module": "dist-esm/index.mjs",
|
|
61
62
|
"source": "src/index.ts",
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, type MockInstance, vi } from 'vitest'
|
|
2
2
|
import { PERFORMANCE_COLORS, PERFORMANCE_PREFIX_COLOR } from './perf'
|
|
3
3
|
import { PerformanceTracker } from './PerformanceTracker'
|
|
4
4
|
|
|
@@ -7,7 +7,7 @@ describe('PerformanceTracker', () => {
|
|
|
7
7
|
let mockPerformanceNow: ReturnType<typeof vi.fn>
|
|
8
8
|
let mockRequestAnimationFrame: ReturnType<typeof vi.fn>
|
|
9
9
|
let mockCancelAnimationFrame: ReturnType<typeof vi.fn>
|
|
10
|
-
let mockConsoleDebug:
|
|
10
|
+
let mockConsoleDebug: MockInstance<typeof console.debug>
|
|
11
11
|
let frameId = 1
|
|
12
12
|
|
|
13
13
|
beforeEach(() => {
|
|
@@ -24,8 +24,7 @@ describe('PerformanceTracker', () => {
|
|
|
24
24
|
vi.stubGlobal('cancelAnimationFrame', mockCancelAnimationFrame)
|
|
25
25
|
|
|
26
26
|
// Mock console.debug
|
|
27
|
-
mockConsoleDebug = vi.
|
|
28
|
-
vi.spyOn(console, 'debug').mockImplementation(mockConsoleDebug)
|
|
27
|
+
mockConsoleDebug = vi.spyOn(console, 'debug').mockImplementation(() => {})
|
|
29
28
|
})
|
|
30
29
|
|
|
31
30
|
afterEach(() => {
|
package/src/lib/version.test.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
|
2
2
|
import { clearRegisteredVersionsForTests, registerTldrawLibraryVersion } from './version'
|
|
3
3
|
|
|
4
4
|
describe('version utilities', () => {
|
|
5
|
-
let mockConsoleLog: ReturnType<typeof vi.fn
|
|
5
|
+
let mockConsoleLog: ReturnType<typeof vi.fn<(...args: any[]) => any>>
|
|
6
6
|
|
|
7
7
|
beforeEach(() => {
|
|
8
8
|
mockConsoleLog = vi.fn()
|