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