Skip to content

Commit

Permalink
fix(editor): Fix sending of push messages when connection is down (no…
Browse files Browse the repository at this point in the history
…-changelog) (#13133)
  • Loading branch information
tomi authored Feb 10, 2025
1 parent bde8420 commit da837fe
Show file tree
Hide file tree
Showing 4 changed files with 180 additions and 3 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
3 changes: 1 addition & 2 deletions packages/editor-ui/src/push-connection/useWebSocketClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,7 @@ export const useWebSocketClient = <T>(options: UseWebSocketClientOptions<T>) =>

const onConnectionLost = (event: CloseEvent) => {
console.warn(`[WebSocketClient] Connection lost, code=${event.code ?? 'unknown'}`);
isConnected.value = false;
stopHeartbeat();
disconnect();
reconnectTimer.scheduleReconnect();
};

Expand Down
173 changes: 173 additions & 0 deletions packages/editor-ui/src/stores/pushConnection.store.test.ts
Original file line number Diff line number Diff line change
@@ -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<typeof useWebSocketClient>;

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();
});
});
});
6 changes: 5 additions & 1 deletion packages/editor-ui/src/stores/pushConnection.store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = () => {
Expand Down

0 comments on commit da837fe

Please # to comment.