Files
n8n/packages/@n8n/crdt/src/protocol.ts
T
alighasami 3d5eaf9445
Security: Sync from Public / sync-from-public (push) Has been cancelled
Test: Benchmark Nightly / build (push) Has been cancelled
Test: Benchmark Nightly / Notify Cats on failure (push) Has been cancelled
CI: Python / Checks (push) Has been cancelled
Test: Evals Python / Workflow Comparison Python (push) Has been cancelled
Util: Check Docs URLs / check-docs-urls (push) Has been cancelled
Test: Visual Storybook / Cloudflare Pages (push) Has been cancelled
Test: E2E Performance / build-and-test-performance (push) Has been cancelled
Test: Workflows Nightly / Run Workflow Tests (push) Has been cancelled
Util: Cleanup CI Docker Images / Delete stale CI images (push) Has been cancelled
Test: Benchmark Destroy Env / build (push) Has been cancelled
Util: Update Node Popularity / update-popularity (push) Has been cancelled
Test: E2E Coverage Weekly / Coverage Tests (push) Has been cancelled
first commit
2026-03-17 16:22:57 +03:30

190 lines
5.5 KiB
TypeScript

/**
* WebSocket message protocol constants.
*
* Following the y-websocket protocol, messages are prefixed with a type byte
* to multiplex different message types over the same connection.
*
* Server protocol format: [messageType: u8, ...payload]
* Worker protocol format: [messageType: u8, docIdLen: u16, docId: utf8, ...payload]
*/
// =============================================================================
// Message Types
// =============================================================================
/**
* Sync message type - contains CRDT document updates.
* Payload is the raw CRDT update bytes.
*/
export const MESSAGE_SYNC = 0;
/**
* Awareness message type - contains ephemeral presence/cursor data.
* Payload is awareness update bytes.
*/
export const MESSAGE_AWARENESS = 1;
/**
* Subscribe message type - request to subscribe to a document.
* Payload is serverUrl as UTF-8 string (empty for local-only docs).
*/
export const MESSAGE_SUBSCRIBE = 2;
/**
* Unsubscribe message type - request to unsubscribe from a document.
* Payload is empty.
*/
export const MESSAGE_UNSUBSCRIBE = 3;
/**
* Connected message type - notification that server connection is established.
* Sent from worker to frontend. Payload is empty.
*/
export const MESSAGE_CONNECTED = 4;
/**
* Disconnected message type - notification that server connection was lost.
* Sent from worker to frontend. Payload is empty.
*/
export const MESSAGE_DISCONNECTED = 5;
/**
* Initial sync message type - notification that initial sync is complete.
* Sent from worker to frontend after first server data is received. Payload is empty.
*/
export const MESSAGE_INITIAL_SYNC = 6;
/**
* Encode a message with type prefix.
* @param messageType - The message type (MESSAGE_SYNC or MESSAGE_AWARENESS)
* @param payload - The message payload bytes
* @returns Encoded message with type prefix
*/
export function encodeMessage(messageType: number, payload: Uint8Array): Uint8Array {
const result = new Uint8Array(1 + payload.length);
result[0] = messageType;
result.set(payload, 1);
return result;
}
/**
* Decode a message to extract type and payload.
* @param data - The encoded message bytes
* @returns Object with messageType and payload
*/
export function decodeMessage(data: Uint8Array): { messageType: number; payload: Uint8Array } {
if (data.length === 0) {
throw new Error('Empty message');
}
return {
messageType: data[0],
payload: data.subarray(1),
};
}
// =============================================================================
// Worker Protocol (with docId for multiplexing)
// =============================================================================
const textEncoder = new TextEncoder();
const textDecoder = new TextDecoder();
/**
* Encode a message with docId prefix for worker routing.
* Format: [messageType: u8, docIdLen: u16, docId: utf8, ...payload]
*
* @param messageType - The message type
* @param docId - Document ID for routing
* @param payload - Optional payload bytes (default: empty)
* @returns Encoded message with docId prefix
*/
export function encodeWithDocId(
messageType: number,
docId: string,
payload: Uint8Array = new Uint8Array(0),
): Uint8Array {
const docIdBytes = textEncoder.encode(docId);
const docIdLen = docIdBytes.length;
// Format: [type: 1][docIdLen: 2][docId: N][payload: M]
const result = new Uint8Array(1 + 2 + docIdLen + payload.length);
result[0] = messageType;
// Write docIdLen as big-endian u16
result[1] = (docIdLen >> 8) & 0xff;
result[2] = docIdLen & 0xff;
result.set(docIdBytes, 3);
result.set(payload, 3 + docIdLen);
return result;
}
/**
* Decode a message with docId prefix.
* @param data - The encoded message bytes
* @returns Object with messageType, docId, and payload
*/
export function decodeWithDocId(data: Uint8Array): {
messageType: number;
docId: string;
payload: Uint8Array;
} {
if (data.length < 3) {
throw new Error('Message too short to contain docId');
}
const messageType = data[0];
// Read docIdLen as big-endian u16
const docIdLen = (data[1] << 8) | data[2];
if (data.length < 3 + docIdLen) {
throw new Error('Message too short for declared docId length');
}
const docId = textDecoder.decode(data.subarray(3, 3 + docIdLen));
const payload = data.subarray(3 + docIdLen);
return { messageType, docId, payload };
}
/**
* Strip docId from a worker message, returning server-format message.
* Converts [type, docIdLen, docId, payload] → [type, payload]
*
* @param data - Worker-format message with docId
* @returns Server-format message without docId
*/
export function stripDocId(data: Uint8Array): Uint8Array {
const { messageType, payload } = decodeWithDocId(data);
return encodeMessage(messageType, payload);
}
/**
* Add docId to a server message, returning worker-format message.
* Converts [type, payload] → [type, docIdLen, docId, payload]
*
* @param docId - Document ID to add
* @param data - Server-format message
* @returns Worker-format message with docId
*/
export function addDocId(docId: string, data: Uint8Array): Uint8Array {
const { messageType, payload } = decodeMessage(data);
return encodeWithDocId(messageType, docId, payload);
}
/**
* Encode a string as UTF-8 bytes.
* Useful for encoding serverUrl in SUBSCRIBE messages.
*/
export function encodeString(str: string): Uint8Array {
return textEncoder.encode(str);
}
/**
* Decode UTF-8 bytes as a string.
* Useful for decoding serverUrl from SUBSCRIBE messages.
*/
export function decodeString(data: Uint8Array): string {
return textDecoder.decode(data);
}