mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-20 19:32:15 +00:00
refactor(editor): Move editor-ui and design-system to frontend dir (no-changelog) (#13564)
This commit is contained in:
@@ -0,0 +1,20 @@
|
||||
/** Mocked EventSource class to help testing */
|
||||
export class MockEventSource extends EventTarget {
|
||||
constructor(public url: string) {
|
||||
super();
|
||||
}
|
||||
|
||||
simulateConnectionOpen() {
|
||||
this.dispatchEvent(new Event('open'));
|
||||
}
|
||||
|
||||
simulateConnectionClose() {
|
||||
this.dispatchEvent(new Event('close'));
|
||||
}
|
||||
|
||||
simulateMessageEvent(data: string) {
|
||||
this.dispatchEvent(new MessageEvent('message', { data }));
|
||||
}
|
||||
|
||||
close = vi.fn();
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import { WebSocketState } from '@/push-connection/useWebSocketClient';
|
||||
|
||||
/** Mocked WebSocket class to help testing */
|
||||
export class MockWebSocket extends EventTarget {
|
||||
readyState: number = WebSocketState.CONNECTING;
|
||||
|
||||
constructor(public url: string) {
|
||||
super();
|
||||
}
|
||||
|
||||
simulateConnectionOpen() {
|
||||
this.dispatchEvent(new Event('open'));
|
||||
this.readyState = WebSocketState.OPEN;
|
||||
}
|
||||
|
||||
simulateConnectionClose(code: number) {
|
||||
this.dispatchEvent(new CloseEvent('close', { code }));
|
||||
this.readyState = WebSocketState.CLOSED;
|
||||
}
|
||||
|
||||
simulateMessageEvent(data: string) {
|
||||
this.dispatchEvent(new MessageEvent('message', { data }));
|
||||
}
|
||||
|
||||
dispatchErrorEvent() {
|
||||
this.dispatchEvent(new Event('error'));
|
||||
}
|
||||
|
||||
send = vi.fn();
|
||||
|
||||
close = vi.fn();
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
|
||||
import { useEventSourceClient } from '../useEventSourceClient';
|
||||
import { MockEventSource } from './mockEventSource';
|
||||
|
||||
describe('useEventSourceClient', () => {
|
||||
let mockEventSource: MockEventSource;
|
||||
|
||||
beforeEach(() => {
|
||||
mockEventSource = new MockEventSource('http://test.com');
|
||||
|
||||
// @ts-expect-error - mock EventSource
|
||||
global.EventSource = vi.fn(() => mockEventSource);
|
||||
|
||||
vi.useFakeTimers();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.clearAllTimers();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
test('should create EventSource connection with provided URL', () => {
|
||||
const url = 'http://test.com';
|
||||
const onMessage = vi.fn();
|
||||
|
||||
const { connect } = useEventSourceClient({ url, onMessage });
|
||||
connect();
|
||||
|
||||
expect(EventSource).toHaveBeenCalledWith(url, { withCredentials: true });
|
||||
});
|
||||
|
||||
test('should update connection status on successful connection', () => {
|
||||
const { connect, isConnected } = useEventSourceClient({
|
||||
url: 'http://test.com',
|
||||
onMessage: vi.fn(),
|
||||
});
|
||||
connect();
|
||||
|
||||
mockEventSource.simulateConnectionOpen();
|
||||
|
||||
expect(isConnected.value).toBe(true);
|
||||
});
|
||||
|
||||
test('should handle incoming messages', () => {
|
||||
const onMessage = vi.fn();
|
||||
const { connect } = useEventSourceClient({ url: 'http://test.com', onMessage });
|
||||
connect();
|
||||
|
||||
mockEventSource.simulateMessageEvent('test data');
|
||||
|
||||
expect(onMessage).toHaveBeenCalledWith('test data');
|
||||
});
|
||||
|
||||
test('should handle disconnection', () => {
|
||||
const { connect, disconnect, isConnected } = useEventSourceClient({
|
||||
url: 'http://test.com',
|
||||
onMessage: vi.fn(),
|
||||
});
|
||||
connect();
|
||||
|
||||
// Simulate successful connection
|
||||
mockEventSource.simulateConnectionOpen();
|
||||
expect(isConnected.value).toBe(true);
|
||||
|
||||
disconnect();
|
||||
|
||||
expect(isConnected.value).toBe(false);
|
||||
expect(mockEventSource.close).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('should handle connection loss', () => {
|
||||
const { connect, isConnected } = useEventSourceClient({
|
||||
url: 'http://test.com',
|
||||
onMessage: vi.fn(),
|
||||
});
|
||||
connect();
|
||||
expect(EventSource).toHaveBeenCalledTimes(1);
|
||||
|
||||
// Simulate successful connection
|
||||
mockEventSource.simulateConnectionOpen();
|
||||
expect(isConnected.value).toBe(true);
|
||||
|
||||
// Simulate connection loss
|
||||
mockEventSource.simulateConnectionClose();
|
||||
expect(isConnected.value).toBe(false);
|
||||
|
||||
// Advance timer to trigger reconnect
|
||||
vi.advanceTimersByTime(1_000);
|
||||
expect(EventSource).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
test('sendMessage should be a noop function', () => {
|
||||
const { connect, sendMessage } = useEventSourceClient({
|
||||
url: 'http://test.com',
|
||||
onMessage: vi.fn(),
|
||||
});
|
||||
connect();
|
||||
|
||||
// Simulate successful connection
|
||||
mockEventSource.simulateConnectionOpen();
|
||||
|
||||
const message = 'test message';
|
||||
// Should not throw error and should do nothing
|
||||
expect(() => sendMessage(message)).not.toThrow();
|
||||
});
|
||||
|
||||
test('should attempt reconnection with increasing delays', () => {
|
||||
const { connect } = useEventSourceClient({
|
||||
url: 'http://test.com',
|
||||
onMessage: vi.fn(),
|
||||
});
|
||||
connect();
|
||||
|
||||
mockEventSource.simulateConnectionOpen();
|
||||
mockEventSource.simulateConnectionClose();
|
||||
|
||||
// First reconnection attempt after 1 second
|
||||
vi.advanceTimersByTime(1_000);
|
||||
expect(EventSource).toHaveBeenCalledTimes(2);
|
||||
|
||||
mockEventSource.simulateConnectionClose();
|
||||
|
||||
// Second reconnection attempt after 2 seconds
|
||||
vi.advanceTimersByTime(2_000);
|
||||
expect(EventSource).toHaveBeenCalledTimes(3);
|
||||
});
|
||||
|
||||
test('should reset connection attempts on successful connection', () => {
|
||||
const { connect } = useEventSourceClient({
|
||||
url: 'http://test.com',
|
||||
onMessage: vi.fn(),
|
||||
});
|
||||
connect();
|
||||
|
||||
// First connection attempt
|
||||
mockEventSource.simulateConnectionOpen();
|
||||
mockEventSource.simulateConnectionClose();
|
||||
|
||||
// First reconnection attempt
|
||||
vi.advanceTimersByTime(1_000);
|
||||
expect(EventSource).toHaveBeenCalledTimes(2);
|
||||
|
||||
// Successful connection
|
||||
mockEventSource.simulateConnectionOpen();
|
||||
|
||||
// Connection lost again
|
||||
mockEventSource.simulateConnectionClose();
|
||||
|
||||
// Should start with initial delay again
|
||||
vi.advanceTimersByTime(1_000);
|
||||
expect(EventSource).toHaveBeenCalledTimes(3);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,88 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import { useHeartbeat } from '../useHeartbeat';
|
||||
|
||||
describe('useHeartbeat', () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.clearAllTimers();
|
||||
});
|
||||
|
||||
it('should start heartbeat and call onHeartbeat at specified intervals', () => {
|
||||
const onHeartbeat = vi.fn();
|
||||
const interval = 1000;
|
||||
|
||||
const heartbeat = useHeartbeat({ interval, onHeartbeat });
|
||||
heartbeat.startHeartbeat();
|
||||
|
||||
// Initially, the callback should not be called
|
||||
expect(onHeartbeat).not.toHaveBeenCalled();
|
||||
|
||||
// Advance timer by interval
|
||||
vi.advanceTimersByTime(interval);
|
||||
expect(onHeartbeat).toHaveBeenCalledTimes(1);
|
||||
|
||||
// Advance timer by another interval
|
||||
vi.advanceTimersByTime(interval);
|
||||
expect(onHeartbeat).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('should stop heartbeat when stopHeartbeat is called', () => {
|
||||
const onHeartbeat = vi.fn();
|
||||
const interval = 1000;
|
||||
|
||||
const heartbeat = useHeartbeat({ interval, onHeartbeat });
|
||||
heartbeat.startHeartbeat();
|
||||
|
||||
// Advance timer by interval
|
||||
vi.advanceTimersByTime(interval);
|
||||
expect(onHeartbeat).toHaveBeenCalledTimes(1);
|
||||
|
||||
// Stop the heartbeat
|
||||
heartbeat.stopHeartbeat();
|
||||
|
||||
// Advance timer by multiple intervals
|
||||
vi.advanceTimersByTime(interval * 3);
|
||||
expect(onHeartbeat).toHaveBeenCalledTimes(1); // Should still be 1
|
||||
});
|
||||
|
||||
it('should be safe to call stopHeartbeat multiple times', () => {
|
||||
const onHeartbeat = vi.fn();
|
||||
const interval = 1000;
|
||||
|
||||
const heartbeat = useHeartbeat({ interval, onHeartbeat });
|
||||
heartbeat.startHeartbeat();
|
||||
|
||||
// Stop multiple times
|
||||
heartbeat.stopHeartbeat();
|
||||
heartbeat.stopHeartbeat();
|
||||
heartbeat.stopHeartbeat();
|
||||
|
||||
vi.advanceTimersByTime(interval * 2);
|
||||
expect(onHeartbeat).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should restart heartbeat after stopping', () => {
|
||||
const onHeartbeat = vi.fn();
|
||||
const interval = 1000;
|
||||
|
||||
const heartbeat = useHeartbeat({ interval, onHeartbeat });
|
||||
|
||||
// First start
|
||||
heartbeat.startHeartbeat();
|
||||
vi.advanceTimersByTime(interval);
|
||||
expect(onHeartbeat).toHaveBeenCalledTimes(1);
|
||||
|
||||
// Stop
|
||||
heartbeat.stopHeartbeat();
|
||||
vi.advanceTimersByTime(interval);
|
||||
expect(onHeartbeat).toHaveBeenCalledTimes(1);
|
||||
|
||||
// Restart
|
||||
heartbeat.startHeartbeat();
|
||||
vi.advanceTimersByTime(interval);
|
||||
expect(onHeartbeat).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,140 @@
|
||||
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
|
||||
import { useWebSocketClient } from '../useWebSocketClient';
|
||||
import { MockWebSocket } from './mockWebSocketClient';
|
||||
|
||||
describe('useWebSocketClient', () => {
|
||||
let mockWebSocket: MockWebSocket;
|
||||
|
||||
beforeEach(() => {
|
||||
mockWebSocket = new MockWebSocket('ws://test.com');
|
||||
|
||||
// @ts-expect-error - mock WebSocket
|
||||
global.WebSocket = vi.fn(() => mockWebSocket);
|
||||
|
||||
vi.useFakeTimers();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.clearAllTimers();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
test('should create WebSocket connection with provided URL', () => {
|
||||
const url = 'ws://test.com';
|
||||
const onMessage = vi.fn();
|
||||
|
||||
const { connect } = useWebSocketClient({ url, onMessage });
|
||||
connect();
|
||||
|
||||
expect(WebSocket).toHaveBeenCalledWith(url);
|
||||
});
|
||||
|
||||
test('should update connection status and start heartbeat on successful connection', () => {
|
||||
const { connect, isConnected } = useWebSocketClient({
|
||||
url: 'ws://test.com',
|
||||
onMessage: vi.fn(),
|
||||
});
|
||||
connect();
|
||||
|
||||
mockWebSocket.simulateConnectionOpen();
|
||||
|
||||
expect(isConnected.value).toBe(true);
|
||||
|
||||
// Advance timer to trigger heartbeat
|
||||
vi.advanceTimersByTime(30_000);
|
||||
expect(mockWebSocket.send).toHaveBeenCalledWith(JSON.stringify({ type: 'heartbeat' }));
|
||||
});
|
||||
|
||||
test('should handle incoming messages', () => {
|
||||
const onMessage = vi.fn();
|
||||
const { connect } = useWebSocketClient({ url: 'ws://test.com', onMessage });
|
||||
connect();
|
||||
|
||||
mockWebSocket.simulateMessageEvent('test data');
|
||||
|
||||
expect(onMessage).toHaveBeenCalledWith('test data');
|
||||
});
|
||||
|
||||
test('should handle disconnection', () => {
|
||||
const { connect, disconnect, isConnected } = useWebSocketClient({
|
||||
url: 'ws://test.com',
|
||||
onMessage: vi.fn(),
|
||||
});
|
||||
connect();
|
||||
|
||||
// Simulate successful connection
|
||||
mockWebSocket.simulateConnectionOpen();
|
||||
|
||||
expect(isConnected.value).toBe(true);
|
||||
|
||||
disconnect();
|
||||
|
||||
expect(isConnected.value).toBe(false);
|
||||
expect(mockWebSocket.close).toHaveBeenCalledWith(1000);
|
||||
});
|
||||
|
||||
test('should handle connection loss', () => {
|
||||
const { connect, isConnected } = useWebSocketClient({
|
||||
url: 'ws://test.com',
|
||||
onMessage: vi.fn(),
|
||||
});
|
||||
connect();
|
||||
expect(WebSocket).toHaveBeenCalledTimes(1);
|
||||
|
||||
// Simulate successful connection
|
||||
mockWebSocket.simulateConnectionOpen();
|
||||
|
||||
expect(isConnected.value).toBe(true);
|
||||
|
||||
// Simulate connection loss
|
||||
mockWebSocket.simulateConnectionClose(1006);
|
||||
|
||||
expect(isConnected.value).toBe(false);
|
||||
// Advance timer to reconnect
|
||||
vi.advanceTimersByTime(1_000);
|
||||
expect(WebSocket).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
test('should throw error when trying to send message while disconnected', () => {
|
||||
const { sendMessage } = useWebSocketClient({ url: 'ws://test.com', onMessage: vi.fn() });
|
||||
|
||||
expect(() => sendMessage('test')).toThrow('Not connected to the server');
|
||||
});
|
||||
|
||||
test('should attempt reconnection with increasing delays', () => {
|
||||
const { connect } = useWebSocketClient({
|
||||
url: 'http://test.com',
|
||||
onMessage: vi.fn(),
|
||||
});
|
||||
connect();
|
||||
|
||||
mockWebSocket.simulateConnectionOpen();
|
||||
mockWebSocket.simulateConnectionClose(1006);
|
||||
|
||||
// First reconnection attempt after 1 second
|
||||
vi.advanceTimersByTime(1_000);
|
||||
expect(WebSocket).toHaveBeenCalledTimes(2);
|
||||
|
||||
mockWebSocket.simulateConnectionClose(1006);
|
||||
|
||||
// Second reconnection attempt after 2 seconds
|
||||
vi.advanceTimersByTime(2_000);
|
||||
expect(WebSocket).toHaveBeenCalledTimes(3);
|
||||
});
|
||||
|
||||
test('should send message when connected', () => {
|
||||
const { connect, sendMessage } = useWebSocketClient({
|
||||
url: 'ws://test.com',
|
||||
onMessage: vi.fn(),
|
||||
});
|
||||
connect();
|
||||
|
||||
// Simulate successful connection
|
||||
mockWebSocket.simulateConnectionOpen();
|
||||
|
||||
const message = 'test message';
|
||||
sendMessage(message);
|
||||
|
||||
expect(mockWebSocket.send).toHaveBeenCalledWith(message);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,69 @@
|
||||
import { useReconnectTimer } from '@/push-connection/useReconnectTimer';
|
||||
import { ref } from 'vue';
|
||||
|
||||
export type UseEventSourceClientOptions = {
|
||||
url: string;
|
||||
onMessage: (data: string) => void;
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates an EventSource connection to the server. Uses reconnection logic
|
||||
* to reconnect if the connection is lost.
|
||||
*/
|
||||
export const useEventSourceClient = (options: UseEventSourceClientOptions) => {
|
||||
const isConnected = ref(false);
|
||||
const eventSource = ref<EventSource | null>(null);
|
||||
|
||||
const onConnected = () => {
|
||||
isConnected.value = true;
|
||||
reconnectTimer.resetConnectionAttempts();
|
||||
};
|
||||
|
||||
const onConnectionLost = () => {
|
||||
console.warn('[EventSourceClient] Connection lost');
|
||||
isConnected.value = false;
|
||||
reconnectTimer.scheduleReconnect();
|
||||
};
|
||||
|
||||
const onMessage = (event: MessageEvent) => {
|
||||
options.onMessage(event.data);
|
||||
};
|
||||
|
||||
const disconnect = () => {
|
||||
if (eventSource.value) {
|
||||
reconnectTimer.stopReconnectTimer();
|
||||
eventSource.value.close();
|
||||
eventSource.value = null;
|
||||
}
|
||||
|
||||
isConnected.value = false;
|
||||
};
|
||||
|
||||
const connect = () => {
|
||||
// Ensure we disconnect any existing connection
|
||||
disconnect();
|
||||
|
||||
eventSource.value = new EventSource(options.url, { withCredentials: true });
|
||||
eventSource.value.addEventListener('open', onConnected);
|
||||
eventSource.value.addEventListener('message', onMessage);
|
||||
eventSource.value.addEventListener('close', onConnectionLost);
|
||||
};
|
||||
|
||||
const reconnectTimer = useReconnectTimer({
|
||||
onAttempt: connect,
|
||||
onAttemptScheduled: (delay) => {
|
||||
console.log(`[EventSourceClient] Attempting to reconnect in ${delay}ms`);
|
||||
},
|
||||
});
|
||||
|
||||
const sendMessage = (_: string) => {
|
||||
// Noop, EventSource does not support sending messages
|
||||
};
|
||||
|
||||
return {
|
||||
isConnected,
|
||||
connect,
|
||||
disconnect,
|
||||
sendMessage,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,32 @@
|
||||
import { ref } from 'vue';
|
||||
|
||||
export type UseHeartbeatOptions = {
|
||||
interval: number;
|
||||
onHeartbeat: () => void;
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a heartbeat timer using the given interval. The timer needs
|
||||
* to be started and stopped manually.
|
||||
*/
|
||||
export const useHeartbeat = (options: UseHeartbeatOptions) => {
|
||||
const { interval, onHeartbeat } = options;
|
||||
|
||||
const heartbeatTimer = ref<ReturnType<typeof setInterval> | null>(null);
|
||||
|
||||
const startHeartbeat = () => {
|
||||
heartbeatTimer.value = setInterval(onHeartbeat, interval);
|
||||
};
|
||||
|
||||
const stopHeartbeat = () => {
|
||||
if (heartbeatTimer.value) {
|
||||
clearInterval(heartbeatTimer.value);
|
||||
heartbeatTimer.value = null;
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
startHeartbeat,
|
||||
stopHeartbeat,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,49 @@
|
||||
import { ref } from 'vue';
|
||||
|
||||
type UseReconnectTimerOptions = {
|
||||
/** Callback that an attempt should be made */
|
||||
onAttempt: () => void;
|
||||
|
||||
/** Callback that a future attempt was scheduled */
|
||||
onAttemptScheduled: (delay: number) => void;
|
||||
};
|
||||
|
||||
/**
|
||||
* A timer for exponential backoff reconnect attempts.
|
||||
*/
|
||||
export const useReconnectTimer = ({ onAttempt, onAttemptScheduled }: UseReconnectTimerOptions) => {
|
||||
const initialReconnectDelay = 1000;
|
||||
const maxReconnectDelay = 15_000;
|
||||
|
||||
const reconnectTimer = ref<ReturnType<typeof setTimeout> | null>(null);
|
||||
const reconnectAttempts = ref(0);
|
||||
|
||||
const scheduleReconnect = () => {
|
||||
const delay = Math.min(initialReconnectDelay * 2 ** reconnectAttempts.value, maxReconnectDelay);
|
||||
|
||||
reconnectAttempts.value++;
|
||||
|
||||
onAttemptScheduled(delay);
|
||||
reconnectTimer.value = setTimeout(() => {
|
||||
onAttempt();
|
||||
}, delay);
|
||||
};
|
||||
|
||||
/** Stops the reconnect timer. NOTE: This does not reset the reconnect attempts. */
|
||||
const stopReconnectTimer = () => {
|
||||
if (reconnectTimer.value) {
|
||||
clearTimeout(reconnectTimer.value);
|
||||
reconnectTimer.value = null;
|
||||
}
|
||||
};
|
||||
|
||||
const resetConnectionAttempts = () => {
|
||||
reconnectAttempts.value = 0;
|
||||
};
|
||||
|
||||
return {
|
||||
scheduleReconnect,
|
||||
stopReconnectTimer,
|
||||
resetConnectionAttempts,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,105 @@
|
||||
import { useHeartbeat } from '@/push-connection/useHeartbeat';
|
||||
import { useReconnectTimer } from '@/push-connection/useReconnectTimer';
|
||||
import { ref } from 'vue';
|
||||
import { createHeartbeatMessage } from '@n8n/api-types';
|
||||
export type UseWebSocketClientOptions<T> = {
|
||||
url: string;
|
||||
onMessage: (data: T) => void;
|
||||
};
|
||||
|
||||
/** Defined here as not available in tests */
|
||||
export const WebSocketState = {
|
||||
CONNECTING: 0,
|
||||
OPEN: 1,
|
||||
CLOSING: 2,
|
||||
CLOSED: 3,
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a WebSocket connection to the server. Uses reconnection logic
|
||||
* to reconnect if the connection is lost.
|
||||
*/
|
||||
export const useWebSocketClient = <T>(options: UseWebSocketClientOptions<T>) => {
|
||||
const isConnected = ref(false);
|
||||
const socket = ref<WebSocket | null>(null);
|
||||
|
||||
/**
|
||||
* Heartbeat timer to keep the connection alive. This is an additional
|
||||
* mechanism to the protocol level ping/pong mechanism the server sends.
|
||||
* This is used the ensure the client notices connection issues.
|
||||
*/
|
||||
const { startHeartbeat, stopHeartbeat } = useHeartbeat({
|
||||
interval: 30_000,
|
||||
onHeartbeat: () => {
|
||||
socket.value?.send(JSON.stringify(createHeartbeatMessage()));
|
||||
},
|
||||
});
|
||||
|
||||
const onConnected = () => {
|
||||
socket.value?.removeEventListener('open', onConnected);
|
||||
isConnected.value = true;
|
||||
startHeartbeat();
|
||||
reconnectTimer.resetConnectionAttempts();
|
||||
};
|
||||
|
||||
const onConnectionLost = (event: CloseEvent) => {
|
||||
console.warn(`[WebSocketClient] Connection lost, code=${event.code ?? 'unknown'}`);
|
||||
disconnect();
|
||||
reconnectTimer.scheduleReconnect();
|
||||
};
|
||||
|
||||
const onMessage = (event: MessageEvent) => {
|
||||
options.onMessage(event.data);
|
||||
};
|
||||
|
||||
const onError = (error: unknown) => {
|
||||
console.warn('[WebSocketClient] Connection error:', error);
|
||||
};
|
||||
|
||||
const disconnect = () => {
|
||||
if (socket.value) {
|
||||
stopHeartbeat();
|
||||
reconnectTimer.stopReconnectTimer();
|
||||
socket.value.removeEventListener('message', onMessage);
|
||||
socket.value.removeEventListener('error', onError);
|
||||
socket.value.removeEventListener('close', onConnectionLost);
|
||||
socket.value.close(1000);
|
||||
socket.value = null;
|
||||
}
|
||||
|
||||
isConnected.value = false;
|
||||
};
|
||||
|
||||
const connect = () => {
|
||||
// Ensure we disconnect any existing connection
|
||||
disconnect();
|
||||
|
||||
socket.value = new WebSocket(options.url);
|
||||
socket.value.addEventListener('open', onConnected);
|
||||
socket.value.addEventListener('message', onMessage);
|
||||
socket.value.addEventListener('error', onError);
|
||||
socket.value.addEventListener('close', onConnectionLost);
|
||||
};
|
||||
|
||||
const reconnectTimer = useReconnectTimer({
|
||||
onAttempt: connect,
|
||||
onAttemptScheduled: (delay) => {
|
||||
console.log(`[WebSocketClient] Attempting to reconnect in ${delay}ms`);
|
||||
},
|
||||
});
|
||||
|
||||
const sendMessage = (serializedMessage: string) => {
|
||||
if (!isConnected.value || !socket.value) {
|
||||
throw new Error('Not connected to the server');
|
||||
}
|
||||
|
||||
socket.value.send(serializedMessage);
|
||||
};
|
||||
|
||||
return {
|
||||
isConnected,
|
||||
connect,
|
||||
disconnect,
|
||||
sendMessage,
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user