@tldraw/sync-core 5.2.0-canary.fe03bcdddf34 → 5.2.0-canary.fff413eea248
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/DOCS.md +662 -0
- package/README.md +9 -1
- package/dist-cjs/index.js +1 -1
- package/dist-cjs/lib/TLSocketRoom.js +4 -1
- package/dist-cjs/lib/TLSocketRoom.js.map +2 -2
- package/dist-cjs/lib/TLSyncClient.js +2 -3
- package/dist-cjs/lib/TLSyncClient.js.map +2 -2
- package/dist-cjs/lib/TLSyncRoom.js +3 -2
- package/dist-cjs/lib/TLSyncRoom.js.map +2 -2
- package/dist-esm/index.mjs +1 -1
- package/dist-esm/lib/TLSocketRoom.mjs +4 -1
- package/dist-esm/lib/TLSocketRoom.mjs.map +2 -2
- package/dist-esm/lib/TLSyncClient.mjs +2 -4
- package/dist-esm/lib/TLSyncClient.mjs.map +2 -2
- package/dist-esm/lib/TLSyncRoom.mjs +3 -2
- package/dist-esm/lib/TLSyncRoom.mjs.map +2 -2
- package/package.json +9 -8
- package/src/lib/ClientWebSocketAdapter.test.ts +486 -508
- package/src/lib/DurableObjectSqliteSyncWrapper.test.ts +102 -0
- package/src/lib/InMemorySyncStorage.test.ts +294 -0
- package/src/lib/MicrotaskNotifier.test.ts +79 -254
- package/src/lib/{NodeSqliteSyncWrapper.test.ts → NodeSqliteWrapper.test.ts} +29 -25
- package/src/lib/SQLiteSyncStorage.test.ts +239 -0
- package/src/lib/ServerSocketAdapter.test.ts +60 -199
- package/src/lib/TLSocketRoom.ts +6 -1
- package/src/lib/TLSyncClient.test.ts +1127 -846
- package/src/lib/TLSyncClient.ts +4 -4
- package/src/lib/TLSyncRoom.ts +5 -2
- package/src/lib/TLSyncStorage.test.ts +225 -0
- package/src/lib/chunk.test.ts +372 -0
- package/src/lib/diff.test.ts +885 -0
- package/src/lib/interval.test.ts +43 -0
- package/src/lib/protocol.test.ts +43 -0
- package/src/lib/recordDiff.test.ts +159 -0
- package/src/test/TLSocketRoom.test.ts +1109 -894
- package/src/test/TLSyncRoom.test.ts +1735 -893
- package/src/test/storageContractSuite.ts +618 -0
- package/src/test/upgradeDowngrade.test.ts +137 -37
- package/src/lib/NodeSqliteSyncWrapper.integration.test.ts +0 -270
- package/src/lib/RoomSession.test.ts +0 -101
- package/src/lib/computeTombstonePruning.test.ts +0 -352
- package/src/lib/server-types.test.ts +0 -44
- package/src/test/InMemorySyncStorage.test.ts +0 -1780
- package/src/test/SQLiteSyncStorage.test.ts +0 -1485
- package/src/test/chunk.test.ts +0 -385
- package/src/test/customMessages.test.ts +0 -36
- package/src/test/diff.test.ts +0 -784
- package/src/test/presenceMode.test.ts +0 -149
- 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
|
-
})
|