@tldraw/sync-core 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 (54) 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/ClientWebSocketAdapter.js +7 -1
  5. package/dist-cjs/lib/ClientWebSocketAdapter.js.map +2 -2
  6. package/dist-cjs/lib/TLSocketRoom.js +4 -1
  7. package/dist-cjs/lib/TLSocketRoom.js.map +2 -2
  8. package/dist-cjs/lib/TLSyncClient.js +2 -3
  9. package/dist-cjs/lib/TLSyncClient.js.map +2 -2
  10. package/dist-cjs/lib/TLSyncRoom.js +27 -3
  11. package/dist-cjs/lib/TLSyncRoom.js.map +2 -2
  12. package/dist-esm/index.mjs +1 -1
  13. package/dist-esm/lib/ClientWebSocketAdapter.mjs +7 -1
  14. package/dist-esm/lib/ClientWebSocketAdapter.mjs.map +2 -2
  15. package/dist-esm/lib/TLSocketRoom.mjs +4 -1
  16. package/dist-esm/lib/TLSocketRoom.mjs.map +2 -2
  17. package/dist-esm/lib/TLSyncClient.mjs +2 -4
  18. package/dist-esm/lib/TLSyncClient.mjs.map +2 -2
  19. package/dist-esm/lib/TLSyncRoom.mjs +27 -3
  20. package/dist-esm/lib/TLSyncRoom.mjs.map +2 -2
  21. package/package.json +12 -8
  22. package/src/lib/ClientWebSocketAdapter.test.ts +502 -505
  23. package/src/lib/ClientWebSocketAdapter.ts +7 -1
  24. package/src/lib/DurableObjectSqliteSyncWrapper.test.ts +102 -0
  25. package/src/lib/InMemorySyncStorage.test.ts +294 -0
  26. package/src/lib/MicrotaskNotifier.test.ts +79 -254
  27. package/src/lib/{NodeSqliteSyncWrapper.test.ts → NodeSqliteWrapper.test.ts} +29 -25
  28. package/src/lib/SQLiteSyncStorage.test.ts +239 -0
  29. package/src/lib/ServerSocketAdapter.test.ts +60 -199
  30. package/src/lib/TLSocketRoom.ts +6 -1
  31. package/src/lib/TLSyncClient.test.ts +1127 -846
  32. package/src/lib/TLSyncClient.ts +4 -4
  33. package/src/lib/TLSyncRoom.ts +34 -4
  34. package/src/lib/TLSyncStorage.test.ts +225 -0
  35. package/src/lib/chunk.test.ts +372 -0
  36. package/src/lib/diff.test.ts +885 -0
  37. package/src/lib/interval.test.ts +43 -0
  38. package/src/lib/protocol.test.ts +43 -0
  39. package/src/lib/recordDiff.test.ts +159 -0
  40. package/src/test/TLSocketRoom.test.ts +1109 -894
  41. package/src/test/TLSyncRoom.test.ts +1754 -870
  42. package/src/test/storageContractSuite.ts +618 -0
  43. package/src/test/upgradeDowngrade.test.ts +140 -40
  44. package/src/lib/NodeSqliteSyncWrapper.integration.test.ts +0 -270
  45. package/src/lib/RoomSession.test.ts +0 -101
  46. package/src/lib/computeTombstonePruning.test.ts +0 -352
  47. package/src/lib/server-types.test.ts +0 -44
  48. package/src/test/InMemorySyncStorage.test.ts +0 -1780
  49. package/src/test/SQLiteSyncStorage.test.ts +0 -1485
  50. package/src/test/chunk.test.ts +0 -385
  51. package/src/test/customMessages.test.ts +0 -36
  52. package/src/test/diff.test.ts +0 -784
  53. package/src/test/presenceMode.test.ts +0 -149
  54. 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
- })