@tldraw/tlschema 4.1.0-next.0df13eab91e1 → 4.1.0-next.2c81540f049b

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (212) hide show
  1. package/dist-cjs/TLStore.js +3 -10
  2. package/dist-cjs/TLStore.js.map +2 -2
  3. package/dist-cjs/assets/TLBaseAsset.js.map +2 -2
  4. package/dist-cjs/assets/TLBookmarkAsset.js.map +2 -2
  5. package/dist-cjs/assets/TLImageAsset.js.map +2 -2
  6. package/dist-cjs/assets/TLVideoAsset.js.map +2 -2
  7. package/dist-cjs/bindings/TLArrowBinding.js.map +2 -2
  8. package/dist-cjs/bindings/TLBaseBinding.js.map +2 -2
  9. package/dist-cjs/createPresenceStateDerivation.js.map +2 -2
  10. package/dist-cjs/createTLSchema.js.map +2 -2
  11. package/dist-cjs/index.d.ts +4416 -223
  12. package/dist-cjs/index.js +1 -1
  13. package/dist-cjs/index.js.map +2 -2
  14. package/dist-cjs/misc/TLColor.js.map +2 -2
  15. package/dist-cjs/misc/TLCursor.js.map +2 -2
  16. package/dist-cjs/misc/TLHandle.js.map +2 -2
  17. package/dist-cjs/misc/TLOpacity.js.map +2 -2
  18. package/dist-cjs/misc/TLRichText.js.map +2 -2
  19. package/dist-cjs/misc/TLScribble.js.map +2 -2
  20. package/dist-cjs/misc/geometry-types.js.map +2 -2
  21. package/dist-cjs/misc/id-validator.js.map +2 -2
  22. package/dist-cjs/records/TLAsset.js.map +2 -2
  23. package/dist-cjs/records/TLBinding.js.map +2 -2
  24. package/dist-cjs/records/TLCamera.js.map +2 -2
  25. package/dist-cjs/records/TLDocument.js.map +2 -2
  26. package/dist-cjs/records/TLInstance.js.map +2 -2
  27. package/dist-cjs/records/TLPage.js.map +2 -2
  28. package/dist-cjs/records/TLPageState.js.map +2 -2
  29. package/dist-cjs/records/TLPointer.js.map +2 -2
  30. package/dist-cjs/records/TLPresence.js.map +2 -2
  31. package/dist-cjs/records/TLRecord.js.map +1 -1
  32. package/dist-cjs/records/TLShape.js.map +2 -2
  33. package/dist-cjs/recordsWithProps.js.map +2 -2
  34. package/dist-cjs/shapes/ShapeWithCrop.js.map +1 -1
  35. package/dist-cjs/shapes/TLArrowShape.js.map +2 -2
  36. package/dist-cjs/shapes/TLBaseShape.js.map +2 -2
  37. package/dist-cjs/shapes/TLBookmarkShape.js.map +2 -2
  38. package/dist-cjs/shapes/TLDrawShape.js.map +2 -2
  39. package/dist-cjs/shapes/TLEmbedShape.js.map +2 -2
  40. package/dist-cjs/shapes/TLFrameShape.js.map +2 -2
  41. package/dist-cjs/shapes/TLGeoShape.js.map +2 -2
  42. package/dist-cjs/shapes/TLGroupShape.js.map +2 -2
  43. package/dist-cjs/shapes/TLHighlightShape.js.map +2 -2
  44. package/dist-cjs/shapes/TLImageShape.js.map +2 -2
  45. package/dist-cjs/shapes/TLLineShape.js.map +2 -2
  46. package/dist-cjs/shapes/TLNoteShape.js.map +2 -2
  47. package/dist-cjs/shapes/TLTextShape.js.map +2 -2
  48. package/dist-cjs/shapes/TLVideoShape.js.map +2 -2
  49. package/dist-cjs/store-migrations.js.map +2 -2
  50. package/dist-cjs/styles/TLColorStyle.js.map +2 -2
  51. package/dist-cjs/styles/TLDashStyle.js.map +2 -2
  52. package/dist-cjs/styles/TLFillStyle.js.map +2 -2
  53. package/dist-cjs/styles/TLFontStyle.js.map +2 -2
  54. package/dist-cjs/styles/TLHorizontalAlignStyle.js.map +2 -2
  55. package/dist-cjs/styles/TLSizeStyle.js.map +2 -2
  56. package/dist-cjs/styles/TLTextAlignStyle.js.map +2 -2
  57. package/dist-cjs/styles/TLVerticalAlignStyle.js.map +2 -2
  58. package/dist-cjs/translations/translations.js +1 -1
  59. package/dist-cjs/translations/translations.js.map +2 -2
  60. package/dist-cjs/util-types.js.map +1 -1
  61. package/dist-esm/TLStore.mjs +3 -10
  62. package/dist-esm/TLStore.mjs.map +2 -2
  63. package/dist-esm/assets/TLBaseAsset.mjs.map +2 -2
  64. package/dist-esm/assets/TLBookmarkAsset.mjs.map +2 -2
  65. package/dist-esm/assets/TLImageAsset.mjs.map +2 -2
  66. package/dist-esm/assets/TLVideoAsset.mjs.map +2 -2
  67. package/dist-esm/bindings/TLArrowBinding.mjs.map +2 -2
  68. package/dist-esm/bindings/TLBaseBinding.mjs.map +2 -2
  69. package/dist-esm/createPresenceStateDerivation.mjs.map +2 -2
  70. package/dist-esm/createTLSchema.mjs.map +2 -2
  71. package/dist-esm/index.d.mts +4416 -223
  72. package/dist-esm/index.mjs +1 -1
  73. package/dist-esm/index.mjs.map +2 -2
  74. package/dist-esm/misc/TLColor.mjs.map +2 -2
  75. package/dist-esm/misc/TLCursor.mjs.map +2 -2
  76. package/dist-esm/misc/TLHandle.mjs.map +2 -2
  77. package/dist-esm/misc/TLOpacity.mjs.map +2 -2
  78. package/dist-esm/misc/TLRichText.mjs.map +2 -2
  79. package/dist-esm/misc/TLScribble.mjs.map +2 -2
  80. package/dist-esm/misc/geometry-types.mjs.map +2 -2
  81. package/dist-esm/misc/id-validator.mjs.map +2 -2
  82. package/dist-esm/records/TLAsset.mjs.map +2 -2
  83. package/dist-esm/records/TLBinding.mjs.map +2 -2
  84. package/dist-esm/records/TLCamera.mjs.map +2 -2
  85. package/dist-esm/records/TLDocument.mjs.map +2 -2
  86. package/dist-esm/records/TLInstance.mjs.map +2 -2
  87. package/dist-esm/records/TLPage.mjs.map +2 -2
  88. package/dist-esm/records/TLPageState.mjs.map +2 -2
  89. package/dist-esm/records/TLPointer.mjs.map +2 -2
  90. package/dist-esm/records/TLPresence.mjs.map +2 -2
  91. package/dist-esm/records/TLShape.mjs.map +2 -2
  92. package/dist-esm/recordsWithProps.mjs.map +2 -2
  93. package/dist-esm/shapes/TLArrowShape.mjs.map +2 -2
  94. package/dist-esm/shapes/TLBaseShape.mjs.map +2 -2
  95. package/dist-esm/shapes/TLBookmarkShape.mjs.map +2 -2
  96. package/dist-esm/shapes/TLDrawShape.mjs.map +2 -2
  97. package/dist-esm/shapes/TLEmbedShape.mjs.map +2 -2
  98. package/dist-esm/shapes/TLFrameShape.mjs.map +2 -2
  99. package/dist-esm/shapes/TLGeoShape.mjs.map +2 -2
  100. package/dist-esm/shapes/TLGroupShape.mjs.map +2 -2
  101. package/dist-esm/shapes/TLHighlightShape.mjs.map +2 -2
  102. package/dist-esm/shapes/TLImageShape.mjs.map +2 -2
  103. package/dist-esm/shapes/TLLineShape.mjs.map +2 -2
  104. package/dist-esm/shapes/TLNoteShape.mjs.map +2 -2
  105. package/dist-esm/shapes/TLTextShape.mjs.map +2 -2
  106. package/dist-esm/shapes/TLVideoShape.mjs.map +2 -2
  107. package/dist-esm/store-migrations.mjs.map +2 -2
  108. package/dist-esm/styles/TLColorStyle.mjs.map +2 -2
  109. package/dist-esm/styles/TLDashStyle.mjs.map +2 -2
  110. package/dist-esm/styles/TLFillStyle.mjs.map +2 -2
  111. package/dist-esm/styles/TLFontStyle.mjs.map +2 -2
  112. package/dist-esm/styles/TLHorizontalAlignStyle.mjs.map +2 -2
  113. package/dist-esm/styles/TLSizeStyle.mjs.map +2 -2
  114. package/dist-esm/styles/TLTextAlignStyle.mjs.map +2 -2
  115. package/dist-esm/styles/TLVerticalAlignStyle.mjs.map +2 -2
  116. package/dist-esm/translations/translations.mjs +1 -1
  117. package/dist-esm/translations/translations.mjs.map +2 -2
  118. package/package.json +5 -5
  119. package/src/TLStore.test.ts +644 -0
  120. package/src/TLStore.ts +205 -20
  121. package/src/assets/TLBaseAsset.ts +90 -7
  122. package/src/assets/TLBookmarkAsset.test.ts +96 -0
  123. package/src/assets/TLBookmarkAsset.ts +52 -2
  124. package/src/assets/TLImageAsset.test.ts +213 -0
  125. package/src/assets/TLImageAsset.ts +60 -2
  126. package/src/assets/TLVideoAsset.test.ts +105 -0
  127. package/src/assets/TLVideoAsset.ts +93 -4
  128. package/src/bindings/TLArrowBinding.test.ts +55 -0
  129. package/src/bindings/TLArrowBinding.ts +132 -10
  130. package/src/bindings/TLBaseBinding.ts +140 -3
  131. package/src/createPresenceStateDerivation.test.ts +158 -0
  132. package/src/createPresenceStateDerivation.ts +71 -2
  133. package/src/createTLSchema.test.ts +181 -0
  134. package/src/createTLSchema.ts +164 -7
  135. package/src/index.ts +32 -0
  136. package/src/misc/TLColor.ts +50 -6
  137. package/src/misc/TLCursor.ts +110 -8
  138. package/src/misc/TLHandle.ts +86 -6
  139. package/src/misc/TLOpacity.ts +51 -2
  140. package/src/misc/TLRichText.ts +56 -3
  141. package/src/misc/TLScribble.ts +105 -5
  142. package/src/misc/geometry-types.ts +30 -2
  143. package/src/misc/id-validator.test.ts +50 -0
  144. package/src/misc/id-validator.ts +20 -1
  145. package/src/records/TLAsset.test.ts +234 -0
  146. package/src/records/TLAsset.ts +165 -8
  147. package/src/records/TLBinding.test.ts +22 -0
  148. package/src/records/TLBinding.ts +277 -11
  149. package/src/records/TLCamera.test.ts +19 -0
  150. package/src/records/TLCamera.ts +118 -7
  151. package/src/records/TLDocument.test.ts +35 -0
  152. package/src/records/TLDocument.ts +148 -8
  153. package/src/records/TLInstance.test.ts +201 -0
  154. package/src/records/TLInstance.ts +117 -9
  155. package/src/records/TLPage.test.ts +110 -0
  156. package/src/records/TLPage.ts +106 -8
  157. package/src/records/TLPageState.test.ts +228 -0
  158. package/src/records/TLPageState.ts +88 -7
  159. package/src/records/TLPointer.test.ts +63 -0
  160. package/src/records/TLPointer.ts +105 -7
  161. package/src/records/TLPresence.test.ts +190 -0
  162. package/src/records/TLPresence.ts +99 -5
  163. package/src/records/TLRecord.test.ts +70 -0
  164. package/src/records/TLRecord.ts +43 -1
  165. package/src/records/TLShape.test.ts +232 -0
  166. package/src/records/TLShape.ts +289 -12
  167. package/src/recordsWithProps.test.ts +188 -0
  168. package/src/recordsWithProps.ts +131 -2
  169. package/src/shapes/ShapeWithCrop.test.ts +18 -0
  170. package/src/shapes/ShapeWithCrop.ts +64 -2
  171. package/src/shapes/TLArrowShape.test.ts +505 -0
  172. package/src/shapes/TLArrowShape.ts +188 -10
  173. package/src/shapes/TLBaseShape.test.ts +142 -0
  174. package/src/shapes/TLBaseShape.ts +103 -4
  175. package/src/shapes/TLBookmarkShape.test.ts +122 -0
  176. package/src/shapes/TLBookmarkShape.ts +58 -4
  177. package/src/shapes/TLDrawShape.test.ts +177 -0
  178. package/src/shapes/TLDrawShape.ts +97 -6
  179. package/src/shapes/TLEmbedShape.test.ts +286 -0
  180. package/src/shapes/TLEmbedShape.ts +57 -4
  181. package/src/shapes/TLFrameShape.test.ts +71 -0
  182. package/src/shapes/TLFrameShape.ts +59 -4
  183. package/src/shapes/TLGeoShape.test.ts +247 -0
  184. package/src/shapes/TLGeoShape.ts +103 -7
  185. package/src/shapes/TLGroupShape.test.ts +59 -0
  186. package/src/shapes/TLGroupShape.ts +52 -4
  187. package/src/shapes/TLHighlightShape.test.ts +325 -0
  188. package/src/shapes/TLHighlightShape.ts +79 -4
  189. package/src/shapes/TLImageShape.test.ts +534 -0
  190. package/src/shapes/TLImageShape.ts +105 -5
  191. package/src/shapes/TLLineShape.test.ts +269 -0
  192. package/src/shapes/TLLineShape.ts +128 -8
  193. package/src/shapes/TLNoteShape.test.ts +1568 -0
  194. package/src/shapes/TLNoteShape.ts +97 -4
  195. package/src/shapes/TLTextShape.test.ts +407 -0
  196. package/src/shapes/TLTextShape.ts +94 -4
  197. package/src/shapes/TLVideoShape.test.ts +112 -0
  198. package/src/shapes/TLVideoShape.ts +99 -4
  199. package/src/store-migrations.test.ts +88 -0
  200. package/src/store-migrations.ts +47 -1
  201. package/src/styles/TLColorStyle.test.ts +439 -0
  202. package/src/styles/TLColorStyle.ts +228 -10
  203. package/src/styles/TLDashStyle.ts +54 -2
  204. package/src/styles/TLFillStyle.ts +54 -2
  205. package/src/styles/TLFontStyle.ts +72 -3
  206. package/src/styles/TLHorizontalAlignStyle.ts +55 -2
  207. package/src/styles/TLSizeStyle.ts +54 -2
  208. package/src/styles/TLTextAlignStyle.ts +52 -2
  209. package/src/styles/TLVerticalAlignStyle.ts +52 -2
  210. package/src/translations/translations.test.ts +378 -35
  211. package/src/translations/translations.ts +157 -10
  212. package/src/util-types.ts +51 -1
@@ -0,0 +1,644 @@
1
+ import { Store } from '@tldraw/store'
2
+ import { annotateError, IndexKey, structuredClone } from '@tldraw/utils'
3
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
4
+ import { createTLSchema } from './createTLSchema'
5
+ import { CameraRecordType } from './records/TLCamera'
6
+ import { TLDOCUMENT_ID } from './records/TLDocument'
7
+ import { TLINSTANCE_ID } from './records/TLInstance'
8
+ import { PageRecordType, TLPageId } from './records/TLPage'
9
+ import { InstancePageStateRecordType } from './records/TLPageState'
10
+ import { TLPOINTER_ID } from './records/TLPointer'
11
+ import { TLRecord } from './records/TLRecord'
12
+ import { TLShapeId } from './records/TLShape'
13
+ import {
14
+ createIntegrityChecker,
15
+ onValidationFailure,
16
+ redactRecordForErrorReporting,
17
+ TLAssetStore,
18
+ TLStoreProps,
19
+ } from './TLStore'
20
+
21
+ // Mock dependencies
22
+ vi.mock('@tldraw/utils', async () => {
23
+ const actual = await vi.importActual('@tldraw/utils')
24
+ return {
25
+ ...actual,
26
+ annotateError: vi.fn(),
27
+ structuredClone: vi.fn((obj) => JSON.parse(JSON.stringify(obj))),
28
+ }
29
+ })
30
+
31
+ describe('TLStore utility functions', () => {
32
+ beforeEach(() => {
33
+ vi.clearAllMocks()
34
+ })
35
+
36
+ describe('redactRecordForErrorReporting', () => {
37
+ it('should redact src field from asset record', () => {
38
+ const assetRecord = {
39
+ id: 'asset:test',
40
+ typeName: 'asset',
41
+ type: 'image',
42
+ src: 'https://secret.com/image.png',
43
+ props: {
44
+ src: 'https://secret.com/props-image.png',
45
+ width: 100,
46
+ height: 100,
47
+ },
48
+ }
49
+
50
+ redactRecordForErrorReporting(assetRecord)
51
+
52
+ expect(assetRecord.src).toBe('<redacted>')
53
+ expect(assetRecord.props.src).toBe('<redacted>')
54
+ expect(assetRecord.props.width).toBe(100) // Other props should remain unchanged
55
+ expect(assetRecord.props.height).toBe(100)
56
+ })
57
+
58
+ it('should redact only props.src if top-level src does not exist', () => {
59
+ const assetRecord = {
60
+ id: 'asset:test',
61
+ typeName: 'asset',
62
+ type: 'video',
63
+ props: {
64
+ src: 'https://secret.com/video.mp4',
65
+ width: 200,
66
+ height: 150,
67
+ },
68
+ }
69
+
70
+ redactRecordForErrorReporting(assetRecord)
71
+
72
+ expect(assetRecord.props.src).toBe('<redacted>')
73
+ expect(assetRecord.props.width).toBe(200)
74
+ expect(assetRecord.props.height).toBe(150)
75
+ })
76
+
77
+ it('should not modify non-asset records', () => {
78
+ const shapeRecord = {
79
+ id: 'shape:test',
80
+ typeName: 'shape',
81
+ type: 'geo',
82
+ x: 100,
83
+ y: 200,
84
+ props: {
85
+ color: 'red',
86
+ size: 'medium',
87
+ },
88
+ }
89
+
90
+ const originalRecord = JSON.parse(JSON.stringify(shapeRecord))
91
+ redactRecordForErrorReporting(shapeRecord)
92
+
93
+ expect(shapeRecord).toEqual(originalRecord)
94
+ })
95
+
96
+ it('should handle asset records without src fields gracefully', () => {
97
+ const assetRecord = {
98
+ id: 'asset:test',
99
+ typeName: 'asset',
100
+ type: 'bookmark',
101
+ props: {
102
+ title: 'Test Bookmark',
103
+ description: 'A test bookmark',
104
+ },
105
+ }
106
+
107
+ const originalRecord = JSON.parse(JSON.stringify(assetRecord))
108
+ redactRecordForErrorReporting(assetRecord)
109
+
110
+ expect(assetRecord).toEqual(originalRecord)
111
+ })
112
+
113
+ it('should handle asset records with only top-level src', () => {
114
+ const assetRecord = {
115
+ id: 'asset:test',
116
+ typeName: 'asset',
117
+ type: 'image',
118
+ src: 'https://secret.com/image.png',
119
+ props: {
120
+ width: 100,
121
+ height: 100,
122
+ },
123
+ }
124
+
125
+ redactRecordForErrorReporting(assetRecord)
126
+
127
+ expect(assetRecord.src).toBe('<redacted>')
128
+ expect(assetRecord.props.width).toBe(100)
129
+ expect(assetRecord.props.height).toBe(100)
130
+ })
131
+ })
132
+ })
133
+
134
+ describe('onValidationFailure', () => {
135
+ beforeEach(() => {
136
+ vi.clearAllMocks()
137
+ })
138
+
139
+ it('should annotate error with correct tags and extras', () => {
140
+ const mockError = new Error('Test validation error')
141
+ const record = {
142
+ id: 'shape:test',
143
+ typeName: 'shape',
144
+ type: 'geo',
145
+ x: 100,
146
+ y: 200,
147
+ } as any
148
+
149
+ const recordBefore = {
150
+ id: 'shape:test',
151
+ typeName: 'shape',
152
+ type: 'geo',
153
+ x: 50,
154
+ y: 150,
155
+ } as any
156
+
157
+ const validationFailure = {
158
+ error: mockError,
159
+ phase: 'updateRecord' as const,
160
+ record,
161
+ recordBefore,
162
+ store: {} as any, // Required by StoreValidationFailure interface
163
+ }
164
+
165
+ expect(() => onValidationFailure(validationFailure)).toThrow(mockError)
166
+
167
+ expect(annotateError).toHaveBeenCalledWith(
168
+ mockError,
169
+ expect.objectContaining({
170
+ tags: expect.objectContaining({
171
+ origin: 'store.validateRecord',
172
+ storePhase: 'updateRecord',
173
+ isExistingValidationIssue: false,
174
+ }),
175
+ })
176
+ )
177
+ })
178
+
179
+ it('should mark initialize phase as existing validation issue', () => {
180
+ const mockError = new Error('Initialize error')
181
+ const record = { id: 'test:1', typeName: 'test' } as any
182
+
183
+ const validationFailure = {
184
+ error: mockError,
185
+ phase: 'initialize' as const,
186
+ record,
187
+ recordBefore: null,
188
+ store: {} as any,
189
+ }
190
+
191
+ expect(() => onValidationFailure(validationFailure)).toThrow(mockError)
192
+
193
+ expect(annotateError).toHaveBeenCalledWith(
194
+ mockError,
195
+ expect.objectContaining({
196
+ tags: expect.objectContaining({
197
+ isExistingValidationIssue: true,
198
+ }),
199
+ })
200
+ )
201
+ })
202
+
203
+ it('should handle missing recordBefore', () => {
204
+ const mockError = new Error('No record before')
205
+ const record = { id: 'test:new', typeName: 'test' } as any
206
+
207
+ const validationFailure = {
208
+ error: mockError,
209
+ phase: 'createRecord' as const,
210
+ record,
211
+ recordBefore: null,
212
+ store: {} as any,
213
+ }
214
+
215
+ expect(() => onValidationFailure(validationFailure)).toThrow(mockError)
216
+
217
+ expect(annotateError).toHaveBeenCalledWith(
218
+ mockError,
219
+ expect.objectContaining({
220
+ tags: expect.objectContaining({
221
+ origin: 'store.validateRecord',
222
+ storePhase: 'createRecord',
223
+ isExistingValidationIssue: false,
224
+ }),
225
+ })
226
+ )
227
+ })
228
+
229
+ it('should redact asset records in error reporting', () => {
230
+ const mockError = new Error('Asset validation error')
231
+ const assetRecord = {
232
+ id: 'asset:image',
233
+ typeName: 'asset',
234
+ type: 'image',
235
+ src: 'https://secret.com/image.png',
236
+ props: {
237
+ src: 'https://secret.com/props-image.png',
238
+ width: 100,
239
+ height: 100,
240
+ },
241
+ } as any
242
+
243
+ const validationFailure = {
244
+ error: mockError,
245
+ phase: 'createRecord' as const,
246
+ record: assetRecord,
247
+ recordBefore: null,
248
+ store: {} as any,
249
+ }
250
+
251
+ expect(() => onValidationFailure(validationFailure)).toThrow(mockError)
252
+
253
+ // The function should call annotateError, and redaction happens internally
254
+ expect(annotateError).toHaveBeenCalledWith(
255
+ mockError,
256
+ expect.objectContaining({
257
+ tags: expect.objectContaining({
258
+ origin: 'store.validateRecord',
259
+ storePhase: 'createRecord',
260
+ isExistingValidationIssue: false,
261
+ }),
262
+ })
263
+ )
264
+ })
265
+
266
+ it('should handle different validation phases correctly', () => {
267
+ const phases = ['initialize', 'createRecord', 'updateRecord', 'tests'] as const
268
+ const mockError = new Error('Phase test')
269
+ const record = { id: 'test:phase', typeName: 'test' } as any
270
+
271
+ phases.forEach((phase) => {
272
+ const validationFailure = {
273
+ error: mockError,
274
+ phase,
275
+ record,
276
+ recordBefore: null,
277
+ store: {} as any,
278
+ }
279
+
280
+ expect(() => onValidationFailure(validationFailure)).toThrow(mockError)
281
+
282
+ expect(annotateError).toHaveBeenCalledWith(
283
+ mockError,
284
+ expect.objectContaining({
285
+ tags: expect.objectContaining({
286
+ storePhase: phase,
287
+ isExistingValidationIssue: phase === 'initialize',
288
+ }),
289
+ })
290
+ )
291
+
292
+ vi.clearAllMocks()
293
+ })
294
+ })
295
+
296
+ it('should use structuredClone for records', () => {
297
+ const mockError = new Error('Clone test')
298
+ const record = { id: 'test:clone', typeName: 'test', nested: { prop: 'value' } } as any
299
+ const recordBefore = { id: 'test:clone', typeName: 'test', nested: { prop: 'old' } } as any
300
+
301
+ const validationFailure = {
302
+ error: mockError,
303
+ phase: 'updateRecord' as const,
304
+ record,
305
+ recordBefore,
306
+ store: {} as any,
307
+ }
308
+
309
+ expect(() => onValidationFailure(validationFailure)).toThrow(mockError)
310
+
311
+ expect(structuredClone).toHaveBeenCalledWith(record)
312
+ expect(structuredClone).toHaveBeenCalledWith(recordBefore)
313
+ })
314
+ })
315
+
316
+ describe('createIntegrityChecker', () => {
317
+ let store: Store<TLRecord, TLStoreProps>
318
+ let mockAssetStore: Required<TLAssetStore>
319
+
320
+ beforeEach(() => {
321
+ mockAssetStore = {
322
+ upload: vi.fn().mockResolvedValue({ src: 'uploaded-url' }),
323
+ resolve: vi.fn().mockResolvedValue('resolved-url'),
324
+ remove: vi.fn().mockResolvedValue(undefined),
325
+ } as Required<TLAssetStore>
326
+
327
+ const schema = createTLSchema()
328
+ store = new Store({
329
+ schema,
330
+ props: {
331
+ defaultName: 'Test Store',
332
+ assets: mockAssetStore,
333
+ onMount: vi.fn(),
334
+ },
335
+ })
336
+ })
337
+
338
+ afterEach(() => {
339
+ vi.clearAllMocks()
340
+ })
341
+
342
+ describe('document and pointer records', () => {
343
+ it('should create missing document record', () => {
344
+ // Remove document record if it exists
345
+ if (store.has(TLDOCUMENT_ID)) {
346
+ store.remove([TLDOCUMENT_ID])
347
+ }
348
+
349
+ const checker = createIntegrityChecker(store)
350
+ checker()
351
+
352
+ expect(store.has(TLDOCUMENT_ID)).toBe(true)
353
+ const document = store.get(TLDOCUMENT_ID)
354
+ expect(document).toBeDefined()
355
+ expect(document!.name).toBe('Test Store')
356
+ })
357
+
358
+ it('should create missing pointer record', () => {
359
+ // Remove pointer record if it exists
360
+ if (store.has(TLPOINTER_ID)) {
361
+ store.remove([TLPOINTER_ID])
362
+ }
363
+
364
+ const checker = createIntegrityChecker(store)
365
+ checker()
366
+
367
+ expect(store.has(TLPOINTER_ID)).toBe(true)
368
+ const pointer = store.get(TLPOINTER_ID)
369
+ expect(pointer).toBeDefined()
370
+ expect(pointer!.typeName).toBe('pointer')
371
+ })
372
+ })
373
+
374
+ describe('page management', () => {
375
+ it('should create default page when none exist', () => {
376
+ // Clear all pages
377
+ const pageIds = store.query.ids('page').get()
378
+ store.remove([...pageIds])
379
+
380
+ const checker = createIntegrityChecker(store)
381
+ checker()
382
+
383
+ const newPageIds = store.query.ids('page').get()
384
+ expect(newPageIds.size).toBe(1)
385
+
386
+ const page = store.get([...newPageIds][0]) as any
387
+ expect(page).toBeDefined()
388
+ expect(page!.name).toBe('Page 1')
389
+ expect(page!.index).toBe('a1')
390
+ })
391
+
392
+ it('should preserve existing pages', () => {
393
+ // First ensure we have at least one page
394
+ const checker = createIntegrityChecker(store)
395
+ checker()
396
+
397
+ const existingPageIds = store.query.ids('page').get()
398
+ expect(existingPageIds.size).toBeGreaterThan(0) // Should have at least one page
399
+
400
+ // Run checker again - should not change pages
401
+ checker()
402
+ const newPageIds = store.query.ids('page').get()
403
+ expect(newPageIds.size).toBe(existingPageIds.size)
404
+ })
405
+ })
406
+
407
+ describe('instance state management', () => {
408
+ it('should create missing instance state', () => {
409
+ // Remove instance if it exists
410
+ if (store.has(TLINSTANCE_ID)) {
411
+ store.remove([TLINSTANCE_ID])
412
+ }
413
+
414
+ // Ensure we have at least one page
415
+ const pageIds = store.query.ids('page').get()
416
+ if (pageIds.size === 0) {
417
+ store.put([
418
+ PageRecordType.create({
419
+ id: 'page:test' as TLPageId,
420
+ name: 'Test Page',
421
+ index: 'a1' as IndexKey,
422
+ meta: {},
423
+ }),
424
+ ])
425
+ }
426
+
427
+ const checker = createIntegrityChecker(store)
428
+ checker()
429
+
430
+ expect(store.has(TLINSTANCE_ID)).toBe(true)
431
+ const instance = store.get(TLINSTANCE_ID)
432
+ expect(instance).toBeDefined()
433
+ expect(instance!.currentPageId).toBeDefined()
434
+ expect(instance!.exportBackground).toBe(true)
435
+ })
436
+
437
+ it('should update instance to reference valid page when current page is invalid', () => {
438
+ // Create a valid page
439
+ const validPageId = 'page:valid' as TLPageId
440
+ store.put([
441
+ PageRecordType.create({
442
+ id: validPageId,
443
+ name: 'Valid Page',
444
+ index: 'a1' as IndexKey,
445
+ meta: {},
446
+ }),
447
+ ])
448
+
449
+ // Create instance with invalid page reference
450
+ const invalidPageId = 'page:invalid' as TLPageId
451
+ store.put([
452
+ store.schema.types.instance.create({
453
+ id: TLINSTANCE_ID,
454
+ currentPageId: invalidPageId,
455
+ exportBackground: true,
456
+ }),
457
+ ])
458
+
459
+ const checker = createIntegrityChecker(store)
460
+ checker()
461
+
462
+ const instance = store.get(TLINSTANCE_ID)
463
+ expect(instance!.currentPageId).toBe(validPageId)
464
+ })
465
+ })
466
+
467
+ describe('page state and camera management', () => {
468
+ it('should create missing page states for existing pages', () => {
469
+ // Create a page
470
+ const pageId = 'page:test' as TLPageId
471
+ store.put([
472
+ PageRecordType.create({
473
+ id: pageId,
474
+ name: 'Test Page',
475
+ index: 'a1' as IndexKey,
476
+ meta: {},
477
+ }),
478
+ ])
479
+
480
+ // Remove any existing page state
481
+ const pageStateId = InstancePageStateRecordType.createId(pageId)
482
+ if (store.has(pageStateId)) {
483
+ store.remove([pageStateId])
484
+ }
485
+
486
+ const checker = createIntegrityChecker(store)
487
+ checker()
488
+
489
+ expect(store.has(pageStateId)).toBe(true)
490
+ const pageState = store.get(pageStateId)
491
+ expect(pageState).toBeDefined()
492
+ expect(pageState!.pageId).toBe(pageId)
493
+ })
494
+
495
+ it('should create missing cameras for existing pages', () => {
496
+ // Create a page
497
+ const pageId = 'page:test' as TLPageId
498
+ store.put([
499
+ PageRecordType.create({
500
+ id: pageId,
501
+ name: 'Test Page',
502
+ index: 'a1' as IndexKey,
503
+ meta: {},
504
+ }),
505
+ ])
506
+
507
+ // Remove any existing camera
508
+ const cameraId = CameraRecordType.createId(pageId)
509
+ if (store.has(cameraId)) {
510
+ store.remove([cameraId])
511
+ }
512
+
513
+ const checker = createIntegrityChecker(store)
514
+ checker()
515
+
516
+ expect(store.has(cameraId)).toBe(true)
517
+ const camera = store.get(cameraId)
518
+ expect(camera).toBeDefined()
519
+ expect(camera!.id).toBe(cameraId)
520
+ })
521
+ })
522
+
523
+ describe('page state cleanup and validation', () => {
524
+ it('should remove page states for non-existent pages', () => {
525
+ // Create page state for non-existent page
526
+ const nonExistentPageId = 'page:nonexistent' as TLPageId
527
+ const orphanPageStateId = InstancePageStateRecordType.createId(nonExistentPageId)
528
+
529
+ store.put([
530
+ InstancePageStateRecordType.create({
531
+ id: orphanPageStateId,
532
+ pageId: nonExistentPageId,
533
+ }),
534
+ ])
535
+
536
+ const checker = createIntegrityChecker(store)
537
+ checker()
538
+
539
+ expect(store.has(orphanPageStateId)).toBe(false)
540
+ })
541
+
542
+ it.each([
543
+ ['croppingShapeId', 'shape:nonexistent' as TLShapeId, null],
544
+ ['focusedGroupId', 'shape:nonexistent' as TLShapeId, null],
545
+ ['hoveredShapeId', 'shape:nonexistent' as TLShapeId, null],
546
+ ])('should clear invalid %s from page states', (fieldName, invalidValue, expectedValue) => {
547
+ const pageId = 'page:test' as TLPageId
548
+ const pageStateId = InstancePageStateRecordType.createId(pageId)
549
+
550
+ store.put([
551
+ PageRecordType.create({
552
+ id: pageId,
553
+ name: 'Test Page',
554
+ index: 'a1' as IndexKey,
555
+ meta: {},
556
+ }),
557
+ InstancePageStateRecordType.create({
558
+ id: pageStateId,
559
+ pageId: pageId,
560
+ [fieldName]: invalidValue,
561
+ }),
562
+ ])
563
+
564
+ const checker = createIntegrityChecker(store)
565
+ checker()
566
+
567
+ const pageState = store.get(pageStateId)
568
+ expect(pageState![fieldName as keyof typeof pageState]).toBe(expectedValue)
569
+ })
570
+
571
+ it.each([
572
+ ['selectedShapeIds', ['shape:nonexistent1', 'shape:nonexistent2'] as TLShapeId[]],
573
+ ['hintingShapeIds', ['shape:hint1', 'shape:hint2'] as TLShapeId[]],
574
+ ['erasingShapeIds', ['shape:erase1', 'shape:erase2'] as TLShapeId[]],
575
+ ])('should filter invalid %s from page states', (fieldName, invalidShapeIds) => {
576
+ const pageId = 'page:test' as TLPageId
577
+ const pageStateId = InstancePageStateRecordType.createId(pageId)
578
+
579
+ store.put([
580
+ PageRecordType.create({
581
+ id: pageId,
582
+ name: 'Test Page',
583
+ index: 'a1' as IndexKey,
584
+ meta: {},
585
+ }),
586
+ InstancePageStateRecordType.create({
587
+ id: pageStateId,
588
+ pageId: pageId,
589
+ [fieldName]: invalidShapeIds,
590
+ }),
591
+ ])
592
+
593
+ const checker = createIntegrityChecker(store)
594
+ checker()
595
+
596
+ const pageState = store.get(pageStateId)
597
+ expect(pageState![fieldName as keyof typeof pageState]).toEqual([])
598
+ })
599
+ })
600
+
601
+ describe('recursive integrity checking', () => {
602
+ it('should recursively call itself when making corrections', () => {
603
+ // Start with empty store - this will trigger multiple corrections
604
+ store.clear()
605
+
606
+ const checker = createIntegrityChecker(store)
607
+
608
+ // This should not throw or hang - it should complete successfully
609
+ expect(() => checker()).not.toThrow()
610
+
611
+ // Verify final state is valid
612
+ expect(store.has(TLDOCUMENT_ID)).toBe(true)
613
+ expect(store.has(TLPOINTER_ID)).toBe(true)
614
+ expect(store.query.ids('page').get().size).toBe(1)
615
+ expect(store.has(TLINSTANCE_ID)).toBe(true)
616
+ })
617
+
618
+ it('should handle complex integrity violations in sequence', () => {
619
+ // Create a scenario with multiple integrity issues
620
+ store.clear()
621
+
622
+ // Add a page without required instance/document/pointer records
623
+ const pageId = 'page:orphan' as TLPageId
624
+ store.put([
625
+ PageRecordType.create({
626
+ id: pageId,
627
+ name: 'Orphan Page',
628
+ index: 'a1' as IndexKey,
629
+ meta: {},
630
+ }),
631
+ ])
632
+
633
+ const checker = createIntegrityChecker(store)
634
+ checker()
635
+
636
+ // All required records should now exist
637
+ expect(store.has(TLDOCUMENT_ID)).toBe(true)
638
+ expect(store.has(TLPOINTER_ID)).toBe(true)
639
+ expect(store.has(TLINSTANCE_ID)).toBe(true)
640
+ expect(store.has(InstancePageStateRecordType.createId(pageId))).toBe(true)
641
+ expect(store.has(CameraRecordType.createId(pageId))).toBe(true)
642
+ })
643
+ })
644
+ })