diff --git a/lib/hooks/objects.js b/lib/hooks/objects.js new file mode 100644 --- /dev/null +++ b/lib/hooks/objects.js @@ -0,0 +1,70 @@ +// @flow + +import * as React from 'react'; + +type CacheEntry = { + +inVal: I, + +outVal: O, + +derivationSelector: I => O, +}; + +function useDerivedObject( + object: { +[string]: I }, + createDerivationSelector: () => I => O, +): { +[string]: O } { + const cacheRef = React.useRef>>(new Map()); + const prevCreateDerivationSelector = React.useRef<() => I => O>( + createDerivationSelector, + ); + const prevResultRef = React.useRef(); + + return React.useMemo(() => { + if (prevCreateDerivationSelector.current !== createDerivationSelector) { + cacheRef.current = new Map(); + prevCreateDerivationSelector.current = createDerivationSelector; + } + + const cache = cacheRef.current; + + const newCache = new Map>(); + let changeOccurred = Object.keys(object).length !== cache.size; + + const result: { [string]: O } = {}; + for (const key in object) { + const inVal = object[key]; + + const cacheEntry = cache.get(key); + if (!cacheEntry) { + changeOccurred = true; + const derivationSelector = createDerivationSelector(); + const outVal = derivationSelector(inVal); + newCache.set(key, { inVal, outVal, derivationSelector }); + result[key] = outVal; + continue; + } + + if (inVal === cacheEntry.inVal) { + newCache.set(key, cacheEntry); + result[key] = cacheEntry.outVal; + continue; + } + + const { derivationSelector } = cacheEntry; + const outVal = derivationSelector(inVal); + if (outVal !== cacheEntry.outVal) { + changeOccurred = true; + } + newCache.set(key, { inVal, outVal, derivationSelector }); + result[key] = outVal; + } + cacheRef.current = newCache; + + if (!changeOccurred && prevResultRef.current) { + return prevResultRef.current; + } + prevResultRef.current = result; + return result; + }, [object, createDerivationSelector]); +} + +export { useDerivedObject }; 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,6 +12,7 @@ } from './call-server-endpoint.js'; import { promiseAll } from './promises.js'; import { useSelector, useDispatch } from './redux-utils.js'; +import { useDerivedObject } from '../hooks/objects.js'; import type { PlatformDetails } from '../types/device-types.js'; import type { Endpoint } from '../types/endpoints.js'; import type { KeyserverInfo } from '../types/keyserver-types.js'; @@ -78,10 +79,52 @@ +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 BindKeyserverCallParams = { +dispatch: Dispatch, +currentUserInfo: ?CurrentUserInfo, - +keyserverInfos: { +[keyserverID: string]: KeyserverInfoPartial }, + +keyserverCallInfos: { +[keyserverID: string]: KeyserverCallInfo }, }; const bindCallKeyserverEndpointSelector: BindKeyserverCallParams => < @@ -92,11 +135,11 @@ ) => Args => Promise = createSelector( (state: BindKeyserverCallParams) => state.dispatch, (state: BindKeyserverCallParams) => state.currentUserInfo, - (state: BindKeyserverCallParams) => state.keyserverInfos, + (state: BindKeyserverCallParams) => state.keyserverCallInfos, ( dispatch: Dispatch, currentUserInfo: ?CurrentUserInfo, - keyserverInfos: { +[keyserverID: string]: KeyserverInfoPartial }, + keyserverCallInfos: { +[keyserverID: string]: KeyserverCallInfo }, ) => { return _memoize( ( @@ -112,9 +155,9 @@ cookie, urlPrefix, sessionID, - connection, + isSocketConnected, lastCommunicatedPlatformDetails, - } = keyserverInfos[keyserverID]; + } = keyserverCallInfos[keyserverID]; const boundCallServerEndpoint = createBoundServerCallsSelector( keyserverID, @@ -124,7 +167,7 @@ cookie, urlPrefix, sessionID, - isSocketConnected: connection?.status === 'connected', + isSocketConnected, lastCommunicatedPlatformDetails, keyserverID, }); @@ -142,29 +185,43 @@ } return promiseAll(promises); }; - const keyserverIDs = Object.keys(keyserverInfos); + const keyserverIDs = Object.keys(keyserverCallInfos); return keyserverCall(callKeyserverEndpoint, keyserverIDs); }, ); }, ); -export type KeyserverCallParamOverride = Partial; +export type KeyserverCallParamOverride = Partial<{ + +dispatch: Dispatch, + +currentUserInfo: ?CurrentUserInfo, + +keyserverInfos: { +[keyserverID: string]: KeyserverInfoPartial }, +}>; function useKeyserverCall( keyserverCall: ActionFunc, paramOverride?: ?KeyserverCallParamOverride, ): Args => Promise { const dispatch = useDispatch(); + const currentUserInfo = useSelector(state => state.currentUserInfo); + const keyserverInfos = useSelector( state => state.keyserverStore.keyserverInfos, ); - const currentUserInfo = useSelector(state => state.currentUserInfo); - const bindCallKeyserverEndpointToAction = bindCallKeyserverEndpointSelector({ + const baseCombinedInfo = { dispatch, keyserverInfos, currentUserInfo, ...paramOverride, + }; + + const { keyserverInfos: keyserverInfoPartials, ...restCombinedInfo } = + baseCombinedInfo; + const keyserverCallInfos = useKeyserverCallInfos(keyserverInfoPartials); + + const bindCallKeyserverEndpointToAction = bindCallKeyserverEndpointSelector({ + ...restCombinedInfo, + keyserverCallInfos, }); return React.useMemo( () => bindCallKeyserverEndpointToAction(keyserverCall),