@tldraw/sync-core 5.2.0-next.a6104dd18d03 → 5.2.0-next.b0c9cf86de67

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