@tldraw/sync-core 4.3.0-canary.c7096a59bf3b → 4.3.0-canary.d039f3a1ab8f

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 (51) hide show
  1. package/dist-cjs/index.d.ts +239 -57
  2. package/dist-cjs/index.js +7 -3
  3. package/dist-cjs/index.js.map +2 -2
  4. package/dist-cjs/lib/InMemorySyncStorage.js +289 -0
  5. package/dist-cjs/lib/InMemorySyncStorage.js.map +7 -0
  6. package/dist-cjs/lib/RoomSession.js.map +1 -1
  7. package/dist-cjs/lib/TLSocketRoom.js +117 -69
  8. package/dist-cjs/lib/TLSocketRoom.js.map +2 -2
  9. package/dist-cjs/lib/TLSyncClient.js +7 -0
  10. package/dist-cjs/lib/TLSyncClient.js.map +2 -2
  11. package/dist-cjs/lib/TLSyncRoom.js +357 -688
  12. package/dist-cjs/lib/TLSyncRoom.js.map +3 -3
  13. package/dist-cjs/lib/TLSyncStorage.js +76 -0
  14. package/dist-cjs/lib/TLSyncStorage.js.map +7 -0
  15. package/dist-cjs/lib/recordDiff.js +52 -0
  16. package/dist-cjs/lib/recordDiff.js.map +7 -0
  17. package/dist-esm/index.d.mts +239 -57
  18. package/dist-esm/index.mjs +12 -5
  19. package/dist-esm/index.mjs.map +2 -2
  20. package/dist-esm/lib/InMemorySyncStorage.mjs +274 -0
  21. package/dist-esm/lib/InMemorySyncStorage.mjs.map +7 -0
  22. package/dist-esm/lib/RoomSession.mjs.map +1 -1
  23. package/dist-esm/lib/TLSocketRoom.mjs +121 -70
  24. package/dist-esm/lib/TLSocketRoom.mjs.map +2 -2
  25. package/dist-esm/lib/TLSyncClient.mjs +7 -0
  26. package/dist-esm/lib/TLSyncClient.mjs.map +2 -2
  27. package/dist-esm/lib/TLSyncRoom.mjs +370 -702
  28. package/dist-esm/lib/TLSyncRoom.mjs.map +3 -3
  29. package/dist-esm/lib/TLSyncStorage.mjs +56 -0
  30. package/dist-esm/lib/TLSyncStorage.mjs.map +7 -0
  31. package/dist-esm/lib/recordDiff.mjs +32 -0
  32. package/dist-esm/lib/recordDiff.mjs.map +7 -0
  33. package/package.json +6 -6
  34. package/src/index.ts +21 -3
  35. package/src/lib/InMemorySyncStorage.ts +357 -0
  36. package/src/lib/RoomSession.test.ts +1 -0
  37. package/src/lib/RoomSession.ts +2 -0
  38. package/src/lib/TLSocketRoom.ts +228 -114
  39. package/src/lib/TLSyncClient.ts +12 -0
  40. package/src/lib/TLSyncRoom.ts +473 -913
  41. package/src/lib/TLSyncStorage.ts +216 -0
  42. package/src/lib/recordDiff.ts +73 -0
  43. package/src/test/InMemorySyncStorage.test.ts +1674 -0
  44. package/src/test/TLSocketRoom.test.ts +255 -49
  45. package/src/test/TLSyncRoom.test.ts +1021 -533
  46. package/src/test/TestServer.ts +12 -1
  47. package/src/test/customMessages.test.ts +1 -1
  48. package/src/test/presenceMode.test.ts +6 -6
  49. package/src/test/upgradeDowngrade.test.ts +282 -8
  50. package/src/test/validation.test.ts +10 -10
  51. package/src/test/pruneTombstones.test.ts +0 -178
@@ -0,0 +1,216 @@
1
+ import { StoreSchema, SynchronousStorage, UnknownRecord } from '@tldraw/store'
2
+ import { isEqual, objectMapEntriesIterable, objectMapValues } from '@tldraw/utils'
3
+ import { TLStoreSnapshot } from 'tldraw'
4
+ import { diffRecord, NetworkDiff, RecordOpType } from './diff'
5
+ import { RoomSnapshot } from './TLSyncRoom'
6
+
7
+ /**
8
+ * Transaction interface for storage operations. Provides methods to read and modify
9
+ * documents, tombstones, and metadata within a transaction.
10
+ *
11
+ * @public
12
+ */
13
+ export interface TLSyncStorageTransaction<R extends UnknownRecord> extends SynchronousStorage<R> {
14
+ /**
15
+ * Get the current clock value.
16
+ * If the clock has incremented during the transaction,
17
+ * the incremented value will be returned.
18
+ *
19
+ * @returns The current clock value
20
+ */
21
+ getClock(): number
22
+
23
+ /**
24
+ * Get all changes (document updates and deletions) since a given clock time.
25
+ * This is the main method for calculating diffs for client sync.
26
+ *
27
+ * @param sinceClock - The clock time to get changes since
28
+ * @returns Changes since the specified clock time
29
+ */
30
+ getChangesSince(sinceClock: number): TLSyncStorageGetChangesSinceResult<R> | undefined
31
+ }
32
+
33
+ /**
34
+ * Options for a transaction.
35
+ * @public
36
+ */
37
+ export interface TLSyncStorageTransactionOptions {
38
+ /**
39
+ * Use this if you need to identify the transaction for logging or debugging purposes
40
+ * or for ignoring certain changes in onChange callbacks
41
+ */
42
+ id?: string
43
+ /**
44
+ * Controls when the storage layer should emit the actual changes that occurred during the transaction.
45
+ *
46
+ * - `'always'` - Always emit the changes, regardless of whether they were applied verbatim
47
+ * - `'when-different'` - Only emit changes if the storage layer modified/embellished the records
48
+ * (e.g., added server timestamps, normalized data, etc.)
49
+ *
50
+ * When changes are emitted, they will be available in the `changes` field of the transaction result.
51
+ * This is useful when the storage layer may transform records and the caller needs to know
52
+ * what actually changed rather than what was requested.
53
+ */
54
+ emitChanges?: 'always' | 'when-different'
55
+ }
56
+
57
+ /**
58
+ * Callback type for a transaction.
59
+ * The conditional return type ensures that the callback is synchronous.
60
+ * @public
61
+ */
62
+ export type TLSyncStorageTransactionCallback<R extends UnknownRecord, T> = (
63
+ txn: TLSyncStorageTransaction<R>
64
+ ) => T extends Promise<any>
65
+ ? {
66
+ __error: 'Transaction callbacks cannot be async. Use synchronous operations only.'
67
+ }
68
+ : T
69
+
70
+ /**
71
+ * Pluggable synchronous transactional storage layer for TLSyncRoom.
72
+ * Provides methods for managing documents, tombstones, and clocks within transactions.
73
+ *
74
+ * @public
75
+ */
76
+ export interface TLSyncStorage<R extends UnknownRecord> {
77
+ transaction<T>(
78
+ callback: TLSyncStorageTransactionCallback<R, T>,
79
+ opts?: TLSyncStorageTransactionOptions
80
+ ): TLSyncStorageTransactionResult<T, R>
81
+
82
+ getClock(): number
83
+
84
+ onChange(callback: (arg: TLSyncStorageOnChangeCallbackProps) => unknown): () => void
85
+
86
+ getSnapshot?(): RoomSnapshot
87
+ }
88
+
89
+ /**
90
+ * Properties passed to the onChange callback.
91
+ * @public
92
+ */
93
+ export interface TLSyncStorageOnChangeCallbackProps {
94
+ /**
95
+ * The ID of the transaction that caused the change.
96
+ * This is useful for ignoring certain changes in onChange callbacks.
97
+ */
98
+ id?: string
99
+ documentClock: number
100
+ }
101
+
102
+ /**
103
+ * Result returned from a storage transaction.
104
+ * @public
105
+ */
106
+ export interface TLSyncStorageTransactionResult<T, R extends UnknownRecord = UnknownRecord> {
107
+ documentClock: number
108
+ didChange: boolean
109
+ result: T
110
+ /**
111
+ * The actual changes that occurred during the transaction, if requested via `emitChanges` option.
112
+ * This is a RecordsDiff where:
113
+ * - `added` contains records that were put (we don't have "from" state for emitted changes)
114
+ * - `removed` contains records that were deleted (with placeholder values since we only have IDs)
115
+ * - `updated` is empty (emitted changes don't track before/after pairs)
116
+ *
117
+ * Only populated when:
118
+ * - `emitChanges: 'always'` was specified, or
119
+ * - `emitChanges: 'when-different'` was specified and the storage layer modified records
120
+ */
121
+ changes?: TLSyncForwardDiff<R>
122
+ }
123
+
124
+ /**
125
+ * Respresents a diff of puts and deletes.
126
+ * @public
127
+ */
128
+ export interface TLSyncForwardDiff<R extends UnknownRecord> {
129
+ puts: Record<string, R | [before: R, after: R]>
130
+ deletes: string[]
131
+ }
132
+
133
+ /**
134
+ * @internal
135
+ */
136
+ export function toNetworkDiff<R extends UnknownRecord>(diff: TLSyncForwardDiff<R>): NetworkDiff<R> {
137
+ const networkDiff: NetworkDiff<R> = {}
138
+ for (const [id, put] of objectMapEntriesIterable(diff.puts)) {
139
+ if (Array.isArray(put)) {
140
+ const patch = diffRecord(put[0], put[1])
141
+ if (patch) {
142
+ networkDiff[id] = [RecordOpType.Patch, patch]
143
+ }
144
+ } else {
145
+ networkDiff[id] = [RecordOpType.Put, put]
146
+ }
147
+ }
148
+ for (const id of diff.deletes) {
149
+ networkDiff[id] = [RecordOpType.Remove]
150
+ }
151
+ return networkDiff
152
+ }
153
+
154
+ /**
155
+ * Result returned from getChangesSince, containing all changes since a given clock time.
156
+ * @public
157
+ */
158
+ export interface TLSyncStorageGetChangesSinceResult<R extends UnknownRecord> {
159
+ /**
160
+ * The changes as a TLSyncForwardDiff.
161
+ */
162
+ diff: TLSyncForwardDiff<R>
163
+ /**
164
+ * If true, the client should wipe all local data and replace with the server's state.
165
+ * This happens when the client's clock is too old and we've lost tombstone history.
166
+ */
167
+ wipeAll: boolean
168
+ }
169
+
170
+ /**
171
+ * Loads a snapshot into storage during a transaction.
172
+ * Migrates the snapshot to the current schema and loads it into storage.
173
+ *
174
+ * @public
175
+ * @param txn - The transaction to load the snapshot into
176
+ * @param schema - The current schema
177
+ * @param snapshot - The snapshot to load
178
+ */
179
+ export function loadSnapshotIntoStorage<R extends UnknownRecord>(
180
+ txn: TLSyncStorageTransaction<R>,
181
+ schema: StoreSchema<R, any>,
182
+ snapshot: RoomSnapshot | TLStoreSnapshot
183
+ ) {
184
+ snapshot = convertStoreSnapshotToRoomSnapshot(snapshot)
185
+ assert(snapshot.schema, 'Schema is required')
186
+ const docIds = new Set<string>()
187
+ for (const doc of snapshot.documents) {
188
+ docIds.add(doc.state.id)
189
+ const existing = txn.get(doc.state.id)
190
+ if (isEqual(existing, doc.state)) continue
191
+ txn.set(doc.state.id, doc.state as R)
192
+ }
193
+ for (const id of txn.keys()) {
194
+ if (!docIds.has(id)) {
195
+ txn.delete(id)
196
+ }
197
+ }
198
+ txn.setSchema(snapshot.schema)
199
+ schema.migrateStorage(txn)
200
+ }
201
+
202
+ export function convertStoreSnapshotToRoomSnapshot(
203
+ snapshot: RoomSnapshot | TLStoreSnapshot
204
+ ): RoomSnapshot {
205
+ if ('documents' in snapshot) return snapshot
206
+ return {
207
+ clock: 0,
208
+ documentClock: 0,
209
+ documents: objectMapValues(snapshot.store).map((state) => ({
210
+ state,
211
+ lastChangedClock: 0,
212
+ })),
213
+ schema: snapshot.schema,
214
+ tombstones: {},
215
+ }
216
+ }
@@ -0,0 +1,73 @@
1
+ import { RecordType, UnknownRecord } from '@tldraw/store'
2
+ import { ObjectDiff, applyObjectDiff, diffRecord } from './diff'
3
+ import { TLSyncError, TLSyncErrorCloseEventReason } from './TLSyncClient'
4
+
5
+ /**
6
+ * Validate a record and compute the diff between two states.
7
+ * Returns null if the states are identical.
8
+ *
9
+ * @param prevState - The previous record state
10
+ * @param newState - The new record state
11
+ * @param recordType - The record type definition for validation
12
+ * @param legacyAppendMode - If true, string append operations will be converted to Put operations
13
+ * @returns Result containing the diff and new state, or null if no changes, or validation error
14
+ *
15
+ * @internal
16
+ */
17
+ export function diffAndValidateRecord<R extends UnknownRecord>(
18
+ prevState: R,
19
+ newState: R,
20
+ recordType: RecordType<R, any>,
21
+ legacyAppendMode = false
22
+ ) {
23
+ const diff = diffRecord(prevState, newState, legacyAppendMode)
24
+ if (!diff) return
25
+ try {
26
+ recordType.validate(newState)
27
+ } catch (error: any) {
28
+ throw new TLSyncError(error.message, TLSyncErrorCloseEventReason.INVALID_RECORD)
29
+ }
30
+ return diff
31
+ }
32
+
33
+ /**
34
+ * Apply a diff to a record state, validate the result, and compute the final diff.
35
+ * Returns null if the diff produces no changes.
36
+ *
37
+ * @param prevState - The previous record state
38
+ * @param diff - The object diff to apply
39
+ * @param recordType - The record type definition for validation
40
+ * @param legacyAppendMode - If true, string append operations will be converted to Put operations
41
+ * @returns Result containing the final diff and new state, or null if no changes, or validation error
42
+ *
43
+ * @internal
44
+ */
45
+ export function applyAndDiffRecord<R extends UnknownRecord>(
46
+ prevState: R,
47
+ diff: ObjectDiff,
48
+ recordType: RecordType<R, any>,
49
+ legacyAppendMode = false
50
+ ): [ObjectDiff, R] | undefined {
51
+ const newState = applyObjectDiff(prevState, diff)
52
+ if (newState === prevState) return
53
+ const actualDiff = diffAndValidateRecord(prevState, newState, recordType, legacyAppendMode)
54
+ if (!actualDiff) return
55
+ return [actualDiff, newState]
56
+ }
57
+
58
+ /**
59
+ * Validate a record without computing a diff. Used when creating new records.
60
+ *
61
+ * @param state - The record state to validate
62
+ * @param recordType - The record type definition for validation
63
+ * @returns Result indicating success or validation error
64
+ *
65
+ * @internal
66
+ */
67
+ export function validateRecord<R extends UnknownRecord>(state: R, recordType: RecordType<R, any>) {
68
+ try {
69
+ recordType.validate(state)
70
+ } catch (error: any) {
71
+ throw new TLSyncError(error.message, TLSyncErrorCloseEventReason.INVALID_RECORD)
72
+ }
73
+ }