@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.
- 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
package/src/test/chunk.test.ts
DELETED
|
@@ -1,385 +0,0 @@
|
|
|
1
|
-
import { assert } from 'tldraw'
|
|
2
|
-
import { describe, expect, it } from 'vitest'
|
|
3
|
-
import { JsonChunkAssembler, chunk } from '../lib/chunk'
|
|
4
|
-
|
|
5
|
-
describe('chunk', () => {
|
|
6
|
-
it('chunks a string', () => {
|
|
7
|
-
expect(chunk('hello there my good world', 5)).toMatchInlineSnapshot(`
|
|
8
|
-
[
|
|
9
|
-
"8_h",
|
|
10
|
-
"7_ell",
|
|
11
|
-
"6_o t",
|
|
12
|
-
"5_her",
|
|
13
|
-
"4_e m",
|
|
14
|
-
"3_y g",
|
|
15
|
-
"2_ood",
|
|
16
|
-
"1_ wo",
|
|
17
|
-
"0_rld",
|
|
18
|
-
]
|
|
19
|
-
`)
|
|
20
|
-
|
|
21
|
-
expect(chunk('hello there my good world', 10)).toMatchInlineSnapshot(`
|
|
22
|
-
[
|
|
23
|
-
"3_h",
|
|
24
|
-
"2_ello the",
|
|
25
|
-
"1_re my go",
|
|
26
|
-
"0_od world",
|
|
27
|
-
]
|
|
28
|
-
`)
|
|
29
|
-
})
|
|
30
|
-
|
|
31
|
-
it('does not chunk the string if it is small enough', () => {
|
|
32
|
-
const chunks = chunk('hello', 100)
|
|
33
|
-
expect(chunks).toMatchInlineSnapshot(`
|
|
34
|
-
[
|
|
35
|
-
"hello",
|
|
36
|
-
]
|
|
37
|
-
`)
|
|
38
|
-
})
|
|
39
|
-
|
|
40
|
-
it('makes sure the chunk length does not exceed the given message size', () => {
|
|
41
|
-
const chunks = chunk('dark and stormy tonight', 4)
|
|
42
|
-
expect(chunks).toMatchInlineSnapshot(`
|
|
43
|
-
[
|
|
44
|
-
"12_d",
|
|
45
|
-
"11_a",
|
|
46
|
-
"10_r",
|
|
47
|
-
"9_k ",
|
|
48
|
-
"8_an",
|
|
49
|
-
"7_d ",
|
|
50
|
-
"6_st",
|
|
51
|
-
"5_or",
|
|
52
|
-
"4_my",
|
|
53
|
-
"3_ t",
|
|
54
|
-
"2_on",
|
|
55
|
-
"1_ig",
|
|
56
|
-
"0_ht",
|
|
57
|
-
]
|
|
58
|
-
`)
|
|
59
|
-
})
|
|
60
|
-
|
|
61
|
-
it('does its best if the chunk size is too small', () => {
|
|
62
|
-
const chunks = chunk('once upon a time', 1)
|
|
63
|
-
expect(chunks).toMatchInlineSnapshot(`
|
|
64
|
-
[
|
|
65
|
-
"15_o",
|
|
66
|
-
"14_n",
|
|
67
|
-
"13_c",
|
|
68
|
-
"12_e",
|
|
69
|
-
"11_ ",
|
|
70
|
-
"10_u",
|
|
71
|
-
"9_p",
|
|
72
|
-
"8_o",
|
|
73
|
-
"7_n",
|
|
74
|
-
"6_ ",
|
|
75
|
-
"5_a",
|
|
76
|
-
"4_ ",
|
|
77
|
-
"3_t",
|
|
78
|
-
"2_i",
|
|
79
|
-
"1_m",
|
|
80
|
-
"0_e",
|
|
81
|
-
]
|
|
82
|
-
`)
|
|
83
|
-
})
|
|
84
|
-
})
|
|
85
|
-
|
|
86
|
-
const testObject = {} as any
|
|
87
|
-
for (let i = 0; i < 1000; i++) {
|
|
88
|
-
testObject['key_' + i] = 'value_' + i
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
describe('json unchunker', () => {
|
|
92
|
-
it.each([1, 5, 20, 200])('unchunks a json string split at %s bytes', (size) => {
|
|
93
|
-
const chunks = chunk(JSON.stringify(testObject), size)
|
|
94
|
-
|
|
95
|
-
const unchunker = new JsonChunkAssembler()
|
|
96
|
-
for (const chunk of chunks.slice(0, -1)) {
|
|
97
|
-
const result = unchunker.handleMessage(chunk)
|
|
98
|
-
expect(result).toBeNull()
|
|
99
|
-
}
|
|
100
|
-
expect(unchunker.handleMessage(chunks[chunks.length - 1])).toMatchObject({ data: testObject })
|
|
101
|
-
|
|
102
|
-
// and the next one should be fine
|
|
103
|
-
expect(unchunker.handleMessage('{"ok": true}')).toMatchObject({ data: { ok: true } })
|
|
104
|
-
})
|
|
105
|
-
|
|
106
|
-
it('returns an error if the json is whack', () => {
|
|
107
|
-
const chunks = chunk('{"hello": world"}', 5)
|
|
108
|
-
const unchunker = new JsonChunkAssembler()
|
|
109
|
-
for (const chunk of chunks.slice(0, -1)) {
|
|
110
|
-
const result = unchunker.handleMessage(chunk)
|
|
111
|
-
expect(result).toBeNull()
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
const node18Error = `Unexpected token w in JSON at position 10`
|
|
115
|
-
const node20Error = `Unexpected token 'w', "\\{"hello": world"}" is not valid JSON`
|
|
116
|
-
const res = unchunker.handleMessage(chunks[chunks.length - 1])
|
|
117
|
-
assert(res, 'expected a result')
|
|
118
|
-
assert('error' in res, 'expected an error')
|
|
119
|
-
expect(res.error?.message).toMatch(new RegExp(`${node18Error}|${node20Error}`))
|
|
120
|
-
|
|
121
|
-
// and the next one should be fine
|
|
122
|
-
expect(unchunker.handleMessage('{"ok": true}')).toMatchObject({ data: { ok: true } })
|
|
123
|
-
})
|
|
124
|
-
it('returns an error if one of the chunks was missing', () => {
|
|
125
|
-
const chunks = chunk('{"hello": world"}', 10)
|
|
126
|
-
|
|
127
|
-
const unchunker = new JsonChunkAssembler()
|
|
128
|
-
expect(unchunker.handleMessage(chunks[0])).toBeNull()
|
|
129
|
-
const res = unchunker.handleMessage(chunks[2])
|
|
130
|
-
assert(res, 'expected a result')
|
|
131
|
-
assert('error' in res, 'expected an error')
|
|
132
|
-
expect(res.error?.message).toMatchInlineSnapshot(`"Chunks received in wrong order"`)
|
|
133
|
-
|
|
134
|
-
// and the next one should be fine
|
|
135
|
-
expect(unchunker.handleMessage('{"ok": true}')).toMatchObject({ data: { ok: true } })
|
|
136
|
-
})
|
|
137
|
-
|
|
138
|
-
it('returns an error if the chunk stream ends abruptly', () => {
|
|
139
|
-
const chunks = chunk('{"hello": world"}', 10)
|
|
140
|
-
|
|
141
|
-
const unchunker = new JsonChunkAssembler()
|
|
142
|
-
expect(unchunker.handleMessage(chunks[0])).toBeNull()
|
|
143
|
-
expect(unchunker.handleMessage(chunks[1])).toBeNull()
|
|
144
|
-
|
|
145
|
-
const res = unchunker.handleMessage('{"hello": "world"}')
|
|
146
|
-
assert(res, 'expected a result')
|
|
147
|
-
assert('error' in res, 'expected an error')
|
|
148
|
-
|
|
149
|
-
expect(res?.error?.message).toMatchInlineSnapshot(`"Unexpected non-chunk message"`)
|
|
150
|
-
|
|
151
|
-
// and the next one should be fine
|
|
152
|
-
expect(unchunker.handleMessage('{"ok": true}')).toMatchObject({ data: { ok: true } })
|
|
153
|
-
})
|
|
154
|
-
|
|
155
|
-
it('returns an error if the chunk syntax is wrong', () => {
|
|
156
|
-
// it only likes json objects
|
|
157
|
-
const unchunker = new JsonChunkAssembler()
|
|
158
|
-
const res = unchunker.handleMessage('["yo"]')
|
|
159
|
-
assert(res, 'expected a result')
|
|
160
|
-
assert('error' in res, 'expected an error')
|
|
161
|
-
|
|
162
|
-
expect(res.error?.message).toMatchInlineSnapshot(`"Invalid chunk: "[\\"yo\\"]...""`)
|
|
163
|
-
|
|
164
|
-
// and the next one should be fine
|
|
165
|
-
expect(unchunker.handleMessage('{"ok": true}')).toMatchObject({ data: { ok: true } })
|
|
166
|
-
})
|
|
167
|
-
|
|
168
|
-
it('handles empty string', () => {
|
|
169
|
-
const unchunker = new JsonChunkAssembler()
|
|
170
|
-
const result = unchunker.handleMessage('{}')
|
|
171
|
-
expect(result).toMatchObject({ data: {}, stringified: '{}' })
|
|
172
|
-
})
|
|
173
|
-
|
|
174
|
-
it('handles complex nested JSON objects', () => {
|
|
175
|
-
const complexObject = {
|
|
176
|
-
array: [1, 2, { nested: true }],
|
|
177
|
-
null: null,
|
|
178
|
-
boolean: false,
|
|
179
|
-
number: 3.14,
|
|
180
|
-
string: 'hello world',
|
|
181
|
-
unicode: '🎨🗼️📐',
|
|
182
|
-
deep: {
|
|
183
|
-
nested: {
|
|
184
|
-
object: {
|
|
185
|
-
with: 'many levels',
|
|
186
|
-
},
|
|
187
|
-
},
|
|
188
|
-
},
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
const chunks = chunk(JSON.stringify(complexObject), 50)
|
|
192
|
-
const unchunker = new JsonChunkAssembler()
|
|
193
|
-
|
|
194
|
-
for (const chunk of chunks.slice(0, -1)) {
|
|
195
|
-
expect(unchunker.handleMessage(chunk)).toBeNull()
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
const result = unchunker.handleMessage(chunks[chunks.length - 1])
|
|
199
|
-
expect(result).toMatchObject({ data: complexObject })
|
|
200
|
-
})
|
|
201
|
-
|
|
202
|
-
it('handles state reset after error', () => {
|
|
203
|
-
const unchunker = new JsonChunkAssembler()
|
|
204
|
-
|
|
205
|
-
// Start a chunk sequence
|
|
206
|
-
expect(unchunker.handleMessage('1_hello')).toBeNull()
|
|
207
|
-
|
|
208
|
-
// Send malformed chunk to trigger error
|
|
209
|
-
const result = unchunker.handleMessage('invalid_chunk_format')
|
|
210
|
-
assert(result && 'error' in result, 'expected error result')
|
|
211
|
-
expect(result.error.message).toContain('Invalid chunk')
|
|
212
|
-
|
|
213
|
-
// Should be able to process normal messages again
|
|
214
|
-
expect(unchunker.handleMessage('{"test": true}')).toMatchObject({ data: { test: true } })
|
|
215
|
-
})
|
|
216
|
-
|
|
217
|
-
it('returns error for invalid chunk number format', () => {
|
|
218
|
-
const unchunker = new JsonChunkAssembler()
|
|
219
|
-
const result = unchunker.handleMessage('abc_invalid_number')
|
|
220
|
-
assert(result && 'error' in result, 'expected error result')
|
|
221
|
-
expect(result.error.message).toContain('Invalid chunk')
|
|
222
|
-
})
|
|
223
|
-
|
|
224
|
-
it('handles single chunk with number prefix correctly', () => {
|
|
225
|
-
const unchunker = new JsonChunkAssembler()
|
|
226
|
-
const result = unchunker.handleMessage('0_{"single": "chunk"}')
|
|
227
|
-
expect(result).toMatchObject({
|
|
228
|
-
data: { single: 'chunk' },
|
|
229
|
-
stringified: '{"single": "chunk"}',
|
|
230
|
-
})
|
|
231
|
-
})
|
|
232
|
-
|
|
233
|
-
it('handles chunks with empty data parts', () => {
|
|
234
|
-
const unchunker = new JsonChunkAssembler()
|
|
235
|
-
|
|
236
|
-
expect(unchunker.handleMessage('1_')).toBeNull() // empty first chunk
|
|
237
|
-
const result = unchunker.handleMessage('0_{"test": true}')
|
|
238
|
-
expect(result).toMatchObject({ data: { test: true } })
|
|
239
|
-
})
|
|
240
|
-
|
|
241
|
-
it('handles non-JSON string messages that are not chunks', () => {
|
|
242
|
-
const unchunker = new JsonChunkAssembler()
|
|
243
|
-
const result = unchunker.handleMessage('not_json_and_not_chunk')
|
|
244
|
-
assert(result && 'error' in result, 'expected error result')
|
|
245
|
-
expect(result.error.message).toContain('Invalid chunk')
|
|
246
|
-
})
|
|
247
|
-
|
|
248
|
-
it('handles chunk sequence interrupted by JSON message', () => {
|
|
249
|
-
const unchunker = new JsonChunkAssembler()
|
|
250
|
-
|
|
251
|
-
// Start chunk sequence
|
|
252
|
-
expect(unchunker.handleMessage('2_hello')).toBeNull()
|
|
253
|
-
|
|
254
|
-
// Interrupt with JSON message - should trigger error and reset state
|
|
255
|
-
const result = unchunker.handleMessage('{"interrupt": true}')
|
|
256
|
-
assert(result && 'error' in result, 'expected error result')
|
|
257
|
-
expect(result.error.message).toBe('Unexpected non-chunk message')
|
|
258
|
-
|
|
259
|
-
// Should be able to process messages normally again
|
|
260
|
-
expect(unchunker.handleMessage('{"ok": true}')).toMatchObject({ data: { ok: true } })
|
|
261
|
-
})
|
|
262
|
-
|
|
263
|
-
it('handles unicode line separators U+2028 and U+2029 in chunks', () => {
|
|
264
|
-
// These characters are common when copy/pasting from Microsoft Word
|
|
265
|
-
// and are valid JSON string content but act as line terminators in JS
|
|
266
|
-
const unchunker = new JsonChunkAssembler()
|
|
267
|
-
|
|
268
|
-
// Test U+2028 (Line Separator) and U+2029 (Paragraph Separator)
|
|
269
|
-
const jsonWithLineSeparators = '{"text": "hello\u2028world\u2029end"}'
|
|
270
|
-
const chunks = chunk(jsonWithLineSeparators, 10)
|
|
271
|
-
|
|
272
|
-
for (const c of chunks.slice(0, -1)) {
|
|
273
|
-
expect(unchunker.handleMessage(c)).toBeNull()
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
const result = unchunker.handleMessage(chunks[chunks.length - 1])
|
|
277
|
-
expect(result).toMatchObject({
|
|
278
|
-
data: { text: 'hello\u2028world\u2029end' },
|
|
279
|
-
})
|
|
280
|
-
})
|
|
281
|
-
|
|
282
|
-
it('handles duplicate or out-of-order chunk numbers', () => {
|
|
283
|
-
const unchunker = new JsonChunkAssembler()
|
|
284
|
-
|
|
285
|
-
// Start with first chunk
|
|
286
|
-
expect(unchunker.handleMessage('2_part1')).toBeNull()
|
|
287
|
-
|
|
288
|
-
// Send chunk with wrong number (should be 1, not 0)
|
|
289
|
-
const result = unchunker.handleMessage('0_part3')
|
|
290
|
-
assert(result && 'error' in result, 'expected error result')
|
|
291
|
-
expect(result.error.message).toBe('Chunks received in wrong order')
|
|
292
|
-
})
|
|
293
|
-
|
|
294
|
-
it('handles JSON parse error in completed chunk sequence', () => {
|
|
295
|
-
const unchunker = new JsonChunkAssembler()
|
|
296
|
-
|
|
297
|
-
// Send chunks that form invalid JSON when combined
|
|
298
|
-
expect(unchunker.handleMessage('1_{"invalid":')).toBeNull()
|
|
299
|
-
const result = unchunker.handleMessage('0_ }')
|
|
300
|
-
|
|
301
|
-
assert(result && 'error' in result, 'expected error result')
|
|
302
|
-
expect(result.error).toBeInstanceOf(Error)
|
|
303
|
-
})
|
|
304
|
-
})
|
|
305
|
-
|
|
306
|
-
describe('chunk function edge cases', () => {
|
|
307
|
-
it('handles empty strings', () => {
|
|
308
|
-
const result = chunk('', 100)
|
|
309
|
-
expect(result).toEqual([''])
|
|
310
|
-
})
|
|
311
|
-
|
|
312
|
-
it('handles single character strings', () => {
|
|
313
|
-
const result = chunk('a', 100)
|
|
314
|
-
expect(result).toEqual(['a'])
|
|
315
|
-
})
|
|
316
|
-
|
|
317
|
-
it('uses default maxSafeMessageSize when not provided', () => {
|
|
318
|
-
// Create a string longer than default max size to test chunking
|
|
319
|
-
const longString = 'x'.repeat(262145) // Larger than 262144 default
|
|
320
|
-
const result = chunk(longString)
|
|
321
|
-
|
|
322
|
-
expect(result.length).toBeGreaterThan(1)
|
|
323
|
-
expect(result[0]).toMatch(/^\d+_x+$/)
|
|
324
|
-
})
|
|
325
|
-
|
|
326
|
-
it('handles strings exactly at the boundary', () => {
|
|
327
|
-
const boundaryString = 'x'.repeat(9) // 9 chars fits in 10 char limit
|
|
328
|
-
const result = chunk(boundaryString, 10)
|
|
329
|
-
|
|
330
|
-
expect(result).toEqual([boundaryString])
|
|
331
|
-
})
|
|
332
|
-
|
|
333
|
-
it('handles strings one character over the boundary', () => {
|
|
334
|
-
const overBoundaryString = 'x'.repeat(11)
|
|
335
|
-
const result = chunk(overBoundaryString, 10)
|
|
336
|
-
|
|
337
|
-
expect(result.length).toBeGreaterThan(1)
|
|
338
|
-
expect(result[0]).toMatch(/^\d+_x+$/)
|
|
339
|
-
})
|
|
340
|
-
|
|
341
|
-
it('preserves unicode characters correctly', () => {
|
|
342
|
-
const unicodeString = '🎨'.repeat(10) + '📐'.repeat(10) + '🗼️'.repeat(10)
|
|
343
|
-
const result = chunk(unicodeString, 20)
|
|
344
|
-
|
|
345
|
-
// Verify chunking works with unicode
|
|
346
|
-
expect(result.length).toBeGreaterThan(1)
|
|
347
|
-
|
|
348
|
-
// Reconstruct and verify
|
|
349
|
-
const reconstructed = result
|
|
350
|
-
.map((chunk) => {
|
|
351
|
-
const match = /^(\d+)_(.*)$/.exec(chunk)
|
|
352
|
-
return match ? match[2] : chunk
|
|
353
|
-
})
|
|
354
|
-
.join('')
|
|
355
|
-
|
|
356
|
-
expect(reconstructed).toEqual(unicodeString)
|
|
357
|
-
})
|
|
358
|
-
|
|
359
|
-
it('ensures no chunk exceeds maxSafeMessageSize', () => {
|
|
360
|
-
const maxSize = 15
|
|
361
|
-
const testString = 'hello world this is a long message'
|
|
362
|
-
const result = chunk(testString, maxSize)
|
|
363
|
-
|
|
364
|
-
for (const chunk of result) {
|
|
365
|
-
expect(chunk.length).toBeLessThanOrEqual(maxSize)
|
|
366
|
-
}
|
|
367
|
-
})
|
|
368
|
-
|
|
369
|
-
it('handles very large strings efficiently', () => {
|
|
370
|
-
const veryLargeString = 'a'.repeat(1000000) // 1MB string
|
|
371
|
-
const result = chunk(veryLargeString, 10000)
|
|
372
|
-
|
|
373
|
-
expect(result.length).toBeGreaterThan(1)
|
|
374
|
-
|
|
375
|
-
// Verify reconstruction works
|
|
376
|
-
const reconstructed = result
|
|
377
|
-
.map((chunk) => {
|
|
378
|
-
const match = /^(\d+)_(.*)$/.exec(chunk)
|
|
379
|
-
return match ? match[2] : chunk
|
|
380
|
-
})
|
|
381
|
-
.join('')
|
|
382
|
-
|
|
383
|
-
expect(reconstructed).toEqual(veryLargeString)
|
|
384
|
-
})
|
|
385
|
-
})
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
import { atom, createRecordType, Store, StoreSchema, UnknownRecord } from 'tldraw'
|
|
2
|
-
import { TLSyncClient } from '../lib/TLSyncClient'
|
|
3
|
-
import { TestServer } from './TestServer'
|
|
4
|
-
import { TestSocketPair } from './TestSocketPair'
|
|
5
|
-
|
|
6
|
-
type Presence = UnknownRecord & { typeName: 'presence' }
|
|
7
|
-
const presenceType = createRecordType<Presence>('presence', {
|
|
8
|
-
scope: 'presence',
|
|
9
|
-
validator: { validate: (record) => record as Presence },
|
|
10
|
-
})
|
|
11
|
-
const schema = StoreSchema.create<Presence>({ presence: presenceType })
|
|
12
|
-
|
|
13
|
-
describe('custom messages', () => {
|
|
14
|
-
it('sends a message to a client', async () => {
|
|
15
|
-
const store = new Store<any, any>({ schema, props: {} })
|
|
16
|
-
|
|
17
|
-
const sessionId = 'test-session-1'
|
|
18
|
-
const server = new TestServer(schema)
|
|
19
|
-
const socketPair = new TestSocketPair(sessionId, server)
|
|
20
|
-
socketPair.connect()
|
|
21
|
-
|
|
22
|
-
const onCustomMessageReceived = vi.fn()
|
|
23
|
-
new TLSyncClient({
|
|
24
|
-
store,
|
|
25
|
-
socket: socketPair.clientSocket,
|
|
26
|
-
onCustomMessageReceived,
|
|
27
|
-
onLoad: vi.fn(),
|
|
28
|
-
onSyncError: vi.fn(),
|
|
29
|
-
presence: atom('', null),
|
|
30
|
-
})
|
|
31
|
-
await socketPair.flushAllEvents()
|
|
32
|
-
server.room.sendCustomMessage(sessionId, 'hello world')
|
|
33
|
-
await socketPair.flushAllEvents()
|
|
34
|
-
expect(onCustomMessageReceived.mock.lastCall).toEqual(['hello world'])
|
|
35
|
-
})
|
|
36
|
-
})
|