@tldraw/sync-core 4.2.0-next.47462e908ff5 → 4.2.0-next.54bc357bbff2

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 (45) hide show
  1. package/dist-cjs/index.d.ts +339 -5
  2. package/dist-cjs/index.js +2 -1
  3. package/dist-cjs/index.js.map +2 -2
  4. package/dist-cjs/lib/ClientWebSocketAdapter.js.map +2 -2
  5. package/dist-cjs/lib/RoomSession.js.map +1 -1
  6. package/dist-cjs/lib/TLSyncClient.js +6 -0
  7. package/dist-cjs/lib/TLSyncClient.js.map +2 -2
  8. package/dist-cjs/lib/TLSyncRoom.js +35 -9
  9. package/dist-cjs/lib/TLSyncRoom.js.map +2 -2
  10. package/dist-cjs/lib/chunk.js +4 -4
  11. package/dist-cjs/lib/chunk.js.map +1 -1
  12. package/dist-cjs/lib/diff.js +29 -29
  13. package/dist-cjs/lib/diff.js.map +2 -2
  14. package/dist-cjs/lib/protocol.js +1 -1
  15. package/dist-cjs/lib/protocol.js.map +1 -1
  16. package/dist-esm/index.d.mts +339 -5
  17. package/dist-esm/index.mjs +3 -2
  18. package/dist-esm/index.mjs.map +2 -2
  19. package/dist-esm/lib/ClientWebSocketAdapter.mjs.map +2 -2
  20. package/dist-esm/lib/RoomSession.mjs.map +1 -1
  21. package/dist-esm/lib/TLSyncClient.mjs +6 -0
  22. package/dist-esm/lib/TLSyncClient.mjs.map +2 -2
  23. package/dist-esm/lib/TLSyncRoom.mjs +35 -9
  24. package/dist-esm/lib/TLSyncRoom.mjs.map +2 -2
  25. package/dist-esm/lib/chunk.mjs +4 -4
  26. package/dist-esm/lib/chunk.mjs.map +1 -1
  27. package/dist-esm/lib/diff.mjs +29 -29
  28. package/dist-esm/lib/diff.mjs.map +2 -2
  29. package/dist-esm/lib/protocol.mjs +1 -1
  30. package/dist-esm/lib/protocol.mjs.map +1 -1
  31. package/package.json +6 -6
  32. package/src/index.ts +3 -3
  33. package/src/lib/ClientWebSocketAdapter.ts +4 -1
  34. package/src/lib/RoomSession.test.ts +3 -0
  35. package/src/lib/RoomSession.ts +28 -42
  36. package/src/lib/TLSyncClient.test.ts +17 -6
  37. package/src/lib/TLSyncClient.ts +31 -17
  38. package/src/lib/TLSyncRoom.ts +42 -7
  39. package/src/lib/chunk.ts +4 -4
  40. package/src/lib/diff.ts +55 -32
  41. package/src/lib/protocol.ts +1 -1
  42. package/src/test/TLSocketRoom.test.ts +2 -2
  43. package/src/test/TLSyncRoom.test.ts +22 -21
  44. package/src/test/TestSocketPair.ts +5 -2
  45. package/src/test/diff.test.ts +200 -0
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/lib/diff.ts"],
4
- "sourcesContent": ["import { RecordsDiff, UnknownRecord } from '@tldraw/store'\nimport { isEqual, objectMapEntries, objectMapValues } from '@tldraw/utils'\n\n/**\n * Constants representing the types of operations that can be applied to records in network diffs.\n * These operations describe how a record has been modified during synchronization.\n *\n * @internal\n */\nexport const RecordOpType = {\n\tPut: 'put',\n\tPatch: 'patch',\n\tRemove: 'remove',\n} as const\n\n/**\n * Union type of all possible record operation types.\n *\n * @internal\n */\nexport type RecordOpType = (typeof RecordOpType)[keyof typeof RecordOpType]\n\n/**\n * Represents a single operation to be applied to a record during synchronization.\n *\n * @param R - The record type being operated on\n *\n * @internal\n */\nexport type RecordOp<R extends UnknownRecord> =\n\t| [typeof RecordOpType.Put, R]\n\t| [typeof RecordOpType.Patch, ObjectDiff]\n\t| [typeof RecordOpType.Remove]\n\n/**\n * A one-way (non-reversible) diff designed for small json footprint. These are mainly intended to\n * be sent over the wire. Either as push requests from the client to the server, or as patch\n * operations in the opposite direction.\n *\n * Each key in this object is the id of a record that has been added, updated, or removed.\n *\n * @internal\n */\nexport interface NetworkDiff<R extends UnknownRecord> {\n\t[id: string]: RecordOp<R>\n}\n\n/**\n * Converts a (reversible, verbose) RecordsDiff into a (non-reversible, concise) NetworkDiff\n * suitable for transmission over the network. This function optimizes the diff representation\n * for minimal bandwidth usage while maintaining all necessary change information.\n *\n * @param diff - The RecordsDiff containing added, updated, and removed records\n * @returns A compact NetworkDiff for network transmission, or null if no changes exist\n *\n * @example\n * ```ts\n * const recordsDiff = {\n * added: { 'shape:1': newShape },\n * updated: { 'shape:2': [oldShape, updatedShape] },\n * removed: { 'shape:3': removedShape }\n * }\n *\n * const networkDiff = getNetworkDiff(recordsDiff)\n * // Returns: {\n * // 'shape:1': ['put', newShape],\n * // 'shape:2': ['patch', { x: ['put', 100] }],\n * // 'shape:3': ['remove']\n * // }\n * ```\n *\n * @internal\n */\nexport function getNetworkDiff<R extends UnknownRecord>(\n\tdiff: RecordsDiff<R>\n): NetworkDiff<R> | null {\n\tlet res: NetworkDiff<R> | null = null\n\n\tfor (const [k, v] of objectMapEntries(diff.added)) {\n\t\tif (!res) res = {}\n\t\tres[k] = [RecordOpType.Put, v]\n\t}\n\n\tfor (const [from, to] of objectMapValues(diff.updated)) {\n\t\tconst diff = diffRecord(from, to)\n\t\tif (diff) {\n\t\t\tif (!res) res = {}\n\t\t\tres[to.id] = [RecordOpType.Patch, diff]\n\t\t}\n\t}\n\n\tfor (const removed of Object.keys(diff.removed)) {\n\t\tif (!res) res = {}\n\t\tres[removed] = [RecordOpType.Remove]\n\t}\n\n\treturn res\n}\n\n/**\n * Constants representing the types of operations that can be applied to individual values\n * within object diffs. These operations describe how object properties have changed.\n *\n * @internal\n */\nexport const ValueOpType = {\n\tPut: 'put',\n\tDelete: 'delete',\n\tAppend: 'append',\n\tPatch: 'patch',\n} as const\n/**\n * Union type of all possible value operation types.\n *\n * @internal\n */\nexport type ValueOpType = (typeof ValueOpType)[keyof typeof ValueOpType]\n\n/**\n * Operation that replaces a value entirely with a new value.\n *\n * @internal\n */\nexport type PutOp = [type: typeof ValueOpType.Put, value: unknown]\n/**\n * Operation that appends new values to the end of an array.\n *\n * @internal\n */\nexport type AppendOp = [type: typeof ValueOpType.Append, values: unknown[], offset: number]\n/**\n * Operation that applies a nested diff to an object or array.\n *\n * @internal\n */\nexport type PatchOp = [type: typeof ValueOpType.Patch, diff: ObjectDiff]\n/**\n * Operation that removes a property from an object.\n *\n * @internal\n */\nexport type DeleteOp = [type: typeof ValueOpType.Delete]\n\n/**\n * Union type representing any value operation that can be applied during diffing.\n *\n * @internal\n */\nexport type ValueOp = PutOp | AppendOp | PatchOp | DeleteOp\n\n/**\n * Represents the differences between two objects as a mapping of property names\n * to the operations needed to transform one object into another.\n *\n * @internal\n */\nexport interface ObjectDiff {\n\t[k: string]: ValueOp\n}\n\n/**\n * Computes the difference between two record objects, generating an ObjectDiff\n * that describes how to transform the previous record into the next record.\n * This function is optimized for tldraw records and treats 'props' as a nested object.\n *\n * @param prev - The previous version of the record\n * @param next - The next version of the record\n * @returns An ObjectDiff describing the changes, or null if no changes exist\n *\n * @example\n * ```ts\n * const oldShape = { id: 'shape:1', x: 100, y: 200, props: { color: 'red' } }\n * const newShape = { id: 'shape:1', x: 150, y: 200, props: { color: 'blue' } }\n *\n * const diff = diffRecord(oldShape, newShape)\n * // Returns: {\n * // x: ['put', 150],\n * // props: ['patch', { color: ['put', 'blue'] }]\n * // }\n * ```\n *\n * @internal\n */\nexport function diffRecord(prev: object, next: object): ObjectDiff | null {\n\treturn diffObject(prev, next, new Set(['props']))\n}\n\nfunction diffObject(prev: object, next: object, nestedKeys?: Set<string>): ObjectDiff | null {\n\tif (prev === next) {\n\t\treturn null\n\t}\n\tlet result: ObjectDiff | null = null\n\tfor (const key of Object.keys(prev)) {\n\t\t// if key is not in next then it was deleted\n\t\tif (!(key in next)) {\n\t\t\tif (!result) result = {}\n\t\t\tresult[key] = [ValueOpType.Delete]\n\t\t\tcontinue\n\t\t}\n\t\t// if key is in both places, then compare values\n\t\tconst prevVal = (prev as any)[key]\n\t\tconst nextVal = (next as any)[key]\n\t\tif (!isEqual(prevVal, nextVal)) {\n\t\t\tif (nestedKeys?.has(key) && prevVal && nextVal) {\n\t\t\t\tconst diff = diffObject(prevVal, nextVal)\n\t\t\t\tif (diff) {\n\t\t\t\t\tif (!result) result = {}\n\t\t\t\t\tresult[key] = [ValueOpType.Patch, diff]\n\t\t\t\t}\n\t\t\t} else if (Array.isArray(nextVal) && Array.isArray(prevVal)) {\n\t\t\t\tconst op = diffArray(prevVal, nextVal)\n\t\t\t\tif (op) {\n\t\t\t\t\tif (!result) result = {}\n\t\t\t\t\tresult[key] = op\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tif (!result) result = {}\n\t\t\t\tresult[key] = [ValueOpType.Put, nextVal]\n\t\t\t}\n\t\t}\n\t}\n\tfor (const key of Object.keys(next)) {\n\t\t// if key is in next but not in prev then it was added\n\t\tif (!(key in prev)) {\n\t\t\tif (!result) result = {}\n\t\t\tresult[key] = [ValueOpType.Put, (next as any)[key]]\n\t\t}\n\t}\n\treturn result\n}\n\nfunction diffValue(valueA: unknown, valueB: unknown): ValueOp | null {\n\tif (Object.is(valueA, valueB)) return null\n\tif (Array.isArray(valueA) && Array.isArray(valueB)) {\n\t\treturn diffArray(valueA, valueB)\n\t} else if (!valueA || !valueB || typeof valueA !== 'object' || typeof valueB !== 'object') {\n\t\treturn isEqual(valueA, valueB) ? null : [ValueOpType.Put, valueB]\n\t} else {\n\t\tconst diff = diffObject(valueA, valueB)\n\t\treturn diff ? [ValueOpType.Patch, diff] : null\n\t}\n}\n\nfunction diffArray(prevArray: unknown[], nextArray: unknown[]): PutOp | AppendOp | PatchOp | null {\n\tif (Object.is(prevArray, nextArray)) return null\n\t// if lengths are equal, check for patch operation\n\tif (prevArray.length === nextArray.length) {\n\t\t// bail out if more than len/5 items need patching\n\t\tconst maxPatchIndexes = Math.max(prevArray.length / 5, 1)\n\t\tconst toPatchIndexes = []\n\t\tfor (let i = 0; i < prevArray.length; i++) {\n\t\t\tif (!isEqual(prevArray[i], nextArray[i])) {\n\t\t\t\ttoPatchIndexes.push(i)\n\t\t\t\tif (toPatchIndexes.length > maxPatchIndexes) {\n\t\t\t\t\treturn [ValueOpType.Put, nextArray]\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tif (toPatchIndexes.length === 0) {\n\t\t\t// same length and no items changed, so no diff\n\t\t\treturn null\n\t\t}\n\t\tconst diff: ObjectDiff = {}\n\t\tfor (const i of toPatchIndexes) {\n\t\t\tconst prevItem = prevArray[i]\n\t\t\tconst nextItem = nextArray[i]\n\t\t\tif (!prevItem || !nextItem) {\n\t\t\t\tdiff[i] = [ValueOpType.Put, nextItem]\n\t\t\t} else if (typeof prevItem === 'object' && typeof nextItem === 'object') {\n\t\t\t\tconst op = diffValue(prevItem, nextItem)\n\t\t\t\tif (op) {\n\t\t\t\t\tdiff[i] = op\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tdiff[i] = [ValueOpType.Put, nextItem]\n\t\t\t}\n\t\t}\n\t\treturn [ValueOpType.Patch, diff]\n\t}\n\n\t// if lengths are not equal, check for append operation, and bail out\n\t// to replace whole array if any shared elems changed\n\tfor (let i = 0; i < prevArray.length; i++) {\n\t\tif (!isEqual(prevArray[i], nextArray[i])) {\n\t\t\treturn [ValueOpType.Put, nextArray]\n\t\t}\n\t}\n\n\treturn [ValueOpType.Append, nextArray.slice(prevArray.length), prevArray.length]\n}\n\n/**\n * Applies an ObjectDiff to an object, returning a new object with the changes applied.\n * This function handles all value operation types and creates a shallow copy when modifications\n * are needed. If no changes are required, the original object is returned.\n *\n * @param object - The object to apply the diff to\n * @param objectDiff - The ObjectDiff containing the operations to apply\n * @returns A new object with the diff applied, or the original object if no changes were needed\n *\n * @example\n * ```ts\n * const original = { x: 100, y: 200, props: { color: 'red' } }\n * const diff = {\n * x: ['put', 150],\n * props: ['patch', { color: ['put', 'blue'] }]\n * }\n *\n * const updated = applyObjectDiff(original, diff)\n * // Returns: { x: 150, y: 200, props: { color: 'blue' } }\n * ```\n *\n * @internal\n */\nexport function applyObjectDiff<T extends object>(object: T, objectDiff: ObjectDiff): T {\n\t// don't patch nulls\n\tif (!object || typeof object !== 'object') return object\n\tconst isArray = Array.isArray(object)\n\tlet newObject: any | undefined = undefined\n\tconst set = (k: any, v: any) => {\n\t\tif (!newObject) {\n\t\t\tif (isArray) {\n\t\t\t\tnewObject = [...object]\n\t\t\t} else {\n\t\t\t\tnewObject = { ...object }\n\t\t\t}\n\t\t}\n\t\tif (isArray) {\n\t\t\tnewObject[Number(k)] = v\n\t\t} else {\n\t\t\tnewObject[k] = v\n\t\t}\n\t}\n\tfor (const [key, op] of Object.entries(objectDiff)) {\n\t\tswitch (op[0]) {\n\t\t\tcase ValueOpType.Put: {\n\t\t\t\tconst value = op[1]\n\t\t\t\tif (!isEqual(object[key as keyof T], value)) {\n\t\t\t\t\tset(key, value)\n\t\t\t\t}\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tcase ValueOpType.Append: {\n\t\t\t\tconst values = op[1]\n\t\t\t\tconst offset = op[2]\n\t\t\t\tconst arr = object[key as keyof T]\n\t\t\t\tif (Array.isArray(arr) && arr.length === offset) {\n\t\t\t\t\tset(key, [...arr, ...values])\n\t\t\t\t}\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tcase ValueOpType.Patch: {\n\t\t\t\tif (object[key as keyof T] && typeof object[key as keyof T] === 'object') {\n\t\t\t\t\tconst diff = op[1]\n\t\t\t\t\tconst patched = applyObjectDiff(object[key as keyof T] as object, diff)\n\t\t\t\t\tif (patched !== object[key as keyof T]) {\n\t\t\t\t\t\tset(key, patched)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tcase ValueOpType.Delete: {\n\t\t\t\tif (key in object) {\n\t\t\t\t\tif (!newObject) {\n\t\t\t\t\t\tif (isArray) {\n\t\t\t\t\t\t\tconsole.error(\"Can't delete array item yet (this should never happen)\")\n\t\t\t\t\t\t\tnewObject = [...object]\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tnewObject = { ...object }\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tdelete newObject[key]\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\treturn newObject ?? object\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { RecordsDiff, UnknownRecord } from '@tldraw/store'\nimport { isEqual, objectMapEntries, objectMapValues } from '@tldraw/utils'\n\n/**\n * Constants representing the types of operations that can be applied to records in network diffs.\n * These operations describe how a record has been modified during synchronization.\n *\n * @internal\n */\nexport const RecordOpType = {\n\tPut: 'put',\n\tPatch: 'patch',\n\tRemove: 'remove',\n} as const\n\n/**\n * Union type of all possible record operation types.\n *\n * @internal\n */\nexport type RecordOpType = (typeof RecordOpType)[keyof typeof RecordOpType]\n\n/**\n * Represents a single operation to be applied to a record during synchronization.\n *\n * @param R - The record type being operated on\n *\n * @internal\n */\nexport type RecordOp<R extends UnknownRecord> =\n\t| [typeof RecordOpType.Put, R]\n\t| [typeof RecordOpType.Patch, ObjectDiff]\n\t| [typeof RecordOpType.Remove]\n\n/**\n * A one-way (non-reversible) diff designed for small json footprint. These are mainly intended to\n * be sent over the wire. Either as push requests from the client to the server, or as patch\n * operations in the opposite direction.\n *\n * Each key in this object is the id of a record that has been added, updated, or removed.\n *\n * @internal\n */\nexport interface NetworkDiff<R extends UnknownRecord> {\n\t[id: string]: RecordOp<R>\n}\n\n/**\n * Converts a (reversible, verbose) RecordsDiff into a (non-reversible, concise) NetworkDiff\n * suitable for transmission over the network. This function optimizes the diff representation\n * for minimal bandwidth usage while maintaining all necessary change information.\n *\n * @param diff - The RecordsDiff containing added, updated, and removed records\n * @returns A compact NetworkDiff for network transmission, or null if no changes exist\n *\n * @example\n * ```ts\n * const recordsDiff = {\n * added: { 'shape:1': newShape },\n * updated: { 'shape:2': [oldShape, updatedShape] },\n * removed: { 'shape:3': removedShape }\n * }\n *\n * const networkDiff = getNetworkDiff(recordsDiff)\n * // Returns: {\n * // 'shape:1': ['put', newShape],\n * // 'shape:2': ['patch', { x: ['put', 100] }],\n * // 'shape:3': ['remove']\n * // }\n * ```\n *\n * @internal\n */\nexport function getNetworkDiff<R extends UnknownRecord>(\n\tdiff: RecordsDiff<R>\n): NetworkDiff<R> | null {\n\tlet res: NetworkDiff<R> | null = null\n\n\tfor (const [k, v] of objectMapEntries(diff.added)) {\n\t\tif (!res) res = {}\n\t\tres[k] = [RecordOpType.Put, v]\n\t}\n\n\tfor (const [from, to] of objectMapValues(diff.updated)) {\n\t\tconst diff = diffRecord(from, to)\n\t\tif (diff) {\n\t\t\tif (!res) res = {}\n\t\t\tres[to.id] = [RecordOpType.Patch, diff]\n\t\t}\n\t}\n\n\tfor (const removed of Object.keys(diff.removed)) {\n\t\tif (!res) res = {}\n\t\tres[removed] = [RecordOpType.Remove]\n\t}\n\n\treturn res\n}\n\n/**\n * Constants representing the types of operations that can be applied to individual values\n * within object diffs. These operations describe how object properties have changed.\n *\n * @internal\n */\nexport const ValueOpType = {\n\tPut: 'put',\n\tDelete: 'delete',\n\tAppend: 'append',\n\tPatch: 'patch',\n} as const\n/**\n * Union type of all possible value operation types.\n *\n * @internal\n */\nexport type ValueOpType = (typeof ValueOpType)[keyof typeof ValueOpType]\n\n/**\n * Operation that replaces a value entirely with a new value.\n *\n * @internal\n */\nexport type PutOp = [type: typeof ValueOpType.Put, value: unknown]\n/**\n * Operation that appends new values to the end of an array or string.\n *\n * @internal\n */\nexport type AppendOp = [type: typeof ValueOpType.Append, value: unknown[] | string, offset: number]\n/**\n * Operation that applies a nested diff to an object or array.\n *\n * @internal\n */\nexport type PatchOp = [type: typeof ValueOpType.Patch, diff: ObjectDiff]\n/**\n * Operation that removes a property from an object.\n *\n * @internal\n */\nexport type DeleteOp = [type: typeof ValueOpType.Delete]\n\n/**\n * Union type representing any value operation that can be applied during diffing.\n *\n * @internal\n */\nexport type ValueOp = PutOp | AppendOp | PatchOp | DeleteOp\n\n/**\n * Represents the differences between two objects as a mapping of property names\n * to the operations needed to transform one object into another.\n *\n * @internal\n */\nexport interface ObjectDiff {\n\t[k: string]: ValueOp\n}\n\n/**\n * Computes the difference between two record objects, generating an ObjectDiff\n * that describes how to transform the previous record into the next record.\n * This function is optimized for tldraw records and treats 'props' as a nested object.\n *\n * @param prev - The previous version of the record\n * @param next - The next version of the record\n * @param legacyAppendMode - If true, string append operations will be converted to Put operations\n * @returns An ObjectDiff describing the changes, or null if no changes exist\n *\n * @example\n * ```ts\n * const oldShape = { id: 'shape:1', x: 100, y: 200, props: { color: 'red' } }\n * const newShape = { id: 'shape:1', x: 150, y: 200, props: { color: 'blue' } }\n *\n * const diff = diffRecord(oldShape, newShape)\n * // Returns: {\n * // x: ['put', 150],\n * // props: ['patch', { color: ['put', 'blue'] }]\n * // }\n * ```\n *\n * @internal\n */\nexport function diffRecord(\n\tprev: object,\n\tnext: object,\n\tlegacyAppendMode = false\n): ObjectDiff | null {\n\treturn diffObject(prev, next, new Set(['props', 'meta']), legacyAppendMode)\n}\n\nfunction diffObject(\n\tprev: object,\n\tnext: object,\n\tnestedKeys: Set<string> | undefined,\n\tlegacyAppendMode: boolean\n): ObjectDiff | null {\n\tif (prev === next) {\n\t\treturn null\n\t}\n\tlet result: ObjectDiff | null = null\n\tfor (const key of Object.keys(prev)) {\n\t\t// if key is not in next then it was deleted\n\t\tif (!(key in next)) {\n\t\t\tif (!result) result = {}\n\t\t\tresult[key] = [ValueOpType.Delete]\n\t\t\tcontinue\n\t\t}\n\t\tconst prevValue = (prev as any)[key]\n\t\tconst nextValue = (next as any)[key]\n\t\tif (\n\t\t\tnestedKeys?.has(key) ||\n\t\t\t(Array.isArray(prevValue) && Array.isArray(nextValue)) ||\n\t\t\t(typeof prevValue === 'string' && typeof nextValue === 'string')\n\t\t) {\n\t\t\t// if key is in both places, then compare values\n\t\t\tconst diff = diffValue(prevValue, nextValue, legacyAppendMode)\n\t\t\tif (diff) {\n\t\t\t\tif (!result) result = {}\n\t\t\t\tresult[key] = diff\n\t\t\t}\n\t\t} else if (!isEqual(prevValue, nextValue)) {\n\t\t\tif (!result) result = {}\n\t\t\tresult[key] = [ValueOpType.Put, nextValue]\n\t\t}\n\t}\n\tfor (const key of Object.keys(next)) {\n\t\t// if key is in next but not in prev then it was added\n\t\tif (!(key in prev)) {\n\t\t\tif (!result) result = {}\n\t\t\tresult[key] = [ValueOpType.Put, (next as any)[key]]\n\t\t}\n\t}\n\treturn result\n}\n\nfunction diffValue(valueA: unknown, valueB: unknown, legacyAppendMode: boolean): ValueOp | null {\n\tif (Object.is(valueA, valueB)) return null\n\tif (Array.isArray(valueA) && Array.isArray(valueB)) {\n\t\treturn diffArray(valueA, valueB, legacyAppendMode)\n\t} else if (typeof valueA === 'string' && typeof valueB === 'string') {\n\t\tif (!legacyAppendMode && valueB.startsWith(valueA)) {\n\t\t\tconst appendedText = valueB.slice(valueA.length)\n\t\t\treturn [ValueOpType.Append, appendedText, valueA.length]\n\t\t}\n\t\treturn [ValueOpType.Put, valueB]\n\t} else if (!valueA || !valueB || typeof valueA !== 'object' || typeof valueB !== 'object') {\n\t\treturn isEqual(valueA, valueB) ? null : [ValueOpType.Put, valueB]\n\t} else {\n\t\tconst diff = diffObject(valueA, valueB, undefined, legacyAppendMode)\n\t\treturn diff ? [ValueOpType.Patch, diff] : null\n\t}\n}\n\nfunction diffArray(\n\tprevArray: unknown[],\n\tnextArray: unknown[],\n\tlegacyAppendMode: boolean\n): PutOp | AppendOp | PatchOp | null {\n\tif (Object.is(prevArray, nextArray)) return null\n\t// if lengths are equal, check for patch operation\n\tif (prevArray.length === nextArray.length) {\n\t\t// bail out if more than len/5 items need patching\n\t\tconst maxPatchIndexes = Math.max(prevArray.length / 5, 1)\n\t\tconst toPatchIndexes = []\n\t\tfor (let i = 0; i < prevArray.length; i++) {\n\t\t\tif (!isEqual(prevArray[i], nextArray[i])) {\n\t\t\t\ttoPatchIndexes.push(i)\n\t\t\t\tif (toPatchIndexes.length > maxPatchIndexes) {\n\t\t\t\t\treturn [ValueOpType.Put, nextArray]\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tif (toPatchIndexes.length === 0) {\n\t\t\t// same length and no items changed, so no diff\n\t\t\treturn null\n\t\t}\n\t\tconst diff: ObjectDiff = {}\n\t\tfor (const i of toPatchIndexes) {\n\t\t\tconst prevItem = prevArray[i]\n\t\t\tconst nextItem = nextArray[i]\n\t\t\tif (!prevItem || !nextItem) {\n\t\t\t\tdiff[i] = [ValueOpType.Put, nextItem]\n\t\t\t} else if (typeof prevItem === 'object' && typeof nextItem === 'object') {\n\t\t\t\tconst op = diffValue(prevItem, nextItem, legacyAppendMode)\n\t\t\t\tif (op) {\n\t\t\t\t\tdiff[i] = op\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tdiff[i] = [ValueOpType.Put, nextItem]\n\t\t\t}\n\t\t}\n\t\treturn [ValueOpType.Patch, diff]\n\t}\n\n\t// if lengths are not equal, check for append operation, and bail out\n\t// to replace whole array if any shared elems changed\n\tfor (let i = 0; i < prevArray.length; i++) {\n\t\tif (!isEqual(prevArray[i], nextArray[i])) {\n\t\t\treturn [ValueOpType.Put, nextArray]\n\t\t}\n\t}\n\n\treturn [ValueOpType.Append, nextArray.slice(prevArray.length), prevArray.length]\n}\n\n/**\n * Applies an ObjectDiff to an object, returning a new object with the changes applied.\n * This function handles all value operation types and creates a shallow copy when modifications\n * are needed. If no changes are required, the original object is returned.\n *\n * @param object - The object to apply the diff to\n * @param objectDiff - The ObjectDiff containing the operations to apply\n * @returns A new object with the diff applied, or the original object if no changes were needed\n *\n * @example\n * ```ts\n * const original = { x: 100, y: 200, props: { color: 'red' } }\n * const diff = {\n * x: ['put', 150],\n * props: ['patch', { color: ['put', 'blue'] }]\n * }\n *\n * const updated = applyObjectDiff(original, diff)\n * // Returns: { x: 150, y: 200, props: { color: 'blue' } }\n * ```\n *\n * @internal\n */\nexport function applyObjectDiff<T extends object>(object: T, objectDiff: ObjectDiff): T {\n\t// don't patch nulls\n\tif (!object || typeof object !== 'object') return object\n\tconst isArray = Array.isArray(object)\n\tlet newObject: any | undefined = undefined\n\tconst set = (k: any, v: any) => {\n\t\tif (!newObject) {\n\t\t\tif (isArray) {\n\t\t\t\tnewObject = [...object]\n\t\t\t} else {\n\t\t\t\tnewObject = { ...object }\n\t\t\t}\n\t\t}\n\t\tif (isArray) {\n\t\t\tnewObject[Number(k)] = v\n\t\t} else {\n\t\t\tnewObject[k] = v\n\t\t}\n\t}\n\tfor (const [key, op] of Object.entries(objectDiff)) {\n\t\tswitch (op[0]) {\n\t\t\tcase ValueOpType.Put: {\n\t\t\t\tconst value = op[1]\n\t\t\t\tif (!isEqual(object[key as keyof T], value)) {\n\t\t\t\t\tset(key, value)\n\t\t\t\t}\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tcase ValueOpType.Append: {\n\t\t\t\tconst value = op[1]\n\t\t\t\tconst offset = op[2]\n\t\t\t\tconst currentValue = object[key as keyof T]\n\t\t\t\tif (Array.isArray(currentValue) && Array.isArray(value) && currentValue.length === offset) {\n\t\t\t\t\tset(key, [...currentValue, ...value])\n\t\t\t\t} else if (\n\t\t\t\t\ttypeof currentValue === 'string' &&\n\t\t\t\t\ttypeof value === 'string' &&\n\t\t\t\t\tcurrentValue.length === offset\n\t\t\t\t) {\n\t\t\t\t\tset(key, currentValue + value)\n\t\t\t\t}\n\t\t\t\t// If validation fails (type mismatch or length mismatch), silently ignore\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tcase ValueOpType.Patch: {\n\t\t\t\tif (object[key as keyof T] && typeof object[key as keyof T] === 'object') {\n\t\t\t\t\tconst diff = op[1]\n\t\t\t\t\tconst patched = applyObjectDiff(object[key as keyof T] as object, diff)\n\t\t\t\t\tif (patched !== object[key as keyof T]) {\n\t\t\t\t\t\tset(key, patched)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tcase ValueOpType.Delete: {\n\t\t\t\tif (key in object) {\n\t\t\t\t\tif (!newObject) {\n\t\t\t\t\t\tif (isArray) {\n\t\t\t\t\t\t\tconsole.error(\"Can't delete array item yet (this should never happen)\")\n\t\t\t\t\t\t\tnewObject = [...object]\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tnewObject = { ...object }\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tdelete newObject[key]\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\treturn newObject ?? object\n}\n"],
5
+ "mappings": "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",
6
6
  "names": ["diff"]
7
7
  }
@@ -1,4 +1,4 @@
1
- const TLSYNC_PROTOCOL_VERSION = 7;
1
+ const TLSYNC_PROTOCOL_VERSION = 8;
2
2
  function getTlsyncProtocolVersion() {
3
3
  return TLSYNC_PROTOCOL_VERSION;
4
4
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/lib/protocol.ts"],
4
- "sourcesContent": ["import { SerializedSchema, UnknownRecord } from '@tldraw/store'\nimport { NetworkDiff, ObjectDiff, RecordOpType } from './diff'\n\nconst TLSYNC_PROTOCOL_VERSION = 7\n\n/**\n * Gets the current tldraw sync protocol version number.\n *\n * This version number is used during WebSocket connection handshake to ensure\n * client and server compatibility. When versions don't match, the connection\n * will be rejected with an incompatibility error.\n *\n * @returns The current protocol version number\n *\n * @example\n * ```ts\n * const version = getTlsyncProtocolVersion()\n * console.log(`Using protocol version: ${version}`)\n * ```\n *\n * @internal\n */\nexport function getTlsyncProtocolVersion() {\n\treturn TLSYNC_PROTOCOL_VERSION\n}\n\n/**\n * Constants defining the different types of protocol incompatibility reasons.\n *\n * These values indicate why a client-server connection was rejected due to\n * version or compatibility issues. Each reason helps diagnose specific problems\n * during the connection handshake.\n *\n * @example\n * ```ts\n * if (error.reason === TLIncompatibilityReason.ClientTooOld) {\n * showUpgradeMessage('Please update your client')\n * }\n * ```\n *\n * @internal\n * @deprecated Replaced by websocket .close status/reason\n */\nexport const TLIncompatibilityReason = {\n\tClientTooOld: 'clientTooOld',\n\tServerTooOld: 'serverTooOld',\n\tInvalidRecord: 'invalidRecord',\n\tInvalidOperation: 'invalidOperation',\n} as const\n\n/**\n * Union type representing all possible incompatibility reason values.\n *\n * This type represents the different reasons why a client-server connection\n * might fail due to protocol or version mismatches.\n *\n * @example\n * ```ts\n * function handleIncompatibility(reason: TLIncompatibilityReason) {\n * switch (reason) {\n * case 'clientTooOld':\n * return 'Client needs to be updated'\n * case 'serverTooOld':\n * return 'Server needs to be updated'\n * }\n * }\n * ```\n *\n * @internal\n * @deprecated replaced by websocket .close status/reason\n */\nexport type TLIncompatibilityReason =\n\t(typeof TLIncompatibilityReason)[keyof typeof TLIncompatibilityReason]\n\n/**\n * Union type representing all possible message types that can be sent from server to client.\n *\n * This encompasses the complete set of server-originated WebSocket messages in the tldraw\n * sync protocol, including connection establishment, data synchronization, and error handling.\n *\n * @param R - The record type being synchronized (extends UnknownRecord)\n *\n * @example\n * ```ts\n * syncClient.onReceiveMessage((message: TLSocketServerSentEvent<MyRecord>) => {\n * switch (message.type) {\n * case 'connect':\n * console.log('Connected to room with clock:', message.serverClock)\n * break\n * case 'data':\n * console.log('Received data updates:', message.data)\n * break\n * }\n * })\n * ```\n *\n * @internal\n */\nexport type TLSocketServerSentEvent<R extends UnknownRecord> =\n\t| {\n\t\t\ttype: 'connect'\n\t\t\thydrationType: 'wipe_all' | 'wipe_presence'\n\t\t\tconnectRequestId: string\n\t\t\tprotocolVersion: number\n\t\t\tschema: SerializedSchema\n\t\t\tdiff: NetworkDiff<R>\n\t\t\tserverClock: number\n\t\t\tisReadonly: boolean\n\t }\n\t| {\n\t\t\ttype: 'incompatibility_error'\n\t\t\t// eslint-disable-next-line @typescript-eslint/no-deprecated\n\t\t\treason: TLIncompatibilityReason\n\t }\n\t| {\n\t\t\ttype: 'pong'\n\t }\n\t| { type: 'data'; data: TLSocketServerSentDataEvent<R>[] }\n\t| { type: 'custom'; data: any }\n\t| TLSocketServerSentDataEvent<R>\n\n/**\n * Union type representing data-related messages sent from server to client.\n *\n * These messages handle the core synchronization operations: applying patches from\n * other clients and confirming the results of client push operations.\n *\n * @param R - The record type being synchronized (extends UnknownRecord)\n *\n * @example\n * ```ts\n * function handleDataEvent(event: TLSocketServerSentDataEvent<MyRecord>) {\n * if (event.type === 'patch') {\n * // Apply changes from other clients\n * applyNetworkDiff(event.diff)\n * } else if (event.type === 'push_result') {\n * // Handle result of our push request\n * if (event.action === 'commit') {\n * console.log('Changes accepted by server')\n * }\n * }\n * }\n * ```\n *\n * @internal\n */\nexport type TLSocketServerSentDataEvent<R extends UnknownRecord> =\n\t| {\n\t\t\ttype: 'patch'\n\t\t\tdiff: NetworkDiff<R>\n\t\t\tserverClock: number\n\t }\n\t| {\n\t\t\ttype: 'push_result'\n\t\t\tclientClock: number\n\t\t\tserverClock: number\n\t\t\taction: 'discard' | 'commit' | { rebaseWithDiff: NetworkDiff<R> }\n\t }\n\n/**\n * Interface defining a client-to-server push request message.\n *\n * Push requests are sent when the client wants to synchronize local changes\n * with the server. They contain document changes and optionally presence updates\n * (like cursor position or user selection).\n *\n * @param R - The record type being synchronized (extends UnknownRecord)\n *\n * @example\n * ```ts\n * const pushRequest: TLPushRequest<MyRecord> = {\n * type: 'push',\n * clientClock: 15,\n * diff: {\n * 'shape:abc123': [RecordOpType.Patch, { x: [ValueOpType.Put, 100] }]\n * },\n * presence: [RecordOpType.Put, { cursor: { x: 150, y: 200 } }]\n * }\n * socket.sendMessage(pushRequest)\n * ```\n *\n * @internal\n */\nexport interface TLPushRequest<R extends UnknownRecord> {\n\ttype: 'push'\n\tclientClock: number\n\tdiff?: NetworkDiff<R>\n\tpresence?: [typeof RecordOpType.Patch, ObjectDiff] | [typeof RecordOpType.Put, R]\n}\n\n/**\n * Interface defining a client-to-server connection request message.\n *\n * This message initiates a WebSocket connection to a sync room. It includes\n * the client's schema, protocol version, and last known server clock for\n * proper synchronization state management.\n *\n * @example\n * ```ts\n * const connectRequest: TLConnectRequest = {\n * type: 'connect',\n * connectRequestId: 'conn-123',\n * lastServerClock: 42,\n * protocolVersion: getTlsyncProtocolVersion(),\n * schema: mySchema.serialize()\n * }\n * socket.sendMessage(connectRequest)\n * ```\n *\n * @internal\n */\nexport interface TLConnectRequest {\n\ttype: 'connect'\n\tconnectRequestId: string\n\tlastServerClock: number\n\tprotocolVersion: number\n\tschema: SerializedSchema\n}\n\n/**\n * Interface defining a client-to-server ping request message.\n *\n * Ping requests are used to measure network latency and ensure the connection\n * is still active. The server responds with a 'pong' message.\n *\n * @example\n * ```ts\n * const pingRequest: TLPingRequest = { type: 'ping' }\n * socket.sendMessage(pingRequest)\n *\n * // Server will respond with { type: 'pong' }\n * ```\n *\n * @internal\n */\nexport interface TLPingRequest {\n\ttype: 'ping'\n}\n\n/**\n * Union type representing all possible message types that can be sent from client to server.\n *\n * This encompasses the complete set of client-originated WebSocket messages in the tldraw\n * sync protocol, covering connection establishment, data synchronization, and connectivity checks.\n *\n * @param R - The record type being synchronized (extends UnknownRecord)\n *\n * @example\n * ```ts\n * function sendMessage(message: TLSocketClientSentEvent<MyRecord>) {\n * switch (message.type) {\n * case 'connect':\n * console.log('Establishing connection...')\n * break\n * case 'push':\n * console.log('Pushing changes:', message.diff)\n * break\n * case 'ping':\n * console.log('Checking connection latency')\n * break\n * }\n * socket.send(JSON.stringify(message))\n * }\n * ```\n *\n * @internal\n */\nexport type TLSocketClientSentEvent<R extends UnknownRecord> =\n\t| TLPushRequest<R>\n\t| TLConnectRequest\n\t| TLPingRequest\n"],
4
+ "sourcesContent": ["import { SerializedSchema, UnknownRecord } from '@tldraw/store'\nimport { NetworkDiff, ObjectDiff, RecordOpType } from './diff'\n\nconst TLSYNC_PROTOCOL_VERSION = 8\n\n/**\n * Gets the current tldraw sync protocol version number.\n *\n * This version number is used during WebSocket connection handshake to ensure\n * client and server compatibility. When versions don't match, the connection\n * will be rejected with an incompatibility error.\n *\n * @returns The current protocol version number\n *\n * @example\n * ```ts\n * const version = getTlsyncProtocolVersion()\n * console.log(`Using protocol version: ${version}`)\n * ```\n *\n * @internal\n */\nexport function getTlsyncProtocolVersion() {\n\treturn TLSYNC_PROTOCOL_VERSION\n}\n\n/**\n * Constants defining the different types of protocol incompatibility reasons.\n *\n * These values indicate why a client-server connection was rejected due to\n * version or compatibility issues. Each reason helps diagnose specific problems\n * during the connection handshake.\n *\n * @example\n * ```ts\n * if (error.reason === TLIncompatibilityReason.ClientTooOld) {\n * showUpgradeMessage('Please update your client')\n * }\n * ```\n *\n * @internal\n * @deprecated Replaced by websocket .close status/reason\n */\nexport const TLIncompatibilityReason = {\n\tClientTooOld: 'clientTooOld',\n\tServerTooOld: 'serverTooOld',\n\tInvalidRecord: 'invalidRecord',\n\tInvalidOperation: 'invalidOperation',\n} as const\n\n/**\n * Union type representing all possible incompatibility reason values.\n *\n * This type represents the different reasons why a client-server connection\n * might fail due to protocol or version mismatches.\n *\n * @example\n * ```ts\n * function handleIncompatibility(reason: TLIncompatibilityReason) {\n * switch (reason) {\n * case 'clientTooOld':\n * return 'Client needs to be updated'\n * case 'serverTooOld':\n * return 'Server needs to be updated'\n * }\n * }\n * ```\n *\n * @internal\n * @deprecated replaced by websocket .close status/reason\n */\nexport type TLIncompatibilityReason =\n\t(typeof TLIncompatibilityReason)[keyof typeof TLIncompatibilityReason]\n\n/**\n * Union type representing all possible message types that can be sent from server to client.\n *\n * This encompasses the complete set of server-originated WebSocket messages in the tldraw\n * sync protocol, including connection establishment, data synchronization, and error handling.\n *\n * @param R - The record type being synchronized (extends UnknownRecord)\n *\n * @example\n * ```ts\n * syncClient.onReceiveMessage((message: TLSocketServerSentEvent<MyRecord>) => {\n * switch (message.type) {\n * case 'connect':\n * console.log('Connected to room with clock:', message.serverClock)\n * break\n * case 'data':\n * console.log('Received data updates:', message.data)\n * break\n * }\n * })\n * ```\n *\n * @internal\n */\nexport type TLSocketServerSentEvent<R extends UnknownRecord> =\n\t| {\n\t\t\ttype: 'connect'\n\t\t\thydrationType: 'wipe_all' | 'wipe_presence'\n\t\t\tconnectRequestId: string\n\t\t\tprotocolVersion: number\n\t\t\tschema: SerializedSchema\n\t\t\tdiff: NetworkDiff<R>\n\t\t\tserverClock: number\n\t\t\tisReadonly: boolean\n\t }\n\t| {\n\t\t\ttype: 'incompatibility_error'\n\t\t\t// eslint-disable-next-line @typescript-eslint/no-deprecated\n\t\t\treason: TLIncompatibilityReason\n\t }\n\t| {\n\t\t\ttype: 'pong'\n\t }\n\t| { type: 'data'; data: TLSocketServerSentDataEvent<R>[] }\n\t| { type: 'custom'; data: any }\n\t| TLSocketServerSentDataEvent<R>\n\n/**\n * Union type representing data-related messages sent from server to client.\n *\n * These messages handle the core synchronization operations: applying patches from\n * other clients and confirming the results of client push operations.\n *\n * @param R - The record type being synchronized (extends UnknownRecord)\n *\n * @example\n * ```ts\n * function handleDataEvent(event: TLSocketServerSentDataEvent<MyRecord>) {\n * if (event.type === 'patch') {\n * // Apply changes from other clients\n * applyNetworkDiff(event.diff)\n * } else if (event.type === 'push_result') {\n * // Handle result of our push request\n * if (event.action === 'commit') {\n * console.log('Changes accepted by server')\n * }\n * }\n * }\n * ```\n *\n * @internal\n */\nexport type TLSocketServerSentDataEvent<R extends UnknownRecord> =\n\t| {\n\t\t\ttype: 'patch'\n\t\t\tdiff: NetworkDiff<R>\n\t\t\tserverClock: number\n\t }\n\t| {\n\t\t\ttype: 'push_result'\n\t\t\tclientClock: number\n\t\t\tserverClock: number\n\t\t\taction: 'discard' | 'commit' | { rebaseWithDiff: NetworkDiff<R> }\n\t }\n\n/**\n * Interface defining a client-to-server push request message.\n *\n * Push requests are sent when the client wants to synchronize local changes\n * with the server. They contain document changes and optionally presence updates\n * (like cursor position or user selection).\n *\n * @param R - The record type being synchronized (extends UnknownRecord)\n *\n * @example\n * ```ts\n * const pushRequest: TLPushRequest<MyRecord> = {\n * type: 'push',\n * clientClock: 15,\n * diff: {\n * 'shape:abc123': [RecordOpType.Patch, { x: [ValueOpType.Put, 100] }]\n * },\n * presence: [RecordOpType.Put, { cursor: { x: 150, y: 200 } }]\n * }\n * socket.sendMessage(pushRequest)\n * ```\n *\n * @internal\n */\nexport interface TLPushRequest<R extends UnknownRecord> {\n\ttype: 'push'\n\tclientClock: number\n\tdiff?: NetworkDiff<R>\n\tpresence?: [typeof RecordOpType.Patch, ObjectDiff] | [typeof RecordOpType.Put, R]\n}\n\n/**\n * Interface defining a client-to-server connection request message.\n *\n * This message initiates a WebSocket connection to a sync room. It includes\n * the client's schema, protocol version, and last known server clock for\n * proper synchronization state management.\n *\n * @example\n * ```ts\n * const connectRequest: TLConnectRequest = {\n * type: 'connect',\n * connectRequestId: 'conn-123',\n * lastServerClock: 42,\n * protocolVersion: getTlsyncProtocolVersion(),\n * schema: mySchema.serialize()\n * }\n * socket.sendMessage(connectRequest)\n * ```\n *\n * @internal\n */\nexport interface TLConnectRequest {\n\ttype: 'connect'\n\tconnectRequestId: string\n\tlastServerClock: number\n\tprotocolVersion: number\n\tschema: SerializedSchema\n}\n\n/**\n * Interface defining a client-to-server ping request message.\n *\n * Ping requests are used to measure network latency and ensure the connection\n * is still active. The server responds with a 'pong' message.\n *\n * @example\n * ```ts\n * const pingRequest: TLPingRequest = { type: 'ping' }\n * socket.sendMessage(pingRequest)\n *\n * // Server will respond with { type: 'pong' }\n * ```\n *\n * @internal\n */\nexport interface TLPingRequest {\n\ttype: 'ping'\n}\n\n/**\n * Union type representing all possible message types that can be sent from client to server.\n *\n * This encompasses the complete set of client-originated WebSocket messages in the tldraw\n * sync protocol, covering connection establishment, data synchronization, and connectivity checks.\n *\n * @param R - The record type being synchronized (extends UnknownRecord)\n *\n * @example\n * ```ts\n * function sendMessage(message: TLSocketClientSentEvent<MyRecord>) {\n * switch (message.type) {\n * case 'connect':\n * console.log('Establishing connection...')\n * break\n * case 'push':\n * console.log('Pushing changes:', message.diff)\n * break\n * case 'ping':\n * console.log('Checking connection latency')\n * break\n * }\n * socket.send(JSON.stringify(message))\n * }\n * ```\n *\n * @internal\n */\nexport type TLSocketClientSentEvent<R extends UnknownRecord> =\n\t| TLPushRequest<R>\n\t| TLConnectRequest\n\t| TLPingRequest\n"],
5
5
  "mappings": "AAGA,MAAM,0BAA0B;AAmBzB,SAAS,2BAA2B;AAC1C,SAAO;AACR;AAmBO,MAAM,0BAA0B;AAAA,EACtC,cAAc;AAAA,EACd,cAAc;AAAA,EACd,eAAe;AAAA,EACf,kBAAkB;AACnB;",
6
6
  "names": []
7
7
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@tldraw/sync-core",
3
3
  "description": "tldraw infinite canvas SDK (multiplayer sync).",
4
- "version": "4.2.0-next.47462e908ff5",
4
+ "version": "4.2.0-next.54bc357bbff2",
5
5
  "author": {
6
6
  "name": "tldraw GB Ltd.",
7
7
  "email": "hello@tldraw.com"
@@ -48,17 +48,17 @@
48
48
  "@types/uuid-readable": "^0.0.3",
49
49
  "react": "^18.3.1",
50
50
  "react-dom": "^18.3.1",
51
- "tldraw": "4.2.0-next.47462e908ff5",
51
+ "tldraw": "4.2.0-next.54bc357bbff2",
52
52
  "typescript": "^5.8.3",
53
53
  "uuid-by-string": "^4.0.0",
54
54
  "uuid-readable": "^0.0.2",
55
55
  "vitest": "^3.2.4"
56
56
  },
57
57
  "dependencies": {
58
- "@tldraw/state": "4.2.0-next.47462e908ff5",
59
- "@tldraw/store": "4.2.0-next.47462e908ff5",
60
- "@tldraw/tlschema": "4.2.0-next.47462e908ff5",
61
- "@tldraw/utils": "4.2.0-next.47462e908ff5",
58
+ "@tldraw/state": "4.2.0-next.54bc357bbff2",
59
+ "@tldraw/store": "4.2.0-next.54bc357bbff2",
60
+ "@tldraw/tlschema": "4.2.0-next.54bc357bbff2",
61
+ "@tldraw/utils": "4.2.0-next.54bc357bbff2",
62
62
  "nanoevents": "^7.0.1",
63
63
  "ws": "^8.18.0"
64
64
  },
package/src/index.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { registerTldrawLibraryVersion } from '@tldraw/utils'
2
- export { chunk } from './lib/chunk'
2
+ export { chunk, JsonChunkAssembler } from './lib/chunk'
3
3
  export { ClientWebSocketAdapter, ReconnectManager } from './lib/ClientWebSocketAdapter'
4
4
  export {
5
5
  applyObjectDiff,
@@ -26,7 +26,7 @@ export {
26
26
  type TLSocketServerSentDataEvent,
27
27
  type TLSocketServerSentEvent,
28
28
  } from './lib/protocol'
29
- export { RoomSessionState, type RoomSession } from './lib/RoomSession'
29
+ export { RoomSessionState, type RoomSession, type RoomSessionBase } from './lib/RoomSession'
30
30
  export type { PersistedRoomSnapshotForSupabase } from './lib/server-types'
31
31
  export type { WebSocketMinimal } from './lib/ServerSocketAdapter'
32
32
  export { TLRemoteSyncError } from './lib/TLRemoteSyncError'
@@ -40,7 +40,7 @@ export {
40
40
  type TLPersistentClientSocket,
41
41
  type TLPersistentClientSocketStatus,
42
42
  type TLPresenceMode,
43
- type TlSocketStatusChangeEvent,
43
+ type TLSocketStatusChangeEvent,
44
44
  type TLSocketStatusListener,
45
45
  } from './lib/TLSyncClient'
46
46
  export {
@@ -72,7 +72,10 @@ function debug(...args: any[]) {
72
72
  * }
73
73
  * ```
74
74
  */
75
- export class ClientWebSocketAdapter implements TLPersistentClientSocket<TLRecord> {
75
+ export class ClientWebSocketAdapter
76
+ implements
77
+ TLPersistentClientSocket<TLSocketClientSentEvent<TLRecord>, TLSocketServerSentEvent<TLRecord>>
78
+ {
76
79
  _ws: WebSocket | null = null
77
80
 
78
81
  isDisposed = false
@@ -54,6 +54,7 @@ describe('RoomSession state transitions', () => {
54
54
  const initialSession: RoomSession<TLRecord, { userId: string }> = {
55
55
  state: RoomSessionState.AwaitingConnectMessage,
56
56
  sessionStartTime: Date.now(),
57
+ supportsStringAppend: true,
57
58
  ...baseSessionData,
58
59
  }
59
60
 
@@ -67,6 +68,7 @@ describe('RoomSession state transitions', () => {
67
68
  isReadonly: initialSession.isReadonly,
68
69
  requiresLegacyRejection: initialSession.requiresLegacyRejection,
69
70
  serializedSchema: mockSerializedSchema,
71
+ supportsStringAppend: true,
70
72
  lastInteractionTime: Date.now(),
71
73
  debounceTimer: null,
72
74
  outstandingDataMessages: [],
@@ -81,6 +83,7 @@ describe('RoomSession state transitions', () => {
81
83
  meta: connectedSession.meta,
82
84
  isReadonly: connectedSession.isReadonly,
83
85
  requiresLegacyRejection: connectedSession.requiresLegacyRejection,
86
+ supportsStringAppend: connectedSession.supportsStringAppend,
84
87
  cancellationTime: Date.now(),
85
88
  }
86
89
 
@@ -64,6 +64,28 @@ export const SESSION_REMOVAL_WAIT_TIME = 5000
64
64
  */
65
65
  export const SESSION_IDLE_TIMEOUT = 20000
66
66
 
67
+ /**
68
+ * Base properties shared by all room session states.
69
+ *
70
+ * @internal
71
+ */
72
+ export interface RoomSessionBase<R extends UnknownRecord, Meta> {
73
+ /** Unique identifier for this session */
74
+ sessionId: string
75
+ /** Presence identifier for live cursor/selection tracking, if available */
76
+ presenceId: string | null
77
+ /** WebSocket connection wrapper for this session */
78
+ socket: TLRoomSocket<R>
79
+ /** Custom metadata associated with this session */
80
+ meta: Meta
81
+ /** Whether this session has read-only permissions */
82
+ isReadonly: boolean
83
+ /** Whether this session requires legacy protocol rejection handling */
84
+ requiresLegacyRejection: boolean
85
+ /** Whether this session supports string append operations */
86
+ supportsStringAppend: boolean
87
+ }
88
+
67
89
  /**
68
90
  * Represents a client session within a collaborative room, tracking the connection
69
91
  * state, permissions, and synchronization details for a single user.
@@ -93,51 +115,21 @@ export const SESSION_IDLE_TIMEOUT = 20000
93
115
  * @internal
94
116
  */
95
117
  export type RoomSession<R extends UnknownRecord, Meta> =
96
- | {
118
+ | (RoomSessionBase<R, Meta> & {
97
119
  /** Current state of the session */
98
120
  state: typeof RoomSessionState.AwaitingConnectMessage
99
- /** Unique identifier for this session */
100
- sessionId: string
101
- /** Presence identifier for live cursor/selection tracking, if available */
102
- presenceId: string | null
103
- /** WebSocket connection wrapper for this session */
104
- socket: TLRoomSocket<R>
105
121
  /** Timestamp when the session was created */
106
122
  sessionStartTime: number
107
- /** Custom metadata associated with this session */
108
- meta: Meta
109
- /** Whether this session has read-only permissions */
110
- isReadonly: boolean
111
- /** Whether this session requires legacy protocol rejection handling */
112
- requiresLegacyRejection: boolean
113
- }
114
- | {
123
+ })
124
+ | (RoomSessionBase<R, Meta> & {
115
125
  /** Current state of the session */
116
126
  state: typeof RoomSessionState.AwaitingRemoval
117
- /** Unique identifier for this session */
118
- sessionId: string
119
- /** Presence identifier for live cursor/selection tracking, if available */
120
- presenceId: string | null
121
- /** WebSocket connection wrapper for this session */
122
- socket: TLRoomSocket<R>
123
127
  /** Timestamp when the session was marked for removal */
124
128
  cancellationTime: number
125
- /** Custom metadata associated with this session */
126
- meta: Meta
127
- /** Whether this session has read-only permissions */
128
- isReadonly: boolean
129
- /** Whether this session requires legacy protocol rejection handling */
130
- requiresLegacyRejection: boolean
131
- }
132
- | {
129
+ })
130
+ | (RoomSessionBase<R, Meta> & {
133
131
  /** Current state of the session */
134
132
  state: typeof RoomSessionState.Connected
135
- /** Unique identifier for this session */
136
- sessionId: string
137
- /** Presence identifier for live cursor/selection tracking, if available */
138
- presenceId: string | null
139
- /** WebSocket connection wrapper for this session */
140
- socket: TLRoomSocket<R>
141
133
  /** Serialized schema information for this connected session */
142
134
  serializedSchema: SerializedSchema
143
135
  /** Timestamp of the last interaction or message from this session */
@@ -146,10 +138,4 @@ export type RoomSession<R extends UnknownRecord, Meta> =
146
138
  debounceTimer: ReturnType<typeof setTimeout> | null
147
139
  /** Queue of data messages waiting to be sent to this session */
148
140
  outstandingDataMessages: TLSocketServerSentDataEvent<R>[]
149
- /** Custom metadata associated with this session */
150
- meta: Meta
151
- /** Whether this session has read-only permissions */
152
- isReadonly: boolean
153
- /** Whether this session requires legacy protocol rejection handling */
154
- requiresLegacyRejection: boolean
155
- }
141
+ })
@@ -20,8 +20,8 @@ import {
20
20
  import {
21
21
  TLPersistentClientSocket,
22
22
  TLPresenceMode,
23
+ TLSocketStatusChangeEvent,
23
24
  TLSyncClient,
24
- TlSocketStatusChangeEvent,
25
25
  } from './TLSyncClient'
26
26
 
27
27
  // Mock store and schema setup
@@ -30,13 +30,19 @@ const protocolVersion = getTlsyncProtocolVersion()
30
30
  type TestRecord = TLRecord
31
31
 
32
32
  // Mock socket implementation for testing
33
- class MockSocket implements TLPersistentClientSocket<TestRecord> {
33
+ class MockSocket
34
+ implements
35
+ TLPersistentClientSocket<
36
+ TLSocketClientSentEvent<TestRecord>,
37
+ TLSocketServerSentEvent<TestRecord>
38
+ >
39
+ {
34
40
  connectionStatus: 'online' | 'offline' | 'error' = 'offline'
35
41
  private messageListeners: Array<(msg: TLSocketServerSentEvent<TestRecord>) => void> = []
36
- private statusListeners: Array<(event: TlSocketStatusChangeEvent) => void> = []
42
+ private statusListeners: Array<(event: TLSocketStatusChangeEvent) => void> = []
37
43
  private sentMessages: TLSocketClientSentEvent<TestRecord>[] = []
38
44
 
39
- sendMessage(msg: TLSocketClientSentEvent<TestRecord>): void {
45
+ sendMessage(msg: TLSocketClientSentEvent<TestRecord>) {
40
46
  if (this.connectionStatus !== 'online') {
41
47
  throw new Error('Cannot send message when not online')
42
48
  }
@@ -51,7 +57,7 @@ class MockSocket implements TLPersistentClientSocket<TestRecord> {
51
57
  }
52
58
  }
53
59
 
54
- onStatusChange(callback: (params: TlSocketStatusChangeEvent) => void) {
60
+ onStatusChange(callback: (event: TLSocketStatusChangeEvent) => void) {
55
61
  this.statusListeners.push(callback)
56
62
  return () => {
57
63
  const index = this.statusListeners.indexOf(callback)
@@ -69,6 +75,11 @@ class MockSocket implements TLPersistentClientSocket<TestRecord> {
69
75
  }, 0)
70
76
  }
71
77
 
78
+ close(): void {
79
+ this.connectionStatus = 'offline'
80
+ this._notifyStatus({ status: 'offline' })
81
+ }
82
+
72
83
  // Test helpers
73
84
  mockServerMessage(message: TLSocketServerSentEvent<TestRecord>) {
74
85
  this.messageListeners.forEach((listener) => listener(message))
@@ -95,7 +106,7 @@ class MockSocket implements TLPersistentClientSocket<TestRecord> {
95
106
  this.sentMessages = []
96
107
  }
97
108
 
98
- private _notifyStatus(event: TlSocketStatusChangeEvent) {
109
+ private _notifyStatus(event: TLSocketStatusChangeEvent) {
99
110
  this.statusListeners.forEach((listener) => listener(event))
100
111
  }
101
112
  }
@@ -31,7 +31,7 @@ import {
31
31
  * @param cb - Callback function that receives the event value
32
32
  * @returns Function to call when you want to unsubscribe from the events
33
33
  *
34
- * @internal
34
+ * @public
35
35
  */
36
36
  export type SubscribingFn<T> = (cb: (val: T) => void) => () => void
37
37
 
@@ -149,9 +149,9 @@ export type TLCustomMessageHandler = (this: null, data: any) => void
149
149
  * Event object describing changes in socket connection status.
150
150
  * Contains either a basic status change or an error with details.
151
151
  *
152
- * @internal
152
+ * @public
153
153
  */
154
- export type TlSocketStatusChangeEvent =
154
+ export type TLSocketStatusChangeEvent =
155
155
  | {
156
156
  /** Connection came online or went offline */
157
157
  status: 'online' | 'offline'
@@ -169,7 +169,7 @@ export type TlSocketStatusChangeEvent =
169
169
  *
170
170
  * @internal
171
171
  */
172
- export type TLSocketStatusListener = (params: TlSocketStatusChangeEvent) => void
172
+ export type TLSocketStatusListener = (params: TLSocketStatusChangeEvent) => void
173
173
 
174
174
  /**
175
175
  * Possible connection states for a persistent client socket.
@@ -183,7 +183,7 @@ export type TLPersistentClientSocketStatus = 'online' | 'offline' | 'error'
183
183
  * Mode for handling presence information in sync sessions.
184
184
  * Controls whether presence data (cursors, selections) is shared with other clients.
185
185
  *
186
- * @internal
186
+ * @public
187
187
  */
188
188
  export type TLPresenceMode =
189
189
  /** No presence sharing - client operates independently */
@@ -217,9 +217,12 @@ export type TLPresenceMode =
217
217
  * }
218
218
  * ```
219
219
  *
220
- * @internal
220
+ * @public
221
221
  */
222
- export interface TLPersistentClientSocket<R extends UnknownRecord = UnknownRecord> {
222
+ export interface TLPersistentClientSocket<
223
+ ClientSentMessage extends object = object,
224
+ ServerSentMessage extends object = object,
225
+ > {
223
226
  /** Current connection state - online means actively connected and ready */
224
227
  connectionStatus: 'online' | 'offline' | 'error'
225
228
 
@@ -227,27 +230,32 @@ export interface TLPersistentClientSocket<R extends UnknownRecord = UnknownRecor
227
230
  * Send a protocol message to the sync server
228
231
  * @param msg - Message to send (connect, push, ping, etc.)
229
232
  */
230
- sendMessage(msg: TLSocketClientSentEvent<R>): void
233
+ sendMessage(msg: ClientSentMessage): void
231
234
 
232
235
  /**
233
236
  * Subscribe to messages received from the server
234
237
  * @param callback - Function called for each received message
235
238
  * @returns Cleanup function to remove the listener
236
239
  */
237
- onReceiveMessage: SubscribingFn<TLSocketServerSentEvent<R>>
240
+ onReceiveMessage: SubscribingFn<ServerSentMessage>
238
241
 
239
242
  /**
240
243
  * Subscribe to connection status changes
241
244
  * @param callback - Function called when connection status changes
242
245
  * @returns Cleanup function to remove the listener
243
246
  */
244
- onStatusChange: SubscribingFn<TlSocketStatusChangeEvent>
247
+ onStatusChange: SubscribingFn<TLSocketStatusChangeEvent>
245
248
 
246
249
  /**
247
250
  * Force a connection restart (disconnect then reconnect)
248
251
  * Used for error recovery or when connection health checks fail
249
252
  */
250
253
  restart(): void
254
+
255
+ /**
256
+ * Close the connection
257
+ */
258
+ close(): void
251
259
  }
252
260
 
253
261
  const PING_INTERVAL = 5000
@@ -312,7 +320,7 @@ const MAX_TIME_TO_WAIT_FOR_SERVER_INTERACTION_BEFORE_RESETTING_CONNECTION = PING
312
320
  * })
313
321
  * ```
314
322
  *
315
- * @internal
323
+ * @public
316
324
  */
317
325
  export class TLSyncClient<R extends UnknownRecord, S extends Store<R> = Store<R>> {
318
326
  /** The last clock time from the most recent server update */
@@ -335,14 +343,19 @@ export class TLSyncClient<R extends UnknownRecord, S extends Store<R> = Store<R>
335
343
 
336
344
  private disposables: Array<() => void> = []
337
345
 
346
+ /** @internal */
338
347
  readonly store: S
339
- readonly socket: TLPersistentClientSocket<R>
348
+ /** @internal */
349
+ readonly socket: TLPersistentClientSocket<TLSocketClientSentEvent<R>, TLSocketServerSentEvent<R>>
340
350
 
351
+ /** @internal */
341
352
  readonly presenceState: Signal<R | null> | undefined
353
+ /** @internal */
342
354
  readonly presenceMode: Signal<TLPresenceMode> | undefined
343
355
 
344
356
  // isOnline is true when we have an open socket connection and we have
345
357
  // established a connection with the server room (i.e. we have received a 'connect' message)
358
+ /** @internal */
346
359
  isConnectedToRoom = false
347
360
 
348
361
  /**
@@ -366,7 +379,7 @@ export class TLSyncClient<R extends UnknownRecord, S extends Store<R> = Store<R>
366
379
  * @param details - Connection details
367
380
  * - isReadonly - Whether the connection is in read-only mode
368
381
  */
369
- public readonly onAfterConnect?: (self: this, details: { isReadonly: boolean }) => void
382
+ private readonly onAfterConnect?: (self: this, details: { isReadonly: boolean }) => void
370
383
 
371
384
  private readonly onCustomMessageReceived?: TLCustomMessageHandler
372
385
 
@@ -380,7 +393,7 @@ export class TLSyncClient<R extends UnknownRecord, S extends Store<R> = Store<R>
380
393
 
381
394
  private readonly presenceType: R['typeName'] | null
382
395
 
383
- didCancel?: () => boolean
396
+ private didCancel?: () => boolean
384
397
 
385
398
  /**
386
399
  * Creates a new TLSyncClient instance to manage synchronization with a remote server.
@@ -400,7 +413,7 @@ export class TLSyncClient<R extends UnknownRecord, S extends Store<R> = Store<R>
400
413
  */
401
414
  constructor(config: {
402
415
  store: S
403
- socket: TLPersistentClientSocket<R>
416
+ socket: TLPersistentClientSocket<any, any>
404
417
  presence: Signal<R | null>
405
418
  presenceMode?: Signal<TLPresenceMode>
406
419
  onLoad(self: TLSyncClient<R, S>): void
@@ -516,6 +529,7 @@ export class TLSyncClient<R extends UnknownRecord, S extends Store<R> = Store<R>
516
529
  }
517
530
  }
518
531
 
532
+ /** @internal */
519
533
  latestConnectRequestId: string | null = null
520
534
 
521
535
  /**
@@ -641,7 +655,7 @@ export class TLSyncClient<R extends UnknownRecord, S extends Store<R> = Store<R>
641
655
  this.lastServerClock = event.serverClock
642
656
  }
643
657
 
644
- incomingDiffBuffer: TLSocketServerSentDataEvent<R>[] = []
658
+ private incomingDiffBuffer: TLSocketServerSentDataEvent<R>[] = []
645
659
 
646
660
  /** Handle events received from the server */
647
661
  private handleServerEvent(event: TLSocketServerSentEvent<R>) {
@@ -701,7 +715,7 @@ export class TLSyncClient<R extends UnknownRecord, S extends Store<R> = Store<R>
701
715
  this.scheduleRebase.cancel?.()
702
716
  }
703
717
 
704
- lastPushedPresenceState: R | null = null
718
+ private lastPushedPresenceState: R | null = null
705
719
 
706
720
  private pushPresence(nextPresence: R | null) {
707
721
  // make sure we push any document changes first