@tldraw/sync-core 4.2.0-next.f100cedfc45b → 4.3.0-canary.d8da2a99f394
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist-cjs/index.d.ts +66 -0
- package/dist-cjs/index.js +2 -1
- package/dist-cjs/index.js.map +2 -2
- package/dist-cjs/lib/RoomSession.js.map +1 -1
- package/dist-cjs/lib/TLSyncRoom.js +35 -9
- package/dist-cjs/lib/TLSyncRoom.js.map +2 -2
- package/dist-cjs/lib/chunk.js +4 -4
- package/dist-cjs/lib/chunk.js.map +1 -1
- package/dist-cjs/lib/diff.js +29 -29
- package/dist-cjs/lib/diff.js.map +2 -2
- package/dist-cjs/lib/protocol.js +1 -1
- package/dist-cjs/lib/protocol.js.map +1 -1
- package/dist-esm/index.d.mts +66 -0
- package/dist-esm/index.mjs +3 -2
- package/dist-esm/index.mjs.map +2 -2
- package/dist-esm/lib/RoomSession.mjs.map +1 -1
- package/dist-esm/lib/TLSyncRoom.mjs +35 -9
- package/dist-esm/lib/TLSyncRoom.mjs.map +2 -2
- package/dist-esm/lib/chunk.mjs +4 -4
- package/dist-esm/lib/chunk.mjs.map +1 -1
- package/dist-esm/lib/diff.mjs +29 -29
- package/dist-esm/lib/diff.mjs.map +2 -2
- package/dist-esm/lib/protocol.mjs +1 -1
- package/dist-esm/lib/protocol.mjs.map +1 -1
- package/package.json +6 -6
- package/src/index.ts +2 -2
- package/src/lib/RoomSession.test.ts +3 -0
- package/src/lib/RoomSession.ts +28 -42
- package/src/lib/TLSyncRoom.ts +42 -7
- package/src/lib/chunk.ts +4 -4
- package/src/lib/diff.ts +55 -32
- package/src/lib/protocol.ts +1 -1
- package/src/test/TLSocketRoom.test.ts +2 -2
- package/src/test/TLSyncRoom.test.ts +22 -21
- package/src/test/diff.test.ts +200 -0
package/dist-cjs/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 */
|
|
@@ -65,6 +129,8 @@ export declare type OmitVoid<T, KS extends keyof T = keyof T> = {
|
|
|
65
129
|
|
|
66
130
|
/* Excluded from this release type: RoomSession */
|
|
67
131
|
|
|
132
|
+
/* Excluded from this release type: RoomSessionBase */
|
|
133
|
+
|
|
68
134
|
/* Excluded from this release type: RoomSessionState */
|
|
69
135
|
|
|
70
136
|
/**
|
package/dist-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.
|
|
54
|
+
"4.3.0-canary.d8da2a99f394",
|
|
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 } 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
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/lib/RoomSession.ts"],
|
|
4
|
-
"sourcesContent": ["import { SerializedSchema, UnknownRecord } from '@tldraw/store'\nimport { TLRoomSocket } from './TLSyncRoom'\nimport { TLSocketServerSentDataEvent } from './protocol'\n\n/**\n * Enumeration of possible states for a room session during its lifecycle.\n *\n * Room sessions progress through these states as clients connect, authenticate,\n * and disconnect from collaborative rooms.\n *\n * @internal\n */\nexport const RoomSessionState = {\n\t/** Session is waiting for the initial connect message from the client */\n\tAwaitingConnectMessage: 'awaiting-connect-message',\n\t/** Session is disconnected but waiting for final cleanup before removal */\n\tAwaitingRemoval: 'awaiting-removal',\n\t/** Session is fully connected and actively synchronizing */\n\tConnected: 'connected',\n} as const\n\n/**\n * Type representing the possible states a room session can be in.\n *\n * @example\n * ```ts\n * const sessionState: RoomSessionState = RoomSessionState.Connected\n * if (sessionState === RoomSessionState.AwaitingConnectMessage) {\n * console.log('Session waiting for connect message')\n * }\n * ```\n *\n * @internal\n */\nexport type RoomSessionState = (typeof RoomSessionState)[keyof typeof RoomSessionState]\n\n/**\n * Maximum time in milliseconds to wait for a connect message after socket connection.\n *\n * If a client connects but doesn't send a connect message within this time,\n * the session will be terminated.\n *\n * @public\n */\nexport const SESSION_START_WAIT_TIME = 10000\n\n/**\n * Time in milliseconds to wait before completely removing a disconnected session.\n *\n * This grace period allows for quick reconnections without losing session state,\n * which is especially helpful for brief network interruptions.\n *\n * @public\n */\nexport const SESSION_REMOVAL_WAIT_TIME = 5000\n\n/**\n * Maximum time in milliseconds a connected session can remain idle before cleanup.\n *\n * Sessions that don't receive any messages or interactions for this duration\n * may be considered for cleanup to free server resources.\n *\n * @public\n */\nexport const SESSION_IDLE_TIMEOUT = 20000\n\n/**\n * Represents a client session within a collaborative room, tracking the connection\n * state, permissions, and synchronization details for a single user.\n *\n * Each session corresponds to one WebSocket connection and progresses through\n * different states during its lifecycle. The session type is a discriminated union\n * based on the current state, ensuring type safety when accessing state-specific properties.\n *\n * @example\n * ```ts\n * // Check session state and access appropriate properties\n * function handleSession(session: RoomSession<MyRecord, UserMeta>) {\n * switch (session.state) {\n * case RoomSessionState.AwaitingConnectMessage:\n * console.log(`Session ${session.sessionId} started at ${session.sessionStartTime}`)\n * break\n * case RoomSessionState.Connected:\n * console.log(`Connected session has ${session.outstandingDataMessages.length} pending messages`)\n * break\n * case RoomSessionState.AwaitingRemoval:\n * console.log(`Session will be removed at ${session.cancellationTime}`)\n * break\n * }\n * }\n * ```\n *\n * @internal\n */\nexport type RoomSession<R extends UnknownRecord, Meta> =\n\t| {\n\t\t\t/** Current state of the session */\n\t\t\tstate: typeof RoomSessionState.AwaitingConnectMessage\n\t\t\t/**
|
|
4
|
+
"sourcesContent": ["import { SerializedSchema, UnknownRecord } from '@tldraw/store'\nimport { TLRoomSocket } from './TLSyncRoom'\nimport { TLSocketServerSentDataEvent } from './protocol'\n\n/**\n * Enumeration of possible states for a room session during its lifecycle.\n *\n * Room sessions progress through these states as clients connect, authenticate,\n * and disconnect from collaborative rooms.\n *\n * @internal\n */\nexport const RoomSessionState = {\n\t/** Session is waiting for the initial connect message from the client */\n\tAwaitingConnectMessage: 'awaiting-connect-message',\n\t/** Session is disconnected but waiting for final cleanup before removal */\n\tAwaitingRemoval: 'awaiting-removal',\n\t/** Session is fully connected and actively synchronizing */\n\tConnected: 'connected',\n} as const\n\n/**\n * Type representing the possible states a room session can be in.\n *\n * @example\n * ```ts\n * const sessionState: RoomSessionState = RoomSessionState.Connected\n * if (sessionState === RoomSessionState.AwaitingConnectMessage) {\n * console.log('Session waiting for connect message')\n * }\n * ```\n *\n * @internal\n */\nexport type RoomSessionState = (typeof RoomSessionState)[keyof typeof RoomSessionState]\n\n/**\n * Maximum time in milliseconds to wait for a connect message after socket connection.\n *\n * If a client connects but doesn't send a connect message within this time,\n * the session will be terminated.\n *\n * @public\n */\nexport const SESSION_START_WAIT_TIME = 10000\n\n/**\n * Time in milliseconds to wait before completely removing a disconnected session.\n *\n * This grace period allows for quick reconnections without losing session state,\n * which is especially helpful for brief network interruptions.\n *\n * @public\n */\nexport const SESSION_REMOVAL_WAIT_TIME = 5000\n\n/**\n * Maximum time in milliseconds a connected session can remain idle before cleanup.\n *\n * Sessions that don't receive any messages or interactions for this duration\n * may be considered for cleanup to free server resources.\n *\n * @public\n */\nexport const SESSION_IDLE_TIMEOUT = 20000\n\n/**\n * Base properties shared by all room session states.\n *\n * @internal\n */\nexport interface RoomSessionBase<R extends UnknownRecord, Meta> {\n\t/** Unique identifier for this session */\n\tsessionId: string\n\t/** Presence identifier for live cursor/selection tracking, if available */\n\tpresenceId: string | null\n\t/** WebSocket connection wrapper for this session */\n\tsocket: TLRoomSocket<R>\n\t/** Custom metadata associated with this session */\n\tmeta: Meta\n\t/** Whether this session has read-only permissions */\n\tisReadonly: boolean\n\t/** Whether this session requires legacy protocol rejection handling */\n\trequiresLegacyRejection: boolean\n\t/** Whether this session supports string append operations */\n\tsupportsStringAppend: boolean\n}\n\n/**\n * Represents a client session within a collaborative room, tracking the connection\n * state, permissions, and synchronization details for a single user.\n *\n * Each session corresponds to one WebSocket connection and progresses through\n * different states during its lifecycle. The session type is a discriminated union\n * based on the current state, ensuring type safety when accessing state-specific properties.\n *\n * @example\n * ```ts\n * // Check session state and access appropriate properties\n * function handleSession(session: RoomSession<MyRecord, UserMeta>) {\n * switch (session.state) {\n * case RoomSessionState.AwaitingConnectMessage:\n * console.log(`Session ${session.sessionId} started at ${session.sessionStartTime}`)\n * break\n * case RoomSessionState.Connected:\n * console.log(`Connected session has ${session.outstandingDataMessages.length} pending messages`)\n * break\n * case RoomSessionState.AwaitingRemoval:\n * console.log(`Session will be removed at ${session.cancellationTime}`)\n * break\n * }\n * }\n * ```\n *\n * @internal\n */\nexport type RoomSession<R extends UnknownRecord, Meta> =\n\t| (RoomSessionBase<R, Meta> & {\n\t\t\t/** Current state of the session */\n\t\t\tstate: typeof RoomSessionState.AwaitingConnectMessage\n\t\t\t/** Timestamp when the session was created */\n\t\t\tsessionStartTime: number\n\t })\n\t| (RoomSessionBase<R, Meta> & {\n\t\t\t/** Current state of the session */\n\t\t\tstate: typeof RoomSessionState.AwaitingRemoval\n\t\t\t/** Timestamp when the session was marked for removal */\n\t\t\tcancellationTime: number\n\t })\n\t| (RoomSessionBase<R, Meta> & {\n\t\t\t/** Current state of the session */\n\t\t\tstate: typeof RoomSessionState.Connected\n\t\t\t/** Serialized schema information for this connected session */\n\t\t\tserializedSchema: SerializedSchema\n\t\t\t/** Timestamp of the last interaction or message from this session */\n\t\t\tlastInteractionTime: number\n\t\t\t/** Timer for debouncing operations, if active */\n\t\t\tdebounceTimer: ReturnType<typeof setTimeout> | null\n\t\t\t/** Queue of data messages waiting to be sent to this session */\n\t\t\toutstandingDataMessages: TLSocketServerSentDataEvent<R>[]\n\t })\n"],
|
|
5
5
|
"mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAYO,MAAM,mBAAmB;AAAA;AAAA,EAE/B,wBAAwB;AAAA;AAAA,EAExB,iBAAiB;AAAA;AAAA,EAEjB,WAAW;AACZ;AAyBO,MAAM,0BAA0B;AAUhC,MAAM,4BAA4B;AAUlC,MAAM,uBAAuB;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -79,10 +79,11 @@ class DocumentState {
|
|
|
79
79
|
*
|
|
80
80
|
* @param state - The new record state
|
|
81
81
|
* @param clock - The new clock value
|
|
82
|
+
* @param legacyAppendMode - If true, string append operations will be converted to Put operations
|
|
82
83
|
* @returns Result containing the diff and new DocumentState, or null if no changes, or validation error
|
|
83
84
|
*/
|
|
84
|
-
replaceState(state, clock) {
|
|
85
|
-
const diff = (0, import_diff.diffRecord)(this.state, state);
|
|
85
|
+
replaceState(state, clock, legacyAppendMode = false) {
|
|
86
|
+
const diff = (0, import_diff.diffRecord)(this.state, state, legacyAppendMode);
|
|
86
87
|
if (!diff) return import_utils.Result.ok(null);
|
|
87
88
|
try {
|
|
88
89
|
this.recordType.validate(state);
|
|
@@ -96,11 +97,12 @@ class DocumentState {
|
|
|
96
97
|
*
|
|
97
98
|
* @param diff - The object diff to apply
|
|
98
99
|
* @param clock - The new clock value
|
|
100
|
+
* @param legacyAppendMode - If true, string append operations will be converted to Put operations
|
|
99
101
|
* @returns Result containing the final diff and new DocumentState, or null if no changes, or validation error
|
|
100
102
|
*/
|
|
101
|
-
mergeDiff(diff, clock) {
|
|
103
|
+
mergeDiff(diff, clock, legacyAppendMode = false) {
|
|
102
104
|
const newState = (0, import_diff.applyObjectDiff)(this.state, diff);
|
|
103
|
-
return this.replaceState(newState, clock);
|
|
105
|
+
return this.replaceState(newState, clock, legacyAppendMode);
|
|
104
106
|
}
|
|
105
107
|
}
|
|
106
108
|
function getDocumentClock(snapshot) {
|
|
@@ -488,7 +490,8 @@ class TLSyncRoom {
|
|
|
488
490
|
cancellationTime: Date.now(),
|
|
489
491
|
meta: session.meta,
|
|
490
492
|
isReadonly: session.isReadonly,
|
|
491
|
-
requiresLegacyRejection: session.requiresLegacyRejection
|
|
493
|
+
requiresLegacyRejection: session.requiresLegacyRejection,
|
|
494
|
+
supportsStringAppend: session.supportsStringAppend
|
|
492
495
|
});
|
|
493
496
|
try {
|
|
494
497
|
session.socket.close();
|
|
@@ -595,10 +598,27 @@ class TLSyncRoom {
|
|
|
595
598
|
meta,
|
|
596
599
|
isReadonly: isReadonly ?? false,
|
|
597
600
|
// this gets set later during handleConnectMessage
|
|
598
|
-
requiresLegacyRejection: false
|
|
601
|
+
requiresLegacyRejection: false,
|
|
602
|
+
supportsStringAppend: true
|
|
599
603
|
});
|
|
600
604
|
return this;
|
|
601
605
|
}
|
|
606
|
+
/**
|
|
607
|
+
* Checks if all connected sessions support string append operations (protocol version 8+).
|
|
608
|
+
* If any client is on an older version, returns false to enable legacy append mode.
|
|
609
|
+
*
|
|
610
|
+
* @returns True if all connected sessions are on protocol version 8 or higher
|
|
611
|
+
*/
|
|
612
|
+
getCanEmitStringAppend() {
|
|
613
|
+
for (const session of this.sessions.values()) {
|
|
614
|
+
if (session.state === import_RoomSession.RoomSessionState.Connected) {
|
|
615
|
+
if (!session.supportsStringAppend) {
|
|
616
|
+
return false;
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
return true;
|
|
621
|
+
}
|
|
602
622
|
/**
|
|
603
623
|
* When we send a diff to a client, if that client is on a lower version than us, we need to make
|
|
604
624
|
* the diff compatible with their version. At the moment this means migrating each affected record
|
|
@@ -733,6 +753,10 @@ class TLSyncRoom {
|
|
|
733
753
|
if (theirProtocolVersion === 6) {
|
|
734
754
|
theirProtocolVersion++;
|
|
735
755
|
}
|
|
756
|
+
if (theirProtocolVersion === 7) {
|
|
757
|
+
theirProtocolVersion++;
|
|
758
|
+
session.supportsStringAppend = false;
|
|
759
|
+
}
|
|
736
760
|
if (theirProtocolVersion == null || theirProtocolVersion < (0, import_protocol.getTlsyncProtocolVersion)()) {
|
|
737
761
|
this.rejectSession(session.sessionId, import_TLSyncClient.TLSyncErrorCloseEventReason.CLIENT_TOO_OLD);
|
|
738
762
|
return;
|
|
@@ -760,6 +784,7 @@ class TLSyncRoom {
|
|
|
760
784
|
lastInteractionTime: Date.now(),
|
|
761
785
|
debounceTimer: null,
|
|
762
786
|
outstandingDataMessages: [],
|
|
787
|
+
supportsStringAppend: session.supportsStringAppend,
|
|
763
788
|
meta: session.meta,
|
|
764
789
|
isReadonly: session.isReadonly,
|
|
765
790
|
requiresLegacyRejection: session.requiresLegacyRejection
|
|
@@ -846,6 +871,7 @@ class TLSyncRoom {
|
|
|
846
871
|
const initialDocumentClock = this.documentClock;
|
|
847
872
|
let didPresenceChange = false;
|
|
848
873
|
(0, import_state.transaction)((rollback) => {
|
|
874
|
+
const legacyAppendMode = !this.getCanEmitStringAppend();
|
|
849
875
|
const docChanges = { diff: null };
|
|
850
876
|
const presenceChanges = { diff: null };
|
|
851
877
|
const propagateOp = (changes, id, op) => {
|
|
@@ -874,7 +900,7 @@ class TLSyncRoom {
|
|
|
874
900
|
const { value: state } = res;
|
|
875
901
|
const doc = this.getDocument(id);
|
|
876
902
|
if (doc) {
|
|
877
|
-
const diff = doc.replaceState(state, this.clock);
|
|
903
|
+
const diff = doc.replaceState(state, this.clock, legacyAppendMode);
|
|
878
904
|
if (!diff.ok) {
|
|
879
905
|
return fail(import_TLSyncClient.TLSyncErrorCloseEventReason.INVALID_RECORD);
|
|
880
906
|
}
|
|
@@ -899,7 +925,7 @@ class TLSyncRoom {
|
|
|
899
925
|
return fail(import_TLSyncClient.TLSyncErrorCloseEventReason.CLIENT_TOO_OLD);
|
|
900
926
|
}
|
|
901
927
|
if (downgraded.value === doc.state) {
|
|
902
|
-
const diff = doc.mergeDiff(patch, this.clock);
|
|
928
|
+
const diff = doc.mergeDiff(patch, this.clock, legacyAppendMode);
|
|
903
929
|
if (!diff.ok) {
|
|
904
930
|
return fail(import_TLSyncClient.TLSyncErrorCloseEventReason.INVALID_RECORD);
|
|
905
931
|
}
|
|
@@ -913,7 +939,7 @@ class TLSyncRoom {
|
|
|
913
939
|
if (upgraded.type === "error") {
|
|
914
940
|
return fail(import_TLSyncClient.TLSyncErrorCloseEventReason.CLIENT_TOO_OLD);
|
|
915
941
|
}
|
|
916
|
-
const diff = doc.replaceState(upgraded.value, this.clock);
|
|
942
|
+
const diff = doc.replaceState(upgraded.value, this.clock, legacyAppendMode);
|
|
917
943
|
if (!diff.ok) {
|
|
918
944
|
return fail(import_TLSyncClient.TLSyncErrorCloseEventReason.INVALID_RECORD);
|
|
919
945
|
}
|