diff --git a/lib/keyserver-conn/call-keyserver-endpoint-provider.react.js b/lib/keyserver-conn/call-keyserver-endpoint-provider.react.js index 66abdecb7..19182ce87 100644 --- a/lib/keyserver-conn/call-keyserver-endpoint-provider.react.js +++ b/lib/keyserver-conn/call-keyserver-endpoint-provider.react.js @@ -1,321 +1,380 @@ // @flow import invariant from 'invariant'; +import _memoize from 'lodash/memoize.js'; import * as React from 'react'; import { createSelector } from 'reselect'; import { useKeyserverCallInfos } from './keyserver-call-infos.js'; -import { setNewSession } from './keyserver-conn-types.js'; +import { + setNewSession, + type SingleKeyserverActionFunc, +} from './keyserver-conn-types.js'; import { canResolveKeyserverSessionInvalidation, resolveKeyserverSessionInvalidation, } from './recovery-utils.js'; import { logInActionSources } from '../types/account-types.js'; import type { PlatformDetails } from '../types/device-types.js'; import type { Endpoint, SocketAPIHandler } from '../types/endpoints.js'; import type { Dispatch } from '../types/redux-types.js'; import type { ClientSessionChange } from '../types/session-types.js'; import type { CurrentUserInfo } from '../types/user-types.js'; import type { CallSingleKeyserverEndpoint, CallSingleKeyserverEndpointOptions, } from '../utils/call-single-keyserver-endpoint.js'; import callSingleKeyserverEndpoint from '../utils/call-single-keyserver-endpoint.js'; import { useSelector, useDispatch } from '../utils/redux-utils.js'; type CreateCallSingleKeyserverEndpointSelector = ( keyserverID: string, ) => ServerCallSelectorParams => CallSingleKeyserverEndpoint; -type GetCallSingleKeyserverEndpoint = ( +type GetBoundSingleKeyserverActionFunc = ( keyserverID: string, -) => CallSingleKeyserverEndpoint; + actionFunc: SingleKeyserverActionFunc, +) => F; + +type SingleKeyserverActionFuncSelectorParams = { + +callSingleKeyserverEndpoint: CallSingleKeyserverEndpoint, +}; +type CreateBoundSingleKeyserverActionFuncSelector = ( + actionFunc: SingleKeyserverActionFunc, +) => SingleKeyserverActionFuncSelectorParams => F; type CallKeyserverEndpointContextType = { +createCallSingleKeyserverEndpointSelector: CreateCallSingleKeyserverEndpointSelector, - +getCallSingleKeyserverEndpoint: GetCallSingleKeyserverEndpoint, + +getBoundSingleKeyserverActionFunc: GetBoundSingleKeyserverActionFunc, }; const CallKeyserverEndpointContext: React.Context = React.createContext(); type OngoingRecoveryAttempt = { +waitingCalls: Array< (callSingleKeyserverEndpoint: ?CallSingleKeyserverEndpoint) => mixed, >, }; export type ServerCallSelectorParams = { +dispatch: Dispatch, +cookie: ?string, +urlPrefix: string, +sessionID: ?string, +currentUserInfo: ?CurrentUserInfo, +isSocketConnected: boolean, +lastCommunicatedPlatformDetails: ?PlatformDetails, }; type BindServerCallsParams = $ReadOnly<{ ...ServerCallSelectorParams, +keyserverID: string, }>; type Props = { +children: React.Node, }; function CallKeyserverEndpointProvider(props: Props): React.Node { // SECTION 1: bindCookieAndUtilsIntoCallServerEndpoint const ongoingRecoveryAttemptsRef = React.useRef< Map, >(new Map()); const bindCookieAndUtilsIntoCallSingleKeyserverEndpoint: ( params: BindServerCallsParams, ) => CallSingleKeyserverEndpoint = React.useCallback(params => { const { dispatch, cookie, urlPrefix, sessionID, currentUserInfo, isSocketConnected, lastCommunicatedPlatformDetails, keyserverID, } = params; const loggedIn = !!(currentUserInfo && !currentUserInfo.anonymous && true); const boundSetNewSession = ( sessionChange: ClientSessionChange, error: ?string, ) => setNewSession( dispatch, sessionChange, { currentUserInfo, cookiesAndSessions: { [keyserverID]: { cookie, sessionID } }, }, error, undefined, keyserverID, ); const canResolveInvalidation = canResolveKeyserverSessionInvalidation(); // This function gets called before callSingleKeyserverEndpoint sends a // request, to make sure that we're not in the middle of trying to recover // an invalidated cookie const waitIfCookieInvalidated = () => { if (!canResolveInvalidation) { // If there is no way to resolve the session invalidation, // just let the caller callSingleKeyserverEndpoint instance continue return Promise.resolve(null); } const ongoingRecoveryAttempt = ongoingRecoveryAttemptsRef.current.get(keyserverID); if (!ongoingRecoveryAttempt) { // Our cookie seems to be valid return Promise.resolve(null); } // Wait to run until we get our new cookie return new Promise(r => ongoingRecoveryAttempt.waitingCalls.push(r), ); }; // This function is a helper for the next function defined below const attemptToResolveInvalidation = async ( sessionChange: ClientSessionChange, ) => { const newAnonymousCookie = sessionChange.cookie; const newSessionChange = await resolveKeyserverSessionInvalidation( dispatch, newAnonymousCookie, urlPrefix, logInActionSources.cookieInvalidationResolutionAttempt, keyserverID, ); const ongoingRecoveryAttempt = ongoingRecoveryAttemptsRef.current.get(keyserverID); ongoingRecoveryAttemptsRef.current.delete(keyserverID); const currentWaitingCalls = ongoingRecoveryAttempt?.waitingCalls ?? []; const newCallSingleKeyserverEndpoint = newSessionChange ? bindCookieAndUtilsIntoCallSingleKeyserverEndpoint({ ...params, cookie: newSessionChange.cookie, sessionID: newSessionChange.sessionID, currentUserInfo: newSessionChange.currentUserInfo, }) : null; for (const func of currentWaitingCalls) { func(newCallSingleKeyserverEndpoint); } return newCallSingleKeyserverEndpoint; }; // If this function is called, callSingleKeyserverEndpoint got a response // invalidating its cookie, and is wondering if it should just like... // give up? Or if there's a chance at redemption const cookieInvalidationRecovery = (sessionChange: ClientSessionChange) => { if (!canResolveInvalidation) { // If there is no way to resolve the session invalidation, // just let the caller callSingleKeyserverEndpoint instance continue return Promise.resolve(null); } if (!loggedIn) { // We don't want to attempt any use native credentials of a logged out // user to log-in after a cookieInvalidation while logged out return Promise.resolve(null); } const ongoingRecoveryAttempt = ongoingRecoveryAttemptsRef.current.get(keyserverID); if (ongoingRecoveryAttempt) { return new Promise(r => ongoingRecoveryAttempt.waitingCalls.push(r), ); } ongoingRecoveryAttemptsRef.current.set(keyserverID, { waitingCalls: [] }); return attemptToResolveInvalidation(sessionChange); }; return ( endpoint: Endpoint, data: Object, options?: ?CallSingleKeyserverEndpointOptions, ) => callSingleKeyserverEndpoint( cookie, boundSetNewSession, waitIfCookieInvalidated, cookieInvalidationRecovery, urlPrefix, sessionID, isSocketConnected, lastCommunicatedPlatformDetails, socketAPIHandler, endpoint, data, dispatch, options, loggedIn, keyserverID, ); }, []); // SECTION 2: createCallSingleKeyserverEndpointSelector // For each keyserver, we have a set of params that configure our connection // to it. These params get bound into callSingleKeyserverEndpoint before it's - // passed to an ActionFunc. This helper function lets us create a selector for - // a given keyserverID that will regenerate the bound + // passed to a SingleKeyserverActionFunc. This helper function lets us create + // a selector for a given keyserverID that will regenerate the bound // callSingleKeyserverEndpoint function only if one of the params changes. // This lets us skip some React render cycles. const createCallSingleKeyserverEndpointSelector = React.useCallback( ( keyserverID: string, ): (ServerCallSelectorParams => CallSingleKeyserverEndpoint) => createSelector( (params: ServerCallSelectorParams) => params.dispatch, (params: ServerCallSelectorParams) => params.cookie, (params: ServerCallSelectorParams) => params.urlPrefix, (params: ServerCallSelectorParams) => params.sessionID, (params: ServerCallSelectorParams) => params.currentUserInfo, (params: ServerCallSelectorParams) => params.isSocketConnected, (params: ServerCallSelectorParams) => params.lastCommunicatedPlatformDetails, ( dispatch: Dispatch, cookie: ?string, urlPrefix: string, sessionID: ?string, currentUserInfo: ?CurrentUserInfo, isSocketConnected: boolean, lastCommunicatedPlatformDetails: ?PlatformDetails, ) => bindCookieAndUtilsIntoCallSingleKeyserverEndpoint({ dispatch, cookie, urlPrefix, sessionID, currentUserInfo, isSocketConnected, lastCommunicatedPlatformDetails, keyserverID, }), ), [bindCookieAndUtilsIntoCallSingleKeyserverEndpoint], ); - // SECTION 3: getCallSingleKeyserverEndpoint + // SECTION 3: getBoundSingleKeyserverActionFunc const dispatch = useDispatch(); const currentUserInfo = useSelector(state => state.currentUserInfo); const keyserverInfos = useSelector( state => state.keyserverStore.keyserverInfos, ); const keyserverCallInfos = useKeyserverCallInfos(keyserverInfos); const callSingleKeyserverEndpointSelectorCacheRef = React.useRef< Map CallSingleKeyserverEndpoint>, >(new Map()); - const getCallSingleKeyserverEndpoint: GetCallSingleKeyserverEndpoint = + const getCallSingleKeyserverEndpoint = React.useCallback( + (keyserverID: string) => { + let selector = + callSingleKeyserverEndpointSelectorCacheRef.current.get(keyserverID); + if (!selector) { + selector = createCallSingleKeyserverEndpointSelector(keyserverID); + callSingleKeyserverEndpointSelectorCacheRef.current.set( + keyserverID, + selector, + ); + } + const keyserverCallInfo = keyserverCallInfos[keyserverID]; + return selector({ + ...keyserverCallInfo, + dispatch, + currentUserInfo, + }); + }, + [ + createCallSingleKeyserverEndpointSelector, + dispatch, + currentUserInfo, + keyserverCallInfos, + ], + ); + + const createBoundSingleKeyserverActionFuncSelector: CreateBoundSingleKeyserverActionFuncSelector = React.useCallback( - (keyserverID: string) => { + actionFunc => + createSelector( + (params: SingleKeyserverActionFuncSelectorParams) => + params.callSingleKeyserverEndpoint, + actionFunc, + ), + [], + ); + + const createBoundSingleKeyserverActionFuncsCache: () => CreateBoundSingleKeyserverActionFuncSelector = + React.useCallback( + () => _memoize(createBoundSingleKeyserverActionFuncSelector), + [createBoundSingleKeyserverActionFuncSelector], + ); + + const boundSingleKeyserverActionFuncSelectorCacheRef = React.useRef< + Map, + >(new Map()); + const getBoundSingleKeyserverActionFunc: GetBoundSingleKeyserverActionFunc = + React.useCallback( + (keyserverID: string, actionFunc: SingleKeyserverActionFunc): F => { let selector = - callSingleKeyserverEndpointSelectorCacheRef.current.get(keyserverID); + boundSingleKeyserverActionFuncSelectorCacheRef.current.get( + keyserverID, + ); if (!selector) { - selector = createCallSingleKeyserverEndpointSelector(keyserverID); - callSingleKeyserverEndpointSelectorCacheRef.current.set( + selector = createBoundSingleKeyserverActionFuncsCache(); + boundSingleKeyserverActionFuncSelectorCacheRef.current.set( keyserverID, selector, ); } - const keyserverCallInfo = keyserverCallInfos[keyserverID]; - return selector({ - ...keyserverCallInfo, - dispatch, - currentUserInfo, + const callEndpoint = getCallSingleKeyserverEndpoint(keyserverID); + return selector(actionFunc)({ + callSingleKeyserverEndpoint: callEndpoint, }); }, [ - createCallSingleKeyserverEndpointSelector, - dispatch, - currentUserInfo, - keyserverCallInfos, + createBoundSingleKeyserverActionFuncsCache, + getCallSingleKeyserverEndpoint, ], ); const value = React.useMemo( () => ({ createCallSingleKeyserverEndpointSelector, - getCallSingleKeyserverEndpoint, + getBoundSingleKeyserverActionFunc, }), - [createCallSingleKeyserverEndpointSelector, getCallSingleKeyserverEndpoint], + [ + createCallSingleKeyserverEndpointSelector, + getBoundSingleKeyserverActionFunc, + ], ); return ( {props.children} ); } function useCallKeyserverEndpointContext(): CallKeyserverEndpointContextType { const callKeyserverEndpointContext = React.useContext( CallKeyserverEndpointContext, ); invariant( callKeyserverEndpointContext, 'callKeyserverEndpointContext should be set', ); return callKeyserverEndpointContext; } let socketAPIHandler: ?SocketAPIHandler = null; function registerActiveSocket(passedSocketAPIHandler: ?SocketAPIHandler) { socketAPIHandler = passedSocketAPIHandler; } export { CallKeyserverEndpointProvider, useCallKeyserverEndpointContext, registerActiveSocket, }; diff --git a/lib/utils/action-utils.js b/lib/utils/action-utils.js index a93ca1b9d..4a49887c8 100644 --- a/lib/utils/action-utils.js +++ b/lib/utils/action-utils.js @@ -1,50 +1,69 @@ // @flow import invariant from 'invariant'; import * as React from 'react'; import { useSelector, useDispatch } from './redux-utils.js'; import { ashoatKeyserverID } from './validation-utils.js'; import { type ServerCallSelectorParams, useCallKeyserverEndpointContext, } from '../keyserver-conn/call-keyserver-endpoint-provider.react.js'; import type { SingleKeyserverActionFunc } from '../keyserver-conn/keyserver-conn-types.js'; import { serverCallStateSelector } from '../selectors/server-calls.js'; function useLegacyAshoatKeyserverCall( serverCall: SingleKeyserverActionFunc, paramOverride?: ?Partial, ): F { - const dispatch = useDispatch(); - const serverCallState = useSelector( - serverCallStateSelector(ashoatKeyserverID), + const { + createCallSingleKeyserverEndpointSelector, + getBoundSingleKeyserverActionFunc, + } = useCallKeyserverEndpointContext(); + + const cachedNonOverridenBoundServerCall = React.useMemo( + () => getBoundSingleKeyserverActionFunc(ashoatKeyserverID, serverCall), + [getBoundSingleKeyserverActionFunc, serverCall], ); - const { createCallSingleKeyserverEndpointSelector } = - useCallKeyserverEndpointContext(); - const selector = React.useMemo( + + const customSelector = React.useMemo( () => createCallSingleKeyserverEndpointSelector(ashoatKeyserverID), [createCallSingleKeyserverEndpointSelector], ); + const dispatch = useDispatch(); + const serverCallState = useSelector( + serverCallStateSelector(ashoatKeyserverID), + ); return React.useMemo(() => { + if (!paramOverride) { + return cachedNonOverridenBoundServerCall; + } + const { urlPrefix, isSocketConnected } = serverCallState; invariant( !!urlPrefix && isSocketConnected !== undefined && isSocketConnected !== null, 'keyserver missing from keyserverStore', ); - const callSingleKeyserverEndpoint = selector({ + const callSingleKeyserverEndpoint = customSelector({ ...serverCallState, urlPrefix, isSocketConnected, dispatch, ...paramOverride, }); return serverCall(callSingleKeyserverEndpoint); - }, [serverCall, serverCallState, dispatch, paramOverride, selector]); + }, [ + cachedNonOverridenBoundServerCall, + serverCall, + serverCallState, + dispatch, + paramOverride, + customSelector, + ]); } export { useLegacyAshoatKeyserverCall };