@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
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
2
|
+
import { interval } from './interval'
|
|
3
|
+
|
|
4
|
+
describe('interval', () => {
|
|
5
|
+
beforeEach(() => {
|
|
6
|
+
vi.useFakeTimers()
|
|
7
|
+
})
|
|
8
|
+
|
|
9
|
+
afterEach(() => {
|
|
10
|
+
vi.useRealTimers()
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
it('[IN1] invokes the callback every N milliseconds', () => {
|
|
14
|
+
const cb = vi.fn()
|
|
15
|
+
const dispose = interval(cb, 100)
|
|
16
|
+
|
|
17
|
+
expect(cb).not.toHaveBeenCalled()
|
|
18
|
+
|
|
19
|
+
vi.advanceTimersByTime(99)
|
|
20
|
+
expect(cb).not.toHaveBeenCalled()
|
|
21
|
+
|
|
22
|
+
vi.advanceTimersByTime(1)
|
|
23
|
+
expect(cb).toHaveBeenCalledTimes(1)
|
|
24
|
+
|
|
25
|
+
vi.advanceTimersByTime(300)
|
|
26
|
+
expect(cb).toHaveBeenCalledTimes(4)
|
|
27
|
+
|
|
28
|
+
dispose()
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
it('[IN1] the dispose function stops the interval', () => {
|
|
32
|
+
const cb = vi.fn()
|
|
33
|
+
const dispose = interval(cb, 100)
|
|
34
|
+
|
|
35
|
+
vi.advanceTimersByTime(250)
|
|
36
|
+
expect(cb).toHaveBeenCalledTimes(2)
|
|
37
|
+
|
|
38
|
+
dispose()
|
|
39
|
+
|
|
40
|
+
vi.advanceTimersByTime(1000)
|
|
41
|
+
expect(cb).toHaveBeenCalledTimes(2)
|
|
42
|
+
})
|
|
43
|
+
})
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { getTlsyncProtocolVersion } from './protocol'
|
|
3
|
+
import { TLRemoteSyncError } from './TLRemoteSyncError'
|
|
4
|
+
import { TLSyncErrorCloseEventCode, TLSyncErrorCloseEventReason } from './TLSyncClient'
|
|
5
|
+
|
|
6
|
+
describe('protocol constants and errors', () => {
|
|
7
|
+
it('[PV1] getTlsyncProtocolVersion() returns 8', () => {
|
|
8
|
+
expect(getTlsyncProtocolVersion()).toBe(8)
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
it('[PV2] TLSyncErrorCloseEventCode is 4099', () => {
|
|
12
|
+
expect(TLSyncErrorCloseEventCode).toBe(4099)
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
describe('TLRemoteSyncError', () => {
|
|
16
|
+
it('[PV3] has name RemoteSyncError', () => {
|
|
17
|
+
expect(new TLRemoteSyncError(TLSyncErrorCloseEventReason.FORBIDDEN).name).toBe(
|
|
18
|
+
'RemoteSyncError'
|
|
19
|
+
)
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
it('[PV3] formats the message as "sync error: <reason>" and exposes .reason', () => {
|
|
23
|
+
const error = new TLRemoteSyncError(TLSyncErrorCloseEventReason.NOT_AUTHENTICATED)
|
|
24
|
+
expect(error.message).toBe('sync error: NOT_AUTHENTICATED')
|
|
25
|
+
expect(error.reason).toBe('NOT_AUTHENTICATED')
|
|
26
|
+
expect(error).toBeInstanceOf(Error)
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
it('[PV3] works with every TLSyncErrorCloseEventReason value', () => {
|
|
30
|
+
for (const reason of Object.values(TLSyncErrorCloseEventReason)) {
|
|
31
|
+
const error = new TLRemoteSyncError(reason)
|
|
32
|
+
expect(error.reason).toBe(reason)
|
|
33
|
+
expect(error.message).toBe(`sync error: ${reason}`)
|
|
34
|
+
}
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it('[PV3] works with custom reason strings', () => {
|
|
38
|
+
const error = new TLRemoteSyncError('ROOM_ARCHIVED')
|
|
39
|
+
expect(error.reason).toBe('ROOM_ARCHIVED')
|
|
40
|
+
expect(error.message).toBe('sync error: ROOM_ARCHIVED')
|
|
41
|
+
})
|
|
42
|
+
})
|
|
43
|
+
})
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { BaseRecord, RecordId, createRecordType } from '@tldraw/store'
|
|
2
|
+
import { vi } from 'vitest'
|
|
3
|
+
import { ValueOpType, type ObjectDiff } from './diff'
|
|
4
|
+
import { applyAndDiffRecord, diffAndValidateRecord, validateRecord } from './recordDiff'
|
|
5
|
+
import { TLSyncError, TLSyncErrorCloseEventReason } from './TLSyncClient'
|
|
6
|
+
|
|
7
|
+
interface TestRecord extends BaseRecord<'test', RecordId<TestRecord>> {
|
|
8
|
+
value: number
|
|
9
|
+
text: string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function setup() {
|
|
13
|
+
const validate = vi.fn((record: unknown): TestRecord => {
|
|
14
|
+
const r = record as TestRecord
|
|
15
|
+
if (r.value < 0) {
|
|
16
|
+
throw new Error('value must not be negative')
|
|
17
|
+
}
|
|
18
|
+
return r
|
|
19
|
+
})
|
|
20
|
+
const recordType = createRecordType<TestRecord>('test', {
|
|
21
|
+
validator: { validate },
|
|
22
|
+
scope: 'document',
|
|
23
|
+
})
|
|
24
|
+
const record = (value: number, text = 'hello'): TestRecord => ({
|
|
25
|
+
id: 'test:a' as TestRecord['id'],
|
|
26
|
+
typeName: 'test',
|
|
27
|
+
value,
|
|
28
|
+
text,
|
|
29
|
+
})
|
|
30
|
+
return { validate, recordType, record }
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
describe('diffAndValidateRecord (RV1, RV2)', () => {
|
|
34
|
+
it('[RV1] returns undefined when the records produce no diff', () => {
|
|
35
|
+
const { recordType, record } = setup()
|
|
36
|
+
expect(diffAndValidateRecord(record(1), record(1), recordType)).toBeUndefined()
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it('[RV1] does not call the validator when there is no diff', () => {
|
|
40
|
+
const { validate, recordType, record } = setup()
|
|
41
|
+
const prev = record(1)
|
|
42
|
+
|
|
43
|
+
diffAndValidateRecord(prev, prev, recordType)
|
|
44
|
+
diffAndValidateRecord(prev, record(1), recordType)
|
|
45
|
+
|
|
46
|
+
expect(validate).not.toHaveBeenCalled()
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it('[RV2] validates the new state when there is a diff', () => {
|
|
50
|
+
const { validate, recordType, record } = setup()
|
|
51
|
+
const next = record(2)
|
|
52
|
+
|
|
53
|
+
const diff = diffAndValidateRecord(record(1), next, recordType)
|
|
54
|
+
|
|
55
|
+
expect(diff).toEqual({ value: [ValueOpType.Put, 2] })
|
|
56
|
+
expect(validate).toHaveBeenCalledTimes(1)
|
|
57
|
+
expect(validate).toHaveBeenCalledWith(next)
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it('[RV2] rethrows a validator throw as TLSyncError with reason INVALID_RECORD', () => {
|
|
61
|
+
const { recordType, record } = setup()
|
|
62
|
+
|
|
63
|
+
let error: unknown
|
|
64
|
+
try {
|
|
65
|
+
diffAndValidateRecord(record(1), record(-1), recordType)
|
|
66
|
+
} catch (e) {
|
|
67
|
+
error = e
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
expect(error).toBeInstanceOf(TLSyncError)
|
|
71
|
+
expect((error as TLSyncError).reason).toBe(TLSyncErrorCloseEventReason.INVALID_RECORD)
|
|
72
|
+
expect((error as TLSyncError).message).toBe('value must not be negative')
|
|
73
|
+
})
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
describe('applyAndDiffRecord (RV3)', () => {
|
|
77
|
+
it('[RV3] returns undefined when the patch applies to a reference-identical result', () => {
|
|
78
|
+
const { validate, recordType, record } = setup()
|
|
79
|
+
const prev = record(1)
|
|
80
|
+
// a put of the current value has no effect (AD2), so applyObjectDiff
|
|
81
|
+
// returns the same reference
|
|
82
|
+
const patch: ObjectDiff = { value: [ValueOpType.Put, 1] }
|
|
83
|
+
|
|
84
|
+
expect(applyAndDiffRecord(prev, patch, recordType)).toBeUndefined()
|
|
85
|
+
expect(validate).not.toHaveBeenCalled()
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
it('[RV3] returns the recomputed effective diff and the new state', () => {
|
|
89
|
+
const { recordType, record } = setup()
|
|
90
|
+
const prev = record(1)
|
|
91
|
+
const patch: ObjectDiff = { value: [ValueOpType.Put, 2] }
|
|
92
|
+
|
|
93
|
+
const result = applyAndDiffRecord(prev, patch, recordType)
|
|
94
|
+
|
|
95
|
+
expect(result).toBeDefined()
|
|
96
|
+
const [actualDiff, newState] = result!
|
|
97
|
+
expect(actualDiff).toEqual({ value: [ValueOpType.Put, 2] })
|
|
98
|
+
expect(newState).toEqual(record(2))
|
|
99
|
+
// the input is not mutated (AD1)
|
|
100
|
+
expect(prev).toEqual(record(1))
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
it('[RV3] drops no-op ops from the returned diff, keeping only the real change', () => {
|
|
104
|
+
const { recordType, record } = setup()
|
|
105
|
+
const prev = record(1, 'hello')
|
|
106
|
+
// the text put is a no-op (same value); the value put is a real change
|
|
107
|
+
const patch: ObjectDiff = {
|
|
108
|
+
text: [ValueOpType.Put, 'hello'],
|
|
109
|
+
value: [ValueOpType.Put, 5],
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const result = applyAndDiffRecord(prev, patch, recordType)
|
|
113
|
+
|
|
114
|
+
expect(result).toBeDefined()
|
|
115
|
+
const [actualDiff, newState] = result!
|
|
116
|
+
expect(actualDiff).toEqual({ value: [ValueOpType.Put, 5] })
|
|
117
|
+
expect(newState).toEqual(record(5, 'hello'))
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
it('[RV3] [RV2] validates the patched state and wraps validator throws', () => {
|
|
121
|
+
const { recordType, record } = setup()
|
|
122
|
+
const patch: ObjectDiff = { value: [ValueOpType.Put, -10] }
|
|
123
|
+
|
|
124
|
+
let error: unknown
|
|
125
|
+
try {
|
|
126
|
+
applyAndDiffRecord(record(1), patch, recordType)
|
|
127
|
+
} catch (e) {
|
|
128
|
+
error = e
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
expect(error).toBeInstanceOf(TLSyncError)
|
|
132
|
+
expect((error as TLSyncError).reason).toBe(TLSyncErrorCloseEventReason.INVALID_RECORD)
|
|
133
|
+
})
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
describe('validateRecord (RV4)', () => {
|
|
137
|
+
it('[RV4] returns normally for a valid record', () => {
|
|
138
|
+
const { validate, recordType, record } = setup()
|
|
139
|
+
const state = record(1)
|
|
140
|
+
|
|
141
|
+
expect(() => validateRecord(state, recordType)).not.toThrow()
|
|
142
|
+
expect(validate).toHaveBeenCalledWith(state)
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
it('[RV4] wraps a validator throw as TLSyncError with reason INVALID_RECORD', () => {
|
|
146
|
+
const { recordType, record } = setup()
|
|
147
|
+
|
|
148
|
+
let error: unknown
|
|
149
|
+
try {
|
|
150
|
+
validateRecord(record(-1), recordType)
|
|
151
|
+
} catch (e) {
|
|
152
|
+
error = e
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
expect(error).toBeInstanceOf(TLSyncError)
|
|
156
|
+
expect((error as TLSyncError).reason).toBe(TLSyncErrorCloseEventReason.INVALID_RECORD)
|
|
157
|
+
expect((error as TLSyncError).message).toBe('value must not be negative')
|
|
158
|
+
})
|
|
159
|
+
})
|