Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/libs/API/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,6 @@ const WRITE_COMMANDS = {
ADD_EMOJI_REACTION: 'AddEmojiReaction',
REMOVE_EMOJI_REACTION: 'RemoveEmojiReaction',
LEAVE_ROOM: 'LeaveRoom',
PUSHER_PING: 'PusherPing',
LEAVE_GROUP_CHAT: 'LeaveGroupChat',
INVITE_TO_ROOM: 'InviteToRoom',
INVITE_TO_GROUP_CHAT: 'InviteToGroupChat',
Expand Down Expand Up @@ -789,7 +788,6 @@ type WriteCommandParameters = {
[WRITE_COMMANDS.INVITE_TO_GROUP_CHAT]: Parameters.InviteToGroupChatParams;
[WRITE_COMMANDS.UPDATE_GROUP_CHAT_AVATAR]: Parameters.UpdateGroupChatAvatarParams;
[WRITE_COMMANDS.UPDATE_ROOM_AVATAR]: Parameters.UpdateRoomAvatarParams;
[WRITE_COMMANDS.PUSHER_PING]: Parameters.PusherPingParams;
[WRITE_COMMANDS.LEAVE_GROUP_CHAT]: Parameters.LeaveGroupChatParams;
[WRITE_COMMANDS.REMOVE_FROM_GROUP_CHAT]: Parameters.RemoveFromGroupChatParams;
[WRITE_COMMANDS.UPDATE_GROUP_CHAT_MEMBER_ROLES]: Parameters.UpdateGroupChatMemberRolesParams;
Expand Down Expand Up @@ -1569,6 +1567,7 @@ const SIDE_EFFECT_REQUEST_COMMANDS = {
IMPORT_PER_DIEM_RATES: 'ImportPerDiemRates',
IMPORT_TAGS_SPREADSHEET: 'ImportTagsSpreadsheet',
OPEN_OLD_DOT_LINK: 'OpenOldDotLink',
PUSHER_PING: 'PusherPing',
RECONNECT_APP: 'ReconnectApp',
REVEAL_EXPENSIFY_TRAVEL_CARD_DETAILS: 'RevealExpensifyTravelCardDetails',
TWO_FACTOR_AUTH_VALIDATE: 'TwoFactorAuth_Validate',
Expand Down Expand Up @@ -1616,6 +1615,7 @@ type SideEffectRequestCommandParameters = {
[SIDE_EFFECT_REQUEST_COMMANDS.IMPORT_PER_DIEM_RATES]: Parameters.ImportPerDiemRatesParams;
[SIDE_EFFECT_REQUEST_COMMANDS.IMPORT_TAGS_SPREADSHEET]: Parameters.ImportTagsSpreadsheetParams;
[SIDE_EFFECT_REQUEST_COMMANDS.OPEN_OLD_DOT_LINK]: Parameters.OpenOldDotLinkParams;
[SIDE_EFFECT_REQUEST_COMMANDS.PUSHER_PING]: Parameters.PusherPingParams;
[SIDE_EFFECT_REQUEST_COMMANDS.REVEAL_EXPENSIFY_TRAVEL_CARD_DETAILS]: Parameters.RevealExpensifyCardDetailsParams;
[SIDE_EFFECT_REQUEST_COMMANDS.GET_MISSING_ONYX_MESSAGES]: Parameters.GetMissingOnyxMessagesParams;
[SIDE_EFFECT_REQUEST_COMMANDS.RECONNECT_APP]: Parameters.ReconnectAppParams;
Expand Down
6 changes: 5 additions & 1 deletion src/libs/actions/User.ts
Original file line number Diff line number Diff line change
Expand Up @@ -846,7 +846,11 @@ function pingPusher() {
lastPingSentTimestamp = pingTimestamp;

const parameters: PusherPingParams = {pingID, pingTimestamp};
API.writeWithNoDuplicatesConflictAction(WRITE_COMMANDS.PUSHER_PING, parameters);

// The heartbeat is a probe, not a user's write, so it must not be persisted to disk, retried, or hold the
// head of the queue while a real write waits behind it. The Logging middleware already logs any failure.
// eslint-disable-next-line rulesdir/no-api-side-effects-method
API.makeRequestWithSideEffects(SIDE_EFFECT_REQUEST_COMMANDS.PUSHER_PING, parameters).catch(() => {});
Comment thread
adhorodyski marked this conversation as resolved.
Log.info(`[Pusher PINGPONG] Sending a PING to the server: ${pingID} timestamp: ${pingTimestamp}`);
}

Expand Down
17 changes: 17 additions & 0 deletions tests/unit/PusherPingPongTest.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import {subscribeToUserEvents} from '@libs/actions/User';
import * as API from '@libs/API';
import {SIDE_EFFECT_REQUEST_COMMANDS} from '@libs/API/types';
import type * as NetworkStateModule from '@libs/NetworkState';
import Pusher from '@libs/Pusher';
import PusherUtils from '@libs/PusherUtils';
Expand All @@ -8,6 +10,8 @@ import ONYXKEYS from '@src/ONYXKEYS';
import Onyx from 'react-native-onyx';

jest.mock('@libs/API');
const mockAPI = jest.mocked(API);

jest.mock('@libs/PusherUtils');
jest.mock('@libs/ActiveClientManager', () => ({
isClientTheLeader: jest.fn(() => true),
Expand All @@ -22,6 +26,8 @@ jest.mock('@libs/NetworkState', () => ({
// The watchdog checks every 60s; each reconnect skips the following check, so while PONGs stay missing it fires on every second check tick (~2 minutes)
const CHECK_INTERVAL_MS = 60_000;

const PING_INTERVAL_MS = 30_000;

describe('Pusher PINGPONG watchdog', () => {
let reconnectSpy: jest.SpyInstance;
let pongCallback: Parameters<typeof PusherUtils.subscribeToPrivateUserChannelEvent>[2];
Expand All @@ -33,6 +39,9 @@ describe('Pusher PINGPONG watchdog', () => {
Onyx.init({keys: ONYXKEYS});
reconnectSpy = jest.spyOn(Pusher, 'reconnect').mockImplementation(() => {});

// The automock returns undefined, and pingPusher chains a .catch on the returned promise
mockAPI.makeRequestWithSideEffects.mockResolvedValue(undefined);

subscribeToUserEvents(123, 'test@example.com', () => undefined);

const callback = jest.mocked(PusherUtils.subscribeToPrivateUserChannelEvent).mock.calls.find(([eventName]) => eventName === Pusher.TYPE.PONG)?.[2];
Expand Down Expand Up @@ -78,4 +87,12 @@ describe('Pusher PINGPONG watchdog', () => {
await jest.advanceTimersByTimeAsync(CHECK_INTERVAL_MS);
expect(reconnectSpy).toHaveBeenCalledTimes(1);
});

it('sends the PING off the durable write queue', async () => {
mockAPI.makeRequestWithSideEffects.mockClear();
await jest.advanceTimersByTimeAsync(PING_INTERVAL_MS);

expect(mockAPI.makeRequestWithSideEffects).toHaveBeenCalledWith(SIDE_EFFECT_REQUEST_COMMANDS.PUSHER_PING, expect.anything());
expect(mockAPI.writeWithNoDuplicatesConflictAction).not.toHaveBeenCalled();
});
});
Loading