diff --git a/lib/keyserver-conn/call-keyserver-endpoint-provider.react.js b/lib/keyserver-conn/call-keyserver-endpoint-provider.react.js index 031d19288..55c614fa6 100644 --- a/lib/keyserver-conn/call-keyserver-endpoint-provider.react.js +++ b/lib/keyserver-conn/call-keyserver-endpoint-provider.react.js @@ -1,264 +1,263 @@ // @flow import invariant from 'invariant'; import * as React from 'react'; import { createSelector } from 'reselect'; import { setNewSession } 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 { CallServerEndpoint, CallServerEndpointOptions, } from '../utils/call-server-endpoint.js'; import callServerEndpoint from '../utils/call-server-endpoint.js'; -export type ActionFunc = (callServerEndpoint: CallServerEndpoint) => F; type CreateCallSingleKeyserverEndpointSelector = ( keyserverID: string, ) => ServerCallSelectorParams => CallServerEndpoint; type CallKeyserverEndpointContextType = { +createCallSingleKeyserverEndpointSelector: CreateCallSingleKeyserverEndpointSelector, }; const CallKeyserverEndpointContext: React.Context = React.createContext(); type OngoingRecoveryAttempt = { +waitingCalls: Array<(callServerEndpoint: ?CallServerEndpoint) => 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 { const ongoingRecoveryAttemptsRef = React.useRef< Map, >(new Map()); const bindCookieAndUtilsIntoCallServerEndpoint: ( params: BindServerCallsParams, ) => CallServerEndpoint = 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 callServerEndpoint 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 callServerEndpoint 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 newCallServerEndpoint = newSessionChange ? bindCookieAndUtilsIntoCallServerEndpoint({ ...params, cookie: newSessionChange.cookie, sessionID: newSessionChange.sessionID, currentUserInfo: newSessionChange.currentUserInfo, }) : null; for (const func of currentWaitingCalls) { func(newCallServerEndpoint); } return newCallServerEndpoint; }; // If this function is called, callServerEndpoint 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 callServerEndpoint 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?: ?CallServerEndpointOptions, ) => callServerEndpoint( cookie, boundSetNewSession, waitIfCookieInvalidated, cookieInvalidationRecovery, urlPrefix, sessionID, isSocketConnected, lastCommunicatedPlatformDetails, socketAPIHandler, endpoint, data, dispatch, options, loggedIn, keyserverID, ); }, []); // For each keyserver, we have a set of params that configure our connection // to it. These params get bound into callServerEndpoint before it's passed to // an ActionFunc. This helper function lets us create a selector for a given // keyserverID that will regenerate the bound callServerEndpoint function only // if one of the params changes. This lets us skip some React render cycles. const createCallSingleKeyserverEndpointSelector = React.useCallback( (keyserverID: string): (ServerCallSelectorParams => CallServerEndpoint) => 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, ) => bindCookieAndUtilsIntoCallServerEndpoint({ dispatch, cookie, urlPrefix, sessionID, currentUserInfo, isSocketConnected, lastCommunicatedPlatformDetails, keyserverID, }), ), [bindCookieAndUtilsIntoCallServerEndpoint], ); const value = React.useMemo( () => ({ createCallSingleKeyserverEndpointSelector, }), [createCallSingleKeyserverEndpointSelector], ); 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 d4543080e..2cbf83210 100644 --- a/lib/utils/action-utils.js +++ b/lib/utils/action-utils.js @@ -1,50 +1,52 @@ // @flow import invariant from 'invariant'; import * as React from 'react'; +import type { CallServerEndpoint } from './call-server-endpoint.js'; import { useSelector, useDispatch } from './redux-utils.js'; import { ashoatKeyserverID } from './validation-utils.js'; import { - type ActionFunc, type ServerCallSelectorParams, useCallKeyserverEndpointContext, } from '../keyserver-conn/call-keyserver-endpoint-provider.react.js'; import { serverCallStateSelector } from '../selectors/server-calls.js'; +type ActionFunc = (callServerEndpoint: CallServerEndpoint) => F; + function useServerCall( serverCall: ActionFunc, paramOverride?: ?Partial, ): F { const dispatch = useDispatch(); const serverCallState = useSelector( serverCallStateSelector(ashoatKeyserverID), ); const { createCallSingleKeyserverEndpointSelector } = useCallKeyserverEndpointContext(); const selector = React.useMemo( () => createCallSingleKeyserverEndpointSelector(ashoatKeyserverID), [createCallSingleKeyserverEndpointSelector], ); return React.useMemo(() => { const { urlPrefix, isSocketConnected } = serverCallState; invariant( !!urlPrefix && isSocketConnected !== undefined && isSocketConnected !== null, 'keyserver missing from keyserverStore', ); const callServerEndpoint = selector({ ...serverCallState, urlPrefix, isSocketConnected, dispatch, ...paramOverride, }); return serverCall(callServerEndpoint); }, [serverCall, serverCallState, dispatch, paramOverride, selector]); } export { useServerCall }; diff --git a/lib/utils/keyserver-call.js b/lib/utils/keyserver-call.js index 304347ce6..8970f7913 100644 --- a/lib/utils/keyserver-call.js +++ b/lib/utils/keyserver-call.js @@ -1,196 +1,196 @@ // @flow import _memoize from 'lodash/memoize.js'; import * as React from 'react'; import { createSelector } from 'reselect'; import type { CallServerEndpoint, CallServerEndpointOptions, } from './call-server-endpoint.js'; import { promiseAll } from './promises.js'; import { useSelector, useDispatch } from './redux-utils.js'; import { useDerivedObject } from '../hooks/objects.js'; import { useCallKeyserverEndpointContext } from '../keyserver-conn/call-keyserver-endpoint-provider.react.js'; import type { PlatformDetails } from '../types/device-types.js'; import type { Endpoint } from '../types/endpoints.js'; import type { KeyserverInfo } from '../types/keyserver-types.js'; import type { Dispatch } from '../types/redux-types.js'; import type { CurrentUserInfo } from '../types/user-types.js'; export type CallKeyserverEndpoint = ( endpoint: Endpoint, requests: { +[keyserverID: string]: ?{ +[string]: mixed } }, options?: ?CallServerEndpointOptions, ) => Promise<{ +[keyserverID: string]: any }>; -export type ActionFunc = ( +type ActionFunc = ( callServerEndpoint: CallKeyserverEndpoint, // The second argument is only used in actions that call all keyservers, // and the request to all keyservers are exactly the same. // An example of such action is fetchEntries. allKeyserverIDs: $ReadOnlyArray, ) => Args => Promise; type KeyserverInfoPartial = $ReadOnly<{ ...Partial, +urlPrefix: $PropertyType, }>; type KeyserverCallInfo = { +cookie: ?string, +urlPrefix: string, +sessionID: ?string, +isSocketConnected: boolean, +lastCommunicatedPlatformDetails: ?PlatformDetails, }; const createKeyserverCallSelector: () => KeyserverInfoPartial => KeyserverCallInfo = () => createSelector( (keyserverInfo: KeyserverInfoPartial) => keyserverInfo.cookie, (keyserverInfo: KeyserverInfoPartial) => keyserverInfo.urlPrefix, (keyserverInfo: KeyserverInfoPartial) => keyserverInfo.sessionID, (keyserverInfo: KeyserverInfoPartial) => keyserverInfo.connection?.status === 'connected', (keyserverInfo: KeyserverInfoPartial) => keyserverInfo.lastCommunicatedPlatformDetails, ( cookie: ?string, urlPrefix: string, sessionID: ?string, isSocketConnected: boolean, lastCommunicatedPlatformDetails: ?PlatformDetails, ) => ({ cookie, urlPrefix, sessionID, isSocketConnected, lastCommunicatedPlatformDetails, }), ); function useKeyserverCallInfos(keyserverInfos: { +[keyserverID: string]: KeyserverInfoPartial, }): { +[keyserverID: string]: KeyserverCallInfo } { return useDerivedObject( keyserverInfos, createKeyserverCallSelector, ); } type BindCallKeyserverSelector = ( keyserverCall: ActionFunc, ) => Args => Promise; function useBindCallKeyserverEndpointSelector( dispatch: Dispatch, currentUserInfo: ?CurrentUserInfo, keyserverCallInfos: { +[keyserverID: string]: KeyserverCallInfo }, ): BindCallKeyserverSelector { const { createCallSingleKeyserverEndpointSelector } = useCallKeyserverEndpointContext(); const getCallSingleKeyserverEndpointSelector = React.useMemo( () => _memoize(createCallSingleKeyserverEndpointSelector), [createCallSingleKeyserverEndpointSelector], ); return React.useMemo( () => _memoize( ( keyserverCall: ActionFunc, ): (Args => Promise) => { const callKeyserverEndpoint = ( endpoint: Endpoint, requests: { +[keyserverID: string]: ?{ +[string]: mixed } }, options?: ?CallServerEndpointOptions, ) => { const bindCallKeyserverEndpoint = (keyserverID: string) => { const { cookie, urlPrefix, sessionID, isSocketConnected, lastCommunicatedPlatformDetails, } = keyserverCallInfos[keyserverID]; const boundCallServerEndpoint = getCallSingleKeyserverEndpointSelector(keyserverID)({ dispatch, currentUserInfo, cookie, urlPrefix, sessionID, isSocketConnected, lastCommunicatedPlatformDetails, }); return boundCallServerEndpoint( endpoint, requests[keyserverID], options, ); }; const promises: { [string]: Promise } = {}; for (const keyserverID in requests) { promises[keyserverID] = bindCallKeyserverEndpoint(keyserverID); } return promiseAll(promises); }; const keyserverIDs = Object.keys(keyserverCallInfos); return keyserverCall(callKeyserverEndpoint, keyserverIDs); }, ), [ dispatch, currentUserInfo, keyserverCallInfos, getCallSingleKeyserverEndpointSelector, ], ); } export type KeyserverCallParamOverride = Partial<{ +dispatch: Dispatch, +currentUserInfo: ?CurrentUserInfo, +keyserverInfos: { +[keyserverID: string]: KeyserverInfoPartial }, }>; function useKeyserverCall( keyserverCall: ActionFunc, paramOverride?: ?KeyserverCallParamOverride, ): Args => Promise { const baseDispatch = useDispatch(); const baseCurrentUserInfo = useSelector(state => state.currentUserInfo); const keyserverInfos = useSelector( state => state.keyserverStore.keyserverInfos, ); const baseCombinedInfo = { dispatch: baseDispatch, currentUserInfo: baseCurrentUserInfo, keyserverInfos, ...paramOverride, }; const { dispatch, currentUserInfo, keyserverInfos: keyserverInfoPartials, } = baseCombinedInfo; const keyserverCallInfos = useKeyserverCallInfos(keyserverInfoPartials); const bindCallKeyserverEndpointToAction = useBindCallKeyserverEndpointSelector( dispatch, currentUserInfo, keyserverCallInfos, ); return React.useMemo( () => bindCallKeyserverEndpointToAction(keyserverCall), [bindCallKeyserverEndpointToAction, keyserverCall], ); } export { useKeyserverCall };