fix(editor): Fix sending of push messages when connection is down (no-changelog) (#13133)

This commit is contained in:
Tomi Turtiainen 2025-02-10 10:53:40 +02:00 committed by GitHub
parent bde84205f9
commit da837feb26
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 180 additions and 3 deletions

View file

@ -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);

View file

@ -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();
};

View file

@ -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();
});
});
});

View file

@ -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 = () => {