@tldraw/sync-core 4.2.0-canary.f59b349f83fb → 4.2.0-canary.fa5328cd004a
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 +339 -5
- package/dist-cjs/index.js +2 -1
- package/dist-cjs/index.js.map +2 -2
- package/dist-cjs/lib/ClientWebSocketAdapter.js.map +2 -2
- package/dist-cjs/lib/RoomSession.js.map +1 -1
- package/dist-cjs/lib/TLSyncClient.js +6 -0
- package/dist-cjs/lib/TLSyncClient.js.map +2 -2
- 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 +339 -5
- package/dist-esm/index.mjs +3 -2
- package/dist-esm/index.mjs.map +2 -2
- package/dist-esm/lib/ClientWebSocketAdapter.mjs.map +2 -2
- package/dist-esm/lib/RoomSession.mjs.map +1 -1
- package/dist-esm/lib/TLSyncClient.mjs +6 -0
- package/dist-esm/lib/TLSyncClient.mjs.map +2 -2
- 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 +3 -3
- package/src/lib/ClientWebSocketAdapter.ts +4 -1
- package/src/lib/RoomSession.test.ts +3 -0
- package/src/lib/RoomSession.ts +28 -42
- package/src/lib/TLSyncClient.test.ts +17 -6
- package/src/lib/TLSyncClient.ts +31 -17
- 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/TestSocketPair.ts +5 -2
- 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": "AACA,SAAS,SAAS,kBAAkB,uBAAuB;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,KAAK,iBAAiB,KAAK,KAAK,GAAG;AAClD,QAAI,CAAC,IAAK,OAAM,CAAC;AACjB,QAAI,CAAC,IAAI,CAAC,aAAa,KAAK,CAAC;AAAA,EAC9B;AAEA,aAAW,CAAC,MAAM,EAAE,KAAK,gBAAgB,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
|
}
|
|
@@ -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": "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-canary.
|
|
4
|
+
"version": "4.2.0-canary.fa5328cd004a",
|
|
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-canary.
|
|
51
|
+
"tldraw": "4.2.0-canary.fa5328cd004a",
|
|
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-canary.
|
|
59
|
-
"@tldraw/store": "4.2.0-canary.
|
|
60
|
-
"@tldraw/tlschema": "4.2.0-canary.
|
|
61
|
-
"@tldraw/utils": "4.2.0-canary.
|
|
58
|
+
"@tldraw/state": "4.2.0-canary.fa5328cd004a",
|
|
59
|
+
"@tldraw/store": "4.2.0-canary.fa5328cd004a",
|
|
60
|
+
"@tldraw/tlschema": "4.2.0-canary.fa5328cd004a",
|
|
61
|
+
"@tldraw/utils": "4.2.0-canary.fa5328cd004a",
|
|
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
|
|
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
|
|
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
|
|
package/src/lib/RoomSession.ts
CHANGED
|
@@ -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
|
-
|
|
108
|
-
|
|
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
|
-
|
|
126
|
-
|
|
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
|
-
|
|
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
|
|
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:
|
|
42
|
+
private statusListeners: Array<(event: TLSocketStatusChangeEvent) => void> = []
|
|
37
43
|
private sentMessages: TLSocketClientSentEvent<TestRecord>[] = []
|
|
38
44
|
|
|
39
|
-
sendMessage(msg: TLSocketClientSentEvent<TestRecord>)
|
|
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: (
|
|
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:
|
|
109
|
+
private _notifyStatus(event: TLSocketStatusChangeEvent) {
|
|
99
110
|
this.statusListeners.forEach((listener) => listener(event))
|
|
100
111
|
}
|
|
101
112
|
}
|
package/src/lib/TLSyncClient.ts
CHANGED
|
@@ -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
|
-
* @
|
|
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
|
-
* @
|
|
152
|
+
* @public
|
|
153
153
|
*/
|
|
154
|
-
export type
|
|
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:
|
|
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
|
-
* @
|
|
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
|
-
* @
|
|
220
|
+
* @public
|
|
221
221
|
*/
|
|
222
|
-
export interface TLPersistentClientSocket<
|
|
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:
|
|
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<
|
|
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<
|
|
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
|
-
* @
|
|
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
|
-
|
|
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
|
-
|
|
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<
|
|
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
|