@tldraw/sync-core 5.2.0-next.ee0fa4d6244f → 5.2.0

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