From 82b4613342abb246393f78840614120cbc9e3ce3 Mon Sep 17 00:00:00 2001 From: VickyStash Date: Mon, 10 Feb 2025 14:22:56 +0100 Subject: [PATCH 01/16] Initial updates to migrate mobile platforms to pusher-websocket-react-native lib --- ios/Podfile.lock | 28 +- package-lock.json | 11 + package.json | 1 + src/CONST.ts | 7 + src/libs/Pusher/pusher/index.native.ts | 406 ++++++++++++++++++ .../Pusher/{pusher.ts => pusher/index.ts} | 17 +- src/libs/PusherConnectionManager.ts | 2 +- src/libs/actions/Session/index.ts | 20 +- 8 files changed, 472 insertions(+), 20 deletions(-) create mode 100644 src/libs/Pusher/pusher/index.native.ts rename src/libs/Pusher/{pusher.ts => pusher/index.ts} (95%) diff --git a/ios/Podfile.lock b/ios/Podfile.lock index a7347a4c5097..714f187cea34 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -310,6 +310,7 @@ PODS: - nanopb/encode (= 2.30908.0) - nanopb/decode (2.30908.0) - nanopb/encode (2.30908.0) + - NWWebSocket (0.5.4) - Onfido (29.7.2) - onfido-react-native-sdk (10.6.0): - DoubleConversion @@ -335,6 +336,12 @@ PODS: - Yoga - Plaid (5.6.0) - PromisesObjC (2.4.0) + - pusher-websocket-react-native (1.3.1): + - PusherSwift (~> 10.1.5) + - React + - PusherSwift (10.1.5): + - NWWebSocket (~> 0.5.4) + - TweetNacl (~> 1.0.0) - RCT-Folly (2024.01.01.00): - boost - DoubleConversion @@ -2842,6 +2849,7 @@ PODS: - SDWebImage/Core (~> 5.17) - SocketRocket (0.7.1) - Turf (2.8.0) + - TweetNacl (1.0.2) - VisionCamera (4.6.1): - VisionCamera/Core (= 4.6.1) - VisionCamera/React (= 4.6.1) @@ -2872,6 +2880,7 @@ DEPENDENCIES: - hermes-engine (from `../node_modules/react-native/sdks/hermes-engine/hermes-engine.podspec`) - lottie-react-native (from `../node_modules/lottie-react-native`) - "onfido-react-native-sdk (from `../node_modules/@onfido/react-native-sdk`)" + - "pusher-websocket-react-native (from `../node_modules/@pusher/pusher-websocket-react-native`)" - RCT-Folly (from `../node_modules/react-native/third-party-podspecs/RCT-Folly.podspec`) - RCT-Folly/Fabric (from `../node_modules/react-native/third-party-podspecs/RCT-Folly.podspec`) - RCTDeprecation (from `../node_modules/react-native/ReactApple/Libraries/RCTFoundation/RCTDeprecation`) @@ -3008,15 +3017,18 @@ SPEC REPOS: - MapboxMaps - MapboxMobileEvents - nanopb + - NWWebSocket - Onfido - Plaid - PromisesObjC + - PusherSwift - SDWebImage - SDWebImageAVIFCoder - SDWebImageSVGCoder - SDWebImageWebPCoder - SocketRocket - Turf + - TweetNacl EXTERNAL SOURCES: AppLogs: @@ -3060,6 +3072,8 @@ EXTERNAL SOURCES: :path: "../node_modules/lottie-react-native" onfido-react-native-sdk: :path: "../node_modules/@onfido/react-native-sdk" + pusher-websocket-react-native: + :path: "../node_modules/@pusher/pusher-websocket-react-native" RCT-Folly: :podspec: "../node_modules/react-native/third-party-podspecs/RCT-Folly.podspec" RCTDeprecation: @@ -3275,8 +3289,8 @@ SPEC CHECKSUMS: AirshipServiceExtension: 9c73369f426396d9fb9ff222d86d842fac76ba46 AppAuth: 501c04eda8a8d11f179dbe8637b7a91bb7e5d2fa AppLogs: 3bc4e9b141dbf265b9464409caaa40416a9ee0e0 - boost: d7090b1a93a9798c029277a8288114f2948f471c - DoubleConversion: f16ae600a246532c4020132d54af21d0ddb2a385 + boost: 26992d1adf73c1c7676360643e687aee6dda994b + DoubleConversion: 76ab83afb40bddeeee456813d9c04f67f78771b5 EXAV: 9773c9799767c9925547b05e41a26a0240bb8ef2 EXImageLoader: 759063a65ab016b836f73972d3bb25404888713d expensify-react-native-background-task: 6f797cf470b627912c246514b1631a205794775d @@ -3296,11 +3310,11 @@ SPEC CHECKSUMS: FirebaseInstallations: 40bd9054049b2eae9a2c38ef1c3dd213df3605cd FirebasePerformance: 0c01a7a496657d7cea86d40c0b1725259d164c6c FirebaseRemoteConfig: 2d6e2cfdb49af79535c8af8a80a4a5009038ec2b - fmt: 10c6e61f4be25dc963c36bd73fc7b1705fe975be + fmt: 4c2741a687cc09f0634a2e2c72a838b99f1ff120 ForkInputMask: 55e3fbab504b22da98483e9f9a6514b98fdd2f3c FullStory: c8a10b2358c0d33c57be84d16e4c440b0434b33d fullstory_react-native: 63a803cca04b0447a71daa73e4df3f7b56e1919d - glog: 08b301085f15bcbb6ff8632a8ebaf239aae04e6a + glog: 69ef571f3de08433d766d614c73a9838a06bf7eb GoogleAppMeasurement: 5ba1164e3c844ba84272555e916d0a6d3d977e91 GoogleDataTransport: 6c09b596d841063d76d4288cc2d2f42cc36e1e2a GoogleSignIn: d4281ab6cf21542b1cfaff85c191f230b399d2db @@ -3318,11 +3332,14 @@ SPEC CHECKSUMS: MapboxMaps: 05822ab0ee74f7d626e6471572439afe35c1c116 MapboxMobileEvents: d044b9edbe0ec7df60f6c2c9634fe9a7f449266b nanopb: a0ba3315591a9ae0a16a309ee504766e90db0c96 + NWWebSocket: 040d22f23438cc09aaeabf537beff67699c3c76d Onfido: f3af62ea1c9a419589c133e3e511e5d2c4f3f8af onfido-react-native-sdk: 4ccfdeb10f9ccb4a5799d2555cdbc2a068a42c0d Plaid: c32f22ffce5ec67c9e6147eaf6c4d7d5f8086d89 PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47 - RCT-Folly: bf5c0376ffe4dd2cf438dcf86db385df9fdce648 + pusher-websocket-react-native: e40c49a1e4ec96d4157375aebcf44943f0f8f62f + PusherSwift: cad631bad86cfff4b8458dce1310a7774e469b1f + RCT-Folly: 4464f4d875961fce86008d45f4ecf6cef6de0740 RCTDeprecation: 2c5e1000b04ab70b53956aa498bf7442c3c6e497 RCTRequired: 5f785a001cf68a551c5f5040fb4c415672dbb481 RCTTypeSafety: 6b98db8965005d32449605c0d005ecb4fee8a0f7 @@ -3428,6 +3445,7 @@ SPEC CHECKSUMS: SDWebImageWebPCoder: e38c0a70396191361d60c092933e22c20d5b1380 SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748 Turf: aa2ede4298009639d10db36aba1a7ebaad072a5e + TweetNacl: 3abf4d1d2082b0114e7a67410e300892448951e6 VisionCamera: c95a8ad535f527562be1fb05fb2fd324578e769c Yoga: 3deb2471faa9916c8a82dda2a22d3fba2620ad37 diff --git a/package-lock.json b/package-lock.json index 79c088f27400..af3257de8f0b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -26,6 +26,7 @@ "@gorhom/portal": "^1.0.14", "@invertase/react-native-apple-authentication": "^2.2.2", "@onfido/react-native-sdk": "10.6.0", + "@pusher/pusher-websocket-react-native": "^1.3.1", "@react-native-camera-roll/camera-roll": "7.4.0", "@react-native-clipboard/clipboard": "^1.15.0", "@react-native-community/geolocation": "3.3.0", @@ -7561,6 +7562,16 @@ "integrity": "sha512-j7P6Rgr3mmtdkeDGTe0E/aYyWEWVtc5yFXtHCRHs28/jptDEWfaVOc5T7cblqy1XKPPfCxJc/8DwQ5YgLOZOVQ==", "dev": true }, + "node_modules/@pusher/pusher-websocket-react-native": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@pusher/pusher-websocket-react-native/-/pusher-websocket-react-native-1.3.1.tgz", + "integrity": "sha512-NUarJuOW79b9DBjH/ena0pOutBR0uXnWLg5mIbzUYIl0A7gASC1dgBd8fJ7s5fJXQgQQXNlrqQ9E1SKz6pIvuA==", + "license": "MIT", + "peerDependencies": { + "react": "*", + "react-native": "*" + } + }, "node_modules/@radix-ui/primitive": { "version": "1.1.0", "dev": true, diff --git a/package.json b/package.json index b79a9e8318ea..a5286e115de3 100644 --- a/package.json +++ b/package.json @@ -93,6 +93,7 @@ "@gorhom/portal": "^1.0.14", "@invertase/react-native-apple-authentication": "^2.2.2", "@onfido/react-native-sdk": "10.6.0", + "@pusher/pusher-websocket-react-native": "^1.3.1", "@react-native-camera-roll/camera-roll": "7.4.0", "@react-native-clipboard/clipboard": "^1.15.0", "@react-native-community/geolocation": "3.3.0", diff --git a/src/CONST.ts b/src/CONST.ts index 285d3ed2e13d..3f12dba51463 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -1588,6 +1588,13 @@ const CONST = { PRIVATE_USER_CHANNEL_PREFIX: 'private-encrypted-user-accountID-', PRIVATE_REPORT_CHANNEL_PREFIX: 'private-report-reportID-', PRESENCE_ACTIVE_GUIDES: 'presence-activeGuides', + STATE: { + CONNECTING: 'CONNECTING', + CONNECTED: 'CONNECTED', + DISCONNECTING: 'DISCONNECTING', + DISCONNECTED: 'DISCONNECTED', + RECONNECTING: 'RECONNECTING', + }, }, EMOJI_SPACER: 'SPACER', diff --git a/src/libs/Pusher/pusher/index.native.ts b/src/libs/Pusher/pusher/index.native.ts new file mode 100644 index 000000000000..ee617e2d5895 --- /dev/null +++ b/src/libs/Pusher/pusher/index.native.ts @@ -0,0 +1,406 @@ +import type {PusherChannel} from '@pusher/pusher-websocket-react-native'; +import {Pusher} from '@pusher/pusher-websocket-react-native'; +import isObject from 'lodash/isObject'; +import {InteractionManager} from 'react-native'; +import Onyx from 'react-native-onyx'; +import type {LiteralUnion, ValueOf} from 'type-fest'; +import Log from '@libs/Log'; +import TYPE from '@libs/Pusher/EventType'; +import type {SocketEventName} from '@libs/Pusher/library/types'; +import {onAuthorizer} from '@userActions/Session'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type {OnyxUpdatesFromServer, ReportUserIsTyping} from '@src/types/onyx'; +import type DeepValueOf from '@src/types/utils/DeepValueOf'; + +type States = { + previous: string; + current: string; +}; + +type Args = { + appKey: string; + cluster: string; + authEndpoint: string; +}; + +type UserIsTypingEvent = ReportUserIsTyping & { + userLogin?: string; +}; + +type UserIsLeavingRoomEvent = Record & { + userLogin?: string; +}; + +type PingPongEvent = Record & { + pingID: string; + pingTimestamp: number; +}; + +type PusherEventMap = { + [TYPE.USER_IS_TYPING]: UserIsTypingEvent; + [TYPE.USER_IS_LEAVING_ROOM]: UserIsLeavingRoomEvent; + [TYPE.PONG]: PingPongEvent; +}; + +type EventData = {chunk?: string; id?: string; index?: number; final?: boolean} & (EventName extends keyof PusherEventMap + ? PusherEventMap[EventName] + : OnyxUpdatesFromServer); + +type EventCallbackError = {type?: ValueOf; data: {code: number; message?: string}}; + +type ChunkedDataEvents = {chunks: unknown[]; receivedFinal: boolean}; + +type SocketEventCallback = (eventName: SocketEventName, data?: States | EventCallbackError) => void; + +type PusherEventName = LiteralUnion, string>; + +let shouldForceOffline = false; +Onyx.connect({ + key: ONYXKEYS.NETWORK, + callback: (network) => { + if (!network) { + return; + } + shouldForceOffline = !!network.shouldForceOffline; + }, +}); + +let socket: Pusher | null; +let pusherSocketID: string | undefined; +const socketEventCallbacks: SocketEventCallback[] = []; + +let resolveInitPromise: () => void; +let initPromise = new Promise((resolve) => { + resolveInitPromise = resolve; +}); + +const eventsBoundToChannels = new Map) => void>>(); + +/** + * Trigger each of the socket event callbacks with the event information + */ +function callSocketEventCallbacks(eventName: SocketEventName, data?: EventCallbackError | States) { + socketEventCallbacks.forEach((cb) => cb(eventName, data)); +} + +/** + * Initialize our pusher lib + * @returns resolves when Pusher has connected + */ +function init(args: Args): Promise { + // eslint-disable-next-line @lwc/lwc/no-async-await + return new Promise(async (resolve) => { + if (socket) { + resolve(); + return; + } + + socket = Pusher.getInstance(); + await socket.init({ + apiKey: args.appKey, + cluster: args.cluster, + onConnectionStateChange: (currentState: string, previousState: string) => { + if (currentState === CONST.PUSHER.STATE.CONNECTED) { + socket?.getSocketId().then((id: string) => { + pusherSocketID = id; + callSocketEventCallbacks('connected'); + resolve(); + }); + } + if (currentState === CONST.PUSHER.STATE.DISCONNECTED) { + callSocketEventCallbacks('disconnected'); + } + callSocketEventCallbacks('state_change', {previous: previousState, current: currentState}); + }, + onError: (message: string, code: number) => callSocketEventCallbacks('error', {data: {code, message}}), + onAuthorizer: async (channelName: string, socketId: string) => { + const res = await onAuthorizer(channelName, socketId); + return { + auth: res.auth, + shared_secret: res.shared_secret, + }; + }, + }); + await socket.connect(); + }).then(resolveInitPromise); +} + +/** + * Returns a Pusher channel for a channel name + */ +function getChannel(channelName: string): PusherChannel | undefined { + if (!socket) { + return; + } + + return socket.getChannel(channelName); +} + +/** + * Binds an event callback to a channel + eventName + */ +function bindEventToChannel(channel: string, eventName?: EventName, eventCallback: (data: EventData) => void = () => {}) { + if (!eventName) { + return; + } + + const chunkedDataEvents: Record = {}; + const callback = (eventData: EventData) => { + if (shouldForceOffline) { + Log.info('[Pusher] Ignoring a Push event because shouldForceOffline = true'); + return; + } + + let data: EventData; + try { + data = isObject(eventData) ? eventData : (JSON.parse(eventData) as EventData); + } catch (err) { + Log.alert('[Pusher] Unable to parse single JSON event data from Pusher', {error: err, eventData}); + return; + } + if (data.id === undefined || data.chunk === undefined || data.final === undefined) { + eventCallback(data); + return; + } + + // If we are chunking the requests, we need to construct a rolling list of all packets that have come through + // Pusher. If we've completed one of these full packets, we'll combine the data and act on the event that it's + // assigned to. + + // If we haven't seen this eventID yet, initialize it into our rolling list of packets. + if (!chunkedDataEvents[data.id]) { + chunkedDataEvents[data.id] = {chunks: [], receivedFinal: false}; + } + + // Add it to the rolling list. + const chunkedEvent = chunkedDataEvents[data.id]; + if (data.index !== undefined) { + chunkedEvent.chunks[data.index] = data.chunk; + } + + // If this is the last packet, mark that we've hit the end. + if (data.final) { + chunkedEvent.receivedFinal = true; + } + + // Only call the event callback if we've received the last packet and we don't have any holes in the complete + // packet. + if (chunkedEvent.receivedFinal && chunkedEvent.chunks.length === Object.keys(chunkedEvent.chunks).length) { + try { + eventCallback(JSON.parse(chunkedEvent.chunks.join('')) as EventData); + } catch (err) { + Log.alert('[Pusher] Unable to parse chunked JSON response from Pusher', { + error: err, + eventData: chunkedEvent.chunks.join(''), + }); + + // Using console.error is helpful here because it will print a usable stack trace to the console to debug where the error comes from + console.error(err); + } + + delete chunkedDataEvents[data.id]; + } + }; + + if (!eventsBoundToChannels.has(channel)) { + eventsBoundToChannels.set(channel, new Map()); + } + + eventsBoundToChannels.get(channel)?.set(eventName, callback); +} + +/** + * Subscribe to a channel and an event + */ +function subscribe( + channelName: string, + eventName?: EventName, + eventCallback: (data: EventData) => void = () => {}, + onResubscribe = () => {}, +): Promise { + return initPromise.then( + () => + new Promise((resolve, reject) => { + InteractionManager.runAfterInteractions(async () => { + // We cannot call subscribe() before init(). Prevent any attempt to do this on dev. + if (!socket) { + throw new Error(`[Pusher] instance not found. Pusher.subscribe() + most likely has been called before Pusher.init()`); + } + + Log.info('[Pusher] Attempting to subscribe to channel', false, {channelName, eventName}); + const channel = getChannel(channelName); + + if (!channel) { + await socket.subscribe({ + channelName, + onEvent: (event) => { + const callback = eventsBoundToChannels.get(event.channelName)?.get(event.eventName); + callback?.(event.data); + }, + onSubscriptionSucceeded: () => { + bindEventToChannel(channelName, eventName, eventCallback); + resolve(); + // When subscribing for the first time we register a success callback that can be + // called multiple times when the subscription succeeds again in the future + // e.g. as a result of Pusher disconnecting and reconnecting. This callback does + // not fire on the first subscription_succeeded event. + onResubscribe(); + }, + onSubscriptionError: (name: string, message: string) => { + Log.hmmm('[Pusher] Issue authenticating with Pusher during subscribe attempt.', { + channelName, + message, + }); + reject(message); + }, + }); + } else { + bindEventToChannel(channelName, eventName, eventCallback); + resolve(); + } + }); + }), + ); +} + +/** + * Unsubscribe from a channel and optionally a specific event + */ +function unsubscribe(channelName: string, eventName: PusherEventName = '') { + const channel = getChannel(channelName); + + if (!channel) { + Log.hmmm('[Pusher] Attempted to unsubscribe or unbind from a channel, but Pusher-JS has no knowledge of it', {channelName, eventName}); + return; + } + + if (eventName) { + Log.info('[Pusher] Unbinding event', false, {eventName, channelName}); + eventsBoundToChannels.get(channelName)?.delete(eventName); + if (eventsBoundToChannels.get(channelName)?.size === 0) { + Log.info(`[Pusher] After unbinding ${eventName} from channel ${channelName}, no other events were bound to that channel. Unsubscribing...`, false); + eventsBoundToChannels.delete(channelName); + socket?.unsubscribe({channelName}); + } + } else { + Log.info('[Pusher] Unsubscribing from channel', false, {channelName}); + eventsBoundToChannels.delete(channelName); + socket?.unsubscribe({channelName}); + } +} + +/** + * Are we already in the process of subscribing to this channel? + */ +function isAlreadySubscribing(channelName: string): boolean { + if (!socket) { + return false; + } + + // const channel = getChannel(channelName); + // return channel ? channel.subscriptionPending : false; + return false; +} + +/** + * Are we already subscribed to this channel? + */ +function isSubscribed(channelName: string): boolean { + if (!socket) { + return false; + } + + const channel = getChannel(channelName); + return !!channel; +} + +/** + * Sends an event over a specific event/channel in pusher. + */ +function sendEvent(channelName: string, eventName: EventName, payload: EventData) { + // Check to see if we are subscribed to this channel before sending the event. Sending client events over channels + // we are not subscribed too will throw errors and cause reconnection attempts. Subscriptions are not instant and + // can happen later than we expect. + if (!isSubscribed(channelName)) { + return; + } + + if (shouldForceOffline) { + Log.info('[Pusher] Ignoring a Send event because shouldForceOffline = true'); + return; + } + + socket?.trigger({channelName, eventName, data: payload}); +} + +/** + * Register a method that will be triggered when a socket event happens (like disconnecting) + */ +function registerSocketEventCallback(cb: SocketEventCallback) { + socketEventCallbacks.push(cb); +} + +/** + * A custom authorizer allows us to take a more fine-grained approach to + * authenticating Pusher. e.g. we can handle failed attempts to authorize + * with an expired authToken and retry the attempt. + */ +// function registerCustomAuthorizer(authorizer: ChannelAuthorizerGenerator) { +// customAuthorizer = authorizer; +// } + +/** + * Disconnect from Pusher + */ +function disconnect() { + if (!socket) { + Log.info('[Pusher] Attempting to disconnect from Pusher before initialisation has occurred, ignoring.'); + return; + } + + socket.disconnect(); + socket = null; + pusherSocketID = ''; + eventsBoundToChannels.clear(); + initPromise = new Promise((resolve) => { + resolveInitPromise = resolve; + }); +} + +/** + * Disconnect and Re-Connect Pusher + */ +function reconnect() { + if (!socket) { + Log.info('[Pusher] Unable to reconnect since Pusher instance does not yet exist.'); + return; + } + + Log.info('[Pusher] Reconnecting to Pusher'); + socket.disconnect(); + socket.connect(); +} + +function getPusherSocketID(): string | undefined { + return pusherSocketID; +} + +export { + init, + subscribe, + unsubscribe, + getChannel, + isSubscribed, + isAlreadySubscribing, + sendEvent, + disconnect, + reconnect, + registerSocketEventCallback, + // registerCustomAuthorizer, + TYPE, + getPusherSocketID, +}; + +export type {EventCallbackError, States, UserIsTypingEvent, UserIsLeavingRoomEvent, PingPongEvent}; diff --git a/src/libs/Pusher/pusher.ts b/src/libs/Pusher/pusher/index.ts similarity index 95% rename from src/libs/Pusher/pusher.ts rename to src/libs/Pusher/pusher/index.ts index cb18bc7f63db..6ca05e788189 100644 --- a/src/libs/Pusher/pusher.ts +++ b/src/libs/Pusher/pusher/index.ts @@ -4,13 +4,13 @@ import {InteractionManager} from 'react-native'; import Onyx from 'react-native-onyx'; import type {LiteralUnion, ValueOf} from 'type-fest'; import Log from '@libs/Log'; +import TYPE from '@libs/Pusher/EventType'; +import Pusher from '@libs/Pusher/library'; +import type {SocketEventName} from '@libs/Pusher/library/types'; import type CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {OnyxUpdatesFromServer, ReportUserIsTyping} from '@src/types/onyx'; import type DeepValueOf from '@src/types/utils/DeepValueOf'; -import TYPE from './EventType'; -import Pusher from './library'; -import type {SocketEventName} from './library/types'; type States = { previous: string; @@ -98,7 +98,7 @@ function callSocketEventCallbacks(eventName: SocketEventName, data?: EventCallba * Initialize our pusher lib * @returns resolves when Pusher has connected */ -function init(args: Args, params?: unknown): Promise { +function init(args: Args): Promise { return new Promise((resolve) => { if (socket) { resolve(); @@ -122,15 +122,6 @@ function init(args: Args, params?: unknown): Promise { } socket = new Pusher(args.appKey, options); - // If we want to pass params in our requests to api.php we'll need to add it to socket.config.auth.params - // as per the documentation - // (https://pusher.com/docs/channels/using_channels/connection#channels-options-parameter). - // Any param mentioned here will show up in $_REQUEST when we call "AuthenticatePusher". Params passed here need - // to pass our inputRules to show up in the request. - if (params) { - socket.config.auth = {}; - socket.config.auth.params = params; - } // Listen for connection errors and log them socket?.connection.bind('error', (error: EventCallbackError) => { diff --git a/src/libs/PusherConnectionManager.ts b/src/libs/PusherConnectionManager.ts index 69ffa8339f5c..fbe6e3c5faf0 100644 --- a/src/libs/PusherConnectionManager.ts +++ b/src/libs/PusherConnectionManager.ts @@ -13,7 +13,7 @@ function init() { * current valid token to generate the signed auth response * needed to subscribe to Pusher channels. */ - registerCustomAuthorizer((channel) => ({ + registerCustomAuthorizer?.((channel) => ({ authorize: (socketId: string, callback: ChannelAuthorizationCallback) => { authenticatePusher(socketId, channel.name, callback); }, diff --git a/src/libs/actions/Session/index.ts b/src/libs/actions/Session/index.ts index 512019b6db6e..da1c310f919c 100644 --- a/src/libs/actions/Session/index.ts +++ b/src/libs/actions/Session/index.ts @@ -895,7 +895,7 @@ const reauthenticatePusher = throttle( {trailing: false}, ); -function authenticatePusher(socketID: string, channelName: string, callback: ChannelAuthorizationCallback) { +function authenticatePusher(socketID: string, channelName: string, callback?: ChannelAuthorizationCallback) { Log.info('[PusherAuthorizer] Attempting to authorize Pusher', false, {channelName}); const params: AuthenticatePusherParams = { @@ -935,6 +935,23 @@ function authenticatePusher(socketID: string, channelName: string, callback: Cha }); } +async function onAuthorizer(channelName: string, socketId: string) { + Log.info('[PusherAuthorizer] Attempting to authorize Pusher', false, {channelName}); + + const params: AuthenticatePusherParams = { + // eslint-disable-next-line @typescript-eslint/naming-convention + socket_id: socketId, + // eslint-disable-next-line @typescript-eslint/naming-convention + channel_name: channelName, + shouldRetry: false, + forceNetworkRequest: true, + }; + + const response = await API.makeRequestWithSideEffects(SIDE_EFFECT_REQUEST_COMMANDS.AUTHENTICATE_PUSHER, params); + + return response; +} + /** * Request a new validation link / magic code to unlink an unvalidated secondary login from a primary login */ @@ -1336,4 +1353,5 @@ export { validateUserAndGetAccessiblePolicies, isUserOnPrivateDomain, resetSMSDeliveryFailureStatus, + onAuthorizer, }; From 2a56b1c90c228a9ad50b04d6814924d98adf74d8 Mon Sep 17 00:00:00 2001 From: VickyStash Date: Mon, 10 Feb 2025 16:06:25 +0100 Subject: [PATCH 02/16] Reuse existing authenticatePusher function --- src/libs/Pusher/pusher/index.native.ts | 10 ++----- src/libs/actions/Session/index.ts | 36 ++++++++++---------------- 2 files changed, 15 insertions(+), 31 deletions(-) diff --git a/src/libs/Pusher/pusher/index.native.ts b/src/libs/Pusher/pusher/index.native.ts index ee617e2d5895..66e0399bf449 100644 --- a/src/libs/Pusher/pusher/index.native.ts +++ b/src/libs/Pusher/pusher/index.native.ts @@ -7,7 +7,7 @@ import type {LiteralUnion, ValueOf} from 'type-fest'; import Log from '@libs/Log'; import TYPE from '@libs/Pusher/EventType'; import type {SocketEventName} from '@libs/Pusher/library/types'; -import {onAuthorizer} from '@userActions/Session'; +import {authenticatePusher} from '@userActions/Session'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {OnyxUpdatesFromServer, ReportUserIsTyping} from '@src/types/onyx'; @@ -114,13 +114,7 @@ function init(args: Args): Promise { callSocketEventCallbacks('state_change', {previous: previousState, current: currentState}); }, onError: (message: string, code: number) => callSocketEventCallbacks('error', {data: {code, message}}), - onAuthorizer: async (channelName: string, socketId: string) => { - const res = await onAuthorizer(channelName, socketId); - return { - auth: res.auth, - shared_secret: res.shared_secret, - }; - }, + onAuthorizer: (channelName: string, socketId: string) => authenticatePusher(socketId, channelName), }); await socket.connect(); }).then(resolveInitPromise); diff --git a/src/libs/actions/Session/index.ts b/src/libs/actions/Session/index.ts index da1c310f919c..5d7757524127 100644 --- a/src/libs/actions/Session/index.ts +++ b/src/libs/actions/Session/index.ts @@ -909,11 +909,11 @@ function authenticatePusher(socketID: string, channelName: string, callback?: Ch // We use makeRequestWithSideEffects here because we need to authorize to Pusher (an external service) each time a user connects to any channel. // eslint-disable-next-line rulesdir/no-api-side-effects-method - API.makeRequestWithSideEffects(SIDE_EFFECT_REQUEST_COMMANDS.AUTHENTICATE_PUSHER, params) + return API.makeRequestWithSideEffects(SIDE_EFFECT_REQUEST_COMMANDS.AUTHENTICATE_PUSHER, params) .then((response) => { if (response?.jsonCode === CONST.JSON_CODE.NOT_AUTHENTICATED) { Log.hmmm('[PusherAuthorizer] Unable to authenticate Pusher because authToken is expired'); - callback(new Error('Pusher failed to authenticate because authToken is expired'), {auth: ''}); + callback?.(new Error('Pusher failed to authenticate because authToken is expired'), {auth: ''}); // Attempt to refresh the authToken then reconnect to Pusher reauthenticatePusher(); @@ -922,36 +922,27 @@ function authenticatePusher(socketID: string, channelName: string, callback?: Ch if (response?.jsonCode !== CONST.JSON_CODE.SUCCESS) { Log.hmmm('[PusherAuthorizer] Unable to authenticate Pusher for reason other than expired session'); - callback(new Error(`Pusher failed to authenticate because code: ${response?.jsonCode} message: ${response?.message}`), {auth: ''}); + callback?.(new Error(`Pusher failed to authenticate because code: ${response?.jsonCode} message: ${response?.message}`), {auth: ''}); return; } Log.info('[PusherAuthorizer] Pusher authenticated successfully', false, {channelName}); - callback(null, response as ChannelAuthorizationData); + if (callback) { + callback(null, response as ChannelAuthorizationData); + } else { + return { + auth: response.auth, + // eslint-disable-next-line @typescript-eslint/naming-convention + shared_secret: response.shared_secret, + }; + } }) .catch((error: unknown) => { Log.hmmm('[PusherAuthorizer] Unhandled error: ', {channelName, error}); - callback(new Error('AuthenticatePusher request failed'), {auth: ''}); + callback?.(new Error('AuthenticatePusher request failed'), {auth: ''}); }); } -async function onAuthorizer(channelName: string, socketId: string) { - Log.info('[PusherAuthorizer] Attempting to authorize Pusher', false, {channelName}); - - const params: AuthenticatePusherParams = { - // eslint-disable-next-line @typescript-eslint/naming-convention - socket_id: socketId, - // eslint-disable-next-line @typescript-eslint/naming-convention - channel_name: channelName, - shouldRetry: false, - forceNetworkRequest: true, - }; - - const response = await API.makeRequestWithSideEffects(SIDE_EFFECT_REQUEST_COMMANDS.AUTHENTICATE_PUSHER, params); - - return response; -} - /** * Request a new validation link / magic code to unlink an unvalidated secondary login from a primary login */ @@ -1353,5 +1344,4 @@ export { validateUserAndGetAccessiblePolicies, isUserOnPrivateDomain, resetSMSDeliveryFailureStatus, - onAuthorizer, }; From 879da1323d389bf352c8561a27c803d85ec8ece5 Mon Sep 17 00:00:00 2001 From: VickyStash Date: Mon, 10 Feb 2025 16:39:04 +0100 Subject: [PATCH 03/16] Fix channel resubscribing --- src/CONST.ts | 4 ++++ src/libs/Pusher/pusher/index.native.ts | 12 +++++++----- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/src/CONST.ts b/src/CONST.ts index 3f12dba51463..f8f65f67bd5c 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -1595,6 +1595,10 @@ const CONST = { DISCONNECTED: 'DISCONNECTED', RECONNECTING: 'RECONNECTING', }, + CHANNEL_STATUS: { + SUBSCRIBING: 'SUBSCRIBING', + SUBSCRIBED: 'SUBSCRIBED', + }, }, EMOJI_SPACER: 'SPACER', diff --git a/src/libs/Pusher/pusher/index.native.ts b/src/libs/Pusher/pusher/index.native.ts index 66e0399bf449..f5873842d002 100644 --- a/src/libs/Pusher/pusher/index.native.ts +++ b/src/libs/Pusher/pusher/index.native.ts @@ -76,6 +76,7 @@ let initPromise = new Promise((resolve) => { }); const eventsBoundToChannels = new Map) => void>>(); +const channels: Record> = {}; /** * Trigger each of the socket event callbacks with the event information @@ -224,9 +225,9 @@ function subscribe( } Log.info('[Pusher] Attempting to subscribe to channel', false, {channelName, eventName}); - const channel = getChannel(channelName); - if (!channel) { + if (!channels[channelName]) { + channels[channelName] = CONST.PUSHER.CHANNEL_STATUS.SUBSCRIBING; await socket.subscribe({ channelName, onEvent: (event) => { @@ -234,6 +235,7 @@ function subscribe( callback?.(event.data); }, onSubscriptionSucceeded: () => { + channels[channelName] = CONST.PUSHER.CHANNEL_STATUS.SUBSCRIBED; bindEventToChannel(channelName, eventName, eventCallback); resolve(); // When subscribing for the first time we register a success callback that can be @@ -243,6 +245,7 @@ function subscribe( onResubscribe(); }, onSubscriptionError: (name: string, message: string) => { + delete channels[channelName]; Log.hmmm('[Pusher] Issue authenticating with Pusher during subscribe attempt.', { channelName, message, @@ -281,6 +284,7 @@ function unsubscribe(channelName: string, eventName: PusherEventName = '') { } else { Log.info('[Pusher] Unsubscribing from channel', false, {channelName}); eventsBoundToChannels.delete(channelName); + delete channels[channelName]; socket?.unsubscribe({channelName}); } } @@ -293,9 +297,7 @@ function isAlreadySubscribing(channelName: string): boolean { return false; } - // const channel = getChannel(channelName); - // return channel ? channel.subscriptionPending : false; - return false; + return channels[channelName] === CONST.PUSHER.CHANNEL_STATUS.SUBSCRIBING; } /** From 5e95a4d0bfc0a262e8c5f93ec6da57d82895f4f6 Mon Sep 17 00:00:00 2001 From: VickyStash Date: Mon, 10 Feb 2025 17:32:54 +0100 Subject: [PATCH 04/16] Update Pusher folder structure --- src/libs/API/index.ts | 2 +- .../Navigation/AppNavigator/AuthScreens.tsx | 2 +- src/libs/Pusher/{pusher => }/index.native.ts | 87 +++------------ src/libs/Pusher/{pusher => }/index.ts | 76 +++---------- src/libs/Pusher/library/index.native.ts | 10 -- src/libs/Pusher/library/index.ts | 10 -- src/libs/Pusher/library/types.ts | 10 -- src/libs/Pusher/types.ts | 101 ++++++++++++++++++ src/libs/PusherConnectionManager.ts | 6 +- src/libs/PusherUtils.ts | 4 +- src/libs/actions/Report.ts | 2 +- src/libs/actions/Session/index.ts | 2 +- src/libs/actions/User.ts | 2 +- tests/utils/PusherHelper.ts | 2 +- tests/utils/TestHelper.ts | 2 +- 15 files changed, 146 insertions(+), 172 deletions(-) rename src/libs/Pusher/{pusher => }/index.native.ts (85%) rename src/libs/Pusher/{pusher => }/index.ts (88%) delete mode 100644 src/libs/Pusher/library/index.native.ts delete mode 100644 src/libs/Pusher/library/index.ts delete mode 100644 src/libs/Pusher/library/types.ts create mode 100644 src/libs/Pusher/types.ts diff --git a/src/libs/API/index.ts b/src/libs/API/index.ts index 1723d5cd55a0..3fa4ba64c377 100644 --- a/src/libs/API/index.ts +++ b/src/libs/API/index.ts @@ -5,7 +5,7 @@ import Log from '@libs/Log'; import {HandleUnusedOptimisticID, Logging, Pagination, Reauthentication, RecheckConnection, SaveResponseInOnyx} from '@libs/Middleware'; import {isOffline} from '@libs/Network/NetworkStore'; import {push as pushToSequentialQueue, waitForIdle as waitForSequentialQueueIdle} from '@libs/Network/SequentialQueue'; -import {getPusherSocketID} from '@libs/Pusher/pusher'; +import {getPusherSocketID} from '@libs/Pusher'; import {processWithMiddleware, use} from '@libs/Request'; import {getLength as getPersistedRequestsLength} from '@userActions/PersistedRequests'; import CONST from '@src/CONST'; diff --git a/src/libs/Navigation/AppNavigator/AuthScreens.tsx b/src/libs/Navigation/AppNavigator/AuthScreens.tsx index 9968251e226a..8120770f52d4 100644 --- a/src/libs/Navigation/AppNavigator/AuthScreens.tsx +++ b/src/libs/Navigation/AppNavigator/AuthScreens.tsx @@ -34,7 +34,7 @@ import type {AuthScreensParamList, CentralPaneName, CentralPaneScreensParamList} import {isOnboardingFlowName} from '@libs/NavigationUtils'; import NetworkConnection from '@libs/NetworkConnection'; import onyxSubscribe from '@libs/onyxSubscribe'; -import * as Pusher from '@libs/Pusher/pusher'; +import * as Pusher from '@libs/Pusher'; import PusherConnectionManager from '@libs/PusherConnectionManager'; import * as ReportUtils from '@libs/ReportUtils'; import * as SearchQueryUtils from '@libs/SearchQueryUtils'; diff --git a/src/libs/Pusher/pusher/index.native.ts b/src/libs/Pusher/index.native.ts similarity index 85% rename from src/libs/Pusher/pusher/index.native.ts rename to src/libs/Pusher/index.native.ts index f5873842d002..00c60726c345 100644 --- a/src/libs/Pusher/pusher/index.native.ts +++ b/src/libs/Pusher/index.native.ts @@ -3,57 +3,25 @@ import {Pusher} from '@pusher/pusher-websocket-react-native'; import isObject from 'lodash/isObject'; import {InteractionManager} from 'react-native'; import Onyx from 'react-native-onyx'; -import type {LiteralUnion, ValueOf} from 'type-fest'; +import type {ValueOf} from 'type-fest'; import Log from '@libs/Log'; -import TYPE from '@libs/Pusher/EventType'; -import type {SocketEventName} from '@libs/Pusher/library/types'; import {authenticatePusher} from '@userActions/Session'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {OnyxUpdatesFromServer, ReportUserIsTyping} from '@src/types/onyx'; -import type DeepValueOf from '@src/types/utils/DeepValueOf'; - -type States = { - previous: string; - current: string; -}; - -type Args = { - appKey: string; - cluster: string; - authEndpoint: string; -}; - -type UserIsTypingEvent = ReportUserIsTyping & { - userLogin?: string; -}; - -type UserIsLeavingRoomEvent = Record & { - userLogin?: string; -}; - -type PingPongEvent = Record & { - pingID: string; - pingTimestamp: number; -}; - -type PusherEventMap = { - [TYPE.USER_IS_TYPING]: UserIsTypingEvent; - [TYPE.USER_IS_LEAVING_ROOM]: UserIsLeavingRoomEvent; - [TYPE.PONG]: PingPongEvent; -}; - -type EventData = {chunk?: string; id?: string; index?: number; final?: boolean} & (EventName extends keyof PusherEventMap - ? PusherEventMap[EventName] - : OnyxUpdatesFromServer); - -type EventCallbackError = {type?: ValueOf; data: {code: number; message?: string}}; - -type ChunkedDataEvents = {chunks: unknown[]; receivedFinal: boolean}; - -type SocketEventCallback = (eventName: SocketEventName, data?: States | EventCallbackError) => void; - -type PusherEventName = LiteralUnion, string>; +import TYPE from './EventType'; +import type { + Args, + ChunkedDataEvents, + EventCallbackError, + EventData, + PingPongEvent, + PusherEventName, + SocketEventCallback, + SocketEventName, + States, + UserIsLeavingRoomEvent, + UserIsTypingEvent, +} from './types'; let shouldForceOffline = false; Onyx.connect({ @@ -338,15 +306,6 @@ function registerSocketEventCallback(cb: SocketEventCallback) { socketEventCallbacks.push(cb); } -/** - * A custom authorizer allows us to take a more fine-grained approach to - * authenticating Pusher. e.g. we can handle failed attempts to authorize - * with an expired authToken and retry the attempt. - */ -// function registerCustomAuthorizer(authorizer: ChannelAuthorizerGenerator) { -// customAuthorizer = authorizer; -// } - /** * Disconnect from Pusher */ @@ -383,20 +342,6 @@ function getPusherSocketID(): string | undefined { return pusherSocketID; } -export { - init, - subscribe, - unsubscribe, - getChannel, - isSubscribed, - isAlreadySubscribing, - sendEvent, - disconnect, - reconnect, - registerSocketEventCallback, - // registerCustomAuthorizer, - TYPE, - getPusherSocketID, -}; +export {init, subscribe, unsubscribe, getChannel, isSubscribed, isAlreadySubscribing, sendEvent, disconnect, reconnect, registerSocketEventCallback, TYPE, getPusherSocketID}; export type {EventCallbackError, States, UserIsTypingEvent, UserIsLeavingRoomEvent, PingPongEvent}; diff --git a/src/libs/Pusher/pusher/index.ts b/src/libs/Pusher/index.ts similarity index 88% rename from src/libs/Pusher/pusher/index.ts rename to src/libs/Pusher/index.ts index 6ca05e788189..22cf79352311 100644 --- a/src/libs/Pusher/pusher/index.ts +++ b/src/libs/Pusher/index.ts @@ -1,68 +1,26 @@ import isObject from 'lodash/isObject'; import type {Channel, ChannelAuthorizerGenerator, Options} from 'pusher-js/with-encryption'; +import Pusher from 'pusher-js/with-encryption'; import {InteractionManager} from 'react-native'; import Onyx from 'react-native-onyx'; -import type {LiteralUnion, ValueOf} from 'type-fest'; import Log from '@libs/Log'; -import TYPE from '@libs/Pusher/EventType'; -import Pusher from '@libs/Pusher/library'; -import type {SocketEventName} from '@libs/Pusher/library/types'; -import type CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {OnyxUpdatesFromServer, ReportUserIsTyping} from '@src/types/onyx'; -import type DeepValueOf from '@src/types/utils/DeepValueOf'; - -type States = { - previous: string; - current: string; -}; - -type Args = { - appKey: string; - cluster: string; - authEndpoint: string; -}; - -type UserIsTypingEvent = ReportUserIsTyping & { - userLogin?: string; -}; - -type UserIsLeavingRoomEvent = Record & { - userLogin?: string; -}; - -type PingPongEvent = Record & { - pingID: string; - pingTimestamp: number; -}; - -type PusherEventMap = { - [TYPE.USER_IS_TYPING]: UserIsTypingEvent; - [TYPE.USER_IS_LEAVING_ROOM]: UserIsLeavingRoomEvent; - [TYPE.PONG]: PingPongEvent; -}; - -type EventData = {chunk?: string; id?: string; index?: number; final?: boolean} & (EventName extends keyof PusherEventMap - ? PusherEventMap[EventName] - : OnyxUpdatesFromServer); - -type EventCallbackError = {type: ValueOf; data: {code: number; message?: string}}; - -type ChunkedDataEvents = {chunks: unknown[]; receivedFinal: boolean}; - -type SocketEventCallback = (eventName: SocketEventName, data?: States | EventCallbackError) => void; - -type PusherWithAuthParams = InstanceType & { - config: { - auth?: { - params?: unknown; - }; - }; -}; - -type PusherEventName = LiteralUnion, string>; - -type PusherSubscribtionErrorData = {type?: string; error?: string; status?: string}; +import TYPE from './EventType'; +import type { + Args, + ChunkedDataEvents, + EventCallbackError, + EventData, + PingPongEvent, + PusherEventName, + PusherSubscribtionErrorData, + PusherWithAuthParams, + SocketEventCallback, + SocketEventName, + States, + UserIsLeavingRoomEvent, + UserIsTypingEvent, +} from './types'; let shouldForceOffline = false; Onyx.connect({ diff --git a/src/libs/Pusher/library/index.native.ts b/src/libs/Pusher/library/index.native.ts deleted file mode 100644 index 4f11506f10fa..000000000000 --- a/src/libs/Pusher/library/index.native.ts +++ /dev/null @@ -1,10 +0,0 @@ -/** - * We use the pusher-js/react-native module to support pusher on native environments. - * @see: https://github.com/pusher/pusher-js - */ -import PusherImplementation from 'pusher-js/react-native'; -import type Pusher from './types'; - -const PusherNative: Pusher = PusherImplementation; - -export default PusherNative; diff --git a/src/libs/Pusher/library/index.ts b/src/libs/Pusher/library/index.ts deleted file mode 100644 index 6a7104a1d2a5..000000000000 --- a/src/libs/Pusher/library/index.ts +++ /dev/null @@ -1,10 +0,0 @@ -/** - * We use the standard pusher-js module to support pusher on web environments. - * @see: https://github.com/pusher/pusher-js - */ -import PusherImplementation from 'pusher-js/with-encryption'; -import type Pusher from './types'; - -const PusherWeb: Pusher = PusherImplementation; - -export default PusherWeb; diff --git a/src/libs/Pusher/library/types.ts b/src/libs/Pusher/library/types.ts deleted file mode 100644 index 566fd8a72774..000000000000 --- a/src/libs/Pusher/library/types.ts +++ /dev/null @@ -1,10 +0,0 @@ -import type PusherClass from 'pusher-js/with-encryption'; -import type {LiteralUnion} from 'type-fest'; - -type Pusher = typeof PusherClass; - -type SocketEventName = LiteralUnion<'error' | 'connected' | 'disconnected' | 'state_change', string>; - -export default Pusher; - -export type {SocketEventName}; diff --git a/src/libs/Pusher/types.ts b/src/libs/Pusher/types.ts new file mode 100644 index 000000000000..e07b0186f71e --- /dev/null +++ b/src/libs/Pusher/types.ts @@ -0,0 +1,101 @@ +import type {PusherChannel} from '@pusher/pusher-websocket-react-native'; +import type PusherClass from 'pusher-js/with-encryption'; +import type {Channel, ChannelAuthorizerGenerator} from 'pusher-js/with-encryption'; +import type {LiteralUnion, ValueOf} from 'type-fest'; +import type CONST from '@src/CONST'; +import type {OnyxUpdatesFromServer, ReportUserIsTyping} from '@src/types/onyx'; +import type DeepValueOf from '@src/types/utils/DeepValueOf'; +import type TYPE from './EventType'; + +type SocketEventName = LiteralUnion<'error' | 'connected' | 'disconnected' | 'state_change', string>; + +type States = { + previous: string; + current: string; +}; + +type Args = { + appKey: string; + cluster: string; + authEndpoint: string; +}; + +type UserIsTypingEvent = ReportUserIsTyping & { + userLogin?: string; +}; + +type UserIsLeavingRoomEvent = Record & { + userLogin?: string; +}; + +type PingPongEvent = Record & { + pingID: string; + pingTimestamp: number; +}; + +type PusherEventMap = { + [TYPE.USER_IS_TYPING]: UserIsTypingEvent; + [TYPE.USER_IS_LEAVING_ROOM]: UserIsLeavingRoomEvent; + [TYPE.PONG]: PingPongEvent; +}; + +type EventData = {chunk?: string; id?: string; index?: number; final?: boolean} & (EventName extends keyof PusherEventMap + ? PusherEventMap[EventName] + : OnyxUpdatesFromServer); + +type EventCallbackError = {type: ValueOf; data: {code: number; message?: string}}; + +type ChunkedDataEvents = {chunks: unknown[]; receivedFinal: boolean}; + +type SocketEventCallback = (eventName: SocketEventName, data?: States | EventCallbackError) => void; + +type PusherWithAuthParams = InstanceType & { + config: { + auth?: { + params?: unknown; + }; + }; +}; + +type PusherEventName = LiteralUnion, string>; + +type PusherSubscribtionErrorData = {type?: string; error?: string; status?: string}; + +type PusherModule = { + init: (args: Args) => Promise; + subscribe: ( + channelName: string, + eventName?: EventName, + eventCallback?: (data: EventData) => void, + onResubscribe?: () => void, + ) => Promise; + unsubscribe: (channelName: string, eventName?: PusherEventName) => void; + getChannel: (channelName: string) => Channel | PusherChannel | undefined; + isSubscribed: (channelName: string) => boolean; + isAlreadySubscribing: (channelName: string) => boolean; + sendEvent: (channelName: string, eventName: EventName, payload: EventData) => void; + disconnect: () => void; + reconnect: () => void; + registerSocketEventCallback: (cb: SocketEventCallback) => void; + registerCustomAuthorizer?: (authorizer: ChannelAuthorizerGenerator) => void; + getPusherSocketID: () => string | undefined; +}; + +export default PusherModule; + +export type { + SocketEventName, + States, + Args, + UserIsTypingEvent, + UserIsLeavingRoomEvent, + PingPongEvent, + PusherEventMap, + EventData, + EventCallbackError, + ChunkedDataEvents, + SocketEventCallback, + PusherWithAuthParams, + PusherEventName, + PusherSubscribtionErrorData, +}; diff --git a/src/libs/PusherConnectionManager.ts b/src/libs/PusherConnectionManager.ts index fbe6e3c5faf0..ee31c892045b 100644 --- a/src/libs/PusherConnectionManager.ts +++ b/src/libs/PusherConnectionManager.ts @@ -2,9 +2,9 @@ import type {ChannelAuthorizationCallback} from 'pusher-js/with-encryption'; import CONST from '@src/CONST'; import {authenticatePusher} from './actions/Session'; import Log from './Log'; -import type {SocketEventName} from './Pusher/library/types'; -import {reconnect, registerCustomAuthorizer, registerSocketEventCallback} from './Pusher/pusher'; -import type {EventCallbackError, States} from './Pusher/pusher'; +import {reconnect, registerCustomAuthorizer, registerSocketEventCallback} from './Pusher'; +import type {EventCallbackError, States} from './Pusher'; +import type {SocketEventName} from './Pusher/types'; function init() { /** diff --git a/src/libs/PusherUtils.ts b/src/libs/PusherUtils.ts index 547aa06e770e..50778160dbfa 100644 --- a/src/libs/PusherUtils.ts +++ b/src/libs/PusherUtils.ts @@ -4,8 +4,8 @@ import CONST from '@src/CONST'; import type {OnyxUpdatesFromServer} from '@src/types/onyx'; import Log from './Log'; import NetworkConnection from './NetworkConnection'; -import {subscribe} from './Pusher/pusher'; -import type {PingPongEvent} from './Pusher/pusher'; +import {subscribe} from './Pusher'; +import type {PingPongEvent} from './Pusher'; type Callback = (data: OnyxUpdate[]) => Promise; diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index bc051f1472bb..9de0c193124f 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -79,7 +79,7 @@ import * as PhoneNumber from '@libs/PhoneNumber'; import getPolicyEmployeeAccountIDs from '@libs/PolicyEmployeeListUtils'; import {extractPolicyIDFromPath, getPolicy} from '@libs/PolicyUtils'; import processReportIDDeeplink from '@libs/processReportIDDeeplink'; -import * as Pusher from '@libs/Pusher/pusher'; +import * as Pusher from '@libs/Pusher'; import * as ReportActionsUtils from '@libs/ReportActionsUtils'; import type {OptimisticAddCommentReportAction, OptimisticChatReport} from '@libs/ReportUtils'; import { diff --git a/src/libs/actions/Session/index.ts b/src/libs/actions/Session/index.ts index 5d7757524127..60c169df433f 100644 --- a/src/libs/actions/Session/index.ts +++ b/src/libs/actions/Session/index.ts @@ -36,7 +36,7 @@ import * as MainQueue from '@libs/Network/MainQueue'; import * as NetworkStore from '@libs/Network/NetworkStore'; import {getCurrentUserEmail} from '@libs/Network/NetworkStore'; import NetworkConnection from '@libs/NetworkConnection'; -import * as Pusher from '@libs/Pusher/pusher'; +import * as Pusher from '@libs/Pusher'; import {getReportIDFromLink, parseReportRouteParams as parseReportRouteParamsReportUtils} from '@libs/ReportUtils'; import * as SessionUtils from '@libs/SessionUtils'; import {clearSoundAssetsCache} from '@libs/Sound'; diff --git a/src/libs/actions/User.ts b/src/libs/actions/User.ts index ffcbc813477b..8ed2506336bf 100644 --- a/src/libs/actions/User.ts +++ b/src/libs/actions/User.ts @@ -32,7 +32,7 @@ import {isOffline} from '@libs/Network/NetworkStore'; import * as SequentialQueue from '@libs/Network/SequentialQueue'; import * as NumberUtils from '@libs/NumberUtils'; import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils'; -import * as Pusher from '@libs/Pusher/pusher'; +import * as Pusher from '@libs/Pusher'; import PusherUtils from '@libs/PusherUtils'; import * as ReportActionsUtils from '@libs/ReportActionsUtils'; import * as ReportUtils from '@libs/ReportUtils'; diff --git a/tests/utils/PusherHelper.ts b/tests/utils/PusherHelper.ts index 8547c25b1235..3fe49b979c38 100644 --- a/tests/utils/PusherHelper.ts +++ b/tests/utils/PusherHelper.ts @@ -1,6 +1,6 @@ +import * as Pusher from '@libs/Pusher'; import CONFIG from '@src/CONFIG'; import CONST from '@src/CONST'; -import * as Pusher from '@src/libs/Pusher/pusher'; import PusherConnectionManager from '@src/libs/PusherConnectionManager'; import type {OnyxServerUpdate} from '@src/types/onyx/OnyxUpdatesFromServer'; import asMutable from '@src/types/utils/asMutable'; diff --git a/tests/utils/TestHelper.ts b/tests/utils/TestHelper.ts index f3814d4b91cb..42763a3741b7 100644 --- a/tests/utils/TestHelper.ts +++ b/tests/utils/TestHelper.ts @@ -5,7 +5,7 @@ import Onyx from 'react-native-onyx'; import type {ConnectOptions} from 'react-native-onyx/dist/types'; import type {ApiCommand, ApiRequestCommandParameters} from '@libs/API/types'; import * as Localize from '@libs/Localize'; -import * as Pusher from '@libs/Pusher/pusher'; +import * as Pusher from '@libs/Pusher'; import PusherConnectionManager from '@libs/PusherConnectionManager'; import CONFIG from '@src/CONFIG'; import CONST from '@src/CONST'; From 0efbb8f9292e6514b3b22f2fcbc79965a8e80d57 Mon Sep 17 00:00:00 2001 From: VickyStash Date: Tue, 11 Feb 2025 10:29:12 +0100 Subject: [PATCH 05/16] TS fixes --- src/libs/Pusher/index.native.ts | 10 +++++----- src/libs/Pusher/types.ts | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/libs/Pusher/index.native.ts b/src/libs/Pusher/index.native.ts index 00c60726c345..2b4dd15efc33 100644 --- a/src/libs/Pusher/index.native.ts +++ b/src/libs/Pusher/index.native.ts @@ -1,4 +1,4 @@ -import type {PusherChannel} from '@pusher/pusher-websocket-react-native'; +import type {PusherAuthorizerResult, PusherChannel} from '@pusher/pusher-websocket-react-native'; import {Pusher} from '@pusher/pusher-websocket-react-native'; import isObject from 'lodash/isObject'; import {InteractionManager} from 'react-native'; @@ -43,7 +43,7 @@ let initPromise = new Promise((resolve) => { resolveInitPromise = resolve; }); -const eventsBoundToChannels = new Map) => void>>(); +const eventsBoundToChannels = new Map) => void>>(); const channels: Record> = {}; /** @@ -82,8 +82,8 @@ function init(args: Args): Promise { } callSocketEventCallbacks('state_change', {previous: previousState, current: currentState}); }, - onError: (message: string, code: number) => callSocketEventCallbacks('error', {data: {code, message}}), - onAuthorizer: (channelName: string, socketId: string) => authenticatePusher(socketId, channelName), + onError: (message: string) => callSocketEventCallbacks('error', {data: {message}}), + onAuthorizer: (channelName: string, socketId: string) => authenticatePusher(socketId, channelName) as Promise, }); await socket.connect(); }).then(resolveInitPromise); @@ -200,7 +200,7 @@ function subscribe( channelName, onEvent: (event) => { const callback = eventsBoundToChannels.get(event.channelName)?.get(event.eventName); - callback?.(event.data); + callback?.(event.data as EventData); }, onSubscriptionSucceeded: () => { channels[channelName] = CONST.PUSHER.CHANNEL_STATUS.SUBSCRIBED; diff --git a/src/libs/Pusher/types.ts b/src/libs/Pusher/types.ts index e07b0186f71e..88348e1c4bad 100644 --- a/src/libs/Pusher/types.ts +++ b/src/libs/Pusher/types.ts @@ -43,7 +43,7 @@ type EventData = {chunk?: string; id?: string; index?: ? PusherEventMap[EventName] : OnyxUpdatesFromServer); -type EventCallbackError = {type: ValueOf; data: {code: number; message?: string}}; +type EventCallbackError = {type?: ValueOf; data: {code?: number; message?: string}}; type ChunkedDataEvents = {chunks: unknown[]; receivedFinal: boolean}; From 8385b859b8da3b6499c166eb1d579504e7225ee7 Mon Sep 17 00:00:00 2001 From: VickyStash Date: Tue, 11 Feb 2025 11:27:42 +0100 Subject: [PATCH 06/16] Code improvements --- src/libs/Pusher/index.native.ts | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/libs/Pusher/index.native.ts b/src/libs/Pusher/index.native.ts index 2b4dd15efc33..93da14987e26 100644 --- a/src/libs/Pusher/index.native.ts +++ b/src/libs/Pusher/index.native.ts @@ -58,15 +58,14 @@ function callSocketEventCallbacks(eventName: SocketEventName, data?: EventCallba * @returns resolves when Pusher has connected */ function init(args: Args): Promise { - // eslint-disable-next-line @lwc/lwc/no-async-await - return new Promise(async (resolve) => { + return new Promise((resolve) => { if (socket) { resolve(); return; } socket = Pusher.getInstance(); - await socket.init({ + socket.init({ apiKey: args.appKey, cluster: args.cluster, onConnectionStateChange: (currentState: string, previousState: string) => { @@ -85,7 +84,7 @@ function init(args: Args): Promise { onError: (message: string) => callSocketEventCallbacks('error', {data: {message}}), onAuthorizer: (channelName: string, socketId: string) => authenticatePusher(socketId, channelName) as Promise, }); - await socket.connect(); + socket.connect(); }).then(resolveInitPromise); } @@ -170,7 +169,7 @@ function bindEventToChannel(channel: string, eventsBoundToChannels.set(channel, new Map()); } - eventsBoundToChannels.get(channel)?.set(eventName, callback); + eventsBoundToChannels.get(channel)?.set(eventName, callback as (eventData: EventData) => void); } /** @@ -185,7 +184,7 @@ function subscribe( return initPromise.then( () => new Promise((resolve, reject) => { - InteractionManager.runAfterInteractions(async () => { + InteractionManager.runAfterInteractions(() => { // We cannot call subscribe() before init(). Prevent any attempt to do this on dev. if (!socket) { throw new Error(`[Pusher] instance not found. Pusher.subscribe() @@ -196,7 +195,7 @@ function subscribe( if (!channels[channelName]) { channels[channelName] = CONST.PUSHER.CHANNEL_STATUS.SUBSCRIBING; - await socket.subscribe({ + socket.subscribe({ channelName, onEvent: (event) => { const callback = eventsBoundToChannels.get(event.channelName)?.get(event.eventName); From 0a6be56796a3b9614c0b48ad3ded30eef6ab7a88 Mon Sep 17 00:00:00 2001 From: VickyStash Date: Tue, 11 Feb 2025 11:54:24 +0100 Subject: [PATCH 07/16] Lint fixes --- tests/utils/PusherHelper.ts | 19 ++++++++++--------- tests/utils/TestHelper.ts | 8 ++++---- 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/tests/utils/PusherHelper.ts b/tests/utils/PusherHelper.ts index 3fe49b979c38..553ded9010f9 100644 --- a/tests/utils/PusherHelper.ts +++ b/tests/utils/PusherHelper.ts @@ -1,9 +1,8 @@ -import * as Pusher from '@libs/Pusher'; +import {getChannel, init, TYPE, unsubscribe} from '@libs/Pusher'; import CONFIG from '@src/CONFIG'; import CONST from '@src/CONST'; import PusherConnectionManager from '@src/libs/PusherConnectionManager'; import type {OnyxServerUpdate} from '@src/types/onyx/OnyxUpdatesFromServer'; -import asMutable from '@src/types/utils/asMutable'; const CHANNEL_NAME = `${CONST.PUSHER.PRIVATE_USER_CHANNEL_PREFIX}1${CONFIG.PUSHER.SUFFIX}`; @@ -12,12 +11,14 @@ function setup() { // channel already in a subscribed state. These methods are normally used to prevent // duplicated subscriptions, but we don't need them for this test so forcing them to // return false will make the testing less complex. - asMutable(Pusher).isSubscribed = jest.fn().mockReturnValue(false); - asMutable(Pusher).isAlreadySubscribing = jest.fn().mockReturnValue(false); + jest.mock('@libs/Pusher', () => ({ + isSubscribed: jest.fn().mockReturnValue(false), + isAlreadySubscribing: jest.fn().mockReturnValue(false), + })); // Connect to Pusher PusherConnectionManager.init(); - Pusher.init({ + init({ appKey: CONFIG.PUSHER.APP_KEY, cluster: CONFIG.PUSHER.CLUSTER, authEndpoint: `${CONFIG.EXPENSIFY.DEFAULT_API_ROOT}api/AuthenticatePusher?`, @@ -27,14 +28,14 @@ function setup() { } function emitOnyxUpdate(args: OnyxServerUpdate[]) { - const channel = Pusher.getChannel(CHANNEL_NAME); - channel?.emit(Pusher.TYPE.MULTIPLE_EVENTS, { + const channel = getChannel(CHANNEL_NAME); + channel?.emit(TYPE.MULTIPLE_EVENTS, { type: 'pusher', lastUpdateID: null, previousUpdateID: null, updates: [ { - eventType: Pusher.TYPE.MULTIPLE_EVENT_TYPE.ONYX_API_UPDATE, + eventType: TYPE.MULTIPLE_EVENT_TYPE.ONYX_API_UPDATE, data: args, }, ], @@ -44,7 +45,7 @@ function emitOnyxUpdate(args: OnyxServerUpdate[]) { function teardown() { // Unsubscribe from account channel after each test since we subscribe in the function // subscribeToUserEvents and we don't want duplicate event subscriptions. - Pusher.unsubscribe(CHANNEL_NAME); + unsubscribe(CHANNEL_NAME); } export default { diff --git a/tests/utils/TestHelper.ts b/tests/utils/TestHelper.ts index 42763a3741b7..edc06433e061 100644 --- a/tests/utils/TestHelper.ts +++ b/tests/utils/TestHelper.ts @@ -4,8 +4,8 @@ import {Linking} from 'react-native'; import Onyx from 'react-native-onyx'; import type {ConnectOptions} from 'react-native-onyx/dist/types'; import type {ApiCommand, ApiRequestCommandParameters} from '@libs/API/types'; -import * as Localize from '@libs/Localize'; -import * as Pusher from '@libs/Pusher'; +import {translateLocal} from '@libs/Localize'; +import {init} from '@libs/Pusher'; import PusherConnectionManager from '@libs/PusherConnectionManager'; import CONFIG from '@src/CONFIG'; import CONST from '@src/CONST'; @@ -47,7 +47,7 @@ function setupApp() { // Connect to Pusher PusherConnectionManager.init(); - Pusher.init({ + init({ appKey: CONFIG.PUSHER.APP_KEY, cluster: CONFIG.PUSHER.CLUSTER, authEndpoint: `${CONFIG.EXPENSIFY.DEFAULT_API_ROOT}api/AuthenticatePusher?`, @@ -330,7 +330,7 @@ function assertFormDataMatchesObject(obj: Report, formData?: FormData) { } async function navigateToSidebarOption(index: number): Promise { - const hintText = Localize.translateLocal('accessibilityHints.navigatesToChat'); + const hintText = translateLocal('accessibilityHints.navigatesToChat'); const optionRow = screen.queryAllByAccessibilityHint(hintText).at(index); if (!optionRow) { return; From 625df9ac8b85aba34bc4a692f00227b4a1d342f9 Mon Sep 17 00:00:00 2001 From: VickyStash Date: Wed, 12 Feb 2025 12:27:42 +0100 Subject: [PATCH 08/16] Add pusher-websocket-react-native mock to fix tests --- .../pusher-websocket-react-native/index.ts | 72 +++++++++++++++++++ src/libs/Pusher/index.native.ts | 11 ++- tests/utils/PusherHelper.ts | 19 +++-- 3 files changed, 90 insertions(+), 12 deletions(-) create mode 100644 __mocks__/@pusher/pusher-websocket-react-native/index.ts diff --git a/__mocks__/@pusher/pusher-websocket-react-native/index.ts b/__mocks__/@pusher/pusher-websocket-react-native/index.ts new file mode 100644 index 000000000000..093cf7c0eceb --- /dev/null +++ b/__mocks__/@pusher/pusher-websocket-react-native/index.ts @@ -0,0 +1,72 @@ +import type {PusherEvent} from '@pusher/pusher-websocket-react-native'; +import {EventEmitter} from 'events'; +import CONST from '@src/CONST'; + +type OnSubscriptionSucceeded = () => void; +type OnEvent = (event: PusherEvent) => void; + +type ChannelCallbacks = { + onSubscriptionSucceeded: OnSubscriptionSucceeded; + onEvent: OnEvent; +}; + +class MockPusher extends EventEmitter { + static instance: MockPusher | null = null; + + channels = new Map(); + + socketId = 'mock-socket-id'; + + connectionState = 'DISCONNECTED'; + + static getInstance() { + if (!MockPusher.instance) { + MockPusher.instance = new MockPusher(); + } + return MockPusher.instance; + } + + init({onConnectionStateChange}: {onConnectionStateChange: (currentState: string, previousState: string) => void}) { + onConnectionStateChange(CONST.PUSHER.STATE.CONNECTED, CONST.PUSHER.STATE.DISCONNECTED); + return Promise.resolve(); + } + + connect() { + this.connectionState = CONST.PUSHER.STATE.CONNECTED; + return Promise.resolve(); + } + + disconnect() { + this.connectionState = CONST.PUSHER.STATE.DISCONNECTED; + return Promise.resolve(); + } + + subscribe({channelName, onEvent, onSubscriptionSucceeded}: {channelName: string; onEvent: OnEvent; onSubscriptionSucceeded: OnSubscriptionSucceeded}) { + if (!this.channels.has(channelName)) { + this.channels.set(channelName, {onEvent, onSubscriptionSucceeded}); + onSubscriptionSucceeded(); + } + return Promise.resolve(); + } + + unsubscribe({channelName}: {channelName: string}) { + this.channels.delete(channelName); + } + + trigger({channelName, eventName, data}: PusherEvent) { + this.channels.get(channelName)?.onSubscriptionSucceeded(); + this.channels.get(channelName)?.onEvent({channelName, eventName, data: JSON.stringify(data)}); + } + + getChannel(channelName: string) { + return this.channels.get(channelName); + } + + // eslint-disable-next-line @typescript-eslint/require-await,@lwc/lwc/no-async-await + async getSocketId() { + return this.socketId; + } +} + +// eslint-disable-next-line import/prefer-default-export +export {MockPusher as Pusher}; diff --git a/src/libs/Pusher/index.native.ts b/src/libs/Pusher/index.native.ts index 93da14987e26..c95bf9e7691b 100644 --- a/src/libs/Pusher/index.native.ts +++ b/src/libs/Pusher/index.native.ts @@ -275,8 +275,7 @@ function isSubscribed(channelName: string): boolean { return false; } - const channel = getChannel(channelName); - return !!channel; + return channels[channelName] === CONST.PUSHER.CHANNEL_STATUS.SUBSCRIBED; } /** @@ -341,6 +340,14 @@ function getPusherSocketID(): string | undefined { return pusherSocketID; } +if (window) { + /** + * Pusher socket for debugging purposes + */ + // @ts-expect-error type mismatch to be fixed + window.getPusherInstance = () => socket; +} + export {init, subscribe, unsubscribe, getChannel, isSubscribed, isAlreadySubscribing, sendEvent, disconnect, reconnect, registerSocketEventCallback, TYPE, getPusherSocketID}; export type {EventCallbackError, States, UserIsTypingEvent, UserIsLeavingRoomEvent, PingPongEvent}; diff --git a/tests/utils/PusherHelper.ts b/tests/utils/PusherHelper.ts index 553ded9010f9..5590acca75f0 100644 --- a/tests/utils/PusherHelper.ts +++ b/tests/utils/PusherHelper.ts @@ -1,8 +1,10 @@ -import {getChannel, init, TYPE, unsubscribe} from '@libs/Pusher'; +import {init, sendEvent, TYPE, unsubscribe} from '@libs/Pusher'; +import * as Pusher from '@libs/Pusher'; import CONFIG from '@src/CONFIG'; import CONST from '@src/CONST'; import PusherConnectionManager from '@src/libs/PusherConnectionManager'; import type {OnyxServerUpdate} from '@src/types/onyx/OnyxUpdatesFromServer'; +import asMutable from '@src/types/utils/asMutable'; const CHANNEL_NAME = `${CONST.PUSHER.PRIVATE_USER_CHANNEL_PREFIX}1${CONFIG.PUSHER.SUFFIX}`; @@ -11,10 +13,8 @@ function setup() { // channel already in a subscribed state. These methods are normally used to prevent // duplicated subscriptions, but we don't need them for this test so forcing them to // return false will make the testing less complex. - jest.mock('@libs/Pusher', () => ({ - isSubscribed: jest.fn().mockReturnValue(false), - isAlreadySubscribing: jest.fn().mockReturnValue(false), - })); + asMutable(Pusher).isSubscribed = jest.fn().mockReturnValue(false); + asMutable(Pusher).isAlreadySubscribing = jest.fn().mockReturnValue(false); // Connect to Pusher PusherConnectionManager.init(); @@ -24,15 +24,14 @@ function setup() { authEndpoint: `${CONFIG.EXPENSIFY.DEFAULT_API_ROOT}api/AuthenticatePusher?`, }); - window.getPusherInstance()?.connection.emit('connected'); + window.getPusherInstance()?.connection?.emit('connected'); } function emitOnyxUpdate(args: OnyxServerUpdate[]) { - const channel = getChannel(CHANNEL_NAME); - channel?.emit(TYPE.MULTIPLE_EVENTS, { + sendEvent(CHANNEL_NAME, TYPE.MULTIPLE_EVENTS, { type: 'pusher', - lastUpdateID: null, - previousUpdateID: null, + lastUpdateID: 0, + previousUpdateID: 0, updates: [ { eventType: TYPE.MULTIPLE_EVENT_TYPE.ONYX_API_UPDATE, From 5cd43fa970e1b738535f06b4d6d2b5ff0b6020c1 Mon Sep 17 00:00:00 2001 From: VickyStash Date: Wed, 12 Feb 2025 14:36:21 +0100 Subject: [PATCH 09/16] Code clean-up --- .../pusher-websocket-react-native/index.ts | 39 ++++++++++++------- tests/utils/PusherHelper.ts | 6 +-- 2 files changed, 27 insertions(+), 18 deletions(-) diff --git a/__mocks__/@pusher/pusher-websocket-react-native/index.ts b/__mocks__/@pusher/pusher-websocket-react-native/index.ts index 093cf7c0eceb..e7b4ccad34ef 100644 --- a/__mocks__/@pusher/pusher-websocket-react-native/index.ts +++ b/__mocks__/@pusher/pusher-websocket-react-native/index.ts @@ -3,6 +3,7 @@ import {EventEmitter} from 'events'; import CONST from '@src/CONST'; type OnSubscriptionSucceeded = () => void; + type OnEvent = (event: PusherEvent) => void; type ChannelCallbacks = { @@ -10,23 +11,35 @@ type ChannelCallbacks = { onEvent: OnEvent; }; -class MockPusher extends EventEmitter { - static instance: MockPusher | null = null; +type InitProps = { + onConnectionStateChange: (currentState: string, previousState: string) => void; +}; + +type SubscribeProps = { + channelName: string; + onEvent: OnEvent; + onSubscriptionSucceeded: OnSubscriptionSucceeded; +}; + +type UnsubscribeProps = {channelName: string}; + +class MockedPusher extends EventEmitter { + static instance: MockedPusher | null = null; channels = new Map(); socketId = 'mock-socket-id'; - connectionState = 'DISCONNECTED'; + connectionState: string = CONST.PUSHER.STATE.DISCONNECTED; static getInstance() { - if (!MockPusher.instance) { - MockPusher.instance = new MockPusher(); + if (!MockedPusher.instance) { + MockedPusher.instance = new MockedPusher(); } - return MockPusher.instance; + return MockedPusher.instance; } - init({onConnectionStateChange}: {onConnectionStateChange: (currentState: string, previousState: string) => void}) { + init({onConnectionStateChange}: InitProps) { onConnectionStateChange(CONST.PUSHER.STATE.CONNECTED, CONST.PUSHER.STATE.DISCONNECTED); return Promise.resolve(); } @@ -41,7 +54,7 @@ class MockPusher extends EventEmitter { return Promise.resolve(); } - subscribe({channelName, onEvent, onSubscriptionSucceeded}: {channelName: string; onEvent: OnEvent; onSubscriptionSucceeded: OnSubscriptionSucceeded}) { + subscribe({channelName, onEvent, onSubscriptionSucceeded}: SubscribeProps) { if (!this.channels.has(channelName)) { this.channels.set(channelName, {onEvent, onSubscriptionSucceeded}); onSubscriptionSucceeded(); @@ -49,12 +62,11 @@ class MockPusher extends EventEmitter { return Promise.resolve(); } - unsubscribe({channelName}: {channelName: string}) { + unsubscribe({channelName}: UnsubscribeProps) { this.channels.delete(channelName); } trigger({channelName, eventName, data}: PusherEvent) { - this.channels.get(channelName)?.onSubscriptionSucceeded(); this.channels.get(channelName)?.onEvent({channelName, eventName, data: JSON.stringify(data)}); } @@ -62,11 +74,10 @@ class MockPusher extends EventEmitter { return this.channels.get(channelName); } - // eslint-disable-next-line @typescript-eslint/require-await,@lwc/lwc/no-async-await - async getSocketId() { - return this.socketId; + getSocketId() { + return Promise.resolve(this.socketId); } } // eslint-disable-next-line import/prefer-default-export -export {MockPusher as Pusher}; +export {MockedPusher as Pusher}; diff --git a/tests/utils/PusherHelper.ts b/tests/utils/PusherHelper.ts index 5590acca75f0..3ffc7e5fb746 100644 --- a/tests/utils/PusherHelper.ts +++ b/tests/utils/PusherHelper.ts @@ -1,10 +1,8 @@ import {init, sendEvent, TYPE, unsubscribe} from '@libs/Pusher'; -import * as Pusher from '@libs/Pusher'; import CONFIG from '@src/CONFIG'; import CONST from '@src/CONST'; import PusherConnectionManager from '@src/libs/PusherConnectionManager'; import type {OnyxServerUpdate} from '@src/types/onyx/OnyxUpdatesFromServer'; -import asMutable from '@src/types/utils/asMutable'; const CHANNEL_NAME = `${CONST.PUSHER.PRIVATE_USER_CHANNEL_PREFIX}1${CONFIG.PUSHER.SUFFIX}`; @@ -13,8 +11,8 @@ function setup() { // channel already in a subscribed state. These methods are normally used to prevent // duplicated subscriptions, but we don't need them for this test so forcing them to // return false will make the testing less complex. - asMutable(Pusher).isSubscribed = jest.fn().mockReturnValue(false); - asMutable(Pusher).isAlreadySubscribing = jest.fn().mockReturnValue(false); + jest.spyOn(require('@libs/Pusher'), 'isSubscribed').mockReturnValue(false); + jest.spyOn(require('@libs/Pusher'), 'isAlreadySubscribing').mockReturnValue(false); // Connect to Pusher PusherConnectionManager.init(); From ea403e036d2d9bea0e331a7c916368bf0442e42f Mon Sep 17 00:00:00 2001 From: VickyStash Date: Wed, 12 Feb 2025 15:04:45 +0100 Subject: [PATCH 10/16] Minor fixes --- __mocks__/@pusher/pusher-websocket-react-native/index.ts | 4 ++-- src/libs/Pusher/index.native.ts | 5 +++-- src/types/modules/pusher.d.ts | 3 ++- tests/utils/PusherHelper.ts | 5 ++++- 4 files changed, 11 insertions(+), 6 deletions(-) diff --git a/__mocks__/@pusher/pusher-websocket-react-native/index.ts b/__mocks__/@pusher/pusher-websocket-react-native/index.ts index e7b4ccad34ef..dd0ea1592dec 100644 --- a/__mocks__/@pusher/pusher-websocket-react-native/index.ts +++ b/__mocks__/@pusher/pusher-websocket-react-native/index.ts @@ -1,5 +1,4 @@ import type {PusherEvent} from '@pusher/pusher-websocket-react-native'; -import {EventEmitter} from 'events'; import CONST from '@src/CONST'; type OnSubscriptionSucceeded = () => void; @@ -23,7 +22,7 @@ type SubscribeProps = { type UnsubscribeProps = {channelName: string}; -class MockedPusher extends EventEmitter { +class MockedPusher { static instance: MockedPusher | null = null; channels = new Map(); @@ -51,6 +50,7 @@ class MockedPusher extends EventEmitter { disconnect() { this.connectionState = CONST.PUSHER.STATE.DISCONNECTED; + this.channels.clear(); return Promise.resolve(); } diff --git a/src/libs/Pusher/index.native.ts b/src/libs/Pusher/index.native.ts index c95bf9e7691b..4348e81964f0 100644 --- a/src/libs/Pusher/index.native.ts +++ b/src/libs/Pusher/index.native.ts @@ -44,7 +44,7 @@ let initPromise = new Promise((resolve) => { }); const eventsBoundToChannels = new Map) => void>>(); -const channels: Record> = {}; +let channels: Record> = {}; /** * Trigger each of the socket event callbacks with the event information @@ -246,6 +246,7 @@ function unsubscribe(channelName: string, eventName: PusherEventName = '') { if (eventsBoundToChannels.get(channelName)?.size === 0) { Log.info(`[Pusher] After unbinding ${eventName} from channel ${channelName}, no other events were bound to that channel. Unsubscribing...`, false); eventsBoundToChannels.delete(channelName); + delete channels[channelName]; socket?.unsubscribe({channelName}); } } else { @@ -316,6 +317,7 @@ function disconnect() { socket.disconnect(); socket = null; pusherSocketID = ''; + channels = {}; eventsBoundToChannels.clear(); initPromise = new Promise((resolve) => { resolveInitPromise = resolve; @@ -344,7 +346,6 @@ if (window) { /** * Pusher socket for debugging purposes */ - // @ts-expect-error type mismatch to be fixed window.getPusherInstance = () => socket; } diff --git a/src/types/modules/pusher.d.ts b/src/types/modules/pusher.d.ts index ffcf7744773a..e0070ac28783 100644 --- a/src/types/modules/pusher.d.ts +++ b/src/types/modules/pusher.d.ts @@ -1,9 +1,10 @@ +import type {Pusher as MobilePusher} from '@pusher/pusher-websocket-react-native'; import type Pusher from 'pusher-js/types/src/core/pusher'; declare global { // eslint-disable-next-line @typescript-eslint/consistent-type-definitions interface Window { - getPusherInstance: () => Pusher | null; + getPusherInstance: () => Pusher | MobilePusher | null; } // eslint-disable-next-line @typescript-eslint/consistent-type-definitions diff --git a/tests/utils/PusherHelper.ts b/tests/utils/PusherHelper.ts index 3ffc7e5fb746..43e5e30f54aa 100644 --- a/tests/utils/PusherHelper.ts +++ b/tests/utils/PusherHelper.ts @@ -22,7 +22,10 @@ function setup() { authEndpoint: `${CONFIG.EXPENSIFY.DEFAULT_API_ROOT}api/AuthenticatePusher?`, }); - window.getPusherInstance()?.connection?.emit('connected'); + const pusher = window.getPusherInstance(); + if (pusher && 'connection' in pusher) { + pusher.connection?.emit('connected'); + } } function emitOnyxUpdate(args: OnyxServerUpdate[]) { From 7cf37cd5f65afb40aff153d7645bdff50e149663 Mon Sep 17 00:00:00 2001 From: VickyStash Date: Wed, 12 Feb 2025 15:07:00 +0100 Subject: [PATCH 11/16] Remove unnecessary mock --- __mocks__/pusher-js/react-native.ts | 9 --------- 1 file changed, 9 deletions(-) delete mode 100644 __mocks__/pusher-js/react-native.ts diff --git a/__mocks__/pusher-js/react-native.ts b/__mocks__/pusher-js/react-native.ts deleted file mode 100644 index 1edec34ffb14..000000000000 --- a/__mocks__/pusher-js/react-native.ts +++ /dev/null @@ -1,9 +0,0 @@ -import {PusherMock} from 'pusher-js-mock'; - -class PusherMockWithDisconnect extends PusherMock { - disconnect() { - return jest.fn(); - } -} - -export default PusherMockWithDisconnect; From 863a59a6d702a124a7b880b6068c2744c3d3b3c4 Mon Sep 17 00:00:00 2001 From: VickyStash Date: Wed, 12 Feb 2025 15:18:34 +0100 Subject: [PATCH 12/16] Remove unnecessary pusher-js-mock lib --- package-lock.json | 9 --------- package.json | 1 - 2 files changed, 10 deletions(-) diff --git a/package-lock.json b/package-lock.json index af3257de8f0b..3158e267d2ad 100644 --- a/package-lock.json +++ b/package-lock.json @@ -255,7 +255,6 @@ "peggy": "^4.0.3", "portfinder": "^1.0.28", "prettier": "^2.8.8", - "pusher-js-mock": "^0.3.3", "react-compiler-healthcheck": "^19.0.0-beta-8a03594-20241020", "react-compiler-runtime": "^19.0.0-beta-8a03594-20241020", "react-is": "^18.3.1", @@ -31460,14 +31459,6 @@ "tweetnacl": "^1.0.3" } }, - "node_modules/pusher-js-mock": { - "version": "0.3.8", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4.2.4" - } - }, "node_modules/qrcode": { "version": "1.5.4", "resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.4.tgz", diff --git a/package.json b/package.json index a5286e115de3..1256b13a7b23 100644 --- a/package.json +++ b/package.json @@ -322,7 +322,6 @@ "peggy": "^4.0.3", "portfinder": "^1.0.28", "prettier": "^2.8.8", - "pusher-js-mock": "^0.3.3", "react-compiler-healthcheck": "^19.0.0-beta-8a03594-20241020", "react-compiler-runtime": "^19.0.0-beta-8a03594-20241020", "react-is": "^18.3.1", From dbd04210fc8b59441dcf9b2ef1d42deecbe0f529 Mon Sep 17 00:00:00 2001 From: VickyStash Date: Wed, 12 Feb 2025 15:51:32 +0100 Subject: [PATCH 13/16] Update Pusher module export since now it's platform-specific --- src/libs/API/index.ts | 4 +-- .../Navigation/AppNavigator/AuthScreens.tsx | 2 +- src/libs/Pusher/index.native.ts | 34 ++++++++++--------- src/libs/Pusher/index.ts | 8 ++--- src/libs/Pusher/types.ts | 1 + src/libs/PusherConnectionManager.ts | 11 +++--- src/libs/PusherUtils.ts | 6 ++-- src/libs/actions/Report.ts | 11 +++--- src/libs/actions/Session/index.ts | 2 +- src/libs/actions/User.ts | 5 +-- tests/utils/PusherHelper.ts | 14 ++++---- tests/utils/TestHelper.ts | 4 +-- 12 files changed, 52 insertions(+), 50 deletions(-) diff --git a/src/libs/API/index.ts b/src/libs/API/index.ts index 3fa4ba64c377..92f23260cc19 100644 --- a/src/libs/API/index.ts +++ b/src/libs/API/index.ts @@ -5,7 +5,7 @@ import Log from '@libs/Log'; import {HandleUnusedOptimisticID, Logging, Pagination, Reauthentication, RecheckConnection, SaveResponseInOnyx} from '@libs/Middleware'; import {isOffline} from '@libs/Network/NetworkStore'; import {push as pushToSequentialQueue, waitForIdle as waitForSequentialQueueIdle} from '@libs/Network/SequentialQueue'; -import {getPusherSocketID} from '@libs/Pusher'; +import Pusher from '@libs/Pusher'; import {processWithMiddleware, use} from '@libs/Request'; import {getLength as getPersistedRequestsLength} from '@userActions/PersistedRequests'; import CONST from '@src/CONST'; @@ -70,7 +70,7 @@ function prepareRequest( // We send the pusherSocketID with all write requests so that the api can include it in push events to prevent Pusher from sending the events to the requesting client. The push event // is sent back to the requesting client in the response data instead, which prevents a replay effect in the UI. See https://github.com/Expensify/App/issues/12775. - pusherSocketID: isWriteRequest ? getPusherSocketID() : undefined, + pusherSocketID: isWriteRequest ? Pusher.getPusherSocketID() : undefined, }; // Assemble all request metadata (used by middlewares, and for persisted requests stored in Onyx) diff --git a/src/libs/Navigation/AppNavigator/AuthScreens.tsx b/src/libs/Navigation/AppNavigator/AuthScreens.tsx index 8120770f52d4..336e99c54c84 100644 --- a/src/libs/Navigation/AppNavigator/AuthScreens.tsx +++ b/src/libs/Navigation/AppNavigator/AuthScreens.tsx @@ -34,7 +34,7 @@ import type {AuthScreensParamList, CentralPaneName, CentralPaneScreensParamList} import {isOnboardingFlowName} from '@libs/NavigationUtils'; import NetworkConnection from '@libs/NetworkConnection'; import onyxSubscribe from '@libs/onyxSubscribe'; -import * as Pusher from '@libs/Pusher'; +import Pusher from '@libs/Pusher'; import PusherConnectionManager from '@libs/PusherConnectionManager'; import * as ReportUtils from '@libs/ReportUtils'; import * as SearchQueryUtils from '@libs/SearchQueryUtils'; diff --git a/src/libs/Pusher/index.native.ts b/src/libs/Pusher/index.native.ts index 4348e81964f0..6da8c7a3053b 100644 --- a/src/libs/Pusher/index.native.ts +++ b/src/libs/Pusher/index.native.ts @@ -9,19 +9,8 @@ import {authenticatePusher} from '@userActions/Session'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import TYPE from './EventType'; -import type { - Args, - ChunkedDataEvents, - EventCallbackError, - EventData, - PingPongEvent, - PusherEventName, - SocketEventCallback, - SocketEventName, - States, - UserIsLeavingRoomEvent, - UserIsTypingEvent, -} from './types'; +import type {Args, ChunkedDataEvents, EventCallbackError, EventData, PusherEventName, SocketEventCallback, SocketEventName, States} from './types'; +import type PusherModule from './types'; let shouldForceOffline = false; Onyx.connect({ @@ -349,6 +338,19 @@ if (window) { window.getPusherInstance = () => socket; } -export {init, subscribe, unsubscribe, getChannel, isSubscribed, isAlreadySubscribing, sendEvent, disconnect, reconnect, registerSocketEventCallback, TYPE, getPusherSocketID}; - -export type {EventCallbackError, States, UserIsTypingEvent, UserIsLeavingRoomEvent, PingPongEvent}; +const MobilePusher: PusherModule = { + init, + subscribe, + unsubscribe, + getChannel, + isSubscribed, + isAlreadySubscribing, + sendEvent, + disconnect, + reconnect, + registerSocketEventCallback, + TYPE, + getPusherSocketID, +}; + +export default MobilePusher; diff --git a/src/libs/Pusher/index.ts b/src/libs/Pusher/index.ts index 22cf79352311..282ac54b6f1b 100644 --- a/src/libs/Pusher/index.ts +++ b/src/libs/Pusher/index.ts @@ -11,16 +11,14 @@ import type { ChunkedDataEvents, EventCallbackError, EventData, - PingPongEvent, PusherEventName, PusherSubscribtionErrorData, PusherWithAuthParams, SocketEventCallback, SocketEventName, States, - UserIsLeavingRoomEvent, - UserIsTypingEvent, } from './types'; +import type PusherModule from './types'; let shouldForceOffline = false; Onyx.connect({ @@ -380,7 +378,7 @@ if (window) { window.getPusherInstance = () => socket; } -export { +const WebPusher: PusherModule = { init, subscribe, unsubscribe, @@ -396,4 +394,4 @@ export { getPusherSocketID, }; -export type {EventCallbackError, States, UserIsTypingEvent, UserIsLeavingRoomEvent, PingPongEvent}; +export default WebPusher; diff --git a/src/libs/Pusher/types.ts b/src/libs/Pusher/types.ts index 88348e1c4bad..d6d1da3297df 100644 --- a/src/libs/Pusher/types.ts +++ b/src/libs/Pusher/types.ts @@ -79,6 +79,7 @@ type PusherModule = { registerSocketEventCallback: (cb: SocketEventCallback) => void; registerCustomAuthorizer?: (authorizer: ChannelAuthorizerGenerator) => void; getPusherSocketID: () => string | undefined; + TYPE: typeof TYPE; }; export default PusherModule; diff --git a/src/libs/PusherConnectionManager.ts b/src/libs/PusherConnectionManager.ts index ee31c892045b..bc3ef45d8592 100644 --- a/src/libs/PusherConnectionManager.ts +++ b/src/libs/PusherConnectionManager.ts @@ -2,9 +2,8 @@ import type {ChannelAuthorizationCallback} from 'pusher-js/with-encryption'; import CONST from '@src/CONST'; import {authenticatePusher} from './actions/Session'; import Log from './Log'; -import {reconnect, registerCustomAuthorizer, registerSocketEventCallback} from './Pusher'; -import type {EventCallbackError, States} from './Pusher'; -import type {SocketEventName} from './Pusher/types'; +import Pusher from './Pusher'; +import type {EventCallbackError, SocketEventName, States} from './Pusher/types'; function init() { /** @@ -13,13 +12,13 @@ function init() { * current valid token to generate the signed auth response * needed to subscribe to Pusher channels. */ - registerCustomAuthorizer?.((channel) => ({ + Pusher.registerCustomAuthorizer?.((channel) => ({ authorize: (socketId: string, callback: ChannelAuthorizationCallback) => { authenticatePusher(socketId, channel.name, callback); }, })); - registerSocketEventCallback((eventName: SocketEventName, error?: EventCallbackError | States) => { + Pusher.registerSocketEventCallback((eventName: SocketEventName, error?: EventCallbackError | States) => { switch (eventName) { case 'error': { if (error && 'type' in error) { @@ -35,7 +34,7 @@ function init() { // On the advice from Pusher directly, they suggested to manually reconnect in those scenarios. if (errorMessage) { Log.hmmm('[PusherConnectionManager] Channels Error 1006 message', {errorMessage}); - reconnect(); + Pusher.reconnect(); } } else if (errorType === CONST.ERROR.PUSHER_ERROR && code === 4201) { // This means the connection was closed because Pusher did not receive a reply from the client when it pinged them for a response diff --git a/src/libs/PusherUtils.ts b/src/libs/PusherUtils.ts index 50778160dbfa..703b5bc4a44c 100644 --- a/src/libs/PusherUtils.ts +++ b/src/libs/PusherUtils.ts @@ -4,8 +4,8 @@ import CONST from '@src/CONST'; import type {OnyxUpdatesFromServer} from '@src/types/onyx'; import Log from './Log'; import NetworkConnection from './NetworkConnection'; -import {subscribe} from './Pusher'; -import type {PingPongEvent} from './Pusher'; +import Pusher from './Pusher'; +import type {PingPongEvent} from './Pusher/types'; type Callback = (data: OnyxUpdate[]) => Promise; @@ -50,7 +50,7 @@ function subscribeToPrivateUserChannelEvent(eventName: string, accountID: string function onSubscriptionFailed(error: Error) { Log.hmmm('Failed to subscribe to Pusher channel', {error, pusherChannelName, eventName}); } - subscribe(pusherChannelName, eventName, onEventPush, onPusherResubscribeToPrivateUserChannel).catch(onSubscriptionFailed); + Pusher.subscribe(pusherChannelName, eventName, onEventPush, onPusherResubscribeToPrivateUserChannel).catch(onSubscriptionFailed); } export default { diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index 9de0c193124f..63a372f4ea96 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -79,7 +79,8 @@ import * as PhoneNumber from '@libs/PhoneNumber'; import getPolicyEmployeeAccountIDs from '@libs/PolicyEmployeeListUtils'; import {extractPolicyIDFromPath, getPolicy} from '@libs/PolicyUtils'; import processReportIDDeeplink from '@libs/processReportIDDeeplink'; -import * as Pusher from '@libs/Pusher'; +import Pusher from '@libs/Pusher'; +import type {UserIsLeavingRoomEvent, UserIsTypingEvent} from '@libs/Pusher/types'; import * as ReportActionsUtils from '@libs/ReportActionsUtils'; import type {OptimisticAddCommentReportAction, OptimisticChatReport} from '@libs/ReportUtils'; import { @@ -398,7 +399,7 @@ function getReportChannelName(reportID: string): string { * * This method makes sure that no matter which we get, we return the "new" format */ -function getNormalizedStatus(typingStatus: Pusher.UserIsTypingEvent | Pusher.UserIsLeavingRoomEvent): ReportUserIsTyping { +function getNormalizedStatus(typingStatus: UserIsTypingEvent | UserIsLeavingRoomEvent): ReportUserIsTyping { let normalizedStatus: ReportUserIsTyping; if (typingStatus.userLogin) { @@ -463,7 +464,7 @@ function subscribeToReportLeavingEvents(reportID: string | undefined) { Onyx.set(`${ONYXKEYS.COLLECTION.REPORT_USER_IS_LEAVING_ROOM}${reportID}`, false); const pusherChannelName = getReportChannelName(reportID); - Pusher.subscribe(pusherChannelName, Pusher.TYPE.USER_IS_LEAVING_ROOM, (leavingStatus: Pusher.UserIsLeavingRoomEvent) => { + Pusher.subscribe(pusherChannelName, Pusher.TYPE.USER_IS_LEAVING_ROOM, (leavingStatus: UserIsLeavingRoomEvent) => { // If the pusher message comes from OldDot, we expect the leaving status to be keyed by user // login OR by 'Concierge'. If the pusher message comes from NewDot, it is keyed by accountID // since personal details are keyed by accountID. @@ -1499,7 +1500,7 @@ function saveReportDraftComment(reportID: string, comment: string | null, callba /** Broadcasts whether or not a user is typing on a report over the report's private pusher channel. */ function broadcastUserIsTyping(reportID: string) { const privateReportChannelName = getReportChannelName(reportID); - const typingStatus: Pusher.UserIsTypingEvent = { + const typingStatus: UserIsTypingEvent = { [currentUserAccountID]: true, }; Pusher.sendEvent(privateReportChannelName, Pusher.TYPE.USER_IS_TYPING, typingStatus); @@ -1508,7 +1509,7 @@ function broadcastUserIsTyping(reportID: string) { /** Broadcasts to the report's private pusher channel whether a user is leaving a report */ function broadcastUserIsLeavingRoom(reportID: string) { const privateReportChannelName = getReportChannelName(reportID); - const leavingStatus: Pusher.UserIsLeavingRoomEvent = { + const leavingStatus: UserIsLeavingRoomEvent = { [currentUserAccountID]: true, }; Pusher.sendEvent(privateReportChannelName, Pusher.TYPE.USER_IS_LEAVING_ROOM, leavingStatus); diff --git a/src/libs/actions/Session/index.ts b/src/libs/actions/Session/index.ts index 60c169df433f..d28d3b771802 100644 --- a/src/libs/actions/Session/index.ts +++ b/src/libs/actions/Session/index.ts @@ -36,7 +36,7 @@ import * as MainQueue from '@libs/Network/MainQueue'; import * as NetworkStore from '@libs/Network/NetworkStore'; import {getCurrentUserEmail} from '@libs/Network/NetworkStore'; import NetworkConnection from '@libs/NetworkConnection'; -import * as Pusher from '@libs/Pusher'; +import Pusher from '@libs/Pusher'; import {getReportIDFromLink, parseReportRouteParams as parseReportRouteParamsReportUtils} from '@libs/ReportUtils'; import * as SessionUtils from '@libs/SessionUtils'; import {clearSoundAssetsCache} from '@libs/Sound'; diff --git a/src/libs/actions/User.ts b/src/libs/actions/User.ts index 8ed2506336bf..0397a06326d2 100644 --- a/src/libs/actions/User.ts +++ b/src/libs/actions/User.ts @@ -32,7 +32,8 @@ import {isOffline} from '@libs/Network/NetworkStore'; import * as SequentialQueue from '@libs/Network/SequentialQueue'; import * as NumberUtils from '@libs/NumberUtils'; import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils'; -import * as Pusher from '@libs/Pusher'; +import Pusher from '@libs/Pusher'; +import type {PingPongEvent} from '@libs/Pusher/types'; import PusherUtils from '@libs/PusherUtils'; import * as ReportActionsUtils from '@libs/ReportActionsUtils'; import * as ReportUtils from '@libs/ReportUtils'; @@ -905,7 +906,7 @@ function subscribeToPusherPong() { lastPongReceivedTimestamp = Date.now(); // Calculate the latency between the client and the server - const pongEvent = pushJSON as Pusher.PingPongEvent; + const pongEvent = pushJSON as PingPongEvent; const latency = Date.now() - Number(pongEvent.pingTimestamp); Log.info(`[Pusher PINGPONG] The event took ${latency} ms`); diff --git a/tests/utils/PusherHelper.ts b/tests/utils/PusherHelper.ts index 43e5e30f54aa..fe84cc9344de 100644 --- a/tests/utils/PusherHelper.ts +++ b/tests/utils/PusherHelper.ts @@ -1,4 +1,4 @@ -import {init, sendEvent, TYPE, unsubscribe} from '@libs/Pusher'; +import Pusher from '@libs/Pusher'; import CONFIG from '@src/CONFIG'; import CONST from '@src/CONST'; import PusherConnectionManager from '@src/libs/PusherConnectionManager'; @@ -11,12 +11,12 @@ function setup() { // channel already in a subscribed state. These methods are normally used to prevent // duplicated subscriptions, but we don't need them for this test so forcing them to // return false will make the testing less complex. - jest.spyOn(require('@libs/Pusher'), 'isSubscribed').mockReturnValue(false); - jest.spyOn(require('@libs/Pusher'), 'isAlreadySubscribing').mockReturnValue(false); + jest.spyOn(Pusher, 'isSubscribed').mockReturnValue(false); + jest.spyOn(Pusher, 'isAlreadySubscribing').mockReturnValue(false); // Connect to Pusher PusherConnectionManager.init(); - init({ + Pusher.init({ appKey: CONFIG.PUSHER.APP_KEY, cluster: CONFIG.PUSHER.CLUSTER, authEndpoint: `${CONFIG.EXPENSIFY.DEFAULT_API_ROOT}api/AuthenticatePusher?`, @@ -29,13 +29,13 @@ function setup() { } function emitOnyxUpdate(args: OnyxServerUpdate[]) { - sendEvent(CHANNEL_NAME, TYPE.MULTIPLE_EVENTS, { + Pusher.sendEvent(CHANNEL_NAME, Pusher.TYPE.MULTIPLE_EVENTS, { type: 'pusher', lastUpdateID: 0, previousUpdateID: 0, updates: [ { - eventType: TYPE.MULTIPLE_EVENT_TYPE.ONYX_API_UPDATE, + eventType: Pusher.TYPE.MULTIPLE_EVENT_TYPE.ONYX_API_UPDATE, data: args, }, ], @@ -45,7 +45,7 @@ function emitOnyxUpdate(args: OnyxServerUpdate[]) { function teardown() { // Unsubscribe from account channel after each test since we subscribe in the function // subscribeToUserEvents and we don't want duplicate event subscriptions. - unsubscribe(CHANNEL_NAME); + Pusher.unsubscribe(CHANNEL_NAME); } export default { diff --git a/tests/utils/TestHelper.ts b/tests/utils/TestHelper.ts index edc06433e061..24c15aa274d1 100644 --- a/tests/utils/TestHelper.ts +++ b/tests/utils/TestHelper.ts @@ -5,7 +5,7 @@ import Onyx from 'react-native-onyx'; import type {ConnectOptions} from 'react-native-onyx/dist/types'; import type {ApiCommand, ApiRequestCommandParameters} from '@libs/API/types'; import {translateLocal} from '@libs/Localize'; -import {init} from '@libs/Pusher'; +import Pusher from '@libs/Pusher'; import PusherConnectionManager from '@libs/PusherConnectionManager'; import CONFIG from '@src/CONFIG'; import CONST from '@src/CONST'; @@ -47,7 +47,7 @@ function setupApp() { // Connect to Pusher PusherConnectionManager.init(); - init({ + Pusher.init({ appKey: CONFIG.PUSHER.APP_KEY, cluster: CONFIG.PUSHER.CLUSTER, authEndpoint: `${CONFIG.EXPENSIFY.DEFAULT_API_ROOT}api/AuthenticatePusher?`, From 416b9bfd809d65f119fe3182a4ae21f46fd76963 Mon Sep 17 00:00:00 2001 From: VickyStash Date: Wed, 12 Feb 2025 16:19:30 +0100 Subject: [PATCH 14/16] Re-run checks From 4a6e373c6c80c7a5f365b282dfa4b18cedd20db6 Mon Sep 17 00:00:00 2001 From: VickyStash Date: Thu, 13 Feb 2025 12:17:23 +0100 Subject: [PATCH 15/16] Android: fix event trigger --- __mocks__/@pusher/pusher-websocket-react-native/index.ts | 2 +- src/libs/Pusher/index.native.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/__mocks__/@pusher/pusher-websocket-react-native/index.ts b/__mocks__/@pusher/pusher-websocket-react-native/index.ts index dd0ea1592dec..07a7a06b4ba2 100644 --- a/__mocks__/@pusher/pusher-websocket-react-native/index.ts +++ b/__mocks__/@pusher/pusher-websocket-react-native/index.ts @@ -67,7 +67,7 @@ class MockedPusher { } trigger({channelName, eventName, data}: PusherEvent) { - this.channels.get(channelName)?.onEvent({channelName, eventName, data: JSON.stringify(data)}); + this.channels.get(channelName)?.onEvent({channelName, eventName, data: data as Record}); } getChannel(channelName: string) { diff --git a/src/libs/Pusher/index.native.ts b/src/libs/Pusher/index.native.ts index 6da8c7a3053b..aef541baf699 100644 --- a/src/libs/Pusher/index.native.ts +++ b/src/libs/Pusher/index.native.ts @@ -284,7 +284,7 @@ function sendEvent(channelName: string, event return; } - socket?.trigger({channelName, eventName, data: payload}); + socket?.trigger({channelName, eventName, data: JSON.stringify(payload)}); } /** From 0b975b1e5f4afdbb4f2a54f163c6ab6d751888d1 Mon Sep 17 00:00:00 2001 From: VickyStash Date: Fri, 14 Feb 2025 12:20:29 +0100 Subject: [PATCH 16/16] Minor update --- __mocks__/@pusher/pusher-websocket-react-native/index.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/__mocks__/@pusher/pusher-websocket-react-native/index.ts b/__mocks__/@pusher/pusher-websocket-react-native/index.ts index 07a7a06b4ba2..b2b59d303251 100644 --- a/__mocks__/@pusher/pusher-websocket-react-native/index.ts +++ b/__mocks__/@pusher/pusher-websocket-react-native/index.ts @@ -20,7 +20,9 @@ type SubscribeProps = { onSubscriptionSucceeded: OnSubscriptionSucceeded; }; -type UnsubscribeProps = {channelName: string}; +type UnsubscribeProps = { + channelName: string; +}; class MockedPusher { static instance: MockedPusher | null = null;