@tldraw/utils 5.2.0-canary.d9412f42c379 → 5.2.0-canary.e00ecc8f3255

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 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
- Please see our [contributing guide](https://github.com/tldraw/tldraw/blob/main/CONTRIBUTING.md). Found a bug? Please [submit an issue](https://github.com/tldraw/tldraw/issues/new).
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-canary.d9412f42c379",
174
+ "5.2.0-canary.e00ecc8f3255",
175
175
  "cjs"
176
176
  );
177
177
  //# sourceMappingURL=index.js.map
@@ -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-canary.d9412f42c379",
105
+ "5.2.0-canary.e00ecc8f3255",
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-canary.d9412f42c379",
4
+ "version": "5.2.0-canary.e00ecc8f3255",
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": "^3.2.4"
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: ReturnType<typeof vi.fn>
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.fn()
28
- vi.spyOn(console, 'debug').mockImplementation(mockConsoleDebug)
27
+ mockConsoleDebug = vi.spyOn(console, 'debug').mockImplementation(() => {})
29
28
  })
30
29
 
31
30
  afterEach(() => {
@@ -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()