diff --git a/lib/reducers/dm-operations-queue-reducer.js b/lib/reducers/dm-operations-queue-reducer.js --- a/lib/reducers/dm-operations-queue-reducer.js +++ b/lib/reducers/dm-operations-queue-reducer.js @@ -1,6 +1,9 @@ // @flow +import _mapValues from 'lodash/fp/mapValues.js'; + import { + pruneDMOpsQueueActionType, type QueuedDMOperations, queueDMOpsActionType, } from '../types/dm-ops.js'; @@ -11,14 +14,26 @@ action: BaseAction, ): QueuedDMOperations { if (action.type === queueDMOpsActionType) { - const { threadID, operation } = action.payload; + const { threadID, operation, timestamp } = action.payload; return { ...store, operations: { ...store.operations, - [threadID]: [...(store.operations[threadID] ?? []), operation], + [threadID]: [ + ...(store.operations[threadID] ?? []), + { operation, timestamp }, + ], }, }; + } else if (action.type === pruneDMOpsQueueActionType) { + return { + ...store, + operations: _mapValues(operations => + operations.filter( + op => op.timestamp >= action.payload.pruneMaxTimestamp, + ), + )(store.operations), + }; } return store; } diff --git a/lib/shared/dm-ops/dm-ops-queue-pruner.react.js b/lib/shared/dm-ops/dm-ops-queue-pruner.react.js new file mode 100644 --- /dev/null +++ b/lib/shared/dm-ops/dm-ops-queue-pruner.react.js @@ -0,0 +1,37 @@ +// @flow +import * as React from 'react'; + +import { pruneDMOpsQueueActionType } from '../../types/dm-ops.js'; +import { useDispatch } from '../../utils/redux-utils.js'; + +const PRUNING_FREQUENCY = 60 * 60 * 1000; +const FIRST_PRUNING_DELAY = 10 * 60 * 1000; +const QUEUED_OPERATION_TTL = 3 * 24 * 60 * 60 * 1000; + +function DMOpsQueuePruner(): React.Node { + const dispatch = useDispatch(); + + const prune = React.useCallback(() => { + const now = Date.now(); + dispatch({ + type: pruneDMOpsQueueActionType, + payload: { + pruneMaxTime: now - QUEUED_OPERATION_TTL, + }, + }); + }, [dispatch]); + + React.useEffect(() => { + const timeoutID = setTimeout(prune, FIRST_PRUNING_DELAY); + const intervalID = setInterval(prune, PRUNING_FREQUENCY); + + return () => { + clearTimeout(timeoutID); + clearInterval(intervalID); + }; + }, [prune]); + + return null; +} + +export { DMOpsQueuePruner }; diff --git a/lib/shared/dm-ops/process-dm-ops.js b/lib/shared/dm-ops/process-dm-ops.js --- a/lib/shared/dm-ops/process-dm-ops.js +++ b/lib/shared/dm-ops/process-dm-ops.js @@ -45,6 +45,7 @@ payload: { operation: dmOp, threadID: processingCheckResult.reason.threadID, + timestamp: Date.now(), }, }); } diff --git a/lib/tunnelbroker/tunnelbroker-context.js b/lib/tunnelbroker/tunnelbroker-context.js --- a/lib/tunnelbroker/tunnelbroker-context.js +++ b/lib/tunnelbroker/tunnelbroker-context.js @@ -9,6 +9,7 @@ import { PeerToPeerMessageHandler } from './peer-to-peer-message-handler.js'; import type { SecondaryTunnelbrokerConnection } from './secondary-tunnelbroker-connection.js'; import { tunnnelbrokerURL } from '../facts/tunnelbroker.js'; +import { DMOpsQueuePruner } from '../shared/dm-ops/dm-ops-queue-pruner.react.js'; import { IdentityClientContext } from '../shared/identity-client-context.js'; import { tunnelbrokerHeartbeatTimeout } from '../shared/timeouts.js'; import { isWebPlatform } from '../types/device-types.js'; @@ -461,6 +462,7 @@ socketSend={socketSend} /> {children} + ); } diff --git a/lib/types/dm-ops.js b/lib/types/dm-ops.js --- a/lib/types/dm-ops.js +++ b/lib/types/dm-ops.js @@ -313,6 +313,12 @@ export type QueueDMOpsPayload = { +operation: DMOperation, +threadID: string, + +timestamp: number, +}; + +export const pruneDMOpsQueueActionType = 'PRUNE_DM_OPS_QUEUE'; +export type PruneDMOpsQueuePayload = { + +pruneMaxTimestamp: number, }; export const scheduleP2PMessagesActionType = 'SCHEDULE_P2P_MESSAGES'; @@ -323,6 +329,9 @@ export type QueuedDMOperations = { +operations: { - +[threadID: string]: $ReadOnlyArray, + +[threadID: string]: $ReadOnlyArray<{ + +operation: DMOperation, + +timestamp: number, + }>, }, }; 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 @@ -45,6 +45,7 @@ ProcessDMOpsPayload, QueuedDMOperations, QueueDMOpsPayload, + PruneDMOpsQueuePayload, } from './dm-ops.js'; import type { DraftStore } from './draft-types.js'; import type { EnabledApps, SupportedApps } from './enabled-apps.js'; @@ -1581,7 +1582,8 @@ +deviceToken: string, }, } - | { +type: 'QUEUE_DM_OPS', +payload: QueueDMOpsPayload }, + | { +type: 'QUEUE_DM_OPS', +payload: QueueDMOpsPayload } + | { +type: 'PRUNE_DM_OPS_QUEUE', +payload: PruneDMOpsQueuePayload }, }>; export type ActionPayload = ?(Object | Array<*> | $ReadOnlyArray<*> | string);