Files
n8n/packages/@n8n/crdt/src/transports/message-port.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

139 lines
3.8 KiB
TypeScript

import type { Unsubscribe } from '../types';
import type { SyncTransport } from './types';
type ReceiveHandler = (data: Uint8Array) => void;
/**
* Message types for MessagePort communication.
* Using a discriminated union to allow future message types.
*/
interface SyncMessage {
type: 'sync';
data: Uint8Array;
}
type PortMessage = SyncMessage;
/**
* Type guard to validate incoming MessagePort messages
*/
function isPortMessage(data: unknown): data is PortMessage {
return (
typeof data === 'object' &&
data !== null &&
'type' in data &&
(data as { type: unknown }).type === 'sync'
);
}
/**
* MessagePortTransport - Transport using MessagePort for SharedWorker/Worker communication.
*
* This transport wraps a MessagePort (from SharedWorker, Worker, or MessageChannel)
* to implement the SyncTransport interface. It's engine-agnostic - the sync protocol
* is handled by SyncProvider.
*
* Usage with SharedWorker:
* ```typescript
* // In main thread
* const worker = new SharedWorker('worker.js');
* const transport = new MessagePortTransport(worker.port);
*
* // In SharedWorker
* self.onconnect = (e) => {
* const port = e.ports[0];
* const transport = new MessagePortTransport(port);
* };
* ```
*
* Usage with MessageChannel (for testing or iframe communication):
* ```typescript
* const channel = new MessageChannel();
* const transport1 = new MessagePortTransport(channel.port1);
* const transport2 = new MessagePortTransport(channel.port2);
* ```
*/
export class MessagePortTransport implements SyncTransport {
private receiveHandlers = new Set<ReceiveHandler>();
private _connected = false;
private messageHandler: ((event: MessageEvent) => void) | null = null;
constructor(private readonly port: MessagePort) {}
get connected(): boolean {
return this._connected;
}
send(data: Uint8Array): void {
if (!this._connected) {
throw new Error('Transport not connected');
}
// Copy the data to avoid transferring ownership of the original buffer.
// This is necessary because the caller may send the same data to multiple
// transports (e.g., hub-and-spoke topology in SharedWorker).
const copy = new Uint8Array(data);
const message: SyncMessage = { type: 'sync', data: copy };
// Transfer the copy's ArrayBuffer for zero-copy delivery to the receiver
this.port.postMessage(message, [copy.buffer]);
}
onReceive(handler: ReceiveHandler): Unsubscribe {
this.receiveHandlers.add(handler);
return () => {
this.receiveHandlers.delete(handler);
};
}
async connect(): Promise<void> {
if (this._connected) {
return await Promise.resolve();
}
this.messageHandler = (event: MessageEvent) => {
if (!isPortMessage(event.data)) return;
const message = event.data;
if (message.type === 'sync') {
// Ensure we have a Uint8Array (may be transferred as ArrayBuffer)
const data =
message.data instanceof Uint8Array ? message.data : new Uint8Array(message.data);
for (const handler of this.receiveHandlers) {
handler(data);
}
}
};
this.port.addEventListener('message', this.messageHandler);
this.port.start(); // Required for MessagePort to begin receiving messages
this._connected = true;
return await Promise.resolve();
}
disconnect(): void {
if (!this._connected) {
return;
}
if (this.messageHandler) {
this.port.removeEventListener('message', this.messageHandler);
this.messageHandler = null;
}
this._connected = false;
// Note: We don't close the port here as it may be reused
}
/** No-op for MessagePortTransport - connection state doesn't change unexpectedly */
onConnectionChange(_handler: (connected: boolean) => void): Unsubscribe {
return () => {};
}
/** No-op for MessagePortTransport - no transport-level errors occur */
onError(_handler: (error: Error) => void): Unsubscribe {
return () => {};
}
}