@tldraw/sync-core 5.2.0-canary.4a316fdfb2bb → 5.2.0-canary.4e00299c3e28

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 (49) hide show
  1. package/DOCS.md +662 -0
  2. package/README.md +9 -1
  3. package/dist-cjs/index.js +1 -1
  4. package/dist-cjs/lib/TLSocketRoom.js +4 -1
  5. package/dist-cjs/lib/TLSocketRoom.js.map +2 -2
  6. package/dist-cjs/lib/TLSyncClient.js +2 -3
  7. package/dist-cjs/lib/TLSyncClient.js.map +2 -2
  8. package/dist-cjs/lib/TLSyncRoom.js +3 -2
  9. package/dist-cjs/lib/TLSyncRoom.js.map +2 -2
  10. package/dist-esm/index.mjs +1 -1
  11. package/dist-esm/lib/TLSocketRoom.mjs +4 -1
  12. package/dist-esm/lib/TLSocketRoom.mjs.map +2 -2
  13. package/dist-esm/lib/TLSyncClient.mjs +2 -4
  14. package/dist-esm/lib/TLSyncClient.mjs.map +2 -2
  15. package/dist-esm/lib/TLSyncRoom.mjs +3 -2
  16. package/dist-esm/lib/TLSyncRoom.mjs.map +2 -2
  17. package/package.json +9 -8
  18. package/src/lib/ClientWebSocketAdapter.test.ts +486 -508
  19. package/src/lib/DurableObjectSqliteSyncWrapper.test.ts +102 -0
  20. package/src/lib/InMemorySyncStorage.test.ts +294 -0
  21. package/src/lib/MicrotaskNotifier.test.ts +79 -254
  22. package/src/lib/{NodeSqliteSyncWrapper.test.ts → NodeSqliteWrapper.test.ts} +29 -25
  23. package/src/lib/SQLiteSyncStorage.test.ts +239 -0
  24. package/src/lib/ServerSocketAdapter.test.ts +60 -199
  25. package/src/lib/TLSocketRoom.ts +6 -1
  26. package/src/lib/TLSyncClient.test.ts +1127 -846
  27. package/src/lib/TLSyncClient.ts +4 -4
  28. package/src/lib/TLSyncRoom.ts +5 -2
  29. package/src/lib/TLSyncStorage.test.ts +225 -0
  30. package/src/lib/chunk.test.ts +372 -0
  31. package/src/lib/diff.test.ts +885 -0
  32. package/src/lib/interval.test.ts +43 -0
  33. package/src/lib/protocol.test.ts +43 -0
  34. package/src/lib/recordDiff.test.ts +159 -0
  35. package/src/test/TLSocketRoom.test.ts +1109 -894
  36. package/src/test/TLSyncRoom.test.ts +1735 -893
  37. package/src/test/storageContractSuite.ts +618 -0
  38. package/src/test/upgradeDowngrade.test.ts +137 -37
  39. package/src/lib/NodeSqliteSyncWrapper.integration.test.ts +0 -270
  40. package/src/lib/RoomSession.test.ts +0 -101
  41. package/src/lib/computeTombstonePruning.test.ts +0 -352
  42. package/src/lib/server-types.test.ts +0 -44
  43. package/src/test/InMemorySyncStorage.test.ts +0 -1780
  44. package/src/test/SQLiteSyncStorage.test.ts +0 -1485
  45. package/src/test/chunk.test.ts +0 -385
  46. package/src/test/customMessages.test.ts +0 -36
  47. package/src/test/diff.test.ts +0 -784
  48. package/src/test/presenceMode.test.ts +0 -149
  49. package/src/test/validation.test.ts +0 -186
@@ -1,149 +0,0 @@
1
- import { atom, computed, Signal } from '@tldraw/state'
2
- import { BaseRecord, createRecordType, RecordId, Store, StoreSchema } from '@tldraw/store'
3
- import { vi } from 'vitest'
4
- import { TLSyncClient } from '../lib/TLSyncClient'
5
- import { TestServer } from './TestServer'
6
- import { TestSocketPair } from './TestSocketPair'
7
-
8
- vi.mock('@tldraw/utils', async () => {
9
- const actual = await vi.importActual('@tldraw/utils')
10
- return {
11
- ...actual,
12
- fpsThrottle: vi.fn((fn) => fn),
13
- }
14
- })
15
-
16
- const disposables: Array<() => void> = []
17
-
18
- afterEach(() => {
19
- for (const dispose of disposables) {
20
- dispose()
21
- }
22
- disposables.length = 0
23
- })
24
-
25
- interface User extends BaseRecord<'user', RecordId<User>> {
26
- name: string
27
- age: number
28
- }
29
-
30
- interface Presence extends BaseRecord<'presence', RecordId<Presence>> {
31
- name: string
32
- age: number
33
- }
34
-
35
- const Presence = createRecordType<Presence>('presence', {
36
- scope: 'presence',
37
- validator: { validate: (value) => value as Presence },
38
- })
39
-
40
- const User = createRecordType<User>('user', {
41
- scope: 'document',
42
- validator: { validate: (value) => value as User },
43
- })
44
-
45
- type R = User | Presence
46
-
47
- const schema = StoreSchema.create<R>({ user: User, presence: Presence })
48
-
49
- class TestInstance {
50
- server: TestServer<R>
51
- socketPair: TestSocketPair<R>
52
- client: TLSyncClient<R>
53
-
54
- hasLoaded = false
55
-
56
- constructor(presenceSignal: Signal<Presence | null>, presenceMode?: 'solo' | 'full') {
57
- this.server = new TestServer(schema)
58
- this.socketPair = new TestSocketPair('test_presence_mode', this.server)
59
- this.socketPair.connect()
60
-
61
- this.client = new TLSyncClient<R>({
62
- store: new Store({ schema, props: {} }),
63
- socket: this.socketPair.clientSocket,
64
- onLoad: () => {
65
- this.hasLoaded = true
66
- },
67
- onSyncError: vi.fn((reason) => {
68
- throw new Error('onSyncError: ' + reason)
69
- }),
70
- presence: presenceSignal,
71
- presenceMode: presenceMode ? computed('', () => presenceMode) : undefined,
72
- })
73
-
74
- disposables.push(() => {
75
- this.client.close()
76
- })
77
- }
78
-
79
- flush() {
80
- this.server.flushDebouncingMessages()
81
-
82
- while (this.socketPair.getNeedsFlushing()) {
83
- this.socketPair.flushClientSentEvents()
84
- this.socketPair.flushServerSentEvents()
85
- }
86
- }
87
- }
88
-
89
- test('presence is pushed on change when mode is full', () => {
90
- const presence = Presence.create({ name: 'bob', age: 10 })
91
- const presenceSignal = atom('', presence)
92
-
93
- const t = new TestInstance(presenceSignal, 'full')
94
- t.socketPair.connect()
95
- t.flush()
96
-
97
- const session = t.server.room.sessions.values().next().value
98
- expect(session).toBeDefined()
99
- expect(session?.presenceId).toBeDefined()
100
- expect(t.server.room.presenceStore.get(session!.presenceId!)).toMatchObject({
101
- name: 'bob',
102
- age: 10,
103
- })
104
-
105
- presenceSignal.set(Presence.create({ name: 'bob', age: 11 }))
106
- t.flush()
107
- expect(t.server.room.presenceStore.get(session!.presenceId!)).toMatchObject({
108
- name: 'bob',
109
- age: 11,
110
- })
111
-
112
- presenceSignal.set(Presence.create({ name: 'bob', age: 12 }))
113
- t.flush()
114
- expect(t.server.room.presenceStore.get(session!.presenceId!)).toMatchObject({
115
- name: 'bob',
116
- age: 12,
117
- })
118
- })
119
-
120
- test('presence is only pushed once on connect when mode is solo', () => {
121
- const presence = Presence.create({ name: 'bob', age: 10 })
122
- const presenceSignal = atom('', presence)
123
-
124
- const t = new TestInstance(presenceSignal, 'solo')
125
- t.socketPair.connect()
126
- t.flush()
127
-
128
- const session = t.server.room.sessions.values().next().value
129
- expect(session).toBeDefined()
130
- expect(session?.presenceId).toBeDefined()
131
- expect(t.server.room.presenceStore.get(session!.presenceId!)).toMatchObject({
132
- name: 'bob',
133
- age: 10,
134
- })
135
-
136
- presenceSignal.set(Presence.create({ name: 'bob', age: 11 }))
137
- t.flush()
138
- expect(t.server.room.presenceStore.get(session!.presenceId!)).not.toMatchObject({
139
- name: 'bob',
140
- age: 11,
141
- })
142
-
143
- presenceSignal.set(Presence.create({ name: 'bob', age: 12 }))
144
- t.flush()
145
- expect(t.server.room.presenceStore.get(session!.presenceId!)).not.toMatchObject({
146
- name: 'bob',
147
- age: 12,
148
- })
149
- })
@@ -1,186 +0,0 @@
1
- import { computed } from '@tldraw/state'
2
- import { RecordId, Store, StoreSchema, UnknownRecord, createRecordType } from '@tldraw/store'
3
- import { vi } from 'vitest'
4
- import { RecordOpType } from '../lib/diff'
5
- import { TLSyncClient, TLSyncErrorCloseEventReason } from '../lib/TLSyncClient'
6
- import { TestServer } from './TestServer'
7
- import { TestSocketPair } from './TestSocketPair'
8
-
9
- // @ts-expect-error
10
- global.requestAnimationFrame = (cb: () => any) => {
11
- cb()
12
- }
13
-
14
- interface Book {
15
- typeName: 'book'
16
- id: RecordId<Book>
17
- title: string
18
- }
19
- const Book = createRecordType<Book>('book', {
20
- scope: 'document',
21
- validator: {
22
- validate: (record: unknown): Book => {
23
- if (typeof record !== 'object' || record === null) {
24
- throw new Error('Expected object')
25
- }
26
- if (!('title' in record)) {
27
- throw new Error('Expected title')
28
- }
29
- if (typeof record.title !== 'string') {
30
- throw new Error('Expected title to be a string')
31
- }
32
- return record as Book
33
- },
34
- },
35
- })
36
- const BookWithoutValidator = createRecordType<Book>('book', {
37
- scope: 'document',
38
- validator: { validate: (record) => record as Book },
39
- })
40
- type Presence = UnknownRecord & { typeName: 'presence' }
41
- const presenceType = createRecordType<Presence>('presence', {
42
- scope: 'presence',
43
- validator: { validate: (record) => record as Presence },
44
- })
45
-
46
- const schema = StoreSchema.create<Book | Presence>({ book: Book, presence: presenceType })
47
- const schemaWithoutValidator = StoreSchema.create<Book | Presence>({
48
- book: BookWithoutValidator,
49
- presence: presenceType,
50
- })
51
-
52
- const disposables: Array<() => void> = []
53
- let consoleSpy: ReturnType<typeof vi.spyOn>
54
- beforeEach(() => {
55
- consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
56
- })
57
- afterEach(() => {
58
- consoleSpy.mockRestore()
59
- for (const dispose of disposables) {
60
- dispose()
61
- }
62
- disposables.length = 0
63
- })
64
-
65
- async function makeTestInstance() {
66
- const server = new TestServer(schema)
67
- const socketPair = new TestSocketPair('test', server)
68
- socketPair.connect()
69
-
70
- const flush = async () => {
71
- await Promise.resolve()
72
- while (socketPair.getNeedsFlushing()) {
73
- socketPair.flushClientSentEvents()
74
- socketPair.flushServerSentEvents()
75
- }
76
- }
77
- let onSyncError = vi.fn()
78
- const client = await new Promise<TLSyncClient<Book | Presence>>((resolve, reject) => {
79
- onSyncError = vi.fn(reject)
80
- const client = new TLSyncClient({
81
- store: new Store<Book | Presence, unknown>({ schema: schemaWithoutValidator, props: {} }),
82
- socket: socketPair.clientSocket as any,
83
- onLoad: resolve,
84
- onSyncError,
85
- presence: computed('', () => null),
86
- })
87
- disposables.push(() => client.close())
88
- flush()
89
- })
90
-
91
- return {
92
- server,
93
- socketPair,
94
- client,
95
- flush,
96
- onSyncError,
97
- }
98
- }
99
-
100
- it('rejects invalid put operations that create a new document', async () => {
101
- const { client, flush, onSyncError, server } = await makeTestInstance()
102
-
103
- const prevServerDocs = server.storage.getSnapshot().documents
104
-
105
- client.store.put([
106
- {
107
- typeName: 'book',
108
- id: Book.createId('1'),
109
- // @ts-expect-error - deliberate invalid data
110
- title: 123 as string,
111
- },
112
- ])
113
- await flush()
114
-
115
- expect(onSyncError).toHaveBeenCalledTimes(1)
116
- expect(onSyncError).toHaveBeenLastCalledWith(TLSyncErrorCloseEventReason.INVALID_RECORD)
117
- expect(server.storage.getSnapshot().documents).toStrictEqual(prevServerDocs)
118
- })
119
-
120
- it('rejects invalid put operations that replace an existing document', async () => {
121
- const { client, flush, onSyncError, server } = await makeTestInstance()
122
-
123
- let prevServerDocs = server.storage.getSnapshot().documents
124
- const book: Book = { typeName: 'book', id: Book.createId('1'), title: 'Annihilation' }
125
- client.store.put([book])
126
- await flush()
127
-
128
- expect(onSyncError).toHaveBeenCalledTimes(0)
129
- expect(server.storage.getSnapshot().documents).not.toStrictEqual(prevServerDocs)
130
- prevServerDocs = server.storage.getSnapshot().documents
131
-
132
- client.socket.sendMessage({
133
- type: 'push',
134
- // @ts-expect-error clientClock is private
135
- clientClock: client.clientClock++,
136
- diff: {
137
- [book.id]: [
138
- RecordOpType.Put,
139
- {
140
- ...book,
141
- // @ts-expect-error - deliberate invalid data
142
- title: 123 as string,
143
- },
144
- ],
145
- },
146
- })
147
- await flush()
148
-
149
- expect(onSyncError).toHaveBeenCalledTimes(1)
150
- expect(onSyncError).toHaveBeenLastCalledWith(TLSyncErrorCloseEventReason.INVALID_RECORD)
151
- expect(server.storage.getSnapshot().documents).toStrictEqual(prevServerDocs)
152
- })
153
-
154
- it('rejects invalid update operations', async () => {
155
- const { client, flush, onSyncError, server } = await makeTestInstance()
156
-
157
- let prevServerDocs = server.storage.getSnapshot().documents
158
-
159
- // create the book
160
- client.store.put([
161
- {
162
- typeName: 'book',
163
- id: Book.createId('1'),
164
- title: 'The silence of the girls',
165
- },
166
- ])
167
- await flush()
168
-
169
- expect(onSyncError).toHaveBeenCalledTimes(0)
170
- expect(server.storage.getSnapshot().documents).not.toStrictEqual(prevServerDocs)
171
- prevServerDocs = server.storage.getSnapshot().documents
172
-
173
- // update the title to be wrong
174
- client.store.put([
175
- {
176
- typeName: 'book',
177
- id: Book.createId('1'),
178
- // @ts-expect-error - deliberate invalid data
179
- title: 123 as string,
180
- },
181
- ])
182
- await flush()
183
- expect(onSyncError).toHaveBeenCalledTimes(1)
184
- expect(onSyncError).toHaveBeenLastCalledWith(TLSyncErrorCloseEventReason.INVALID_RECORD)
185
- expect(server.storage.getSnapshot().documents).toStrictEqual(prevServerDocs)
186
- })