diff --git a/lib/actions/holder-actions.js b/lib/actions/holder-actions.js --- a/lib/actions/holder-actions.js +++ b/lib/actions/holder-actions.js @@ -1,7 +1,23 @@ // @flow +import type { BlobHashAndHolder } from '../types/holder-types.js'; +type MultipleBlobHolders = $ReadOnlyArray; + export const storeEstablishedHolderActionType = 'STORE_ESTABLISHED_HOLDER'; -export type StoreEstablishedHolderPayload = { - +blobHash: string, - +holder: string, +export type StoreEstablishedHolderPayload = BlobHashAndHolder; + +export const processHoldersActionTypes = Object.freeze({ + started: 'PROCESS_HOLDERS_STARTED', + success: 'PROCESS_HOLDERS_SUCCESS', + failed: 'PROCESS_HOLDERS_FAILED', +}); +export type ProcessHoldersStartedPayload = { + holdersToAdd: MultipleBlobHolders, + holdersToRemove: MultipleBlobHolders, +}; +export type ProcessHoldersFinishedPayload = { + added: MultipleBlobHolders, + removed: MultipleBlobHolders, + notAdded: MultipleBlobHolders, + notRemoved: MultipleBlobHolders, }; diff --git a/lib/reducers/holder-reducer.js b/lib/reducers/holder-reducer.js --- a/lib/reducers/holder-reducer.js +++ b/lib/reducers/holder-reducer.js @@ -1,9 +1,35 @@ // @flow -import { storeEstablishedHolderActionType } from '../actions/holder-actions.js'; -import { type HolderStore } from '../types/holder-types.js'; +import _mapValues from 'lodash/fp/mapValues.js'; +import _omitBy from 'lodash/fp/omitBy.js'; + +import { + processHoldersActionTypes, + storeEstablishedHolderActionType, +} from '../actions/holder-actions.js'; +import { + type HolderInfo, + type HolderStatus, + type HolderStore, + type BlobHashAndHolder, +} from '../types/holder-types.js'; import type { BaseAction } from '../types/redux-types.js'; +const makeResultToStoreEntryMapper: ( + status: HolderStatus, +) => BlobHashAndHolder => [string, HolderInfo] = + status => + ({ blobHash, holder }) => [blobHash, { holder, status }]; + +const mapResultsToStoreEntries: (status: HolderStatus) => ( + results: $ReadOnlyArray, +) => { + +[blobHash: string]: HolderInfo, +} = status => { + const mapFn = makeResultToStoreEntryMapper(status); + return results => Object.fromEntries(results.map(mapFn)); +}; + function reduceHolderStore( store: HolderStore, action: BaseAction, @@ -17,6 +43,60 @@ [blobHash]: { holder, status: 'ESTABLISHED' }, }, }; + } else if (action.type === processHoldersActionTypes.started) { + // marks processed holders as pending + const { holdersToAdd, holdersToRemove } = action.payload; + const pendingEstablishmentHolders = mapResultsToStoreEntries( + 'PENDING_ESTABLISHMENT', + )(holdersToAdd); + const pendingRemovalHolders = + mapResultsToStoreEntries('PENDING_REMOVAL')(holdersToRemove); + return { + ...store, + storedHolders: { + ...store.storedHolders, + ...pendingEstablishmentHolders, + ...pendingRemovalHolders, + }, + }; + } else if (action.type === processHoldersActionTypes.failed) { + // marks all pending holders as failed + const storedHolders = _mapValues((holderInfo: HolderInfo): HolderInfo => ({ + ...holderInfo, + status: + holderInfo.status === 'PENDING_ESTABLISHMENT' + ? 'NOT_ESTABLISHED' + : holderInfo.status === 'PENDING_REMOVAL' + ? 'NOT_REMOVED' + : holderInfo.status, + }))(store.storedHolders); + return { + ...store, + storedHolders, + }; + } else if (action.type === processHoldersActionTypes.success) { + const { added, removed, notAdded, notRemoved } = action.payload; + + const removedBlobHashes = new Set(removed.map(({ blobHash }) => blobHash)); + const filteredStore = _omitBy((_, blobHash) => + removedBlobHashes.has(blobHash), + )(store.storedHolders); + + const holdersAdded = mapResultsToStoreEntries('ESTABLISHED')(added); + const holdersNotAdded = + mapResultsToStoreEntries('NOT_ESTABLISHED')(notAdded); + const holdersNotRemoved = + mapResultsToStoreEntries('NOT_REMOVED')(notRemoved); + + return { + ...store, + storedHolders: { + ...filteredStore, + ...holdersAdded, + ...holdersNotAdded, + ...holdersNotRemoved, + }, + }; } return store; } diff --git a/lib/reducers/holder-reducer.test.js b/lib/reducers/holder-reducer.test.js --- a/lib/reducers/holder-reducer.test.js +++ b/lib/reducers/holder-reducer.test.js @@ -1,10 +1,19 @@ // @flow import { reduceHolderStore } from './holder-reducer.js'; -import { storeEstablishedHolderActionType } from '../actions/holder-actions.js'; +import { + processHoldersActionTypes, + storeEstablishedHolderActionType, +} from '../actions/holder-actions.js'; import { type HolderStore } from '../types/holder-types.js'; import type { BaseAction } from '../types/redux-types.js'; +const mockLoadingInfo = { + fetchIndex: 0, + trackMultipleRequests: false, + customKeyName: null, +}; + describe('reduceHolderStore', () => { it('STORE_ESTABLISHED_HOLDER', () => { const store: HolderStore = { @@ -20,4 +29,78 @@ foo: { holder: 'bar', status: 'ESTABLISHED' }, }); }); + + it('PROCESS_HOLDERS_STARTED', () => { + const store: HolderStore = { + storedHolders: { + blob1: { holder: 'holder1', status: 'ESTABLISHED' }, + }, + }; + const action: BaseAction = { + type: processHoldersActionTypes.started, + payload: { + holdersToRemove: [{ blobHash: 'blob1', holder: 'holder1' }], + holdersToAdd: [{ blobHash: 'blob2', holder: 'holder2' }], + }, + loadingInfo: mockLoadingInfo, + }; + + expect(reduceHolderStore(store, action).storedHolders).toStrictEqual({ + blob1: { holder: 'holder1', status: 'PENDING_REMOVAL' }, + blob2: { holder: 'holder2', status: 'PENDING_ESTABLISHMENT' }, + }); + }); + + it('PROCESS_HOLDERS_FAILED', () => { + const store: HolderStore = { + storedHolders: { + blob1: { holder: 'holder1', status: 'ESTABLISHED' }, + blob2: { holder: 'holder2', status: 'PENDING_ESTABLISHMENT' }, + blob3: { holder: 'holder3', status: 'PENDING_REMOVAL' }, + }, + }; + const action: BaseAction = { + type: processHoldersActionTypes.failed, + error: true, + payload: new Error('mock error'), + loadingInfo: mockLoadingInfo, + }; + + expect(reduceHolderStore(store, action).storedHolders).toStrictEqual({ + blob1: { holder: 'holder1', status: 'ESTABLISHED' }, + blob2: { holder: 'holder2', status: 'NOT_ESTABLISHED' }, + blob3: { holder: 'holder3', status: 'NOT_REMOVED' }, + }); + }); + + it('PROCESS_HOLDERS_SUCCESS', () => { + const store: HolderStore = { + storedHolders: { + blob1: { holder: 'holder1', status: 'ESTABLISHED' }, + blob2: { holder: 'holder2', status: 'PENDING_ESTABLISHMENT' }, + blob3: { holder: 'holder3', status: 'PENDING_ESTABLISHMENT' }, + blob4: { holder: 'holder4', status: 'PENDING_REMOVAL' }, + blob5: { holder: 'holder5', status: 'PENDING_REMOVAL' }, + }, + }; + + const action: BaseAction = { + type: processHoldersActionTypes.success, + payload: { + added: [{ blobHash: 'blob2', holder: 'holder2' }], + notAdded: [{ blobHash: 'blob3', holder: 'holder3' }], + removed: [{ blobHash: 'blob4', holder: 'holder4' }], + notRemoved: [{ blobHash: 'blob5', holder: 'holder5' }], + }, + loadingInfo: mockLoadingInfo, + }; + + expect(reduceHolderStore(store, action).storedHolders).toStrictEqual({ + blob1: { holder: 'holder1', status: 'ESTABLISHED' }, + blob2: { holder: 'holder2', status: 'ESTABLISHED' }, + blob3: { holder: 'holder3', status: 'NOT_ESTABLISHED' }, + // blob4 removed + blob5: { holder: 'holder5', status: 'NOT_REMOVED' }, + }); + }); }); diff --git a/lib/types/holder-types.js b/lib/types/holder-types.js --- a/lib/types/holder-types.js +++ b/lib/types/holder-types.js @@ -22,3 +22,8 @@ +type: 'establish_holder' | 'remove_holder', +blobHash: string, }; + +export type BlobHashAndHolder = { + +blobHash: string, + +holder: string, +}; diff --git a/lib/types/redux-types.js b/lib/types/redux-types.js --- a/lib/types/redux-types.js +++ b/lib/types/redux-types.js @@ -164,7 +164,11 @@ SetDeviceTokenActionPayload, SetDeviceTokenStartedPayload, } from '../actions/device-actions.js'; -import type { StoreEstablishedHolderPayload } from '../actions/holder-actions.js'; +import type { + ProcessHoldersStartedPayload, + ProcessHoldersFinishedPayload, + StoreEstablishedHolderPayload, +} from '../actions/holder-actions.js'; import type { UpdateConnectionStatusPayload, SetLateResponsePayload, @@ -1613,6 +1617,22 @@ | { +type: 'STORE_ESTABLISHED_HOLDER', +payload: StoreEstablishedHolderPayload, + } + | { + +type: 'PROCESS_HOLDERS_STARTED', + +payload: ProcessHoldersStartedPayload, + +loadingInfo: LoadingInfo, + } + | { + +type: 'PROCESS_HOLDERS_FAILED', + +error: true, + +payload: Error, + +loadingInfo: LoadingInfo, + } + | { + +type: 'PROCESS_HOLDERS_SUCCESS', + +payload: ProcessHoldersFinishedPayload, + +loadingInfo: LoadingInfo, }, }>;