From da837feb262106773307d468388e1e1087748174 Mon Sep 17 00:00:00 2001 From: Tomi Turtiainen <10324676+tomi@users.noreply.github.com> Date: Mon, 10 Feb 2025 10:53:40 +0200 Subject: [PATCH] fix(editor): Fix sending of push messages when connection is down (no-changelog) (#13133) --- .../src/push-connection/useReconnectTimer.ts | 1 + .../src/push-connection/useWebSocketClient.ts | 3 +- .../src/stores/pushConnection.store.test.ts | 173 ++++++++++++++++++ .../src/stores/pushConnection.store.ts | 6 +- 4 files changed, 180 insertions(+), 3 deletions(-) create mode 100644 packages/editor-ui/src/stores/pushConnection.store.test.ts diff --git a/packages/editor-ui/src/push-connection/useReconnectTimer.ts b/packages/editor-ui/src/push-connection/useReconnectTimer.ts index 695b4ab611..bc62a2d897 100644 --- a/packages/editor-ui/src/push-connection/useReconnectTimer.ts +++ b/packages/editor-ui/src/push-connection/useReconnectTimer.ts @@ -29,6 +29,7 @@ export const useReconnectTimer = ({ onAttempt, onAttemptScheduled }: UseReconnec }, delay); }; + /** Stops the reconnect timer. NOTE: This does not reset the reconnect attempts. */ const stopReconnectTimer = () => { if (reconnectTimer.value) { clearTimeout(reconnectTimer.value); diff --git a/packages/editor-ui/src/push-connection/useWebSocketClient.ts b/packages/editor-ui/src/push-connection/useWebSocketClient.ts index 235e25be54..5a83d279db 100644 --- a/packages/editor-ui/src/push-connection/useWebSocketClient.ts +++ b/packages/editor-ui/src/push-connection/useWebSocketClient.ts @@ -44,8 +44,7 @@ export const useWebSocketClient = (options: UseWebSocketClientOptions) => const onConnectionLost = (event: CloseEvent) => { console.warn(`[WebSocketClient] Connection lost, code=${event.code ?? 'unknown'}`); - isConnected.value = false; - stopHeartbeat(); + disconnect(); reconnectTimer.scheduleReconnect(); }; diff --git a/packages/editor-ui/src/stores/pushConnection.store.test.ts b/packages/editor-ui/src/stores/pushConnection.store.test.ts new file mode 100644 index 0000000000..0d4bfdc69d --- /dev/null +++ b/packages/editor-ui/src/stores/pushConnection.store.test.ts @@ -0,0 +1,173 @@ +import { setActivePinia, createPinia } from 'pinia'; +import { describe, test, expect, vi } from 'vitest'; +import { usePushConnectionStore } from './pushConnection.store'; +import { useWebSocketClient } from '@/push-connection/useWebSocketClient'; +import { ref } from 'vue'; + +type WebSocketClient = ReturnType; + +vi.mock('@/push-connection/useWebSocketClient', () => ({ + useWebSocketClient: vi.fn(), +})); + +vi.mock('@/push-connection/useEventSourceClient', () => ({ + useEventSourceClient: vi.fn().mockReturnValue({ + isConnected: { value: false }, + connect: vi.fn(), + disconnect: vi.fn(), + sendMessage: vi.fn(), + }), +})); + +vi.mock('./root.store', () => ({ + useRootStore: vi.fn().mockReturnValue({ + restUrl: 'http://localhost:5678/api/v1', + pushRef: 'test-push-ref', + }), +})); + +vi.mock('./settings.store', () => ({ + useSettingsStore: vi.fn().mockReturnValue({ + pushBackend: 'websocket', + }), +})); + +describe('usePushConnectionStore', () => { + afterEach(() => { + vi.clearAllMocks(); + }); + + const createTestInitialState = ({ + isConnected = false, + }: { + isConnected?: boolean; + } = {}) => { + // Mock connected state + let onMessage: (data: unknown) => void = vi.fn(); + const mockWebSocketClient: WebSocketClient = { + isConnected: ref(isConnected), + connect: vi.fn(), + disconnect: vi.fn(), + sendMessage: vi.fn(), + }; + + vi.mocked(useWebSocketClient).mockImplementation((opts) => { + onMessage = opts.onMessage; + return mockWebSocketClient; + }); + + setActivePinia(createPinia()); + + return { + store: usePushConnectionStore(), + mockWebSocketClient, + onMessage, + }; + }; + + test('should initialize with default values', () => { + const { store } = createTestInitialState(); + + expect(store.isConnected).toBe(false); + expect(store.isConnectionRequested).toBe(false); + expect(store.onMessageReceivedHandlers).toEqual([]); + }); + + test('should handle event listeners', () => { + const { store } = createTestInitialState(); + const handler = vi.fn(); + + const removeListener = store.addEventListener(handler); + expect(store.onMessageReceivedHandlers).toHaveLength(1); + + removeListener(); + expect(store.onMessageReceivedHandlers).toHaveLength(0); + }); + + describe('connection handling', () => { + test('should connect and disconnect', () => { + const { store, mockWebSocketClient } = createTestInitialState(); + + store.pushConnect(); + expect(store.isConnectionRequested).toBe(true); + expect(mockWebSocketClient.connect).toHaveBeenCalled(); + + store.pushDisconnect(); + expect(store.isConnectionRequested).toBe(false); + expect(mockWebSocketClient.disconnect).toHaveBeenCalled(); + }); + + test('should show correct connection status', () => { + const { store, mockWebSocketClient } = createTestInitialState({ + isConnected: true, + }); + + expect(store.isConnected).toBe(true); + expect(mockWebSocketClient.isConnected.value).toBe(true); + + mockWebSocketClient.isConnected.value = false; + expect(store.isConnected).toBe(false); + }); + }); + + describe('sending messages', () => { + test('should handle message sending when connected', () => { + const { store, mockWebSocketClient } = createTestInitialState({ + isConnected: true, + }); + const testMessage = { type: 'test', data: 'message' }; + + store.send(testMessage); + + expect(mockWebSocketClient.sendMessage).toHaveBeenCalledWith(JSON.stringify(testMessage)); + }); + + test('should queue messages when disconnected and send them when connected', async () => { + const { store, mockWebSocketClient } = createTestInitialState(); + const testMessage = { type: 'test', data: 'message' }; + + store.send(testMessage); + store.send(testMessage); + + expect(mockWebSocketClient.sendMessage).not.toHaveBeenCalled(); + + mockWebSocketClient.isConnected.value = true; + + // Wait for the queue to be processed + await new Promise(setImmediate); + + expect(mockWebSocketClient.sendMessage).toHaveBeenCalledTimes(2); + }); + }); + + describe('receiving messages', () => { + test('should process received messages', async () => { + const { store, onMessage } = createTestInitialState({ + isConnected: true, + }); + const handler = vi.fn(); + const testMessage = { type: 'test', data: 'message' }; + + store.addEventListener(handler); + + // Simulate receiving a message + onMessage(JSON.stringify(testMessage)); + + expect(handler).toHaveBeenCalledWith(testMessage); + }); + + test('should handle invalid received messages', async () => { + const { store, onMessage } = createTestInitialState({ + isConnected: true, + }); + const handler = vi.fn(); + + store.addEventListener(handler); + + // Simulate receiving an invalid message + onMessage('invalid json'); + + expect(handler).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/packages/editor-ui/src/stores/pushConnection.store.ts b/packages/editor-ui/src/stores/pushConnection.store.ts index 33a0ede331..eeedcbf6ac 100644 --- a/packages/editor-ui/src/stores/pushConnection.store.ts +++ b/packages/editor-ui/src/stores/pushConnection.store.ts @@ -77,7 +77,11 @@ export const usePushConnectionStore = defineStore(STORES.PUSH, () => { : useEventSourceClient({ url, onMessage }); function serializeAndSend(message: unknown) { - client.sendMessage(JSON.stringify(message)); + if (client.isConnected.value) { + client.sendMessage(JSON.stringify(message)); + } else { + outgoingQueue.value.push(message); + } } const pushConnect = () => {