diff --git a/lib/components/keyserver-connection-handler.js b/lib/components/keyserver-connection-handler.js index 5079a607d..f3f42d066 100644 --- a/lib/components/keyserver-connection-handler.js +++ b/lib/components/keyserver-connection-handler.js @@ -1,234 +1,338 @@ // @flow import invariant from 'invariant'; import * as React from 'react'; import { keyserverAuthActionTypes, logOutActionTypes, useKeyserverAuth, useLogOut, } from '../actions/user-actions.js'; import { extractKeyserverIDFromID } from '../keyserver-conn/keyserver-call-utils.js'; +import { setSessionRecoveryInProgressActionType } from '../keyserver-conn/keyserver-conn-types.js'; +import { resolveKeyserverSessionInvalidation } from '../keyserver-conn/recovery-utils.js'; import { filterThreadIDsInFilterList } from '../reducers/calendar-filters-reducer.js'; import { connectionSelector, cookieSelector, deviceTokenSelector, + urlPrefixSelector, } from '../selectors/keyserver-selectors.js'; import { isLoggedInToKeyserver } from '../selectors/user-selectors.js'; import { IdentityClientContext } from '../shared/identity-client-context.js'; import { OlmSessionCreatorContext } from '../shared/olm-session-creator-context.js'; import type { BaseSocketProps } from '../socket/socket.react.js'; import { logInActionSources } from '../types/account-types.js'; import { authoritativeKeyserverID } from '../utils/authoritative-keyserver.js'; import { useDispatchActionPromise } from '../utils/redux-promise-utils.js'; -import { useSelector } from '../utils/redux-utils.js'; +import { useSelector, useDispatch } from '../utils/redux-utils.js'; import { usingCommServicesAccessToken } from '../utils/services-utils.js'; import sleep from '../utils/sleep.js'; type Props = { ...BaseSocketProps, +socketComponent: React.ComponentType, }; const AUTH_RETRY_DELAY_MS = 60000; const CANCELLED_ERROR = 'cancelled'; function KeyserverConnectionHandler(props: Props) { const { socketComponent: Socket, ...socketProps } = props; const { keyserverID } = props; const dispatchActionPromise = useDispatchActionPromise(); const callLogOut = useLogOut(); const keyserverAuth = useKeyserverAuth(); const hasConnectionIssue = useSelector( state => !!connectionSelector(keyserverID)(state)?.connectionIssue, ); const cookie = useSelector(cookieSelector(keyserverID)); const dataLoaded = useSelector(state => state.dataLoaded); const keyserverDeviceToken = useSelector(deviceTokenSelector(keyserverID)); // We have an assumption that we should be always connected to Ashoat's // keyserver. It is possible that a token which it has is correct, so we can // try to use it. In worst case it is invalid and our push-handler will try // to fix it. const ashoatKeyserverDeviceToken = useSelector( deviceTokenSelector(authoritativeKeyserverID()), ); const deviceToken = keyserverDeviceToken ?? ashoatKeyserverDeviceToken; const navInfo = useSelector(state => state.navInfo); const calendarFilters = useSelector(state => state.calendarFilters); const calendarQuery = React.useMemo(() => { const filters = filterThreadIDsInFilterList( calendarFilters, (threadID: string) => extractKeyserverIDFromID(threadID) === keyserverID, ); return { startDate: navInfo.startDate, endDate: navInfo.endDate, filters, }; }, [calendarFilters, keyserverID, navInfo.endDate, navInfo.startDate]); React.useEffect(() => { if (hasConnectionIssue && !usingCommServicesAccessToken) { void dispatchActionPromise(logOutActionTypes, callLogOut()); } }, [callLogOut, hasConnectionIssue, dispatchActionPromise]); const identityContext = React.useContext(IdentityClientContext); invariant(identityContext, 'Identity context should be set'); const { identityClient, getAuthMetadata } = identityContext; const olmSessionCreator = React.useContext(OlmSessionCreatorContext); invariant(olmSessionCreator, 'Olm session creator should be set'); const [authInProgress, setAuthInProgress] = React.useState(false); const performAuth = React.useCallback(() => { setAuthInProgress(true); let cancelled = false; const cancel = () => { cancelled = true; setAuthInProgress(false); }; const promise = (async () => { try { const keyserverKeys = await identityClient.getKeyserverKeys(keyserverID); if (cancelled) { throw new Error(CANCELLED_ERROR); } const [notifsSession, contentSession, { userID, deviceID }] = await Promise.all([ olmSessionCreator.notificationsSessionCreator( cookie, keyserverKeys.identityKeysBlob.notificationIdentityPublicKeys, keyserverKeys.notifInitializationInfo, keyserverID, ), olmSessionCreator.contentSessionCreator( keyserverKeys.identityKeysBlob.primaryIdentityPublicKeys, keyserverKeys.contentInitializationInfo, ), getAuthMetadata(), ]); invariant(userID, 'userID should be set'); invariant(deviceID, 'deviceID should be set'); const deviceTokenUpdateInput = deviceToken ? { [keyserverID]: { deviceToken } } : {}; if (cancelled) { throw new Error(CANCELLED_ERROR); } await dispatchActionPromise( keyserverAuthActionTypes, (async () => { const result = await keyserverAuth({ userID, deviceID, doNotRegister: false, calendarQuery, deviceTokenUpdateInput, logInActionSource: process.env.BROWSER ? logInActionSources.keyserverAuthFromWeb : logInActionSources.keyserverAuthFromNative, keyserverData: { [keyserverID]: { initialContentEncryptedMessage: contentSession, initialNotificationsEncryptedMessage: notifsSession, }, }, }); if (cancelled) { throw new Error(CANCELLED_ERROR); } return result; })(), ); } catch (e) { if (cancelled) { return; } console.log( `Error while authenticating to keyserver with id ${keyserverID}`, e, ); if (!dataLoaded && keyserverID === authoritativeKeyserverID()) { await dispatchActionPromise(logOutActionTypes, callLogOut()); } } finally { if (!cancelled) { await sleep(AUTH_RETRY_DELAY_MS); setAuthInProgress(false); } } })(); return [promise, cancel]; }, [ calendarQuery, callLogOut, cookie, dataLoaded, deviceToken, dispatchActionPromise, getAuthMetadata, identityClient, keyserverAuth, keyserverID, olmSessionCreator, ]); + const sessionRecoveryInProgress = useSelector( + state => + state.keyserverStore.keyserverInfos[keyserverID]?.connection + .sessionRecoveryInProgress, + ); + + const dispatch = useDispatch(); + const urlPrefix = useSelector(urlPrefixSelector(keyserverID)); + const performRecovery = React.useCallback(() => { + invariant( + urlPrefix, + `urlPrefix for ${keyserverID} should be set during performRecovery`, + ); + + setAuthInProgress(true); + + let cancelled = false; + const cancel = () => { + cancelled = true; + setAuthInProgress(false); + }; + + const promise = (async () => { + try { + const sessionChange = await resolveKeyserverSessionInvalidation( + dispatch, + cookie, + urlPrefix, + logInActionSources.cookieInvalidationResolutionAttempt, + keyserverID, + ); + if (cancelled) { + // TODO: cancellation won't work because above call handles Redux + // dispatch directly + throw new Error(CANCELLED_ERROR); + } + if ( + !sessionChange || + sessionChange.cookieInvalidated || + !sessionChange.cookie || + !sessionChange.cookie.startsWith('user=') + ) { + dispatch({ + type: setSessionRecoveryInProgressActionType, + payload: { sessionRecoveryInProgress: false, keyserverID }, + }); + } + } catch (e) { + if (cancelled) { + return; + } + + console.log( + `Error while recovering session with keyserver id ${keyserverID}`, + e, + ); + + dispatch({ + type: setSessionRecoveryInProgressActionType, + payload: { sessionRecoveryInProgress: false, keyserverID }, + }); + } finally { + if (!cancelled) { + setAuthInProgress(false); + } + } + })(); + return [promise, cancel]; + }, [dispatch, cookie, urlPrefix, keyserverID]); + const cancelPendingAuth = React.useRef void>(null); const prevPerformAuth = React.useRef(performAuth); const isUserAuthenticated = useSelector(isLoggedInToKeyserver(keyserverID)); const hasAccessToken = useSelector(state => !!state.commServicesAccessToken); + const cancelPendingRecovery = React.useRef void>(null); + const prevPerformRecovery = React.useRef(performRecovery); + React.useEffect(() => { + if (sessionRecoveryInProgress && isUserAuthenticated) { + cancelPendingAuth.current?.(); + cancelPendingAuth.current = null; + + if (prevPerformRecovery.current !== performRecovery) { + cancelPendingRecovery.current?.(); + cancelPendingRecovery.current = null; + prevPerformRecovery.current = performRecovery; + } + + if (!authInProgress) { + const [, cancel] = performRecovery(); + cancelPendingRecovery.current = cancel; + } + + return; + } + + cancelPendingRecovery.current?.(); + cancelPendingRecovery.current = null; + if (!hasAccessToken) { cancelPendingAuth.current?.(); cancelPendingAuth.current = null; } if ( !usingCommServicesAccessToken || isUserAuthenticated || !hasAccessToken ) { return; } if (prevPerformAuth.current !== performAuth) { cancelPendingAuth.current?.(); cancelPendingAuth.current = null; } prevPerformAuth.current = performAuth; if (authInProgress) { return; } const [, cancel] = performAuth(); cancelPendingAuth.current = cancel; - }, [authInProgress, hasAccessToken, isUserAuthenticated, performAuth]); + }, [ + sessionRecoveryInProgress, + authInProgress, + performRecovery, + hasAccessToken, + isUserAuthenticated, + performAuth, + ]); return ; } const Handler: React.ComponentType = React.memo( KeyserverConnectionHandler, ); export default Handler; diff --git a/lib/keyserver-conn/call-keyserver-endpoint-provider.react.js b/lib/keyserver-conn/call-keyserver-endpoint-provider.react.js index d51e7d66f..0438e9086 100644 --- a/lib/keyserver-conn/call-keyserver-endpoint-provider.react.js +++ b/lib/keyserver-conn/call-keyserver-endpoint-provider.react.js @@ -1,470 +1,485 @@ // @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 { + useKeyserverCallInfos, + type KeyserverCallInfo, +} from './keyserver-call-infos.js'; import { setNewSession, type SingleKeyserverActionFunc, type ActionFunc, + setSessionRecoveryInProgressActionType, } from './keyserver-conn-types.js'; -import { - canResolveKeyserverSessionInvalidation, - resolveKeyserverSessionInvalidation, -} from './recovery-utils.js'; -import { logInActionSources } from '../types/account-types.js'; +import { canResolveKeyserverSessionInvalidation } from './recovery-utils.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 { promiseAll } from '../utils/promises.js'; import { useSelector, useDispatch } from '../utils/redux-utils.js'; type CreateCallSingleKeyserverEndpointSelector = ( keyserverID: string, ) => ServerCallSelectorParams => CallSingleKeyserverEndpoint; type GetBoundSingleKeyserverActionFunc = ( keyserverID: string, actionFunc: SingleKeyserverActionFunc, ) => F; type SingleKeyserverActionFuncSelectorParams = { +callSingleKeyserverEndpoint: CallSingleKeyserverEndpoint, }; type CreateBoundSingleKeyserverActionFuncSelector = ( actionFunc: SingleKeyserverActionFunc, ) => SingleKeyserverActionFuncSelectorParams => F; type GetBoundKeyserverActionFunc = ( actionFunc: ActionFunc, ) => Args => Promise; type CallKeyserverEndpointContextType = { +createCallSingleKeyserverEndpointSelector: CreateCallSingleKeyserverEndpointSelector, +getBoundSingleKeyserverActionFunc: GetBoundSingleKeyserverActionFunc, +getBoundKeyserverActionFunc: GetBoundKeyserverActionFunc, +registerActiveSocket: ( keyserverID: string, socketAPIHandler: ?SocketAPIHandler, ) => mixed, }; 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, +sessionRecoveryInProgress: boolean, +canRecoverSession?: ?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 socketAPIHandlers = React.useRef>( new Map(), ); const registerActiveSocket = React.useCallback( (keyserverID: string, socketAPIHandler: ?SocketAPIHandler) => socketAPIHandlers.current.set(keyserverID, socketAPIHandler), [], ); const bindCookieAndUtilsIntoCallSingleKeyserverEndpoint: ( params: BindServerCallsParams, ) => CallSingleKeyserverEndpoint = React.useCallback(params => { const { dispatch, cookie, urlPrefix, sessionID, currentUserInfo, isSocketConnected, + sessionRecoveryInProgress, canRecoverSession, 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 = canRecoverSession && 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) { + if (!sessionRecoveryInProgress) { // Our cookie seems to be valid return Promise.resolve(null); } + const recoveryAttempts = ongoingRecoveryAttemptsRef.current; + let keyserverRecoveryAttempts = recoveryAttempts.get(keyserverID); + if (!keyserverRecoveryAttempts) { + keyserverRecoveryAttempts = { waitingCalls: [] }; + recoveryAttempts.set(keyserverID, keyserverRecoveryAttempts); + } + const ongoingRecoveryAttempts = keyserverRecoveryAttempts; // Wait to run until we get our new cookie return new Promise(r => - ongoingRecoveryAttempt.waitingCalls.push(r), - ); - }; - // These functions are helpers for cookieInvalidationRecovery, defined below - const attemptToResolveInvalidationHelper = async ( - sessionChange: ClientSessionChange, - ) => { - const newAnonymousCookie = sessionChange.cookie; - const newSessionChange = await resolveKeyserverSessionInvalidation( - dispatch, - newAnonymousCookie, - urlPrefix, - logInActionSources.cookieInvalidationResolutionAttempt, - keyserverID, - ); - - return newSessionChange - ? bindCookieAndUtilsIntoCallSingleKeyserverEndpoint({ - ...params, - cookie: newSessionChange.cookie, - sessionID: newSessionChange.sessionID, - currentUserInfo: newSessionChange.currentUserInfo, - }) - : null; - }; - const attemptToResolveInvalidation = ( - sessionChange: ClientSessionChange, - ) => { - return new Promise( - // eslint-disable-next-line no-async-promise-executor - async (resolve, reject) => { - try { - const newCallSingleKeyserverEndpoint = - await attemptToResolveInvalidationHelper(sessionChange); - const ongoingRecoveryAttempt = - ongoingRecoveryAttemptsRef.current.get(keyserverID); - ongoingRecoveryAttemptsRef.current.delete(keyserverID); - const currentWaitingCalls = - ongoingRecoveryAttempt?.waitingCalls ?? []; - - resolve(newCallSingleKeyserverEndpoint); - - for (const func of currentWaitingCalls) { - func(newCallSingleKeyserverEndpoint); - } - } catch (e) { - reject(e); - } - }, + ongoingRecoveryAttempts.waitingCalls.push(r), ); }; // 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, error: ?string, ) => { if (!canResolveInvalidation) { // When invalidation recovery is supported, we let that code call // setNewSession. When it isn't supported, we call it directly here. // Once usingCommServicesAccessToken is true, we should consider // removing this call... see description of D10952 for details. boundSetNewSession(sessionChange, error); // 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), - ); + + const recoveryAttempts = ongoingRecoveryAttemptsRef.current; + let keyserverRecoveryAttempts = recoveryAttempts.get(keyserverID); + if (!keyserverRecoveryAttempts) { + keyserverRecoveryAttempts = { waitingCalls: [] }; + recoveryAttempts.set(keyserverID, keyserverRecoveryAttempts); + } + if (!sessionRecoveryInProgress) { + dispatch({ + type: setSessionRecoveryInProgressActionType, + payload: { sessionRecoveryInProgress: true, keyserverID }, + }); } - ongoingRecoveryAttemptsRef.current.set(keyserverID, { waitingCalls: [] }); - return attemptToResolveInvalidation(sessionChange); + const ongoingRecoveryAttempts = keyserverRecoveryAttempts; + return new Promise(r => + ongoingRecoveryAttempts.waitingCalls.push(r), + ); }; return ( endpoint: Endpoint, data: Object, options?: ?CallSingleKeyserverEndpointOptions, ) => callSingleKeyserverEndpoint( cookie, boundSetNewSession, waitIfCookieInvalidated, cookieInvalidationRecovery, urlPrefix, sessionID, isSocketConnected, lastCommunicatedPlatformDetails, socketAPIHandlers.current.get(keyserverID), 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 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.sessionRecoveryInProgress, (params: ServerCallSelectorParams) => params.canRecoverSession, (params: ServerCallSelectorParams) => params.lastCommunicatedPlatformDetails, ( dispatch: Dispatch, cookie: ?string, urlPrefix: string, sessionID: ?string, currentUserInfo: ?CurrentUserInfo, isSocketConnected: boolean, sessionRecoveryInProgress: boolean, canRecoverSession: ?boolean, lastCommunicatedPlatformDetails: ?PlatformDetails, ) => bindCookieAndUtilsIntoCallSingleKeyserverEndpoint({ dispatch, cookie, urlPrefix, sessionID, currentUserInfo, isSocketConnected, sessionRecoveryInProgress, canRecoverSession, lastCommunicatedPlatformDetails, keyserverID, }), ), [bindCookieAndUtilsIntoCallSingleKeyserverEndpoint], ); - // SECTION 3: getBoundSingleKeyserverActionFunc + // SECTION 3: getCallSingleKeyserverEndpoint 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 = 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, canRecoverSession: true, }); }, [ createCallSingleKeyserverEndpointSelector, dispatch, currentUserInfo, keyserverCallInfos, ], ); + // SECTION 4: flush waitingCalls when sessionRecoveryInProgress flips to false + + const prevKeyserverCallInfosRef = React.useRef<{ + +[keyserverID: string]: KeyserverCallInfo, + }>(keyserverCallInfos); + React.useEffect(() => { + const sessionRecoveriesConcluded = new Set(); + const prevKeyserverCallInfos = prevKeyserverCallInfosRef.current; + for (const keyserverID in keyserverCallInfos) { + const prevKeyserverCallInfo = prevKeyserverCallInfos[keyserverID]; + if (!prevKeyserverCallInfo) { + continue; + } + const keyserverCallInfo = keyserverCallInfos[keyserverID]; + if ( + !keyserverCallInfo.sessionRecoveryInProgress && + prevKeyserverCallInfo.sessionRecoveryInProgress + ) { + sessionRecoveriesConcluded.add(keyserverID); + } + } + + for (const keyserverID of sessionRecoveriesConcluded) { + const recoveryAttempts = ongoingRecoveryAttemptsRef.current; + const keyserverRecoveryAttempts = recoveryAttempts.get(keyserverID); + if (!keyserverRecoveryAttempts) { + continue; + } + const { waitingCalls } = keyserverRecoveryAttempts; + if (waitingCalls.length === 0) { + continue; + } + + const { cookie } = keyserverCallInfos[keyserverID]; + const hasUserCookie = cookie && cookie.startsWith('user='); + + const boundCallSingleKeyserverEndpoint = hasUserCookie + ? getCallSingleKeyserverEndpoint(keyserverID) + : null; + for (const waitingCall of waitingCalls) { + waitingCall(boundCallSingleKeyserverEndpoint); + } + } + + prevKeyserverCallInfosRef.current = keyserverCallInfos; + }, [keyserverCallInfos, getCallSingleKeyserverEndpoint]); + + // SECTION 5: getBoundSingleKeyserverActionFunc + const createBoundSingleKeyserverActionFuncSelector: CreateBoundSingleKeyserverActionFuncSelector = React.useCallback( 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 = boundSingleKeyserverActionFuncSelectorCacheRef.current.get( keyserverID, ); if (!selector) { selector = createBoundSingleKeyserverActionFuncsCache(); boundSingleKeyserverActionFuncSelectorCacheRef.current.set( keyserverID, selector, ); } const callEndpoint = getCallSingleKeyserverEndpoint(keyserverID); return selector(actionFunc)({ callSingleKeyserverEndpoint: callEndpoint, }); }, [ createBoundSingleKeyserverActionFuncsCache, getCallSingleKeyserverEndpoint, ], ); - // SECTION 4: getBoundKeyserverActionFunc + // SECTION 6: getBoundKeyserverActionFunc const callKeyserverEndpoint = React.useCallback( ( endpoint: Endpoint, requests: { +[keyserverID: string]: ?{ +[string]: mixed } }, options?: ?CallSingleKeyserverEndpointOptions, ) => { const makeCallToSingleKeyserver = (keyserverID: string) => { const boundCallSingleKeyserverEndpoint = getCallSingleKeyserverEndpoint(keyserverID); return boundCallSingleKeyserverEndpoint( endpoint, requests[keyserverID], options, ); }; const promises: { [string]: Promise } = {}; for (const keyserverID in requests) { promises[keyserverID] = makeCallToSingleKeyserver(keyserverID); } return promiseAll(promises); }, [getCallSingleKeyserverEndpoint], ); const keyserverIDs = React.useMemo( () => Object.keys(keyserverCallInfos), [keyserverCallInfos], ); const getBoundKeyserverActionFunc: GetBoundKeyserverActionFunc = React.useMemo( () => _memoize(actionFunc => actionFunc(callKeyserverEndpoint, keyserverIDs)), [callKeyserverEndpoint, keyserverIDs], ); const value = React.useMemo( () => ({ createCallSingleKeyserverEndpointSelector, getBoundSingleKeyserverActionFunc, getBoundKeyserverActionFunc, registerActiveSocket, }), [ createCallSingleKeyserverEndpointSelector, getBoundSingleKeyserverActionFunc, getBoundKeyserverActionFunc, registerActiveSocket, ], ); return ( {props.children} ); } function useCallKeyserverEndpointContext(): CallKeyserverEndpointContextType { const callKeyserverEndpointContext = React.useContext( CallKeyserverEndpointContext, ); invariant( callKeyserverEndpointContext, 'callKeyserverEndpointContext should be set', ); return callKeyserverEndpointContext; } export { CallKeyserverEndpointProvider, useCallKeyserverEndpointContext };