diff --git a/lib/keyserver-conn/call-keyserver-endpoint-provider.react.js b/lib/keyserver-conn/call-keyserver-endpoint-provider.react.js --- a/lib/keyserver-conn/call-keyserver-endpoint-provider.react.js +++ b/lib/keyserver-conn/call-keyserver-endpoint-provider.react.js @@ -1,6 +1,8 @@ // @flow +import invariant from 'invariant'; import _memoize from 'lodash/memoize.js'; +import * as React from 'react'; import { createSelector } from 'reselect'; import { setNewSession } from './keyserver-conn-types.js'; @@ -20,139 +22,25 @@ } from '../utils/call-server-endpoint.js'; import callServerEndpoint from '../utils/call-server-endpoint.js'; +export type ActionFunc = (callServerEndpoint: CallServerEndpoint) => F; +type CreateBoundServerCallsSelectorType = ( + ActionFunc, +) => BindServerCallsParams => F; +type CallKeyserverEndpointContextType = { + +bindCookieAndUtilsIntoCallServerEndpoint: ( + params: BindServerCallsParams, + ) => CallServerEndpoint, + +createBoundServerCallsSelector: CreateBoundServerCallsSelectorType, +}; + +const CallKeyserverEndpointContext: React.Context = + React.createContext(); + let currentlyWaitingForNewCookie = false; let serverEndpointCallsWaitingForNewCookie: (( callServerEndpoint: ?CallServerEndpoint, ) => void)[] = []; -// Third param is optional and gets called with newCookie if we get a new cookie -// Necessary to propagate cookie in cookieInvalidationRecovery below -function bindCookieAndUtilsIntoCallServerEndpoint( - params: BindServerCallsParams, -): CallServerEndpoint { - 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); - } - if (!currentlyWaitingForNewCookie) { - // Our cookie seems to be valid - return Promise.resolve(null); - } - // Wait to run until we get our new cookie - return new Promise(r => - serverEndpointCallsWaitingForNewCookie.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, - ); - - currentlyWaitingForNewCookie = false; - const currentWaitingCalls = serverEndpointCallsWaitingForNewCookie; - serverEndpointCallsWaitingForNewCookie = []; - - 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); - } - if (currentlyWaitingForNewCookie) { - return new Promise(r => - serverEndpointCallsWaitingForNewCookie.push(r), - ); - } - currentlyWaitingForNewCookie = true; - 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, - ); -} - -export type ActionFunc = (callServerEndpoint: CallServerEndpoint) => F; export type BindServerCallsParams = { +dispatch: Dispatch, +cookie: ?string, @@ -164,56 +52,213 @@ +keyserverID: string, }; -// All server calls needs to include some information from the Redux state -// (namely, the cookie). This information is used deep in the server call, -// at the point where callServerEndpoint is called. We don't want to bother -// propagating the cookie (and any future config info that callServerEndpoint -// needs) through to the server calls so they can pass it to callServerEndpoint. -// Instead, we "curry" the cookie onto callServerEndpoint within react-redux's -// connect's mapStateToProps function, and then pass that "bound" -// callServerEndpoint that no longer needs the cookie as a parameter on to -// the server call. -const baseCreateBoundServerCallsSelector = ( - actionFunc: ActionFunc, -): (BindServerCallsParams => F) => - createSelector( - (state: BindServerCallsParams) => state.dispatch, - (state: BindServerCallsParams) => state.cookie, - (state: BindServerCallsParams) => state.urlPrefix, - (state: BindServerCallsParams) => state.sessionID, - (state: BindServerCallsParams) => state.currentUserInfo, - (state: BindServerCallsParams) => state.isSocketConnected, - (state: BindServerCallsParams) => state.lastCommunicatedPlatformDetails, - (state: BindServerCallsParams) => state.keyserverID, - ( - dispatch: Dispatch, - cookie: ?string, - urlPrefix: string, - sessionID: ?string, - currentUserInfo: ?CurrentUserInfo, - isSocketConnected: boolean, - lastCommunicatedPlatformDetails: ?PlatformDetails, - keyserverID: string, +type Props = { + +children: React.Node, +}; +function CallKeyserverEndpointProvider(props: Props): React.Node { + 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); + } + if (!currentlyWaitingForNewCookie) { + // Our cookie seems to be valid + return Promise.resolve(null); + } + // Wait to run until we get our new cookie + return new Promise(r => + serverEndpointCallsWaitingForNewCookie.push(r), + ); + }; + // This function is a helper for the next function defined below + const attemptToResolveInvalidation = async ( + sessionChange: ClientSessionChange, ) => { - const boundCallServerEndpoint = bindCookieAndUtilsIntoCallServerEndpoint({ + const newAnonymousCookie = sessionChange.cookie; + const newSessionChange = await resolveKeyserverSessionInvalidation( dispatch, + newAnonymousCookie, + urlPrefix, + logInActionSources.cookieInvalidationResolutionAttempt, + keyserverID, + ); + + currentlyWaitingForNewCookie = false; + const currentWaitingCalls = serverEndpointCallsWaitingForNewCookie; + serverEndpointCallsWaitingForNewCookie = []; + + 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); + } + if (currentlyWaitingForNewCookie) { + return new Promise(r => + serverEndpointCallsWaitingForNewCookie.push(r), + ); + } + currentlyWaitingForNewCookie = true; + return attemptToResolveInvalidation(sessionChange); + }; + + return ( + endpoint: Endpoint, + data: Object, + options?: ?CallServerEndpointOptions, + ) => + callServerEndpoint( cookie, + boundSetNewSession, + waitIfCookieInvalidated, + cookieInvalidationRecovery, urlPrefix, sessionID, - currentUserInfo, isSocketConnected, lastCommunicatedPlatformDetails, + socketAPIHandler, + endpoint, + data, + dispatch, + options, + loggedIn, keyserverID, - }); - return actionFunc(boundCallServerEndpoint); - }, + ); + }, []); + + // All server calls needs to include some information from the Redux state + // (namely, the cookie). This information is used deep in the server call, + // at the point where callServerEndpoint is called. We don't want to bother + // propagating the cookie (and any future config info that callServerEndpoint + // needs) through to the server calls so they can pass it to + // callServerEndpoint. Instead, we "curry" the cookie onto callServerEndpoint + // within react-redux's connect's mapStateToProps function, and then pass that + // "bound" callServerEndpoint that no longer needs the cookie as a parameter + // on to the server call. + const baseCreateBoundServerCallsSelector = React.useCallback( + (actionFunc: ActionFunc): (BindServerCallsParams => F) => + createSelector( + (state: BindServerCallsParams) => state.dispatch, + (state: BindServerCallsParams) => state.cookie, + (state: BindServerCallsParams) => state.urlPrefix, + (state: BindServerCallsParams) => state.sessionID, + (state: BindServerCallsParams) => state.currentUserInfo, + (state: BindServerCallsParams) => state.isSocketConnected, + (state: BindServerCallsParams) => state.lastCommunicatedPlatformDetails, + (state: BindServerCallsParams) => state.keyserverID, + ( + dispatch: Dispatch, + cookie: ?string, + urlPrefix: string, + sessionID: ?string, + currentUserInfo: ?CurrentUserInfo, + isSocketConnected: boolean, + lastCommunicatedPlatformDetails: ?PlatformDetails, + keyserverID: string, + ) => { + const boundCallServerEndpoint = + bindCookieAndUtilsIntoCallServerEndpoint({ + dispatch, + cookie, + urlPrefix, + sessionID, + currentUserInfo, + isSocketConnected, + lastCommunicatedPlatformDetails, + keyserverID, + }); + return actionFunc(boundCallServerEndpoint); + }, + ), + [bindCookieAndUtilsIntoCallServerEndpoint], ); -type CreateBoundServerCallsSelectorType = ( - ActionFunc, -) => BindServerCallsParams => F; -const createBoundServerCallsSelector: CreateBoundServerCallsSelectorType = - (_memoize(baseCreateBoundServerCallsSelector): any); + const createBoundServerCallsSelector: CreateBoundServerCallsSelectorType = + React.useMemo( + () => _memoize(baseCreateBoundServerCallsSelector), + [baseCreateBoundServerCallsSelector], + ); + + const value = React.useMemo( + () => ({ + bindCookieAndUtilsIntoCallServerEndpoint, + createBoundServerCallsSelector, + }), + [bindCookieAndUtilsIntoCallServerEndpoint, createBoundServerCallsSelector], + ); + + 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) { @@ -221,7 +266,7 @@ } export { - createBoundServerCallsSelector, + CallKeyserverEndpointProvider, + useCallKeyserverEndpointContext, registerActiveSocket, - bindCookieAndUtilsIntoCallServerEndpoint, }; diff --git a/lib/utils/action-utils.js b/lib/utils/action-utils.js --- a/lib/utils/action-utils.js +++ b/lib/utils/action-utils.js @@ -8,7 +8,7 @@ import { type ActionFunc, type BindServerCallsParams, - createBoundServerCallsSelector, + useCallKeyserverEndpointContext, } from '../keyserver-conn/call-keyserver-endpoint-provider.react.js'; import { serverCallStateSelector } from '../selectors/server-calls.js'; @@ -20,6 +20,7 @@ const serverCallState = useSelector( serverCallStateSelector(ashoatKeyserverID), ); + const { createBoundServerCallsSelector } = useCallKeyserverEndpointContext(); return React.useMemo(() => { const { urlPrefix, isSocketConnected } = serverCallState; invariant( @@ -37,7 +38,13 @@ ...paramOverride, keyserverID: ashoatKeyserverID, }); - }, [serverCall, serverCallState, dispatch, paramOverride]); + }, [ + serverCall, + serverCallState, + dispatch, + paramOverride, + createBoundServerCallsSelector, + ]); } export { useServerCall }; diff --git a/lib/utils/keyserver-call.js b/lib/utils/keyserver-call.js --- a/lib/utils/keyserver-call.js +++ b/lib/utils/keyserver-call.js @@ -12,7 +12,7 @@ import { useSelector, useDispatch } from './redux-utils.js'; import { useDerivedObject } from '../hooks/objects.js'; import { - bindCookieAndUtilsIntoCallServerEndpoint, + useCallKeyserverEndpointContext, type BindServerCallsParams, } from '../keyserver-conn/call-keyserver-endpoint-provider.react.js'; import type { PlatformDetails } from '../types/device-types.js'; @@ -39,6 +39,9 @@ // The first argument of the memoized function is used as the map cache key. const baseCreateBoundServerCallsSelector = ( keyserverID: string, + bindCookieAndUtilsIntoCallServerEndpoint: ( + params: BindServerCallsParams, + ) => CallServerEndpoint, ): (BindServerCallsParams => CallServerEndpoint) => createSelector( (state: BindServerCallsParams) => state.dispatch, @@ -71,6 +74,9 @@ type CreateBoundServerCallsSelectorType = ( keyserverID: string, + bindCookieAndUtilsIntoCallServerEndpoint: ( + params: BindServerCallsParams, + ) => CallServerEndpoint, ) => BindServerCallsParams => CallServerEndpoint; const createBoundServerCallsSelector: CreateBoundServerCallsSelectorType = _memoize(baseCreateBoundServerCallsSelector); @@ -130,6 +136,8 @@ currentUserInfo: ?CurrentUserInfo, keyserverCallInfos: { +[keyserverID: string]: KeyserverCallInfo }, ): BindCallKeyserverSelector { + const { bindCookieAndUtilsIntoCallServerEndpoint } = + useCallKeyserverEndpointContext(); return React.useMemo( () => _memoize( @@ -152,6 +160,7 @@ const boundCallServerEndpoint = createBoundServerCallsSelector( keyserverID, + bindCookieAndUtilsIntoCallServerEndpoint, )({ dispatch, currentUserInfo, @@ -180,7 +189,12 @@ return keyserverCall(callKeyserverEndpoint, keyserverIDs); }, ), - [dispatch, currentUserInfo, keyserverCallInfos], + [ + dispatch, + currentUserInfo, + keyserverCallInfos, + bindCookieAndUtilsIntoCallServerEndpoint, + ], ); } diff --git a/native/root.react.js b/native/root.react.js --- a/native/root.react.js +++ b/native/root.react.js @@ -29,6 +29,7 @@ import IntegrityHandler from 'lib/components/integrity-handler.react.js'; import KeyserverConnectionsHandler from 'lib/components/keyserver-connections-handler.js'; import { MediaCacheProvider } from 'lib/components/media-cache-provider.react.js'; +import { CallKeyserverEndpointProvider } from 'lib/keyserver-conn/call-keyserver-endpoint-provider.react.js'; import { TunnelbrokerProvider } from 'lib/tunnelbroker/tunnelbroker-context.js'; import { actionLogger } from 'lib/utils/action-logger.js'; @@ -296,64 +297,66 @@ } return ( - - - - - - - - - - - - - - - - - - - - - - {gated} - - - - - - {navigation} - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + {gated} + + + + + + {navigation} + + + + + + + + + + + + + + + + + + + ); } diff --git a/web/root.js b/web/root.js --- a/web/root.js +++ b/web/root.js @@ -11,6 +11,7 @@ import IntegrityHandler from 'lib/components/integrity-handler.react.js'; import KeyserverConnectionsHandler from 'lib/components/keyserver-connections-handler.js'; +import { CallKeyserverEndpointProvider } from 'lib/keyserver-conn/call-keyserver-endpoint-provider.react.js'; import { reduxLoggerMiddleware } from 'lib/utils/action-logger.js'; import { @@ -42,20 +43,22 @@ const RootProvider = (): React.Node => ( - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + );