@tldraw/sync-core 5.2.0-next.79b13319d317 → 5.2.0-next.7b677d60c152

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