@tldraw/sync-core 4.2.0-canary.6b589c8b88df → 4.2.0-canary.6c9544477685
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 +64 -0
- package/dist-cjs/index.js +2 -1
- package/dist-cjs/index.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 +13 -6
- package/dist-cjs/lib/diff.js.map +2 -2
- package/dist-esm/index.d.mts +64 -0
- package/dist-esm/index.mjs +3 -2
- package/dist-esm/index.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 +13 -6
- package/dist-esm/lib/diff.mjs.map +2 -2
- package/package.json +6 -6
- package/src/index.ts +1 -1
- package/src/lib/chunk.ts +4 -4
- package/src/lib/diff.ts +23 -7
package/dist-cjs/index.d.ts
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 */
|
package/dist-cjs/index.js
CHANGED
|
@@ -20,6 +20,7 @@ var index_exports = {};
|
|
|
20
20
|
__export(index_exports, {
|
|
21
21
|
ClientWebSocketAdapter: () => import_ClientWebSocketAdapter.ClientWebSocketAdapter,
|
|
22
22
|
DocumentState: () => import_TLSyncRoom.DocumentState,
|
|
23
|
+
JsonChunkAssembler: () => import_chunk.JsonChunkAssembler,
|
|
23
24
|
ReconnectManager: () => import_ClientWebSocketAdapter.ReconnectManager,
|
|
24
25
|
RecordOpType: () => import_diff.RecordOpType,
|
|
25
26
|
RoomSessionState: () => import_RoomSession.RoomSessionState,
|
|
@@ -50,7 +51,7 @@ var import_TLSyncClient = require("./lib/TLSyncClient");
|
|
|
50
51
|
var import_TLSyncRoom = require("./lib/TLSyncRoom");
|
|
51
52
|
(0, import_utils.registerTldrawLibraryVersion)(
|
|
52
53
|
"@tldraw/sync-core",
|
|
53
|
-
"4.2.0-canary.
|
|
54
|
+
"4.2.0-canary.6c9544477685",
|
|
54
55
|
"cjs"
|
|
55
56
|
);
|
|
56
57
|
//# sourceMappingURL=index.js.map
|
package/dist-cjs/index.js.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, 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;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,mBAA6C;AAC7C,
|
|
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;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,mBAA6C;AAC7C,mBAA0C;AAC1C,oCAAyD;AACzD,kBAcO;AACP,sBASO;AACP,yBAAyE;AAGzE,+BAAkC;AAClC,0BAA4D;AAC5D,0BAWO;AACP,wBAMO;AAAA,IAEP;AAAA,EACE;AAAA,EACA;AAAA,EACA;AACF;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/dist-cjs/lib/chunk.js
CHANGED
|
@@ -50,13 +50,13 @@ class JsonChunkAssembler {
|
|
|
50
50
|
state = "idle";
|
|
51
51
|
/**
|
|
52
52
|
* Processes a single message, which can be either a complete JSON object or a chunk.
|
|
53
|
-
* For complete JSON objects (starting with '{'), parses immediately.
|
|
54
|
-
* For chunks (prefixed with "{number}_"), accumulates until all chunks received.
|
|
53
|
+
* For complete JSON objects (starting with '\{'), parses immediately.
|
|
54
|
+
* For chunks (prefixed with "\{number\}_"), accumulates until all chunks received.
|
|
55
55
|
*
|
|
56
56
|
* @param msg - The message to process, either JSON or chunk format
|
|
57
57
|
* @returns Result object with data/stringified on success, error object on failure, or null for incomplete chunks
|
|
58
|
-
* -
|
|
59
|
-
* -
|
|
58
|
+
* - `\{ data: object, stringified: string \}` - Successfully parsed complete message
|
|
59
|
+
* - `\{ error: Error \}` - Parse error or invalid chunk sequence
|
|
60
60
|
* - `null` - Chunk received but more chunks expected
|
|
61
61
|
*
|
|
62
62
|
* @example
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/lib/chunk.ts"],
|
|
4
|
-
"sourcesContent": ["// quarter of a megabyte, max possible utf-8 string size\n\n// cloudflare workers only accept messages of max 1mb\nconst MAX_CLIENT_SENT_MESSAGE_SIZE_BYTES = 1024 * 1024\n// utf-8 is max 4 bytes per char\nconst MAX_BYTES_PER_CHAR = 4\n\n// in the (admittedly impossible) worst case, the max size is 1/4 of a megabyte\nconst MAX_SAFE_MESSAGE_SIZE = MAX_CLIENT_SENT_MESSAGE_SIZE_BYTES / MAX_BYTES_PER_CHAR\n\n/**\n * Splits a string into smaller chunks suitable for transmission over WebSockets.\n * This function ensures messages don't exceed size limits imposed by platforms like Cloudflare Workers (1MB max).\n * Each chunk is prefixed with a number indicating how many more chunks follow.\n *\n * @param msg - The string to split into chunks\n * @param maxSafeMessageSize - Maximum safe size for each chunk in characters. Defaults to quarter megabyte to account for UTF-8 encoding\n * @returns Array of chunked strings, each prefixed with \"\\{number\\}_\" where number indicates remaining chunks\n *\n * @example\n * ```ts\n * // Small message - returns as single chunk\n * chunk('hello world') // ['hello world']\n *\n * // Large message - splits into multiple chunks\n * chunk('very long message...', 10)\n * // ['2_very long', '1_ message', '0_...']\n * ```\n *\n * @internal\n */\nexport function chunk(msg: string, maxSafeMessageSize = MAX_SAFE_MESSAGE_SIZE) {\n\tif (msg.length < maxSafeMessageSize) {\n\t\treturn [msg]\n\t} else {\n\t\tconst chunks = []\n\t\tlet chunkNumber = 0\n\t\tlet offset = msg.length\n\t\twhile (offset > 0) {\n\t\t\tconst prefix = `${chunkNumber}_`\n\t\t\tconst chunkSize = Math.max(Math.min(maxSafeMessageSize - prefix.length, offset), 1)\n\t\t\tchunks.unshift(prefix + msg.slice(offset - chunkSize, offset))\n\t\t\toffset -= chunkSize\n\t\t\tchunkNumber++\n\t\t}\n\t\treturn chunks\n\t}\n}\n\nconst chunkRe = /^(\\d+)_(.*)$/\n\n/**\n * Assembles chunked JSON messages back into complete objects.\n * Handles both regular JSON messages and chunked messages created by the chunk() function.\n * Maintains internal state to track partially received chunked messages.\n *\n * @example\n * ```ts\n * const assembler = new JsonChunkAssembler()\n *\n * // Handle regular JSON message\n * const result1 = assembler.handleMessage('{\"hello\": \"world\"}')\n * // Returns: { data: { hello: \"world\" }, stringified: '{\"hello\": \"world\"}' }\n *\n * // Handle chunked message\n * assembler.handleMessage('1_hello') // Returns: null (partial)\n * const result2 = assembler.handleMessage('0_ world')\n * // Returns: { data: \"hello world\", stringified: \"hello world\" }\n * ```\n *\n * @public\n */\nexport class JsonChunkAssembler {\n\t/**\n\t * Current assembly state - either 'idle' or tracking chunks being received\n\t */\n\tstate:\n\t\t| 'idle'\n\t\t| {\n\t\t\t\tchunksReceived: string[]\n\t\t\t\ttotalChunks: number\n\t\t } = 'idle'\n\n\t/**\n\t * Processes a single message, which can be either a complete JSON object or a chunk.\n\t * For complete JSON objects (starting with '{'), parses immediately.\n\t * For chunks (prefixed with \"{number}_\"), accumulates until all chunks received.\n\t *\n\t * @param msg - The message to process, either JSON or chunk format\n\t * @returns Result object with data/stringified on success, error object on failure, or null for incomplete chunks\n\t * \t-
|
|
4
|
+
"sourcesContent": ["// quarter of a megabyte, max possible utf-8 string size\n\n// cloudflare workers only accept messages of max 1mb\nconst MAX_CLIENT_SENT_MESSAGE_SIZE_BYTES = 1024 * 1024\n// utf-8 is max 4 bytes per char\nconst MAX_BYTES_PER_CHAR = 4\n\n// in the (admittedly impossible) worst case, the max size is 1/4 of a megabyte\nconst MAX_SAFE_MESSAGE_SIZE = MAX_CLIENT_SENT_MESSAGE_SIZE_BYTES / MAX_BYTES_PER_CHAR\n\n/**\n * Splits a string into smaller chunks suitable for transmission over WebSockets.\n * This function ensures messages don't exceed size limits imposed by platforms like Cloudflare Workers (1MB max).\n * Each chunk is prefixed with a number indicating how many more chunks follow.\n *\n * @param msg - The string to split into chunks\n * @param maxSafeMessageSize - Maximum safe size for each chunk in characters. Defaults to quarter megabyte to account for UTF-8 encoding\n * @returns Array of chunked strings, each prefixed with \"\\{number\\}_\" where number indicates remaining chunks\n *\n * @example\n * ```ts\n * // Small message - returns as single chunk\n * chunk('hello world') // ['hello world']\n *\n * // Large message - splits into multiple chunks\n * chunk('very long message...', 10)\n * // ['2_very long', '1_ message', '0_...']\n * ```\n *\n * @internal\n */\nexport function chunk(msg: string, maxSafeMessageSize = MAX_SAFE_MESSAGE_SIZE) {\n\tif (msg.length < maxSafeMessageSize) {\n\t\treturn [msg]\n\t} else {\n\t\tconst chunks = []\n\t\tlet chunkNumber = 0\n\t\tlet offset = msg.length\n\t\twhile (offset > 0) {\n\t\t\tconst prefix = `${chunkNumber}_`\n\t\t\tconst chunkSize = Math.max(Math.min(maxSafeMessageSize - prefix.length, offset), 1)\n\t\t\tchunks.unshift(prefix + msg.slice(offset - chunkSize, offset))\n\t\t\toffset -= chunkSize\n\t\t\tchunkNumber++\n\t\t}\n\t\treturn chunks\n\t}\n}\n\nconst chunkRe = /^(\\d+)_(.*)$/\n\n/**\n * Assembles chunked JSON messages back into complete objects.\n * Handles both regular JSON messages and chunked messages created by the chunk() function.\n * Maintains internal state to track partially received chunked messages.\n *\n * @example\n * ```ts\n * const assembler = new JsonChunkAssembler()\n *\n * // Handle regular JSON message\n * const result1 = assembler.handleMessage('{\"hello\": \"world\"}')\n * // Returns: { data: { hello: \"world\" }, stringified: '{\"hello\": \"world\"}' }\n *\n * // Handle chunked message\n * assembler.handleMessage('1_hello') // Returns: null (partial)\n * const result2 = assembler.handleMessage('0_ world')\n * // Returns: { data: \"hello world\", stringified: \"hello world\" }\n * ```\n *\n * @public\n */\nexport class JsonChunkAssembler {\n\t/**\n\t * Current assembly state - either 'idle' or tracking chunks being received\n\t */\n\tstate:\n\t\t| 'idle'\n\t\t| {\n\t\t\t\tchunksReceived: string[]\n\t\t\t\ttotalChunks: number\n\t\t } = 'idle'\n\n\t/**\n\t * Processes a single message, which can be either a complete JSON object or a chunk.\n\t * For complete JSON objects (starting with '\\{'), parses immediately.\n\t * For chunks (prefixed with \"\\{number\\}_\"), accumulates until all chunks received.\n\t *\n\t * @param msg - The message to process, either JSON or chunk format\n\t * @returns Result object with data/stringified on success, error object on failure, or null for incomplete chunks\n\t * \t- `\\{ data: object, stringified: string \\}` - Successfully parsed complete message\n\t * \t- `\\{ error: Error \\}` - Parse error or invalid chunk sequence\n\t * \t- `null` - Chunk received but more chunks expected\n\t *\n\t * @example\n\t * ```ts\n\t * const assembler = new JsonChunkAssembler()\n\t *\n\t * // Complete JSON message\n\t * const result = assembler.handleMessage('{\"key\": \"value\"}')\n\t * if (result && 'data' in result) {\n\t * console.log(result.data) // { key: \"value\" }\n\t * }\n\t *\n\t * // Chunked message sequence\n\t * assembler.handleMessage('2_hel') // null - more chunks expected\n\t * assembler.handleMessage('1_lo ') // null - more chunks expected\n\t * assembler.handleMessage('0_wor') // { data: \"hello wor\", stringified: \"hello wor\" }\n\t * ```\n\t */\n\thandleMessage(msg: string): { error: Error } | { stringified: string; data: object } | null {\n\t\tif (msg.startsWith('{')) {\n\t\t\tconst error = this.state === 'idle' ? undefined : new Error('Unexpected non-chunk message')\n\t\t\tthis.state = 'idle'\n\t\t\treturn error ? { error } : { data: JSON.parse(msg), stringified: msg }\n\t\t} else {\n\t\t\tconst match = chunkRe.exec(msg)!\n\t\t\tif (!match) {\n\t\t\t\tthis.state = 'idle'\n\t\t\t\treturn { error: new Error('Invalid chunk: ' + JSON.stringify(msg.slice(0, 20) + '...')) }\n\t\t\t}\n\t\t\tconst numChunksRemaining = Number(match[1])\n\t\t\tconst data = match[2]\n\n\t\t\tif (this.state === 'idle') {\n\t\t\t\tthis.state = {\n\t\t\t\t\tchunksReceived: [data],\n\t\t\t\t\ttotalChunks: numChunksRemaining + 1,\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tthis.state.chunksReceived.push(data)\n\t\t\t\tif (numChunksRemaining !== this.state.totalChunks - this.state.chunksReceived.length) {\n\t\t\t\t\tthis.state = 'idle'\n\t\t\t\t\treturn { error: new Error(`Chunks received in wrong order`) }\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (this.state.chunksReceived.length === this.state.totalChunks) {\n\t\t\t\ttry {\n\t\t\t\t\tconst stringified = this.state.chunksReceived.join('')\n\t\t\t\t\tconst data = JSON.parse(stringified)\n\t\t\t\t\treturn { data, stringified }\n\t\t\t\t} catch (e) {\n\t\t\t\t\treturn { error: e as Error }\n\t\t\t\t} finally {\n\t\t\t\t\tthis.state = 'idle'\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn null\n\t\t}\n\t}\n}\n"],
|
|
5
5
|
"mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAGA,MAAM,qCAAqC,OAAO;AAElD,MAAM,qBAAqB;AAG3B,MAAM,wBAAwB,qCAAqC;AAuB5D,SAAS,MAAM,KAAa,qBAAqB,uBAAuB;AAC9E,MAAI,IAAI,SAAS,oBAAoB;AACpC,WAAO,CAAC,GAAG;AAAA,EACZ,OAAO;AACN,UAAM,SAAS,CAAC;AAChB,QAAI,cAAc;AAClB,QAAI,SAAS,IAAI;AACjB,WAAO,SAAS,GAAG;AAClB,YAAM,SAAS,GAAG,WAAW;AAC7B,YAAM,YAAY,KAAK,IAAI,KAAK,IAAI,qBAAqB,OAAO,QAAQ,MAAM,GAAG,CAAC;AAClF,aAAO,QAAQ,SAAS,IAAI,MAAM,SAAS,WAAW,MAAM,CAAC;AAC7D,gBAAU;AACV;AAAA,IACD;AACA,WAAO;AAAA,EACR;AACD;AAEA,MAAM,UAAU;AAuBT,MAAM,mBAAmB;AAAA;AAAA;AAAA;AAAA,EAI/B,QAKO;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EA6BP,cAAc,KAA8E;AAC3F,QAAI,IAAI,WAAW,GAAG,GAAG;AACxB,YAAM,QAAQ,KAAK,UAAU,SAAS,SAAY,IAAI,MAAM,8BAA8B;AAC1F,WAAK,QAAQ;AACb,aAAO,QAAQ,EAAE,MAAM,IAAI,EAAE,MAAM,KAAK,MAAM,GAAG,GAAG,aAAa,IAAI;AAAA,IACtE,OAAO;AACN,YAAM,QAAQ,QAAQ,KAAK,GAAG;AAC9B,UAAI,CAAC,OAAO;AACX,aAAK,QAAQ;AACb,eAAO,EAAE,OAAO,IAAI,MAAM,oBAAoB,KAAK,UAAU,IAAI,MAAM,GAAG,EAAE,IAAI,KAAK,CAAC,EAAE;AAAA,MACzF;AACA,YAAM,qBAAqB,OAAO,MAAM,CAAC,CAAC;AAC1C,YAAM,OAAO,MAAM,CAAC;AAEpB,UAAI,KAAK,UAAU,QAAQ;AAC1B,aAAK,QAAQ;AAAA,UACZ,gBAAgB,CAAC,IAAI;AAAA,UACrB,aAAa,qBAAqB;AAAA,QACnC;AAAA,MACD,OAAO;AACN,aAAK,MAAM,eAAe,KAAK,IAAI;AACnC,YAAI,uBAAuB,KAAK,MAAM,cAAc,KAAK,MAAM,eAAe,QAAQ;AACrF,eAAK,QAAQ;AACb,iBAAO,EAAE,OAAO,IAAI,MAAM,gCAAgC,EAAE;AAAA,QAC7D;AAAA,MACD;AACA,UAAI,KAAK,MAAM,eAAe,WAAW,KAAK,MAAM,aAAa;AAChE,YAAI;AACH,gBAAM,cAAc,KAAK,MAAM,eAAe,KAAK,EAAE;AACrD,gBAAMA,QAAO,KAAK,MAAM,WAAW;AACnC,iBAAO,EAAE,MAAAA,OAAM,YAAY;AAAA,QAC5B,SAAS,GAAG;AACX,iBAAO,EAAE,OAAO,EAAW;AAAA,QAC5B,UAAE;AACD,eAAK,QAAQ;AAAA,QACd;AAAA,MACD;AACA,aAAO;AAAA,IACR;AAAA,EACD;AACD;",
|
|
6
6
|
"names": ["data"]
|
|
7
7
|
}
|
package/dist-cjs/lib/diff.js
CHANGED
|
@@ -57,9 +57,9 @@ const ValueOpType = {
|
|
|
57
57
|
Patch: "patch"
|
|
58
58
|
};
|
|
59
59
|
function diffRecord(prev, next, legacyAppendMode = false) {
|
|
60
|
-
return diffObject(prev, next, legacyAppendMode);
|
|
60
|
+
return diffObject(prev, next, /* @__PURE__ */ new Set(["props", "meta"]), legacyAppendMode);
|
|
61
61
|
}
|
|
62
|
-
function diffObject(prev, next, legacyAppendMode) {
|
|
62
|
+
function diffObject(prev, next, nestedKeys, legacyAppendMode) {
|
|
63
63
|
if (prev === next) {
|
|
64
64
|
return null;
|
|
65
65
|
}
|
|
@@ -70,10 +70,17 @@ function diffObject(prev, next, legacyAppendMode) {
|
|
|
70
70
|
result[key] = [ValueOpType.Delete];
|
|
71
71
|
continue;
|
|
72
72
|
}
|
|
73
|
-
const
|
|
74
|
-
|
|
73
|
+
const prevValue = prev[key];
|
|
74
|
+
const nextValue = next[key];
|
|
75
|
+
if (nestedKeys?.has(key) || Array.isArray(prevValue) && Array.isArray(nextValue) || typeof prevValue === "string" && typeof nextValue === "string") {
|
|
76
|
+
const diff = diffValue(prevValue, nextValue, legacyAppendMode);
|
|
77
|
+
if (diff) {
|
|
78
|
+
if (!result) result = {};
|
|
79
|
+
result[key] = diff;
|
|
80
|
+
}
|
|
81
|
+
} else if (!(0, import_utils.isEqual)(prevValue, nextValue)) {
|
|
75
82
|
if (!result) result = {};
|
|
76
|
-
result[key] =
|
|
83
|
+
result[key] = [ValueOpType.Put, nextValue];
|
|
77
84
|
}
|
|
78
85
|
}
|
|
79
86
|
for (const key of Object.keys(next)) {
|
|
@@ -97,7 +104,7 @@ function diffValue(valueA, valueB, legacyAppendMode) {
|
|
|
97
104
|
} else if (!valueA || !valueB || typeof valueA !== "object" || typeof valueB !== "object") {
|
|
98
105
|
return (0, import_utils.isEqual)(valueA, valueB) ? null : [ValueOpType.Put, valueB];
|
|
99
106
|
} else {
|
|
100
|
-
const diff = diffObject(valueA, valueB, legacyAppendMode);
|
|
107
|
+
const diff = diffObject(valueA, valueB, void 0, legacyAppendMode);
|
|
101
108
|
return diff ? [ValueOpType.Patch, diff] : null;
|
|
102
109
|
}
|
|
103
110
|
}
|
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 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, legacyAppendMode)\n}\n\nfunction diffObject(prev: object, next: object, legacyAppendMode: boolean): 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 diff = diffValue((prev as any)[key], (next as any)[key], legacyAppendMode)\n\t\tif (diff) {\n\t\t\tif (!result) result = {}\n\t\t\tresult[key] = diff\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, 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": ";;;;;;;;;;;;;;;;;;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;AA0EO,SAAS,WACf,MACA,MACA,mBAAmB,OACC;AACpB,SAAO,WAAW,MAAM,MAAM,gBAAgB;
|
|
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": ";;;;;;;;;;;;;;;;;;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;AA0EO,SAAS,WACf,MACA,MACA,mBAAmB,OACC;AACpB,SAAO,WAAW,MAAM,MAAM,oBAAI,IAAI,CAAC,SAAS,MAAM,CAAC,GAAG,gBAAgB;AAC3E;AAEA,SAAS,WACR,MACA,MACA,YACA,kBACoB;AACpB,MAAI,SAAS,MAAM;AAClB,WAAO;AAAA,EACR;AACA,MAAI,SAA4B;AAChC,aAAW,OAAO,OAAO,KAAK,IAAI,GAAG;AAEpC,QAAI,EAAE,OAAO,OAAO;AACnB,UAAI,CAAC,OAAQ,UAAS,CAAC;AACvB,aAAO,GAAG,IAAI,CAAC,YAAY,MAAM;AACjC;AAAA,IACD;AACA,UAAM,YAAa,KAAa,GAAG;AACnC,UAAM,YAAa,KAAa,GAAG;AACnC,QACC,YAAY,IAAI,GAAG,KAClB,MAAM,QAAQ,SAAS,KAAK,MAAM,QAAQ,SAAS,KACnD,OAAO,cAAc,YAAY,OAAO,cAAc,UACtD;AAED,YAAM,OAAO,UAAU,WAAW,WAAW,gBAAgB;AAC7D,UAAI,MAAM;AACT,YAAI,CAAC,OAAQ,UAAS,CAAC;AACvB,eAAO,GAAG,IAAI;AAAA,MACf;AAAA,IACD,WAAW,KAAC,sBAAQ,WAAW,SAAS,GAAG;AAC1C,UAAI,CAAC,OAAQ,UAAS,CAAC;AACvB,aAAO,GAAG,IAAI,CAAC,YAAY,KAAK,SAAS;AAAA,IAC1C;AAAA,EACD;AACA,aAAW,OAAO,OAAO,KAAK,IAAI,GAAG;AAEpC,QAAI,EAAE,OAAO,OAAO;AACnB,UAAI,CAAC,OAAQ,UAAS,CAAC;AACvB,aAAO,GAAG,IAAI,CAAC,YAAY,KAAM,KAAa,GAAG,CAAC;AAAA,IACnD;AAAA,EACD;AACA,SAAO;AACR;AAEA,SAAS,UAAU,QAAiB,QAAiB,kBAA2C;AAC/F,MAAI,OAAO,GAAG,QAAQ,MAAM,EAAG,QAAO;AACtC,MAAI,MAAM,QAAQ,MAAM,KAAK,MAAM,QAAQ,MAAM,GAAG;AACnD,WAAO,UAAU,QAAQ,QAAQ,gBAAgB;AAAA,EAClD,WAAW,OAAO,WAAW,YAAY,OAAO,WAAW,UAAU;AACpE,QAAI,CAAC,oBAAoB,OAAO,WAAW,MAAM,GAAG;AACnD,YAAM,eAAe,OAAO,MAAM,OAAO,MAAM;AAC/C,aAAO,CAAC,YAAY,QAAQ,cAAc,OAAO,MAAM;AAAA,IACxD;AACA,WAAO,CAAC,YAAY,KAAK,MAAM;AAAA,EAChC,WAAW,CAAC,UAAU,CAAC,UAAU,OAAO,WAAW,YAAY,OAAO,WAAW,UAAU;AAC1F,eAAO,sBAAQ,QAAQ,MAAM,IAAI,OAAO,CAAC,YAAY,KAAK,MAAM;AAAA,EACjE,OAAO;AACN,UAAM,OAAO,WAAW,QAAQ,QAAQ,QAAW,gBAAgB;AACnE,WAAO,OAAO,CAAC,YAAY,OAAO,IAAI,IAAI;AAAA,EAC3C;AACD;AAEA,SAAS,UACR,WACA,WACA,kBACoC;AACpC,MAAI,OAAO,GAAG,WAAW,SAAS,EAAG,QAAO;AAE5C,MAAI,UAAU,WAAW,UAAU,QAAQ;AAE1C,UAAM,kBAAkB,KAAK,IAAI,UAAU,SAAS,GAAG,CAAC;AACxD,UAAM,iBAAiB,CAAC;AACxB,aAAS,IAAI,GAAG,IAAI,UAAU,QAAQ,KAAK;AAC1C,UAAI,KAAC,sBAAQ,UAAU,CAAC,GAAG,UAAU,CAAC,CAAC,GAAG;AACzC,uBAAe,KAAK,CAAC;AACrB,YAAI,eAAe,SAAS,iBAAiB;AAC5C,iBAAO,CAAC,YAAY,KAAK,SAAS;AAAA,QACnC;AAAA,MACD;AAAA,IACD;AACA,QAAI,eAAe,WAAW,GAAG;AAEhC,aAAO;AAAA,IACR;AACA,UAAM,OAAmB,CAAC;AAC1B,eAAW,KAAK,gBAAgB;AAC/B,YAAM,WAAW,UAAU,CAAC;AAC5B,YAAM,WAAW,UAAU,CAAC;AAC5B,UAAI,CAAC,YAAY,CAAC,UAAU;AAC3B,aAAK,CAAC,IAAI,CAAC,YAAY,KAAK,QAAQ;AAAA,MACrC,WAAW,OAAO,aAAa,YAAY,OAAO,aAAa,UAAU;AACxE,cAAM,KAAK,UAAU,UAAU,UAAU,gBAAgB;AACzD,YAAI,IAAI;AACP,eAAK,CAAC,IAAI;AAAA,QACX;AAAA,MACD,OAAO;AACN,aAAK,CAAC,IAAI,CAAC,YAAY,KAAK,QAAQ;AAAA,MACrC;AAAA,IACD;AACA,WAAO,CAAC,YAAY,OAAO,IAAI;AAAA,EAChC;AAIA,WAAS,IAAI,GAAG,IAAI,UAAU,QAAQ,KAAK;AAC1C,QAAI,KAAC,sBAAQ,UAAU,CAAC,GAAG,UAAU,CAAC,CAAC,GAAG;AACzC,aAAO,CAAC,YAAY,KAAK,SAAS;AAAA,IACnC;AAAA,EACD;AAEA,SAAO,CAAC,YAAY,QAAQ,UAAU,MAAM,UAAU,MAAM,GAAG,UAAU,MAAM;AAChF;AAyBO,SAAS,gBAAkC,QAAW,YAA2B;AAEvF,MAAI,CAAC,UAAU,OAAO,WAAW,SAAU,QAAO;AAClD,QAAM,UAAU,MAAM,QAAQ,MAAM;AACpC,MAAI,YAA6B;AACjC,QAAM,MAAM,CAAC,GAAQ,MAAW;AAC/B,QAAI,CAAC,WAAW;AACf,UAAI,SAAS;AACZ,oBAAY,CAAC,GAAG,MAAM;AAAA,MACvB,OAAO;AACN,oBAAY,EAAE,GAAG,OAAO;AAAA,MACzB;AAAA,IACD;AACA,QAAI,SAAS;AACZ,gBAAU,OAAO,CAAC,CAAC,IAAI;AAAA,IACxB,OAAO;AACN,gBAAU,CAAC,IAAI;AAAA,IAChB;AAAA,EACD;AACA,aAAW,CAAC,KAAK,EAAE,KAAK,OAAO,QAAQ,UAAU,GAAG;AACnD,YAAQ,GAAG,CAAC,GAAG;AAAA,MACd,KAAK,YAAY,KAAK;AACrB,cAAM,QAAQ,GAAG,CAAC;AAClB,YAAI,KAAC,sBAAQ,OAAO,GAAc,GAAG,KAAK,GAAG;AAC5C,cAAI,KAAK,KAAK;AAAA,QACf;AACA;AAAA,MACD;AAAA,MACA,KAAK,YAAY,QAAQ;AACxB,cAAM,QAAQ,GAAG,CAAC;AAClB,cAAM,SAAS,GAAG,CAAC;AACnB,cAAM,eAAe,OAAO,GAAc;AAC1C,YAAI,MAAM,QAAQ,YAAY,KAAK,MAAM,QAAQ,KAAK,KAAK,aAAa,WAAW,QAAQ;AAC1F,cAAI,KAAK,CAAC,GAAG,cAAc,GAAG,KAAK,CAAC;AAAA,QACrC,WACC,OAAO,iBAAiB,YACxB,OAAO,UAAU,YACjB,aAAa,WAAW,QACvB;AACD,cAAI,KAAK,eAAe,KAAK;AAAA,QAC9B;AAEA;AAAA,MACD;AAAA,MACA,KAAK,YAAY,OAAO;AACvB,YAAI,OAAO,GAAc,KAAK,OAAO,OAAO,GAAc,MAAM,UAAU;AACzE,gBAAM,OAAO,GAAG,CAAC;AACjB,gBAAM,UAAU,gBAAgB,OAAO,GAAc,GAAa,IAAI;AACtE,cAAI,YAAY,OAAO,GAAc,GAAG;AACvC,gBAAI,KAAK,OAAO;AAAA,UACjB;AAAA,QACD;AACA;AAAA,MACD;AAAA,MACA,KAAK,YAAY,QAAQ;AACxB,YAAI,OAAO,QAAQ;AAClB,cAAI,CAAC,WAAW;AACf,gBAAI,SAAS;AACZ,sBAAQ,MAAM,wDAAwD;AACtE,0BAAY,CAAC,GAAG,MAAM;AAAA,YACvB,OAAO;AACN,0BAAY,EAAE,GAAG,OAAO;AAAA,YACzB;AAAA,UACD;AACA,iBAAO,UAAU,GAAG;AAAA,QACrB;AAAA,MACD;AAAA,IACD;AAAA,EACD;AAEA,SAAO,aAAa;AACrB;",
|
|
6
6
|
"names": ["diff"]
|
|
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 */
|
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.2.0-canary.
|
|
29
|
+
"4.2.0-canary.6c9544477685",
|
|
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, 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,
|
|
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
|
}
|
package/dist-esm/lib/chunk.mjs
CHANGED
|
@@ -26,13 +26,13 @@ class JsonChunkAssembler {
|
|
|
26
26
|
state = "idle";
|
|
27
27
|
/**
|
|
28
28
|
* Processes a single message, which can be either a complete JSON object or a chunk.
|
|
29
|
-
* For complete JSON objects (starting with '{'), parses immediately.
|
|
30
|
-
* For chunks (prefixed with "{number}_"), accumulates until all chunks received.
|
|
29
|
+
* For complete JSON objects (starting with '\{'), parses immediately.
|
|
30
|
+
* For chunks (prefixed with "\{number\}_"), accumulates until all chunks received.
|
|
31
31
|
*
|
|
32
32
|
* @param msg - The message to process, either JSON or chunk format
|
|
33
33
|
* @returns Result object with data/stringified on success, error object on failure, or null for incomplete chunks
|
|
34
|
-
* -
|
|
35
|
-
* -
|
|
34
|
+
* - `\{ data: object, stringified: string \}` - Successfully parsed complete message
|
|
35
|
+
* - `\{ error: Error \}` - Parse error or invalid chunk sequence
|
|
36
36
|
* - `null` - Chunk received but more chunks expected
|
|
37
37
|
*
|
|
38
38
|
* @example
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/lib/chunk.ts"],
|
|
4
|
-
"sourcesContent": ["// quarter of a megabyte, max possible utf-8 string size\n\n// cloudflare workers only accept messages of max 1mb\nconst MAX_CLIENT_SENT_MESSAGE_SIZE_BYTES = 1024 * 1024\n// utf-8 is max 4 bytes per char\nconst MAX_BYTES_PER_CHAR = 4\n\n// in the (admittedly impossible) worst case, the max size is 1/4 of a megabyte\nconst MAX_SAFE_MESSAGE_SIZE = MAX_CLIENT_SENT_MESSAGE_SIZE_BYTES / MAX_BYTES_PER_CHAR\n\n/**\n * Splits a string into smaller chunks suitable for transmission over WebSockets.\n * This function ensures messages don't exceed size limits imposed by platforms like Cloudflare Workers (1MB max).\n * Each chunk is prefixed with a number indicating how many more chunks follow.\n *\n * @param msg - The string to split into chunks\n * @param maxSafeMessageSize - Maximum safe size for each chunk in characters. Defaults to quarter megabyte to account for UTF-8 encoding\n * @returns Array of chunked strings, each prefixed with \"\\{number\\}_\" where number indicates remaining chunks\n *\n * @example\n * ```ts\n * // Small message - returns as single chunk\n * chunk('hello world') // ['hello world']\n *\n * // Large message - splits into multiple chunks\n * chunk('very long message...', 10)\n * // ['2_very long', '1_ message', '0_...']\n * ```\n *\n * @internal\n */\nexport function chunk(msg: string, maxSafeMessageSize = MAX_SAFE_MESSAGE_SIZE) {\n\tif (msg.length < maxSafeMessageSize) {\n\t\treturn [msg]\n\t} else {\n\t\tconst chunks = []\n\t\tlet chunkNumber = 0\n\t\tlet offset = msg.length\n\t\twhile (offset > 0) {\n\t\t\tconst prefix = `${chunkNumber}_`\n\t\t\tconst chunkSize = Math.max(Math.min(maxSafeMessageSize - prefix.length, offset), 1)\n\t\t\tchunks.unshift(prefix + msg.slice(offset - chunkSize, offset))\n\t\t\toffset -= chunkSize\n\t\t\tchunkNumber++\n\t\t}\n\t\treturn chunks\n\t}\n}\n\nconst chunkRe = /^(\\d+)_(.*)$/\n\n/**\n * Assembles chunked JSON messages back into complete objects.\n * Handles both regular JSON messages and chunked messages created by the chunk() function.\n * Maintains internal state to track partially received chunked messages.\n *\n * @example\n * ```ts\n * const assembler = new JsonChunkAssembler()\n *\n * // Handle regular JSON message\n * const result1 = assembler.handleMessage('{\"hello\": \"world\"}')\n * // Returns: { data: { hello: \"world\" }, stringified: '{\"hello\": \"world\"}' }\n *\n * // Handle chunked message\n * assembler.handleMessage('1_hello') // Returns: null (partial)\n * const result2 = assembler.handleMessage('0_ world')\n * // Returns: { data: \"hello world\", stringified: \"hello world\" }\n * ```\n *\n * @public\n */\nexport class JsonChunkAssembler {\n\t/**\n\t * Current assembly state - either 'idle' or tracking chunks being received\n\t */\n\tstate:\n\t\t| 'idle'\n\t\t| {\n\t\t\t\tchunksReceived: string[]\n\t\t\t\ttotalChunks: number\n\t\t } = 'idle'\n\n\t/**\n\t * Processes a single message, which can be either a complete JSON object or a chunk.\n\t * For complete JSON objects (starting with '{'), parses immediately.\n\t * For chunks (prefixed with \"{number}_\"), accumulates until all chunks received.\n\t *\n\t * @param msg - The message to process, either JSON or chunk format\n\t * @returns Result object with data/stringified on success, error object on failure, or null for incomplete chunks\n\t * \t-
|
|
4
|
+
"sourcesContent": ["// quarter of a megabyte, max possible utf-8 string size\n\n// cloudflare workers only accept messages of max 1mb\nconst MAX_CLIENT_SENT_MESSAGE_SIZE_BYTES = 1024 * 1024\n// utf-8 is max 4 bytes per char\nconst MAX_BYTES_PER_CHAR = 4\n\n// in the (admittedly impossible) worst case, the max size is 1/4 of a megabyte\nconst MAX_SAFE_MESSAGE_SIZE = MAX_CLIENT_SENT_MESSAGE_SIZE_BYTES / MAX_BYTES_PER_CHAR\n\n/**\n * Splits a string into smaller chunks suitable for transmission over WebSockets.\n * This function ensures messages don't exceed size limits imposed by platforms like Cloudflare Workers (1MB max).\n * Each chunk is prefixed with a number indicating how many more chunks follow.\n *\n * @param msg - The string to split into chunks\n * @param maxSafeMessageSize - Maximum safe size for each chunk in characters. Defaults to quarter megabyte to account for UTF-8 encoding\n * @returns Array of chunked strings, each prefixed with \"\\{number\\}_\" where number indicates remaining chunks\n *\n * @example\n * ```ts\n * // Small message - returns as single chunk\n * chunk('hello world') // ['hello world']\n *\n * // Large message - splits into multiple chunks\n * chunk('very long message...', 10)\n * // ['2_very long', '1_ message', '0_...']\n * ```\n *\n * @internal\n */\nexport function chunk(msg: string, maxSafeMessageSize = MAX_SAFE_MESSAGE_SIZE) {\n\tif (msg.length < maxSafeMessageSize) {\n\t\treturn [msg]\n\t} else {\n\t\tconst chunks = []\n\t\tlet chunkNumber = 0\n\t\tlet offset = msg.length\n\t\twhile (offset > 0) {\n\t\t\tconst prefix = `${chunkNumber}_`\n\t\t\tconst chunkSize = Math.max(Math.min(maxSafeMessageSize - prefix.length, offset), 1)\n\t\t\tchunks.unshift(prefix + msg.slice(offset - chunkSize, offset))\n\t\t\toffset -= chunkSize\n\t\t\tchunkNumber++\n\t\t}\n\t\treturn chunks\n\t}\n}\n\nconst chunkRe = /^(\\d+)_(.*)$/\n\n/**\n * Assembles chunked JSON messages back into complete objects.\n * Handles both regular JSON messages and chunked messages created by the chunk() function.\n * Maintains internal state to track partially received chunked messages.\n *\n * @example\n * ```ts\n * const assembler = new JsonChunkAssembler()\n *\n * // Handle regular JSON message\n * const result1 = assembler.handleMessage('{\"hello\": \"world\"}')\n * // Returns: { data: { hello: \"world\" }, stringified: '{\"hello\": \"world\"}' }\n *\n * // Handle chunked message\n * assembler.handleMessage('1_hello') // Returns: null (partial)\n * const result2 = assembler.handleMessage('0_ world')\n * // Returns: { data: \"hello world\", stringified: \"hello world\" }\n * ```\n *\n * @public\n */\nexport class JsonChunkAssembler {\n\t/**\n\t * Current assembly state - either 'idle' or tracking chunks being received\n\t */\n\tstate:\n\t\t| 'idle'\n\t\t| {\n\t\t\t\tchunksReceived: string[]\n\t\t\t\ttotalChunks: number\n\t\t } = 'idle'\n\n\t/**\n\t * Processes a single message, which can be either a complete JSON object or a chunk.\n\t * For complete JSON objects (starting with '\\{'), parses immediately.\n\t * For chunks (prefixed with \"\\{number\\}_\"), accumulates until all chunks received.\n\t *\n\t * @param msg - The message to process, either JSON or chunk format\n\t * @returns Result object with data/stringified on success, error object on failure, or null for incomplete chunks\n\t * \t- `\\{ data: object, stringified: string \\}` - Successfully parsed complete message\n\t * \t- `\\{ error: Error \\}` - Parse error or invalid chunk sequence\n\t * \t- `null` - Chunk received but more chunks expected\n\t *\n\t * @example\n\t * ```ts\n\t * const assembler = new JsonChunkAssembler()\n\t *\n\t * // Complete JSON message\n\t * const result = assembler.handleMessage('{\"key\": \"value\"}')\n\t * if (result && 'data' in result) {\n\t * console.log(result.data) // { key: \"value\" }\n\t * }\n\t *\n\t * // Chunked message sequence\n\t * assembler.handleMessage('2_hel') // null - more chunks expected\n\t * assembler.handleMessage('1_lo ') // null - more chunks expected\n\t * assembler.handleMessage('0_wor') // { data: \"hello wor\", stringified: \"hello wor\" }\n\t * ```\n\t */\n\thandleMessage(msg: string): { error: Error } | { stringified: string; data: object } | null {\n\t\tif (msg.startsWith('{')) {\n\t\t\tconst error = this.state === 'idle' ? undefined : new Error('Unexpected non-chunk message')\n\t\t\tthis.state = 'idle'\n\t\t\treturn error ? { error } : { data: JSON.parse(msg), stringified: msg }\n\t\t} else {\n\t\t\tconst match = chunkRe.exec(msg)!\n\t\t\tif (!match) {\n\t\t\t\tthis.state = 'idle'\n\t\t\t\treturn { error: new Error('Invalid chunk: ' + JSON.stringify(msg.slice(0, 20) + '...')) }\n\t\t\t}\n\t\t\tconst numChunksRemaining = Number(match[1])\n\t\t\tconst data = match[2]\n\n\t\t\tif (this.state === 'idle') {\n\t\t\t\tthis.state = {\n\t\t\t\t\tchunksReceived: [data],\n\t\t\t\t\ttotalChunks: numChunksRemaining + 1,\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tthis.state.chunksReceived.push(data)\n\t\t\t\tif (numChunksRemaining !== this.state.totalChunks - this.state.chunksReceived.length) {\n\t\t\t\t\tthis.state = 'idle'\n\t\t\t\t\treturn { error: new Error(`Chunks received in wrong order`) }\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (this.state.chunksReceived.length === this.state.totalChunks) {\n\t\t\t\ttry {\n\t\t\t\t\tconst stringified = this.state.chunksReceived.join('')\n\t\t\t\t\tconst data = JSON.parse(stringified)\n\t\t\t\t\treturn { data, stringified }\n\t\t\t\t} catch (e) {\n\t\t\t\t\treturn { error: e as Error }\n\t\t\t\t} finally {\n\t\t\t\t\tthis.state = 'idle'\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn null\n\t\t}\n\t}\n}\n"],
|
|
5
5
|
"mappings": "AAGA,MAAM,qCAAqC,OAAO;AAElD,MAAM,qBAAqB;AAG3B,MAAM,wBAAwB,qCAAqC;AAuB5D,SAAS,MAAM,KAAa,qBAAqB,uBAAuB;AAC9E,MAAI,IAAI,SAAS,oBAAoB;AACpC,WAAO,CAAC,GAAG;AAAA,EACZ,OAAO;AACN,UAAM,SAAS,CAAC;AAChB,QAAI,cAAc;AAClB,QAAI,SAAS,IAAI;AACjB,WAAO,SAAS,GAAG;AAClB,YAAM,SAAS,GAAG,WAAW;AAC7B,YAAM,YAAY,KAAK,IAAI,KAAK,IAAI,qBAAqB,OAAO,QAAQ,MAAM,GAAG,CAAC;AAClF,aAAO,QAAQ,SAAS,IAAI,MAAM,SAAS,WAAW,MAAM,CAAC;AAC7D,gBAAU;AACV;AAAA,IACD;AACA,WAAO;AAAA,EACR;AACD;AAEA,MAAM,UAAU;AAuBT,MAAM,mBAAmB;AAAA;AAAA;AAAA;AAAA,EAI/B,QAKO;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EA6BP,cAAc,KAA8E;AAC3F,QAAI,IAAI,WAAW,GAAG,GAAG;AACxB,YAAM,QAAQ,KAAK,UAAU,SAAS,SAAY,IAAI,MAAM,8BAA8B;AAC1F,WAAK,QAAQ;AACb,aAAO,QAAQ,EAAE,MAAM,IAAI,EAAE,MAAM,KAAK,MAAM,GAAG,GAAG,aAAa,IAAI;AAAA,IACtE,OAAO;AACN,YAAM,QAAQ,QAAQ,KAAK,GAAG;AAC9B,UAAI,CAAC,OAAO;AACX,aAAK,QAAQ;AACb,eAAO,EAAE,OAAO,IAAI,MAAM,oBAAoB,KAAK,UAAU,IAAI,MAAM,GAAG,EAAE,IAAI,KAAK,CAAC,EAAE;AAAA,MACzF;AACA,YAAM,qBAAqB,OAAO,MAAM,CAAC,CAAC;AAC1C,YAAM,OAAO,MAAM,CAAC;AAEpB,UAAI,KAAK,UAAU,QAAQ;AAC1B,aAAK,QAAQ;AAAA,UACZ,gBAAgB,CAAC,IAAI;AAAA,UACrB,aAAa,qBAAqB;AAAA,QACnC;AAAA,MACD,OAAO;AACN,aAAK,MAAM,eAAe,KAAK,IAAI;AACnC,YAAI,uBAAuB,KAAK,MAAM,cAAc,KAAK,MAAM,eAAe,QAAQ;AACrF,eAAK,QAAQ;AACb,iBAAO,EAAE,OAAO,IAAI,MAAM,gCAAgC,EAAE;AAAA,QAC7D;AAAA,MACD;AACA,UAAI,KAAK,MAAM,eAAe,WAAW,KAAK,MAAM,aAAa;AAChE,YAAI;AACH,gBAAM,cAAc,KAAK,MAAM,eAAe,KAAK,EAAE;AACrD,gBAAMA,QAAO,KAAK,MAAM,WAAW;AACnC,iBAAO,EAAE,MAAAA,OAAM,YAAY;AAAA,QAC5B,SAAS,GAAG;AACX,iBAAO,EAAE,OAAO,EAAW;AAAA,QAC5B,UAAE;AACD,eAAK,QAAQ;AAAA,QACd;AAAA,MACD;AACA,aAAO;AAAA,IACR;AAAA,EACD;AACD;",
|
|
6
6
|
"names": ["data"]
|
|
7
7
|
}
|
package/dist-esm/lib/diff.mjs
CHANGED
|
@@ -30,9 +30,9 @@ const ValueOpType = {
|
|
|
30
30
|
Patch: "patch"
|
|
31
31
|
};
|
|
32
32
|
function diffRecord(prev, next, legacyAppendMode = false) {
|
|
33
|
-
return diffObject(prev, next, legacyAppendMode);
|
|
33
|
+
return diffObject(prev, next, /* @__PURE__ */ new Set(["props", "meta"]), legacyAppendMode);
|
|
34
34
|
}
|
|
35
|
-
function diffObject(prev, next, legacyAppendMode) {
|
|
35
|
+
function diffObject(prev, next, nestedKeys, legacyAppendMode) {
|
|
36
36
|
if (prev === next) {
|
|
37
37
|
return null;
|
|
38
38
|
}
|
|
@@ -43,10 +43,17 @@ function diffObject(prev, next, legacyAppendMode) {
|
|
|
43
43
|
result[key] = [ValueOpType.Delete];
|
|
44
44
|
continue;
|
|
45
45
|
}
|
|
46
|
-
const
|
|
47
|
-
|
|
46
|
+
const prevValue = prev[key];
|
|
47
|
+
const nextValue = next[key];
|
|
48
|
+
if (nestedKeys?.has(key) || Array.isArray(prevValue) && Array.isArray(nextValue) || typeof prevValue === "string" && typeof nextValue === "string") {
|
|
49
|
+
const diff = diffValue(prevValue, nextValue, legacyAppendMode);
|
|
50
|
+
if (diff) {
|
|
51
|
+
if (!result) result = {};
|
|
52
|
+
result[key] = diff;
|
|
53
|
+
}
|
|
54
|
+
} else if (!isEqual(prevValue, nextValue)) {
|
|
48
55
|
if (!result) result = {};
|
|
49
|
-
result[key] =
|
|
56
|
+
result[key] = [ValueOpType.Put, nextValue];
|
|
50
57
|
}
|
|
51
58
|
}
|
|
52
59
|
for (const key of Object.keys(next)) {
|
|
@@ -70,7 +77,7 @@ function diffValue(valueA, valueB, legacyAppendMode) {
|
|
|
70
77
|
} else if (!valueA || !valueB || typeof valueA !== "object" || typeof valueB !== "object") {
|
|
71
78
|
return isEqual(valueA, valueB) ? null : [ValueOpType.Put, valueB];
|
|
72
79
|
} else {
|
|
73
|
-
const diff = diffObject(valueA, valueB, legacyAppendMode);
|
|
80
|
+
const diff = diffObject(valueA, valueB, void 0, legacyAppendMode);
|
|
74
81
|
return diff ? [ValueOpType.Patch, diff] : null;
|
|
75
82
|
}
|
|
76
83
|
}
|
|
@@ -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 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, legacyAppendMode)\n}\n\nfunction diffObject(prev: object, next: object, legacyAppendMode: boolean): 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 diff = diffValue((prev as any)[key], (next as any)[key], legacyAppendMode)\n\t\tif (diff) {\n\t\t\tif (!result) result = {}\n\t\t\tresult[key] = diff\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, 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": "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;AA0EO,SAAS,WACf,MACA,MACA,mBAAmB,OACC;AACpB,SAAO,WAAW,MAAM,MAAM,gBAAgB;
|
|
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/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.6c9544477685",
|
|
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.6c9544477685",
|
|
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.6c9544477685",
|
|
59
|
+
"@tldraw/store": "4.2.0-canary.6c9544477685",
|
|
60
|
+
"@tldraw/tlschema": "4.2.0-canary.6c9544477685",
|
|
61
|
+
"@tldraw/utils": "4.2.0-canary.6c9544477685",
|
|
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,
|
package/src/lib/chunk.ts
CHANGED
|
@@ -83,13 +83,13 @@ export class JsonChunkAssembler {
|
|
|
83
83
|
|
|
84
84
|
/**
|
|
85
85
|
* Processes a single message, which can be either a complete JSON object or a chunk.
|
|
86
|
-
* For complete JSON objects (starting with '{'), parses immediately.
|
|
87
|
-
* For chunks (prefixed with "{number}_"), accumulates until all chunks received.
|
|
86
|
+
* For complete JSON objects (starting with '\{'), parses immediately.
|
|
87
|
+
* For chunks (prefixed with "\{number\}_"), accumulates until all chunks received.
|
|
88
88
|
*
|
|
89
89
|
* @param msg - The message to process, either JSON or chunk format
|
|
90
90
|
* @returns Result object with data/stringified on success, error object on failure, or null for incomplete chunks
|
|
91
|
-
* -
|
|
92
|
-
* -
|
|
91
|
+
* - `\{ data: object, stringified: string \}` - Successfully parsed complete message
|
|
92
|
+
* - `\{ error: Error \}` - Parse error or invalid chunk sequence
|
|
93
93
|
* - `null` - Chunk received but more chunks expected
|
|
94
94
|
*
|
|
95
95
|
* @example
|
package/src/lib/diff.ts
CHANGED
|
@@ -187,10 +187,15 @@ export function diffRecord(
|
|
|
187
187
|
next: object,
|
|
188
188
|
legacyAppendMode = false
|
|
189
189
|
): ObjectDiff | null {
|
|
190
|
-
return diffObject(prev, next, legacyAppendMode)
|
|
190
|
+
return diffObject(prev, next, new Set(['props', 'meta']), legacyAppendMode)
|
|
191
191
|
}
|
|
192
192
|
|
|
193
|
-
function diffObject(
|
|
193
|
+
function diffObject(
|
|
194
|
+
prev: object,
|
|
195
|
+
next: object,
|
|
196
|
+
nestedKeys: Set<string> | undefined,
|
|
197
|
+
legacyAppendMode: boolean
|
|
198
|
+
): ObjectDiff | null {
|
|
194
199
|
if (prev === next) {
|
|
195
200
|
return null
|
|
196
201
|
}
|
|
@@ -202,11 +207,22 @@ function diffObject(prev: object, next: object, legacyAppendMode: boolean): Obje
|
|
|
202
207
|
result[key] = [ValueOpType.Delete]
|
|
203
208
|
continue
|
|
204
209
|
}
|
|
205
|
-
|
|
206
|
-
const
|
|
207
|
-
if (
|
|
210
|
+
const prevValue = (prev as any)[key]
|
|
211
|
+
const nextValue = (next as any)[key]
|
|
212
|
+
if (
|
|
213
|
+
nestedKeys?.has(key) ||
|
|
214
|
+
(Array.isArray(prevValue) && Array.isArray(nextValue)) ||
|
|
215
|
+
(typeof prevValue === 'string' && typeof nextValue === 'string')
|
|
216
|
+
) {
|
|
217
|
+
// if key is in both places, then compare values
|
|
218
|
+
const diff = diffValue(prevValue, nextValue, legacyAppendMode)
|
|
219
|
+
if (diff) {
|
|
220
|
+
if (!result) result = {}
|
|
221
|
+
result[key] = diff
|
|
222
|
+
}
|
|
223
|
+
} else if (!isEqual(prevValue, nextValue)) {
|
|
208
224
|
if (!result) result = {}
|
|
209
|
-
result[key] =
|
|
225
|
+
result[key] = [ValueOpType.Put, nextValue]
|
|
210
226
|
}
|
|
211
227
|
}
|
|
212
228
|
for (const key of Object.keys(next)) {
|
|
@@ -232,7 +248,7 @@ function diffValue(valueA: unknown, valueB: unknown, legacyAppendMode: boolean):
|
|
|
232
248
|
} else if (!valueA || !valueB || typeof valueA !== 'object' || typeof valueB !== 'object') {
|
|
233
249
|
return isEqual(valueA, valueB) ? null : [ValueOpType.Put, valueB]
|
|
234
250
|
} else {
|
|
235
|
-
const diff = diffObject(valueA, valueB, legacyAppendMode)
|
|
251
|
+
const diff = diffObject(valueA, valueB, undefined, legacyAppendMode)
|
|
236
252
|
return diff ? [ValueOpType.Patch, diff] : null
|
|
237
253
|
}
|
|
238
254
|
}
|