@tldraw/store 3.16.0-internal.51e99e128bd4 → 3.16.0-internal.71f83a8a571b

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/src/lib/Store.ts CHANGED
@@ -501,21 +501,11 @@ export class Store<R extends UnknownRecord = UnknownRecord, Props = unknown> {
501
501
  }
502
502
  }
503
503
 
504
- /**
505
- * @deprecated use `getSnapshot` from the 'tldraw' package instead.
506
- */
507
- getSnapshot(scope: RecordScope | 'all' = 'document') {
508
- console.warn(
509
- '[tldraw] `Store.getSnapshot` is deprecated and will be removed in a future release. Use `getSnapshot` from the `tldraw` package instead.'
510
- )
511
- return this.getStoreSnapshot(scope)
512
- }
513
-
514
504
  /**
515
505
  * Migrate a serialized snapshot of the store and its schema.
516
506
  *
517
507
  * ```ts
518
- * const snapshot = store.getSnapshot()
508
+ * const snapshot = store.getStoreSnapshot()
519
509
  * store.migrateSnapshot(snapshot)
520
510
  * ```
521
511
  *
@@ -566,17 +556,6 @@ export class Store<R extends UnknownRecord = UnknownRecord, Props = unknown> {
566
556
  }
567
557
  }
568
558
 
569
- /**
570
- * @public
571
- * @deprecated use `loadSnapshot` from the 'tldraw' package instead.
572
- */
573
- loadSnapshot(snapshot: StoreSnapshot<R>) {
574
- console.warn(
575
- "[tldraw] `Store.loadSnapshot` is deprecated and will be removed in a future release. Use `loadSnapshot` from the 'tldraw' package instead."
576
- )
577
- this.loadStoreSnapshot(snapshot)
578
- }
579
-
580
559
  /**
581
560
  * Get an array of all values in the store.
582
561
  *
@@ -349,6 +349,7 @@ export class StoreSchema<R extends UnknownRecord, P = unknown> {
349
349
 
350
350
  /**
351
351
  * @deprecated This is only here for legacy reasons, don't use it unless you have david's blessing!
352
+ * @internal
352
353
  */
353
354
  serializeEarliestVersion(): SerializedSchema {
354
355
  return {
@@ -2,45 +2,6 @@ import { assert, objectMapEntries } from '@tldraw/utils'
2
2
  import { UnknownRecord } from './BaseRecord'
3
3
  import { SerializedStore } from './Store'
4
4
 
5
- let didWarn = false
6
-
7
- /**
8
- * @public
9
- * @deprecated use `createShapePropsMigrationSequence` instead. See [the docs](https://tldraw.dev/docs/persistence#Updating-legacy-shape-migrations-defineMigrations) for how to migrate.
10
- */
11
- export function defineMigrations(opts: {
12
- firstVersion?: number
13
- currentVersion?: number
14
- migrators?: Record<number, LegacyMigration>
15
- subTypeKey?: string
16
- subTypeMigrations?: Record<string, LegacyBaseMigrationsInfo>
17
- }): LegacyMigrations {
18
- const { currentVersion, firstVersion, migrators = {}, subTypeKey, subTypeMigrations } = opts
19
- if (!didWarn) {
20
- console.warn(
21
- `The 'defineMigrations' function is deprecated and will be removed in a future release. Use the new migrations API instead. See the migration guide for more info: https://tldraw.dev/docs/persistence#Updating-legacy-shape-migrations-defineMigrations`
22
- )
23
- didWarn = true
24
- }
25
-
26
- // Some basic guards against impossible version combinations, some of which will be caught by TypeScript
27
- if (typeof currentVersion === 'number' && typeof firstVersion === 'number') {
28
- if ((currentVersion as number) === (firstVersion as number)) {
29
- throw Error(`Current version is equal to initial version.`)
30
- } else if (currentVersion < firstVersion) {
31
- throw Error(`Current version is lower than initial version.`)
32
- }
33
- }
34
-
35
- return {
36
- firstVersion: (firstVersion as number) ?? 0, // defaults
37
- currentVersion: (currentVersion as number) ?? 0, // defaults
38
- migrators,
39
- subTypeKey,
40
- subTypeMigrations,
41
- }
42
- }
43
-
44
5
  function squashDependsOn(sequence: Array<Migration | StandaloneDependsOn>): Migration[] {
45
6
  const result: Migration[] = []
46
7
  for (let i = sequence.length - 1; i >= 0; i--) {
@@ -199,38 +160,126 @@ export interface MigrationSequence {
199
160
  sequence: Migration[]
200
161
  }
201
162
 
163
+ /**
164
+ * Sorts migrations using a distance-minimizing topological sort.
165
+ *
166
+ * This function respects two types of dependencies:
167
+ * 1. Implicit sequence dependencies (foo/1 must come before foo/2)
168
+ * 2. Explicit dependencies via `dependsOn` property
169
+ *
170
+ * The algorithm minimizes the total distance between migrations and their explicit
171
+ * dependencies in the final ordering, while maintaining topological correctness.
172
+ * This means when migration A depends on migration B, A will be scheduled as close
173
+ * as possible to B (while respecting all constraints).
174
+ *
175
+ * Implementation uses Kahn's algorithm with priority scoring:
176
+ * - Builds dependency graph and calculates in-degrees
177
+ * - Uses priority queue that prioritizes migrations which unblock explicit dependencies
178
+ * - Processes migrations in urgency order while maintaining topological constraints
179
+ * - Detects cycles by ensuring all migrations are processed
180
+ *
181
+ * @param migrations - Array of migrations to sort
182
+ * @returns Sorted array of migrations in execution order
183
+ */
202
184
  export function sortMigrations(migrations: Migration[]): Migration[] {
203
- // we do a topological sort using dependsOn and implicit dependencies between migrations in the same sequence
204
- const byId = new Map(migrations.map((m) => [m.id, m]))
205
- const isProcessing = new Set<MigrationId>()
185
+ if (migrations.length === 0) return []
206
186
 
207
- const result: Migration[] = []
187
+ // Build dependency graph and calculate in-degrees
188
+ const byId = new Map(migrations.map((m) => [m.id, m]))
189
+ const dependents = new Map<MigrationId, Set<MigrationId>>() // who depends on this
190
+ const inDegree = new Map<MigrationId, number>()
191
+ const explicitDeps = new Map<MigrationId, Set<MigrationId>>() // explicit dependsOn relationships
208
192
 
209
- function process(m: Migration) {
210
- assert(!isProcessing.has(m.id), `Circular dependency in migrations: ${m.id}`)
211
- isProcessing.add(m.id)
193
+ // Initialize
194
+ for (const m of migrations) {
195
+ inDegree.set(m.id, 0)
196
+ dependents.set(m.id, new Set())
197
+ explicitDeps.set(m.id, new Set())
198
+ }
212
199
 
200
+ // Add implicit sequence dependencies and explicit dependencies
201
+ for (const m of migrations) {
213
202
  const { version, sequenceId } = parseMigrationId(m.id)
214
- const parent = byId.get(`${sequenceId}/${version - 1}`)
215
- if (parent) {
216
- process(parent)
203
+
204
+ // Implicit dependency on previous in sequence
205
+ const prevId = `${sequenceId}/${version - 1}` as MigrationId
206
+ if (byId.has(prevId)) {
207
+ dependents.get(prevId)!.add(m.id)
208
+ inDegree.set(m.id, inDegree.get(m.id)! + 1)
217
209
  }
218
210
 
211
+ // Explicit dependencies
219
212
  if (m.dependsOn) {
220
- for (const dep of m.dependsOn) {
221
- const depMigration = byId.get(dep)
222
- if (depMigration) {
223
- process(depMigration)
213
+ for (const depId of m.dependsOn) {
214
+ if (byId.has(depId)) {
215
+ dependents.get(depId)!.add(m.id)
216
+ explicitDeps.get(m.id)!.add(depId)
217
+ inDegree.set(m.id, inDegree.get(m.id)! + 1)
218
+ }
219
+ }
220
+ }
221
+ }
222
+
223
+ // Priority queue: migrations ready to process (in-degree 0)
224
+ const ready = migrations.filter((m) => inDegree.get(m.id) === 0)
225
+ const result: Migration[] = []
226
+ const processed = new Set<MigrationId>()
227
+
228
+ while (ready.length > 0) {
229
+ // Calculate urgency scores for ready migrations and pick the best one
230
+ let bestCandidate: Migration | undefined
231
+ let bestCandidateScore = -Infinity
232
+
233
+ for (const m of ready) {
234
+ let urgencyScore = 0
235
+
236
+ for (const depId of dependents.get(m.id) || []) {
237
+ if (!processed.has(depId)) {
238
+ // Priority 1: Count all unprocessed dependents (to break ties)
239
+ urgencyScore += 1
240
+
241
+ // Priority 2: If this migration is explicitly depended on by others, boost priority
242
+ if (explicitDeps.get(depId)!.has(m.id)) {
243
+ urgencyScore += 100
244
+ }
224
245
  }
225
246
  }
247
+
248
+ if (
249
+ urgencyScore > bestCandidateScore ||
250
+ // Tiebreaker: prefer lower sequence/version
251
+ (urgencyScore === bestCandidateScore && m.id.localeCompare(bestCandidate?.id ?? '') < 0)
252
+ ) {
253
+ bestCandidate = m
254
+ bestCandidateScore = urgencyScore
255
+ }
226
256
  }
227
257
 
228
- byId.delete(m.id)
229
- result.push(m)
258
+ const nextMigration = bestCandidate!
259
+ ready.splice(ready.indexOf(nextMigration), 1)
260
+
261
+ // Cycle detection - if we have processed everything and still have items left, there's a cycle
262
+ // This is handled by Kahn's algorithm naturally - if we finish with items unprocessed, there's a cycle
263
+
264
+ // Process this migration
265
+ result.push(nextMigration)
266
+ processed.add(nextMigration.id)
267
+
268
+ // Update in-degrees and add newly ready migrations
269
+ for (const depId of dependents.get(nextMigration.id) || []) {
270
+ if (!processed.has(depId)) {
271
+ inDegree.set(depId, inDegree.get(depId)! - 1)
272
+ if (inDegree.get(depId) === 0) {
273
+ ready.push(byId.get(depId)!)
274
+ }
275
+ }
276
+ }
230
277
  }
231
278
 
232
- for (const m of byId.values()) {
233
- process(m)
279
+ // Check for cycles - if we didn't process all migrations, there's a cycle
280
+ if (result.length !== migrations.length) {
281
+ const unprocessed = migrations.filter((m) => !processed.has(m.id))
282
+ assert(false, `Circular dependency in migrations: ${unprocessed[0].id}`)
234
283
  }
235
284
 
236
285
  return result
@@ -31,6 +31,38 @@ describe(sortMigrations, () => {
31
31
  ).toEqual(['bar/1', 'bar/2', 'foo/1', 'foo/2'])
32
32
  })
33
33
 
34
+ it('should minimize distance between dependencies and dependents', () => {
35
+ // bar/3 depends on foo/1 - should process bar sequence immediately after foo/1
36
+ expect(
37
+ sort([m('foo/2'), m('bar/3', { dependsOn: ['foo/1'] }), m('foo/1'), m('bar/1'), m('bar/2')])
38
+ ).toEqual(['foo/1', 'bar/1', 'bar/2', 'bar/3', 'foo/2'])
39
+ })
40
+
41
+ it('should minimize total distance for multiple explicit dependencies', () => {
42
+ // Both bar/2 and baz/1 depend on foo/1 - minimize total distance
43
+ expect(
44
+ sort([
45
+ m('foo/2'),
46
+ m('bar/2', { dependsOn: ['foo/1'] }),
47
+ m('foo/1'),
48
+ m('bar/1'),
49
+ m('baz/1', { dependsOn: ['foo/1'] }),
50
+ ])
51
+ ).toEqual(['foo/1', 'bar/1', 'bar/2', 'baz/1', 'foo/2'])
52
+ })
53
+
54
+ it('should handle chain of explicit dependencies optimally', () => {
55
+ // foo/1 -> bar/1 -> baz/1 chain should be consecutive
56
+ expect(
57
+ sort([
58
+ m('foo/2'),
59
+ m('bar/1', { dependsOn: ['foo/1'] }),
60
+ m('foo/1'),
61
+ m('baz/1', { dependsOn: ['bar/1'] }),
62
+ ])
63
+ ).toEqual(['foo/1', 'bar/1', 'baz/1', 'foo/2'])
64
+ })
65
+
34
66
  it('should fail if a cycle is created', () => {
35
67
  expect(() => {
36
68
  sort([m('foo/1', { dependsOn: ['foo/1'] })])
@@ -1,232 +0,0 @@
1
- import { defineMigrations } from '../migrate'
2
-
3
- const Versions = {
4
- Initial: 0,
5
- January: 1,
6
- February: 2,
7
- March: 3,
8
- } as const
9
-
10
- describe('define migrations tests', () => {
11
- it('defines migrations', () => {
12
- expect(() => {
13
- // no versions
14
- // eslint-disable-next-line @typescript-eslint/no-deprecated
15
- defineMigrations({
16
- firstVersion: Versions.Initial,
17
- })
18
- }).not.toThrow()
19
-
20
- expect(() => {
21
- // no versions
22
- // eslint-disable-next-line @typescript-eslint/no-deprecated
23
- defineMigrations({
24
- firstVersion: Versions.February,
25
- })
26
- }).not.toThrow()
27
-
28
- expect(() => {
29
- // empty migrators
30
- // eslint-disable-next-line @typescript-eslint/no-deprecated
31
- defineMigrations({
32
- migrators: {},
33
- })
34
- }).not.toThrow()
35
-
36
- expect(() => {
37
- // no versions!
38
- // eslint-disable-next-line @typescript-eslint/no-deprecated
39
- defineMigrations({
40
- migrators: {
41
- [Versions.February]: {
42
- up: (rec: any) => rec,
43
- down: (rec: any) => rec,
44
- },
45
- },
46
- })
47
- }).not.toThrow()
48
-
49
- expect(() => {
50
- // wrong current version!
51
- // eslint-disable-next-line @typescript-eslint/no-deprecated
52
- defineMigrations({
53
- currentVersion: Versions.January,
54
- migrators: {
55
- [Versions.February]: {
56
- up: (rec: any) => rec,
57
- down: (rec: any) => rec,
58
- },
59
- },
60
- })
61
- }).not.toThrow()
62
-
63
- expect(() => {
64
- // eslint-disable-next-line @typescript-eslint/no-deprecated
65
- defineMigrations({
66
- currentVersion: Versions.February,
67
- migrators: {
68
- // has a default zero version
69
- [Versions.January]: {
70
- up: (rec: any) => rec,
71
- down: (rec: any) => rec,
72
- },
73
- // has a current version
74
- [Versions.February]: {
75
- up: (rec: any) => rec,
76
- down: (rec: any) => rec,
77
- },
78
- },
79
- })
80
- }).not.toThrow()
81
-
82
- expect(() => {
83
- // can't provide only first version
84
- // eslint-disable-next-line @typescript-eslint/no-deprecated
85
- defineMigrations({
86
- firstVersion: Versions.January,
87
- migrators: {},
88
- })
89
- }).not.toThrow()
90
-
91
- expect(() => {
92
- // same version
93
- // eslint-disable-next-line @typescript-eslint/no-deprecated
94
- defineMigrations({
95
- firstVersion: Versions.Initial,
96
- currentVersion: Versions.Initial,
97
- migrators: {},
98
- })
99
- }).toThrow()
100
-
101
- expect(() => {
102
- // only first version
103
- // eslint-disable-next-line @typescript-eslint/no-deprecated
104
- defineMigrations({
105
- firstVersion: Versions.January,
106
- migrators: {},
107
- })
108
- }).not.toThrow()
109
-
110
- expect(() => {
111
- // missing only version
112
- // eslint-disable-next-line @typescript-eslint/no-deprecated
113
- defineMigrations({
114
- firstVersion: Versions.January,
115
- currentVersion: Versions.January,
116
- migrators: {},
117
- })
118
- }).toThrow()
119
-
120
- expect(() => {
121
- // only version, explicit start and current
122
- // eslint-disable-next-line @typescript-eslint/no-deprecated
123
- defineMigrations({
124
- firstVersion: Versions.January,
125
- currentVersion: Versions.January,
126
- migrators: {
127
- [Versions.January]: {
128
- up: (rec: any) => rec,
129
- down: (rec: any) => rec,
130
- },
131
- },
132
- })
133
- }).toThrow()
134
-
135
- expect(() => {
136
- // missing later versions
137
- // eslint-disable-next-line @typescript-eslint/no-deprecated
138
- defineMigrations({
139
- firstVersion: Versions.January,
140
- currentVersion: Versions.February,
141
- migrators: {},
142
- })
143
- }).not.toThrow()
144
-
145
- expect(() => {
146
- // missing later versions
147
- // eslint-disable-next-line @typescript-eslint/no-deprecated
148
- defineMigrations({
149
- firstVersion: Versions.Initial,
150
- currentVersion: Versions.February,
151
- migrators: {
152
- [Versions.January]: {
153
- up: (rec: any) => rec,
154
- down: (rec: any) => rec,
155
- },
156
- },
157
- })
158
- }).not.toThrow()
159
-
160
- expect(() => {
161
- // missing earlier versions
162
- // eslint-disable-next-line @typescript-eslint/no-deprecated
163
- defineMigrations({
164
- firstVersion: Versions.Initial,
165
- currentVersion: Versions.February,
166
- migrators: {
167
- [Versions.February]: {
168
- up: (rec: any) => rec,
169
- down: (rec: any) => rec,
170
- },
171
- },
172
- })
173
- }).not.toThrow()
174
-
175
- expect(() => {
176
- // got em all
177
- // eslint-disable-next-line @typescript-eslint/no-deprecated
178
- defineMigrations({
179
- firstVersion: Versions.Initial,
180
- currentVersion: Versions.February,
181
- migrators: {
182
- [Versions.January]: {
183
- up: (rec: any) => rec,
184
- down: (rec: any) => rec,
185
- },
186
- [Versions.February]: {
187
- up: (rec: any) => rec,
188
- down: (rec: any) => rec,
189
- },
190
- },
191
- })
192
- }).not.toThrow()
193
-
194
- expect(() => {
195
- // got em all starting later
196
- // eslint-disable-next-line @typescript-eslint/no-deprecated
197
- defineMigrations({
198
- firstVersion: Versions.January,
199
- currentVersion: Versions.March,
200
- migrators: {
201
- [Versions.February]: {
202
- up: (rec: any) => rec,
203
- down: (rec: any) => rec,
204
- },
205
- [Versions.March]: {
206
- up: (rec: any) => rec,
207
- down: (rec: any) => rec,
208
- },
209
- },
210
- })
211
- }).not.toThrow()
212
-
213
- expect(() => {
214
- // first migration should be first version + 1
215
- // eslint-disable-next-line @typescript-eslint/no-deprecated
216
- defineMigrations({
217
- firstVersion: Versions.February,
218
- currentVersion: Versions.March,
219
- migrators: {
220
- [Versions.February]: {
221
- up: (rec: any) => rec,
222
- down: (rec: any) => rec,
223
- },
224
- [Versions.March]: {
225
- up: (rec: any) => rec,
226
- down: (rec: any) => rec,
227
- },
228
- },
229
- })
230
- }).not.toThrow()
231
- })
232
- })