@tldraw/store 5.2.0-next.e2b8d10bf10e → 5.2.0

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 (64) hide show
  1. package/DOCS.md +790 -0
  2. package/README.md +9 -1
  3. package/dist-cjs/index.js +1 -1
  4. package/dist-cjs/lib/ImmutableMap.js +0 -13
  5. package/dist-cjs/lib/ImmutableMap.js.map +2 -2
  6. package/dist-cjs/lib/RecordType.js +1 -1
  7. package/dist-cjs/lib/RecordType.js.map +2 -2
  8. package/dist-cjs/lib/RecordsDiff.js +26 -14
  9. package/dist-cjs/lib/RecordsDiff.js.map +2 -2
  10. package/dist-cjs/lib/Store.js +6 -5
  11. package/dist-cjs/lib/Store.js.map +2 -2
  12. package/dist-cjs/lib/StoreSchema.js +1 -1
  13. package/dist-cjs/lib/StoreSchema.js.map +2 -2
  14. package/dist-cjs/lib/executeQuery.js +1 -1
  15. package/dist-cjs/lib/executeQuery.js.map +2 -2
  16. package/dist-esm/index.mjs +1 -1
  17. package/dist-esm/lib/ImmutableMap.mjs +0 -13
  18. package/dist-esm/lib/ImmutableMap.mjs.map +2 -2
  19. package/dist-esm/lib/RecordType.mjs +1 -1
  20. package/dist-esm/lib/RecordType.mjs.map +2 -2
  21. package/dist-esm/lib/RecordsDiff.mjs +26 -14
  22. package/dist-esm/lib/RecordsDiff.mjs.map +2 -2
  23. package/dist-esm/lib/Store.mjs +7 -6
  24. package/dist-esm/lib/Store.mjs.map +2 -2
  25. package/dist-esm/lib/StoreSchema.mjs +1 -1
  26. package/dist-esm/lib/StoreSchema.mjs.map +2 -2
  27. package/dist-esm/lib/executeQuery.mjs +1 -1
  28. package/dist-esm/lib/executeQuery.mjs.map +2 -2
  29. package/package.json +9 -5
  30. package/src/lib/{test/AtomMap.test.ts → AtomMap.test.ts} +77 -111
  31. package/src/lib/AtomSet.test.ts +116 -0
  32. package/src/lib/BaseRecord.test.ts +10 -22
  33. package/src/lib/ImmutableMap.test.ts +114 -71
  34. package/src/lib/ImmutableMap.ts +1 -0
  35. package/src/lib/IncrementalSetConstructor.test.ts +76 -81
  36. package/src/lib/RecordType.test.ts +216 -0
  37. package/src/lib/RecordType.ts +1 -1
  38. package/src/lib/RecordsDiff.test.ts +112 -106
  39. package/src/lib/RecordsDiff.ts +43 -18
  40. package/src/lib/Store.test.ts +570 -630
  41. package/src/lib/Store.ts +9 -10
  42. package/src/lib/StoreListeners.test.ts +462 -0
  43. package/src/lib/StoreQueries.test.ts +586 -434
  44. package/src/lib/StoreSchema.test.ts +1012 -174
  45. package/src/lib/StoreSchema.ts +1 -1
  46. package/src/lib/StoreSideEffects.test.ts +546 -158
  47. package/src/lib/devFreeze.test.ts +94 -124
  48. package/src/lib/executeQuery.test.ts +77 -31
  49. package/src/lib/executeQuery.ts +3 -1
  50. package/src/lib/migrate.test.ts +273 -296
  51. package/src/lib/setUtils.test.ts +38 -79
  52. package/src/lib/test/createMigrations.test.ts +0 -75
  53. package/src/lib/test/dependsOn.test.ts +0 -166
  54. package/src/lib/test/getMigrationsSince.test.ts +0 -121
  55. package/src/lib/test/migrate.test.ts +0 -118
  56. package/src/lib/test/migratePersistedRecord.test.ts +0 -265
  57. package/src/lib/test/migrationCaching.test.ts +0 -209
  58. package/src/lib/test/recordStore.test.ts +0 -1567
  59. package/src/lib/test/recordStoreQueries.test.ts +0 -814
  60. package/src/lib/test/recordType.test.ts +0 -19
  61. package/src/lib/test/sortMigrations.test.ts +0 -83
  62. package/src/lib/test/upgradeSchema.test.ts +0 -80
  63. package/src/lib/test/validate.test.ts +0 -178
  64. package/src/lib/test/validateMigrations.test.ts +0 -165
@@ -1,19 +0,0 @@
1
- import { testSchemaV1 } from './testSchema.v1'
2
-
3
- describe('parseId', () => {
4
- it('should return the part after the colon', () => {
5
- expect(testSchemaV1.types.user.parseId('user:123' as any)).toBe('123')
6
- expect(testSchemaV1.types.shape.parseId('shape:xyz' as any)).toBe('xyz')
7
- })
8
- it('should throw if the typename does not match', () => {
9
- expect(() => testSchemaV1.types.user.parseId('shape:123' as any)).toThrow()
10
- expect(() => testSchemaV1.types.shape.parseId('user:xyz' as any)).toThrow()
11
- })
12
- })
13
-
14
- describe('createId', () => {
15
- it('should prepend the typename and a colon', () => {
16
- expect(testSchemaV1.types.user.createId('123')).toBe('user:123')
17
- expect(testSchemaV1.types.shape.createId('xyz')).toBe('shape:xyz')
18
- })
19
- })
@@ -1,83 +0,0 @@
1
- import { Migration, MigrationId, sortMigrations } from '../migrate'
2
-
3
- describe(sortMigrations, () => {
4
- const m = (id: MigrationId, others?: { dependsOn?: MigrationId[] }): Migration => ({
5
- ...others,
6
- id,
7
- scope: 'record',
8
- up() {
9
- // noop
10
- },
11
- })
12
- const sort = (migrations: Migration[]) => {
13
- return sortMigrations(migrations).map((m) => m.id)
14
- }
15
- it('should sort migrations based on version number', () => {
16
- expect(sort([m('foo/2'), m('foo/1')])).toEqual(['foo/1', 'foo/2'])
17
- expect(sort([m('foo/1'), m('foo/2')])).toEqual(['foo/1', 'foo/2'])
18
- })
19
- it('should sort multiple migration sequences based on version number', () => {
20
- const result = sort([m('foo/2'), m('bar/2'), m('foo/1'), m('bar/1')])
21
- expect(result.filter((id) => id.startsWith('foo/'))).toEqual(['foo/1', 'foo/2'])
22
- expect(result.filter((id) => id.startsWith('bar/'))).toEqual(['bar/1', 'bar/2'])
23
- })
24
- it('should use dependsOn to resolve inter-sequence dependencies', () => {
25
- expect(
26
- sort([m('foo/2'), m('bar/2'), m('foo/1'), m('bar/1', { dependsOn: ['foo/2'] })])
27
- ).toEqual(['foo/1', 'foo/2', 'bar/1', 'bar/2'])
28
-
29
- expect(
30
- sort([m('foo/2'), m('bar/2'), m('foo/1', { dependsOn: ['bar/2'] }), m('bar/1')])
31
- ).toEqual(['bar/1', 'bar/2', 'foo/1', 'foo/2'])
32
- })
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
-
66
- it('should fail if a cycle is created', () => {
67
- expect(() => {
68
- sort([m('foo/1', { dependsOn: ['foo/1'] })])
69
- }).toThrowErrorMatchingInlineSnapshot(`[Error: Circular dependency in migrations: foo/1]`)
70
-
71
- expect(() => {
72
- sort([m('foo/1', { dependsOn: ['foo/2'] }), m('foo/2')])
73
- }).toThrowErrorMatchingInlineSnapshot(`[Error: Circular dependency in migrations: foo/1]`)
74
-
75
- expect(() => {
76
- sort([m('foo/1', { dependsOn: ['bar/1'] }), m('bar/1', { dependsOn: ['foo/1'] })])
77
- }).toThrowErrorMatchingInlineSnapshot(`[Error: Circular dependency in migrations: foo/1]`)
78
-
79
- expect(() => {
80
- sort([m('bar/1', { dependsOn: ['foo/1'] }), m('foo/1', { dependsOn: ['bar/1'] })])
81
- }).toThrowErrorMatchingInlineSnapshot(`[Error: Circular dependency in migrations: bar/1]`)
82
- })
83
- })
@@ -1,80 +0,0 @@
1
- import { SerializedSchemaV1, upgradeSchema } from '../StoreSchema'
2
-
3
- describe('upgradeSchema', () => {
4
- it('should upgrade a schema from v1 to v2, assuming its working with tldraw data', () => {
5
- const v1: SerializedSchemaV1 = {
6
- schemaVersion: 1,
7
- storeVersion: 4,
8
- recordVersions: {
9
- asset: {
10
- version: 1,
11
- subTypeKey: 'type',
12
- subTypeVersions: { image: 2, video: 2, bookmark: 0 },
13
- },
14
- camera: { version: 1 },
15
- document: { version: 2 },
16
- instance: { version: 22 },
17
- instance_page_state: { version: 5 },
18
- page: { version: 1 },
19
- shape: {
20
- version: 3,
21
- subTypeKey: 'type',
22
- subTypeVersions: {
23
- group: 0,
24
- text: 1,
25
- bookmark: 1,
26
- draw: 1,
27
- geo: 7,
28
- note: 4,
29
- line: 1,
30
- frame: 0,
31
- arrow: 1,
32
- highlight: 0,
33
- embed: 4,
34
- image: 2,
35
- video: 1,
36
- },
37
- },
38
- instance_presence: { version: 5 },
39
- pointer: { version: 1 },
40
- },
41
- }
42
-
43
- expect(upgradeSchema(v1)).toMatchInlineSnapshot(`
44
- {
45
- "ok": true,
46
- "value": {
47
- "schemaVersion": 2,
48
- "sequences": {
49
- "com.tldraw.asset": 1,
50
- "com.tldraw.asset.bookmark": 0,
51
- "com.tldraw.asset.image": 2,
52
- "com.tldraw.asset.video": 2,
53
- "com.tldraw.camera": 1,
54
- "com.tldraw.document": 2,
55
- "com.tldraw.instance": 22,
56
- "com.tldraw.instance_page_state": 5,
57
- "com.tldraw.instance_presence": 5,
58
- "com.tldraw.page": 1,
59
- "com.tldraw.pointer": 1,
60
- "com.tldraw.shape": 3,
61
- "com.tldraw.shape.arrow": 1,
62
- "com.tldraw.shape.bookmark": 1,
63
- "com.tldraw.shape.draw": 1,
64
- "com.tldraw.shape.embed": 4,
65
- "com.tldraw.shape.frame": 0,
66
- "com.tldraw.shape.geo": 7,
67
- "com.tldraw.shape.group": 0,
68
- "com.tldraw.shape.highlight": 0,
69
- "com.tldraw.shape.image": 2,
70
- "com.tldraw.shape.line": 1,
71
- "com.tldraw.shape.note": 4,
72
- "com.tldraw.shape.text": 1,
73
- "com.tldraw.shape.video": 1,
74
- "com.tldraw.store": 4,
75
- },
76
- },
77
- }
78
- `)
79
- })
80
- })
@@ -1,178 +0,0 @@
1
- import { BaseRecord, IdOf, RecordId } from '../BaseRecord'
2
- import { createRecordType } from '../RecordType'
3
- import { SerializedStore, Store } from '../Store'
4
- import { StoreSchema } from '../StoreSchema'
5
-
6
- interface Book extends BaseRecord<'book', RecordId<Book>> {
7
- title: string
8
- author: IdOf<Author>
9
- numPages: number
10
- }
11
-
12
- const Book = createRecordType<Book>('book', {
13
- validator: {
14
- validate(value) {
15
- const book = value as Book
16
- if (!book.id.startsWith('book:')) throw Error()
17
- if (book.typeName !== 'book') throw Error()
18
- if (typeof book.title !== 'string') throw Error()
19
- if (!Number.isFinite(book.numPages)) throw Error()
20
- if (book.numPages < 0) throw Error()
21
- return book
22
- },
23
- },
24
- scope: 'document',
25
- })
26
-
27
- interface Author extends BaseRecord<'author', RecordId<Author>> {
28
- name: string
29
- isPseudonym: boolean
30
- }
31
-
32
- const Author = createRecordType<Author>('author', {
33
- validator: {
34
- validate(value) {
35
- const author = value as Author
36
- if (author.typeName !== 'author') throw Error()
37
- if (!author.id.startsWith('author:')) throw Error()
38
- if (typeof author.name !== 'string') throw Error()
39
- if (typeof author.isPseudonym !== 'boolean') throw Error()
40
- return author
41
- },
42
- },
43
- scope: 'document',
44
- }).withDefaultProperties(() => ({
45
- isPseudonym: false,
46
- }))
47
-
48
- const schema = StoreSchema.create<Book | Author>({
49
- book: Book,
50
- author: Author,
51
- })
52
-
53
- describe('Store with validation', () => {
54
- let store: Store<Book | Author>
55
-
56
- beforeEach(() => {
57
- store = new Store({ schema, props: {} })
58
- })
59
-
60
- it('Accepts valid records and rejects invalid records', () => {
61
- store.put([Author.create({ name: 'J.R.R Tolkein', id: Author.createId('tolkein') })])
62
-
63
- expect(store.query.records('author').get()).toEqual([
64
- { id: 'author:tolkein', typeName: 'author', name: 'J.R.R Tolkein', isPseudonym: false },
65
- ])
66
-
67
- expect(() => {
68
- store.put([
69
- {
70
- id: Book.createId('the-hobbit'),
71
- typeName: 'book',
72
- title: 'The Hobbit',
73
- numPages: -1, // <---- Invalid!
74
- author: Author.createId('tolkein'),
75
- },
76
- ])
77
- }).toThrow()
78
-
79
- expect(store.query.records('book').get()).toEqual([])
80
- })
81
- })
82
-
83
- describe('Validating initial data', () => {
84
- let snapshot: SerializedStore<Book | Author>
85
-
86
- beforeEach(() => {
87
- const authorId = Author.createId('tolkein')
88
- const authorRecord = Author.create({ name: 'J.R.R Tolkein', id: authorId })
89
- const bookId = Book.createId('the-hobbit')
90
- const bookRecord = Book.create({
91
- title: 'The Hobbit',
92
- numPages: 300,
93
- author: authorId,
94
- id: bookId,
95
- })
96
-
97
- snapshot = {
98
- [authorId]: authorRecord,
99
- [bookId]: bookRecord,
100
- }
101
- })
102
-
103
- it('Validates initial data', () => {
104
- expect(() => {
105
- new Store<Book | Author>({ schema, initialData: snapshot, props: {} })
106
- }).not.toThrow()
107
-
108
- expect(() => {
109
- // @ts-expect-error
110
- snapshot[0].name = 4
111
-
112
- new Store<Book | Author>({ schema, initialData: snapshot, props: {} })
113
- }).toThrow()
114
- })
115
- })
116
-
117
- describe('Create & update validations', () => {
118
- const authorId = Author.createId('tolkein')
119
- const bookId = Book.createId('the-hobbit')
120
- const initialAuthor = Author.create({ name: 'J.R.R Tolkein', id: authorId })
121
- const invalidBook = Book.create({
122
- // @ts-expect-error - deliberately invalid data
123
- title: 4,
124
- numPages: 300,
125
- author: authorId,
126
- id: bookId,
127
- })
128
- const validBook = Book.create({
129
- title: 'The Hobbit',
130
- numPages: 300,
131
- author: authorId,
132
- id: bookId,
133
- })
134
-
135
- it('Prevents creating a store with invalid records', () => {
136
- expect(
137
- () =>
138
- new Store<Book | Author>({
139
- schema,
140
- initialData: { [bookId]: invalidBook, [authorId]: initialAuthor },
141
- props: {},
142
- })
143
- ).toThrow()
144
- })
145
-
146
- it('Prevents updating invalid records to a store', () => {
147
- const store = new Store<Book | Author>({
148
- schema,
149
- initialData: { [bookId]: validBook, [authorId]: initialAuthor },
150
- props: {},
151
- })
152
-
153
- expect(() => {
154
- store.put([invalidBook])
155
- }).toThrow()
156
- })
157
-
158
- it('Prevents adding invalid records to a store', () => {
159
- const newAuthorId = Author.createId('shearing')
160
- const store = new Store<Book | Author>({
161
- schema,
162
- initialData: { [bookId]: validBook, [authorId]: initialAuthor },
163
- props: {},
164
- })
165
-
166
- expect(() => {
167
- store.put([
168
- Author.create({
169
- // @ts-expect-error - deliberately invalid data
170
- name: 5,
171
- id: newAuthorId,
172
- }),
173
- ])
174
- }).toThrow()
175
-
176
- expect(store.get(newAuthorId)).toBeUndefined()
177
- })
178
- })
@@ -1,165 +0,0 @@
1
- import { validateMigrations } from '../migrate'
2
-
3
- describe(validateMigrations, () => {
4
- it('should throw if a migration id is invalid', () => {
5
- expect(() =>
6
- validateMigrations({
7
- retroactive: false,
8
- sequence: [
9
- {
10
- id: 'foo.1' as any,
11
- scope: 'record',
12
- up() {
13
- // noop
14
- },
15
- },
16
- ],
17
-
18
- sequenceId: 'foo',
19
- })
20
- ).toThrowErrorMatchingInlineSnapshot(
21
- `[Error: Every migration in sequence 'foo' must have an id starting with 'foo/'. Got invalid id: 'foo.1']`
22
- )
23
-
24
- expect(() =>
25
- validateMigrations({
26
- retroactive: false,
27
- sequence: [
28
- {
29
- id: 'foo/one' as any,
30
- scope: 'record',
31
- up() {
32
- // noop
33
- },
34
- },
35
- ],
36
-
37
- sequenceId: 'foo',
38
- })
39
- ).toThrowErrorMatchingInlineSnapshot(`[Error: Invalid migration id: 'foo/one']`)
40
-
41
- expect(() =>
42
- validateMigrations({
43
- retroactive: false,
44
- sequence: [
45
- {
46
- id: 'foo/1' as any,
47
- scope: 'record',
48
- up() {
49
- // noop
50
- },
51
- },
52
- {
53
- id: 'foo.2' as any,
54
- scope: 'record',
55
- up() {
56
- // noop
57
- },
58
- },
59
- ],
60
-
61
- sequenceId: 'foo',
62
- })
63
- ).toThrowErrorMatchingInlineSnapshot(
64
- `[Error: Every migration in sequence 'foo' must have an id starting with 'foo/'. Got invalid id: 'foo.2']`
65
- )
66
-
67
- expect(() =>
68
- validateMigrations({
69
- retroactive: false,
70
- sequence: [
71
- {
72
- id: 'foo/1' as any,
73
- scope: 'record',
74
- up() {
75
- // noop
76
- },
77
- },
78
- {
79
- id: 'foo/two' as any,
80
- scope: 'record',
81
- up() {
82
- // noop
83
- },
84
- },
85
- ],
86
-
87
- sequenceId: 'foo',
88
- })
89
- ).toThrowErrorMatchingInlineSnapshot(`[Error: Invalid migration id: 'foo/two']`)
90
- })
91
-
92
- it('should throw if the sequenceId is invalid', () => {
93
- expect(() =>
94
- validateMigrations({
95
- retroactive: false,
96
- sequence: [],
97
- sequenceId: 'foo/bar',
98
- })
99
- ).toThrowErrorMatchingInlineSnapshot(`[Error: sequenceId cannot contain a '/', got foo/bar]`)
100
-
101
- expect(() =>
102
- validateMigrations({
103
- retroactive: false,
104
- sequence: [],
105
- sequenceId: '',
106
- })
107
- ).toThrowErrorMatchingInlineSnapshot(`[Error: sequenceId must be a non-empty string]`)
108
- })
109
-
110
- it('should throw if the version numbers do not start at 1', () => {
111
- expect(() =>
112
- validateMigrations({
113
- retroactive: false,
114
- sequence: [
115
- {
116
- id: 'foo/2',
117
- scope: 'record',
118
- up() {
119
- // noop
120
- },
121
- },
122
- ],
123
-
124
- sequenceId: 'foo',
125
- })
126
- ).toThrowErrorMatchingInlineSnapshot(
127
- `[Error: Expected the first migrationId to be 'foo/1' but got 'foo/2']`
128
- )
129
- })
130
-
131
- it('should throw if the version numbers do not increase monotonically', () => {
132
- expect(() =>
133
- validateMigrations({
134
- retroactive: false,
135
- sequence: [
136
- {
137
- id: 'foo/1',
138
- scope: 'record',
139
- up() {
140
- // noop
141
- },
142
- },
143
- {
144
- id: 'foo/2',
145
- scope: 'record',
146
- up() {
147
- // noop
148
- },
149
- },
150
- {
151
- id: 'foo/4',
152
- scope: 'record',
153
- up() {
154
- // noop
155
- },
156
- },
157
- ],
158
-
159
- sequenceId: 'foo',
160
- })
161
- ).toThrowErrorMatchingInlineSnapshot(
162
- `[Error: Migration id numbers must increase in increments of 1, expected foo/3 but got 'foo/4']`
163
- )
164
- })
165
- })