@tldraw/sync-core 4.2.0-next.f100cedfc45b → 4.3.0-canary.d8da2a99f394
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.
- package/dist-cjs/index.d.ts +66 -0
- package/dist-cjs/index.js +2 -1
- package/dist-cjs/index.js.map +2 -2
- package/dist-cjs/lib/RoomSession.js.map +1 -1
- package/dist-cjs/lib/TLSyncRoom.js +35 -9
- package/dist-cjs/lib/TLSyncRoom.js.map +2 -2
- package/dist-cjs/lib/chunk.js +4 -4
- package/dist-cjs/lib/chunk.js.map +1 -1
- package/dist-cjs/lib/diff.js +29 -29
- package/dist-cjs/lib/diff.js.map +2 -2
- package/dist-cjs/lib/protocol.js +1 -1
- package/dist-cjs/lib/protocol.js.map +1 -1
- package/dist-esm/index.d.mts +66 -0
- package/dist-esm/index.mjs +3 -2
- package/dist-esm/index.mjs.map +2 -2
- package/dist-esm/lib/RoomSession.mjs.map +1 -1
- package/dist-esm/lib/TLSyncRoom.mjs +35 -9
- package/dist-esm/lib/TLSyncRoom.mjs.map +2 -2
- package/dist-esm/lib/chunk.mjs +4 -4
- package/dist-esm/lib/chunk.mjs.map +1 -1
- package/dist-esm/lib/diff.mjs +29 -29
- package/dist-esm/lib/diff.mjs.map +2 -2
- package/dist-esm/lib/protocol.mjs +1 -1
- package/dist-esm/lib/protocol.mjs.map +1 -1
- package/package.json +6 -6
- package/src/index.ts +2 -2
- package/src/lib/RoomSession.test.ts +3 -0
- package/src/lib/RoomSession.ts +28 -42
- package/src/lib/TLSyncRoom.ts +42 -7
- package/src/lib/chunk.ts +4 -4
- package/src/lib/diff.ts +55 -32
- package/src/lib/protocol.ts +1 -1
- package/src/test/TLSocketRoom.test.ts +2 -2
- package/src/test/TLSyncRoom.test.ts +22 -21
- package/src/test/diff.test.ts +200 -0
package/dist-cjs/lib/diff.js.map
CHANGED
|
@@ -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": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AACA,mBAA2D;AAQpD,MAAM,eAAe;AAAA,EAC3B,KAAK;AAAA,EACL,OAAO;AAAA,EACP,QAAQ;AACT;AA4DO,SAAS,eACf,MACwB;AACxB,MAAI,MAA6B;AAEjC,aAAW,CAAC,GAAG,CAAC,SAAK,+BAAiB,KAAK,KAAK,GAAG;AAClD,QAAI,CAAC,IAAK,OAAM,CAAC;AACjB,QAAI,CAAC,IAAI,CAAC,aAAa,KAAK,CAAC;AAAA,EAC9B;AAEA,aAAW,CAAC,MAAM,EAAE,SAAK,8BAAgB,KAAK,OAAO,GAAG;AACvD,UAAMA,QAAO,WAAW,MAAM,EAAE;AAChC,QAAIA,OAAM;AACT,UAAI,CAAC,IAAK,OAAM,CAAC;AACjB,UAAI,GAAG,EAAE,IAAI,CAAC,aAAa,OAAOA,KAAI;AAAA,IACvC;AAAA,EACD;AAEA,aAAW,WAAW,OAAO,KAAK,KAAK,OAAO,GAAG;AAChD,QAAI,CAAC,IAAK,OAAM,CAAC;AACjB,QAAI,OAAO,IAAI,CAAC,aAAa,MAAM;AAAA,EACpC;AAEA,SAAO;AACR;AAQO,MAAM,cAAc;AAAA,EAC1B,KAAK;AAAA,EACL,QAAQ;AAAA,EACR,QAAQ;AAAA,EACR,OAAO;AACR;
|
|
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
|
}
|
package/dist-cjs/lib/protocol.js
CHANGED
|
@@ -22,7 +22,7 @@ __export(protocol_exports, {
|
|
|
22
22
|
getTlsyncProtocolVersion: () => getTlsyncProtocolVersion
|
|
23
23
|
});
|
|
24
24
|
module.exports = __toCommonJS(protocol_exports);
|
|
25
|
-
const TLSYNC_PROTOCOL_VERSION =
|
|
25
|
+
const TLSYNC_PROTOCOL_VERSION = 8;
|
|
26
26
|
function getTlsyncProtocolVersion() {
|
|
27
27
|
return TLSYNC_PROTOCOL_VERSION;
|
|
28
28
|
}
|
|
@@ -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 =
|
|
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": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;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/dist-esm/index.d.mts
CHANGED
|
@@ -30,6 +30,70 @@ import { UnknownRecord } from '@tldraw/store';
|
|
|
30
30
|
|
|
31
31
|
/* Excluded from this release type: getTlsyncProtocolVersion */
|
|
32
32
|
|
|
33
|
+
/**
|
|
34
|
+
* Assembles chunked JSON messages back into complete objects.
|
|
35
|
+
* Handles both regular JSON messages and chunked messages created by the chunk() function.
|
|
36
|
+
* Maintains internal state to track partially received chunked messages.
|
|
37
|
+
*
|
|
38
|
+
* @example
|
|
39
|
+
* ```ts
|
|
40
|
+
* const assembler = new JsonChunkAssembler()
|
|
41
|
+
*
|
|
42
|
+
* // Handle regular JSON message
|
|
43
|
+
* const result1 = assembler.handleMessage('{"hello": "world"}')
|
|
44
|
+
* // Returns: { data: { hello: "world" }, stringified: '{"hello": "world"}' }
|
|
45
|
+
*
|
|
46
|
+
* // Handle chunked message
|
|
47
|
+
* assembler.handleMessage('1_hello') // Returns: null (partial)
|
|
48
|
+
* const result2 = assembler.handleMessage('0_ world')
|
|
49
|
+
* // Returns: { data: "hello world", stringified: "hello world" }
|
|
50
|
+
* ```
|
|
51
|
+
*
|
|
52
|
+
* @public
|
|
53
|
+
*/
|
|
54
|
+
export declare class JsonChunkAssembler {
|
|
55
|
+
/**
|
|
56
|
+
* Current assembly state - either 'idle' or tracking chunks being received
|
|
57
|
+
*/
|
|
58
|
+
state: 'idle' | {
|
|
59
|
+
chunksReceived: string[];
|
|
60
|
+
totalChunks: number;
|
|
61
|
+
};
|
|
62
|
+
/**
|
|
63
|
+
* Processes a single message, which can be either a complete JSON object or a chunk.
|
|
64
|
+
* For complete JSON objects (starting with '\{'), parses immediately.
|
|
65
|
+
* For chunks (prefixed with "\{number\}_"), accumulates until all chunks received.
|
|
66
|
+
*
|
|
67
|
+
* @param msg - The message to process, either JSON or chunk format
|
|
68
|
+
* @returns Result object with data/stringified on success, error object on failure, or null for incomplete chunks
|
|
69
|
+
* - `\{ data: object, stringified: string \}` - Successfully parsed complete message
|
|
70
|
+
* - `\{ error: Error \}` - Parse error or invalid chunk sequence
|
|
71
|
+
* - `null` - Chunk received but more chunks expected
|
|
72
|
+
*
|
|
73
|
+
* @example
|
|
74
|
+
* ```ts
|
|
75
|
+
* const assembler = new JsonChunkAssembler()
|
|
76
|
+
*
|
|
77
|
+
* // Complete JSON message
|
|
78
|
+
* const result = assembler.handleMessage('{"key": "value"}')
|
|
79
|
+
* if (result && 'data' in result) {
|
|
80
|
+
* console.log(result.data) // { key: "value" }
|
|
81
|
+
* }
|
|
82
|
+
*
|
|
83
|
+
* // Chunked message sequence
|
|
84
|
+
* assembler.handleMessage('2_hel') // null - more chunks expected
|
|
85
|
+
* assembler.handleMessage('1_lo ') // null - more chunks expected
|
|
86
|
+
* assembler.handleMessage('0_wor') // { data: "hello wor", stringified: "hello wor" }
|
|
87
|
+
* ```
|
|
88
|
+
*/
|
|
89
|
+
handleMessage(msg: string): {
|
|
90
|
+
data: object;
|
|
91
|
+
stringified: string;
|
|
92
|
+
} | {
|
|
93
|
+
error: Error;
|
|
94
|
+
} | null;
|
|
95
|
+
}
|
|
96
|
+
|
|
33
97
|
/* Excluded from this release type: NetworkDiff */
|
|
34
98
|
|
|
35
99
|
/* Excluded from this release type: ObjectDiff */
|
|
@@ -65,6 +129,8 @@ export declare type OmitVoid<T, KS extends keyof T = keyof T> = {
|
|
|
65
129
|
|
|
66
130
|
/* Excluded from this release type: RoomSession */
|
|
67
131
|
|
|
132
|
+
/* Excluded from this release type: RoomSessionBase */
|
|
133
|
+
|
|
68
134
|
/* Excluded from this release type: RoomSessionState */
|
|
69
135
|
|
|
70
136
|
/**
|
package/dist-esm/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { registerTldrawLibraryVersion } from "@tldraw/utils";
|
|
2
|
-
import { chunk } from "./lib/chunk.mjs";
|
|
2
|
+
import { chunk, JsonChunkAssembler } from "./lib/chunk.mjs";
|
|
3
3
|
import { ClientWebSocketAdapter, ReconnectManager } from "./lib/ClientWebSocketAdapter.mjs";
|
|
4
4
|
import {
|
|
5
5
|
applyObjectDiff,
|
|
@@ -26,12 +26,13 @@ import {
|
|
|
26
26
|
} from "./lib/TLSyncRoom.mjs";
|
|
27
27
|
registerTldrawLibraryVersion(
|
|
28
28
|
"@tldraw/sync-core",
|
|
29
|
-
"4.
|
|
29
|
+
"4.3.0-canary.d8da2a99f394",
|
|
30
30
|
"esm"
|
|
31
31
|
);
|
|
32
32
|
export {
|
|
33
33
|
ClientWebSocketAdapter,
|
|
34
34
|
DocumentState,
|
|
35
|
+
JsonChunkAssembler,
|
|
35
36
|
ReconnectManager,
|
|
36
37
|
RecordOpType,
|
|
37
38
|
RoomSessionState,
|
package/dist-esm/index.mjs.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/index.ts"],
|
|
4
|
-
"sourcesContent": ["import { registerTldrawLibraryVersion } from '@tldraw/utils'\nexport { chunk } from './lib/chunk'\nexport { ClientWebSocketAdapter, ReconnectManager } from './lib/ClientWebSocketAdapter'\nexport {\n\tapplyObjectDiff,\n\tdiffRecord,\n\tgetNetworkDiff,\n\tRecordOpType,\n\tValueOpType,\n\ttype AppendOp,\n\ttype DeleteOp,\n\ttype NetworkDiff,\n\ttype ObjectDiff,\n\ttype PatchOp,\n\ttype PutOp,\n\ttype RecordOp,\n\ttype ValueOp,\n} from './lib/diff'\nexport {\n\tgetTlsyncProtocolVersion,\n\tTLIncompatibilityReason,\n\ttype TLConnectRequest,\n\ttype TLPingRequest,\n\ttype TLPushRequest,\n\ttype TLSocketClientSentEvent,\n\ttype TLSocketServerSentDataEvent,\n\ttype TLSocketServerSentEvent,\n} from './lib/protocol'\nexport { RoomSessionState, type RoomSession } from './lib/RoomSession'\nexport type { PersistedRoomSnapshotForSupabase } from './lib/server-types'\nexport type { WebSocketMinimal } from './lib/ServerSocketAdapter'\nexport { TLRemoteSyncError } from './lib/TLRemoteSyncError'\nexport { TLSocketRoom, type OmitVoid, type TLSyncLog } from './lib/TLSocketRoom'\nexport {\n\tTLSyncClient,\n\tTLSyncErrorCloseEventCode,\n\tTLSyncErrorCloseEventReason,\n\ttype SubscribingFn,\n\ttype TLCustomMessageHandler,\n\ttype TLPersistentClientSocket,\n\ttype TLPersistentClientSocketStatus,\n\ttype TLPresenceMode,\n\ttype TLSocketStatusChangeEvent,\n\ttype TLSocketStatusListener,\n} from './lib/TLSyncClient'\nexport {\n\tDocumentState,\n\tTLSyncRoom,\n\ttype RoomSnapshot,\n\ttype RoomStoreMethods,\n\ttype TLRoomSocket,\n} from './lib/TLSyncRoom'\n\nregisterTldrawLibraryVersion(\n\t(globalThis as any).TLDRAW_LIBRARY_NAME,\n\t(globalThis as any).TLDRAW_LIBRARY_VERSION,\n\t(globalThis as any).TLDRAW_LIBRARY_MODULES\n)\n"],
|
|
5
|
-
"mappings": "AAAA,SAAS,oCAAoC;AAC7C,SAAS,
|
|
4
|
+
"sourcesContent": ["import { registerTldrawLibraryVersion } from '@tldraw/utils'\nexport { chunk, JsonChunkAssembler } from './lib/chunk'\nexport { ClientWebSocketAdapter, ReconnectManager } from './lib/ClientWebSocketAdapter'\nexport {\n\tapplyObjectDiff,\n\tdiffRecord,\n\tgetNetworkDiff,\n\tRecordOpType,\n\tValueOpType,\n\ttype AppendOp,\n\ttype DeleteOp,\n\ttype NetworkDiff,\n\ttype ObjectDiff,\n\ttype PatchOp,\n\ttype PutOp,\n\ttype RecordOp,\n\ttype ValueOp,\n} from './lib/diff'\nexport {\n\tgetTlsyncProtocolVersion,\n\tTLIncompatibilityReason,\n\ttype TLConnectRequest,\n\ttype TLPingRequest,\n\ttype TLPushRequest,\n\ttype TLSocketClientSentEvent,\n\ttype TLSocketServerSentDataEvent,\n\ttype TLSocketServerSentEvent,\n} from './lib/protocol'\nexport { RoomSessionState, type RoomSession, type RoomSessionBase } from './lib/RoomSession'\nexport type { PersistedRoomSnapshotForSupabase } from './lib/server-types'\nexport type { WebSocketMinimal } from './lib/ServerSocketAdapter'\nexport { TLRemoteSyncError } from './lib/TLRemoteSyncError'\nexport { TLSocketRoom, type OmitVoid, type TLSyncLog } from './lib/TLSocketRoom'\nexport {\n\tTLSyncClient,\n\tTLSyncErrorCloseEventCode,\n\tTLSyncErrorCloseEventReason,\n\ttype SubscribingFn,\n\ttype TLCustomMessageHandler,\n\ttype TLPersistentClientSocket,\n\ttype TLPersistentClientSocketStatus,\n\ttype TLPresenceMode,\n\ttype TLSocketStatusChangeEvent,\n\ttype TLSocketStatusListener,\n} from './lib/TLSyncClient'\nexport {\n\tDocumentState,\n\tTLSyncRoom,\n\ttype RoomSnapshot,\n\ttype RoomStoreMethods,\n\ttype TLRoomSocket,\n} from './lib/TLSyncRoom'\n\nregisterTldrawLibraryVersion(\n\t(globalThis as any).TLDRAW_LIBRARY_NAME,\n\t(globalThis as any).TLDRAW_LIBRARY_VERSION,\n\t(globalThis as any).TLDRAW_LIBRARY_MODULES\n)\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,oCAAoC;AAC7C,SAAS,OAAO,0BAA0B;AAC1C,SAAS,wBAAwB,wBAAwB;AACzD;AAAA,EACC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OASM;AACP;AAAA,EACC;AAAA,EACA;AAAA,OAOM;AACP,SAAS,wBAAgE;AAGzE,SAAS,yBAAyB;AAClC,SAAS,oBAAmD;AAC5D;AAAA,EACC;AAAA,EACA;AAAA,EACA;AAAA,OAQM;AACP;AAAA,EACC;AAAA,EACA;AAAA,OAIM;AAEP;AAAA,EACE;AAAA,EACA;AAAA,EACA;AACF;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/lib/RoomSession.ts"],
|
|
4
|
-
"sourcesContent": ["import { SerializedSchema, UnknownRecord } from '@tldraw/store'\nimport { TLRoomSocket } from './TLSyncRoom'\nimport { TLSocketServerSentDataEvent } from './protocol'\n\n/**\n * Enumeration of possible states for a room session during its lifecycle.\n *\n * Room sessions progress through these states as clients connect, authenticate,\n * and disconnect from collaborative rooms.\n *\n * @internal\n */\nexport const RoomSessionState = {\n\t/** Session is waiting for the initial connect message from the client */\n\tAwaitingConnectMessage: 'awaiting-connect-message',\n\t/** Session is disconnected but waiting for final cleanup before removal */\n\tAwaitingRemoval: 'awaiting-removal',\n\t/** Session is fully connected and actively synchronizing */\n\tConnected: 'connected',\n} as const\n\n/**\n * Type representing the possible states a room session can be in.\n *\n * @example\n * ```ts\n * const sessionState: RoomSessionState = RoomSessionState.Connected\n * if (sessionState === RoomSessionState.AwaitingConnectMessage) {\n * console.log('Session waiting for connect message')\n * }\n * ```\n *\n * @internal\n */\nexport type RoomSessionState = (typeof RoomSessionState)[keyof typeof RoomSessionState]\n\n/**\n * Maximum time in milliseconds to wait for a connect message after socket connection.\n *\n * If a client connects but doesn't send a connect message within this time,\n * the session will be terminated.\n *\n * @public\n */\nexport const SESSION_START_WAIT_TIME = 10000\n\n/**\n * Time in milliseconds to wait before completely removing a disconnected session.\n *\n * This grace period allows for quick reconnections without losing session state,\n * which is especially helpful for brief network interruptions.\n *\n * @public\n */\nexport const SESSION_REMOVAL_WAIT_TIME = 5000\n\n/**\n * Maximum time in milliseconds a connected session can remain idle before cleanup.\n *\n * Sessions that don't receive any messages or interactions for this duration\n * may be considered for cleanup to free server resources.\n *\n * @public\n */\nexport const SESSION_IDLE_TIMEOUT = 20000\n\n/**\n * Represents a client session within a collaborative room, tracking the connection\n * state, permissions, and synchronization details for a single user.\n *\n * Each session corresponds to one WebSocket connection and progresses through\n * different states during its lifecycle. The session type is a discriminated union\n * based on the current state, ensuring type safety when accessing state-specific properties.\n *\n * @example\n * ```ts\n * // Check session state and access appropriate properties\n * function handleSession(session: RoomSession<MyRecord, UserMeta>) {\n * switch (session.state) {\n * case RoomSessionState.AwaitingConnectMessage:\n * console.log(`Session ${session.sessionId} started at ${session.sessionStartTime}`)\n * break\n * case RoomSessionState.Connected:\n * console.log(`Connected session has ${session.outstandingDataMessages.length} pending messages`)\n * break\n * case RoomSessionState.AwaitingRemoval:\n * console.log(`Session will be removed at ${session.cancellationTime}`)\n * break\n * }\n * }\n * ```\n *\n * @internal\n */\nexport type RoomSession<R extends UnknownRecord, Meta> =\n\t| {\n\t\t\t/** Current state of the session */\n\t\t\tstate: typeof RoomSessionState.AwaitingConnectMessage\n\t\t\t/**
|
|
4
|
+
"sourcesContent": ["import { SerializedSchema, UnknownRecord } from '@tldraw/store'\nimport { TLRoomSocket } from './TLSyncRoom'\nimport { TLSocketServerSentDataEvent } from './protocol'\n\n/**\n * Enumeration of possible states for a room session during its lifecycle.\n *\n * Room sessions progress through these states as clients connect, authenticate,\n * and disconnect from collaborative rooms.\n *\n * @internal\n */\nexport const RoomSessionState = {\n\t/** Session is waiting for the initial connect message from the client */\n\tAwaitingConnectMessage: 'awaiting-connect-message',\n\t/** Session is disconnected but waiting for final cleanup before removal */\n\tAwaitingRemoval: 'awaiting-removal',\n\t/** Session is fully connected and actively synchronizing */\n\tConnected: 'connected',\n} as const\n\n/**\n * Type representing the possible states a room session can be in.\n *\n * @example\n * ```ts\n * const sessionState: RoomSessionState = RoomSessionState.Connected\n * if (sessionState === RoomSessionState.AwaitingConnectMessage) {\n * console.log('Session waiting for connect message')\n * }\n * ```\n *\n * @internal\n */\nexport type RoomSessionState = (typeof RoomSessionState)[keyof typeof RoomSessionState]\n\n/**\n * Maximum time in milliseconds to wait for a connect message after socket connection.\n *\n * If a client connects but doesn't send a connect message within this time,\n * the session will be terminated.\n *\n * @public\n */\nexport const SESSION_START_WAIT_TIME = 10000\n\n/**\n * Time in milliseconds to wait before completely removing a disconnected session.\n *\n * This grace period allows for quick reconnections without losing session state,\n * which is especially helpful for brief network interruptions.\n *\n * @public\n */\nexport const SESSION_REMOVAL_WAIT_TIME = 5000\n\n/**\n * Maximum time in milliseconds a connected session can remain idle before cleanup.\n *\n * Sessions that don't receive any messages or interactions for this duration\n * may be considered for cleanup to free server resources.\n *\n * @public\n */\nexport const SESSION_IDLE_TIMEOUT = 20000\n\n/**\n * Base properties shared by all room session states.\n *\n * @internal\n */\nexport interface RoomSessionBase<R extends UnknownRecord, Meta> {\n\t/** Unique identifier for this session */\n\tsessionId: string\n\t/** Presence identifier for live cursor/selection tracking, if available */\n\tpresenceId: string | null\n\t/** WebSocket connection wrapper for this session */\n\tsocket: TLRoomSocket<R>\n\t/** Custom metadata associated with this session */\n\tmeta: Meta\n\t/** Whether this session has read-only permissions */\n\tisReadonly: boolean\n\t/** Whether this session requires legacy protocol rejection handling */\n\trequiresLegacyRejection: boolean\n\t/** Whether this session supports string append operations */\n\tsupportsStringAppend: boolean\n}\n\n/**\n * Represents a client session within a collaborative room, tracking the connection\n * state, permissions, and synchronization details for a single user.\n *\n * Each session corresponds to one WebSocket connection and progresses through\n * different states during its lifecycle. The session type is a discriminated union\n * based on the current state, ensuring type safety when accessing state-specific properties.\n *\n * @example\n * ```ts\n * // Check session state and access appropriate properties\n * function handleSession(session: RoomSession<MyRecord, UserMeta>) {\n * switch (session.state) {\n * case RoomSessionState.AwaitingConnectMessage:\n * console.log(`Session ${session.sessionId} started at ${session.sessionStartTime}`)\n * break\n * case RoomSessionState.Connected:\n * console.log(`Connected session has ${session.outstandingDataMessages.length} pending messages`)\n * break\n * case RoomSessionState.AwaitingRemoval:\n * console.log(`Session will be removed at ${session.cancellationTime}`)\n * break\n * }\n * }\n * ```\n *\n * @internal\n */\nexport type RoomSession<R extends UnknownRecord, Meta> =\n\t| (RoomSessionBase<R, Meta> & {\n\t\t\t/** Current state of the session */\n\t\t\tstate: typeof RoomSessionState.AwaitingConnectMessage\n\t\t\t/** Timestamp when the session was created */\n\t\t\tsessionStartTime: number\n\t })\n\t| (RoomSessionBase<R, Meta> & {\n\t\t\t/** Current state of the session */\n\t\t\tstate: typeof RoomSessionState.AwaitingRemoval\n\t\t\t/** Timestamp when the session was marked for removal */\n\t\t\tcancellationTime: number\n\t })\n\t| (RoomSessionBase<R, Meta> & {\n\t\t\t/** Current state of the session */\n\t\t\tstate: typeof RoomSessionState.Connected\n\t\t\t/** Serialized schema information for this connected session */\n\t\t\tserializedSchema: SerializedSchema\n\t\t\t/** Timestamp of the last interaction or message from this session */\n\t\t\tlastInteractionTime: number\n\t\t\t/** Timer for debouncing operations, if active */\n\t\t\tdebounceTimer: ReturnType<typeof setTimeout> | null\n\t\t\t/** Queue of data messages waiting to be sent to this session */\n\t\t\toutstandingDataMessages: TLSocketServerSentDataEvent<R>[]\n\t })\n"],
|
|
5
5
|
"mappings": "AAYO,MAAM,mBAAmB;AAAA;AAAA,EAE/B,wBAAwB;AAAA;AAAA,EAExB,iBAAiB;AAAA;AAAA,EAEjB,WAAW;AACZ;AAyBO,MAAM,0BAA0B;AAUhC,MAAM,4BAA4B;AAUlC,MAAM,uBAAuB;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -79,10 +79,11 @@ class DocumentState {
|
|
|
79
79
|
*
|
|
80
80
|
* @param state - The new record state
|
|
81
81
|
* @param clock - The new clock value
|
|
82
|
+
* @param legacyAppendMode - If true, string append operations will be converted to Put operations
|
|
82
83
|
* @returns Result containing the diff and new DocumentState, or null if no changes, or validation error
|
|
83
84
|
*/
|
|
84
|
-
replaceState(state, clock) {
|
|
85
|
-
const diff = diffRecord(this.state, state);
|
|
85
|
+
replaceState(state, clock, legacyAppendMode = false) {
|
|
86
|
+
const diff = diffRecord(this.state, state, legacyAppendMode);
|
|
86
87
|
if (!diff) return Result.ok(null);
|
|
87
88
|
try {
|
|
88
89
|
this.recordType.validate(state);
|
|
@@ -96,11 +97,12 @@ class DocumentState {
|
|
|
96
97
|
*
|
|
97
98
|
* @param diff - The object diff to apply
|
|
98
99
|
* @param clock - The new clock value
|
|
100
|
+
* @param legacyAppendMode - If true, string append operations will be converted to Put operations
|
|
99
101
|
* @returns Result containing the final diff and new DocumentState, or null if no changes, or validation error
|
|
100
102
|
*/
|
|
101
|
-
mergeDiff(diff, clock) {
|
|
103
|
+
mergeDiff(diff, clock, legacyAppendMode = false) {
|
|
102
104
|
const newState = applyObjectDiff(this.state, diff);
|
|
103
|
-
return this.replaceState(newState, clock);
|
|
105
|
+
return this.replaceState(newState, clock, legacyAppendMode);
|
|
104
106
|
}
|
|
105
107
|
}
|
|
106
108
|
function getDocumentClock(snapshot) {
|
|
@@ -488,7 +490,8 @@ class TLSyncRoom {
|
|
|
488
490
|
cancellationTime: Date.now(),
|
|
489
491
|
meta: session.meta,
|
|
490
492
|
isReadonly: session.isReadonly,
|
|
491
|
-
requiresLegacyRejection: session.requiresLegacyRejection
|
|
493
|
+
requiresLegacyRejection: session.requiresLegacyRejection,
|
|
494
|
+
supportsStringAppend: session.supportsStringAppend
|
|
492
495
|
});
|
|
493
496
|
try {
|
|
494
497
|
session.socket.close();
|
|
@@ -595,10 +598,27 @@ class TLSyncRoom {
|
|
|
595
598
|
meta,
|
|
596
599
|
isReadonly: isReadonly ?? false,
|
|
597
600
|
// this gets set later during handleConnectMessage
|
|
598
|
-
requiresLegacyRejection: false
|
|
601
|
+
requiresLegacyRejection: false,
|
|
602
|
+
supportsStringAppend: true
|
|
599
603
|
});
|
|
600
604
|
return this;
|
|
601
605
|
}
|
|
606
|
+
/**
|
|
607
|
+
* Checks if all connected sessions support string append operations (protocol version 8+).
|
|
608
|
+
* If any client is on an older version, returns false to enable legacy append mode.
|
|
609
|
+
*
|
|
610
|
+
* @returns True if all connected sessions are on protocol version 8 or higher
|
|
611
|
+
*/
|
|
612
|
+
getCanEmitStringAppend() {
|
|
613
|
+
for (const session of this.sessions.values()) {
|
|
614
|
+
if (session.state === RoomSessionState.Connected) {
|
|
615
|
+
if (!session.supportsStringAppend) {
|
|
616
|
+
return false;
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
return true;
|
|
621
|
+
}
|
|
602
622
|
/**
|
|
603
623
|
* When we send a diff to a client, if that client is on a lower version than us, we need to make
|
|
604
624
|
* the diff compatible with their version. At the moment this means migrating each affected record
|
|
@@ -733,6 +753,10 @@ class TLSyncRoom {
|
|
|
733
753
|
if (theirProtocolVersion === 6) {
|
|
734
754
|
theirProtocolVersion++;
|
|
735
755
|
}
|
|
756
|
+
if (theirProtocolVersion === 7) {
|
|
757
|
+
theirProtocolVersion++;
|
|
758
|
+
session.supportsStringAppend = false;
|
|
759
|
+
}
|
|
736
760
|
if (theirProtocolVersion == null || theirProtocolVersion < getTlsyncProtocolVersion()) {
|
|
737
761
|
this.rejectSession(session.sessionId, TLSyncErrorCloseEventReason.CLIENT_TOO_OLD);
|
|
738
762
|
return;
|
|
@@ -760,6 +784,7 @@ class TLSyncRoom {
|
|
|
760
784
|
lastInteractionTime: Date.now(),
|
|
761
785
|
debounceTimer: null,
|
|
762
786
|
outstandingDataMessages: [],
|
|
787
|
+
supportsStringAppend: session.supportsStringAppend,
|
|
763
788
|
meta: session.meta,
|
|
764
789
|
isReadonly: session.isReadonly,
|
|
765
790
|
requiresLegacyRejection: session.requiresLegacyRejection
|
|
@@ -846,6 +871,7 @@ class TLSyncRoom {
|
|
|
846
871
|
const initialDocumentClock = this.documentClock;
|
|
847
872
|
let didPresenceChange = false;
|
|
848
873
|
transaction((rollback) => {
|
|
874
|
+
const legacyAppendMode = !this.getCanEmitStringAppend();
|
|
849
875
|
const docChanges = { diff: null };
|
|
850
876
|
const presenceChanges = { diff: null };
|
|
851
877
|
const propagateOp = (changes, id, op) => {
|
|
@@ -874,7 +900,7 @@ class TLSyncRoom {
|
|
|
874
900
|
const { value: state } = res;
|
|
875
901
|
const doc = this.getDocument(id);
|
|
876
902
|
if (doc) {
|
|
877
|
-
const diff = doc.replaceState(state, this.clock);
|
|
903
|
+
const diff = doc.replaceState(state, this.clock, legacyAppendMode);
|
|
878
904
|
if (!diff.ok) {
|
|
879
905
|
return fail(TLSyncErrorCloseEventReason.INVALID_RECORD);
|
|
880
906
|
}
|
|
@@ -899,7 +925,7 @@ class TLSyncRoom {
|
|
|
899
925
|
return fail(TLSyncErrorCloseEventReason.CLIENT_TOO_OLD);
|
|
900
926
|
}
|
|
901
927
|
if (downgraded.value === doc.state) {
|
|
902
|
-
const diff = doc.mergeDiff(patch, this.clock);
|
|
928
|
+
const diff = doc.mergeDiff(patch, this.clock, legacyAppendMode);
|
|
903
929
|
if (!diff.ok) {
|
|
904
930
|
return fail(TLSyncErrorCloseEventReason.INVALID_RECORD);
|
|
905
931
|
}
|
|
@@ -913,7 +939,7 @@ class TLSyncRoom {
|
|
|
913
939
|
if (upgraded.type === "error") {
|
|
914
940
|
return fail(TLSyncErrorCloseEventReason.CLIENT_TOO_OLD);
|
|
915
941
|
}
|
|
916
|
-
const diff = doc.replaceState(upgraded.value, this.clock);
|
|
942
|
+
const diff = doc.replaceState(upgraded.value, this.clock, legacyAppendMode);
|
|
917
943
|
if (!diff.ok) {
|
|
918
944
|
return fail(TLSyncErrorCloseEventReason.INVALID_RECORD);
|
|
919
945
|
}
|