Skip to content

Commit a2a4f54

Browse files
author
edgar rodriguez
committed
version 1.0.0-beta
2 parents 4e386b6 + 6b5843f commit a2a4f54

33 files changed

+5751
-0
lines changed

features/cache.feature

+41
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
Feature: cache of actions
2+
3+
Feature Description
4+
5+
Scenario: Cach incoming target action
6+
Scenario: Ignore no target action
7+
Scenario: Retry cached actions
8+
Scenario: Remove cached action
9+
Scenario: Remove old actions
10+
11+
Background:
12+
Given target actions config is provided
13+
And a throttle time config is provided
14+
15+
Scenario: target action success within cooling time
16+
Given a target action is dispatched
17+
When an ACK is dispatched within cooling time
18+
Then the cached action is not in the cache store
19+
20+
Scenario: target action fails inside cooling time
21+
Given a target action is dispatched
22+
When an NACK is dispatched within cooling time
23+
Then the cached action is not in the cache store
24+
25+
Scenario: neither ACK nor NACK after cooling time
26+
Given a target action is dispatched
27+
And the cooling time passed
28+
When a retry all action is dispached
29+
Then the cached action is retired
30+
Then and cached action retry time is incremented
31+
32+
Scenario: neither ACK nor NACK after ttl
33+
Given a pass ttl cached action
34+
When a retry action is dispached
35+
Then all pass ttl cached actions were removed from the store
36+
37+
Scenario: need to reset the store
38+
Given a reset is dispatched
39+
Then the cache store is empied
40+
41+
Scenario: retry cached actions with on the fly actions

features/retry.feature

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
Feature: Retry all actions
2+
3+
Feature Description
4+
5+
Scenario: Retry actions
6+
Scenario: On the fly action
7+
Scenario:

jest.config.js

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
module.exports = {
2+
preset: 'ts-jest',
3+
testEnvironment: 'node',
4+
verbose: true
5+
};

lib/cooldown/index.d.ts

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { Config, VisitorNode, CachedAction, CacheableAction, RarAction } from "../core/index";
2+
import { Duration, Moment } from "moment";
3+
export declare const CANCEL_COOLDOWN = "CANCEL_COOLDOWN";
4+
export declare const COOL_AND_RETRY_ALL = "COOL_AND_RETRY_ALL";
5+
export declare type cancelCooldownAction = RarAction & {
6+
[CANCEL_COOLDOWN]: CacheableAction;
7+
};
8+
export declare type coolAndRetryAllAction = RarAction & {
9+
[COOL_AND_RETRY_ALL]: true;
10+
};
11+
export declare type cooldownConfg = {
12+
cooldownTime: Duration;
13+
};
14+
export declare const coolDownUntilKey = "coolDownUntil";
15+
export declare type cooldownWrapAction = {
16+
[coolDownUntilKey]: Moment;
17+
};
18+
export declare function coolDownUntil(wrap: CachedAction<unknown>, config: Config<cooldownConfg, cooldownWrapAction>): Moment;
19+
export declare function coolAndRetryAllActionCreator(): coolAndRetryAllAction;
20+
export declare function cancelCooldownActionCreator(action: CacheableAction): cancelCooldownAction;
21+
export declare function Cooldown(config: Config<cooldownConfg, cooldownWrapAction>): VisitorNode<cooldownWrapAction>;

lib/cooldown/index.js

+52
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
"use strict";
2+
Object.defineProperty(exports, "__esModule", { value: true });
3+
const ramda_1 = require("ramda");
4+
const index_1 = require("../core/index");
5+
const now_1 = require("../now");
6+
exports.CANCEL_COOLDOWN = `CANCEL_COOLDOWN`;
7+
exports.COOL_AND_RETRY_ALL = `COOL_AND_RETRY_ALL`;
8+
exports.coolDownUntilKey = 'coolDownUntil';
9+
function coolDownUntil(wrap, config) {
10+
return now_1.now().add(config.cache[wrap.action.type].cooldownTime);
11+
}
12+
exports.coolDownUntil = coolDownUntil;
13+
function coolAndRetryAllActionCreator() {
14+
return {
15+
type: index_1.REDUX_ACTION_RETRY,
16+
[index_1.RETRY_ALL]: true,
17+
[exports.COOL_AND_RETRY_ALL]: true,
18+
};
19+
}
20+
exports.coolAndRetryAllActionCreator = coolAndRetryAllActionCreator;
21+
function cancelCooldownActionCreator(action) {
22+
return {
23+
type: index_1.REDUX_ACTION_RETRY,
24+
[exports.CANCEL_COOLDOWN]: action
25+
};
26+
}
27+
exports.cancelCooldownActionCreator = cancelCooldownActionCreator;
28+
function Cooldown(config) {
29+
return {
30+
shouldRetryAction(action, cachedAction) {
31+
return (action[exports.COOL_AND_RETRY_ALL])
32+
? true
33+
: now_1.now().isSameOrAfter(cachedAction[exports.coolDownUntilKey]);
34+
},
35+
actionWrapper(action) {
36+
return {
37+
[exports.coolDownUntilKey]: coolDownUntil(action, config)
38+
};
39+
},
40+
reducer: (state, action) => {
41+
if (action.type === index_1.REDUX_ACTION_RETRY && action[exports.CANCEL_COOLDOWN]) {
42+
const cancelCooldownAction = action;
43+
const coincidenceIndex = ramda_1.findIndex((cachedAction => cachedAction.action.meta[index_1.REDUX_ACTION_RETRY].id === cancelCooldownAction[exports.CANCEL_COOLDOWN].meta[index_1.REDUX_ACTION_RETRY].id), state.cache);
44+
return (coincidenceIndex < 0)
45+
? state
46+
: ramda_1.set(ramda_1.lensPath(['cache', coincidenceIndex, exports.coolDownUntilKey]), now_1.now(), state);
47+
}
48+
return state;
49+
}
50+
};
51+
}
52+
exports.Cooldown = Cooldown;

lib/core/index.d.ts

+69
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
/// <reference types="ramda" />
2+
import { Reducer, AnyAction } from 'redux';
3+
export interface State<U> {
4+
cache: CachedAction<U>[];
5+
}
6+
export declare const INITIAL_STATE: {
7+
cache: any[];
8+
};
9+
export interface ShouldDoBasedOnActionAndCachedAction<U> {
10+
(action: AnyAction, cachedAction: CachedAction<U>): boolean;
11+
}
12+
export interface ActionWrapper<U> {
13+
(x: CachedAction<U>): U;
14+
}
15+
export interface VisitorNode<U> {
16+
shouldRetryAction?: ShouldDoBasedOnActionAndCachedAction<U>;
17+
actionWrapper?: ActionWrapper<U>;
18+
reducer?: Reducer<State<U>, AnyAction>;
19+
}
20+
export declare type configExtension<T = {}, U = {}> = (config: Config<T, U>) => VisitorNode<U>;
21+
export declare type cacheConfig<T> = {
22+
[s: string]: {
23+
[P in keyof T]: T[P];
24+
} & {
25+
type: string;
26+
};
27+
};
28+
export interface Config<T, U> {
29+
stateKeyName: string;
30+
cache: cacheConfig<T>;
31+
extensions: configExtension<T, U>[];
32+
}
33+
export declare const REDUX_ACTION_RETRY = "REDUX_ACTION_RETRY";
34+
export declare const RESET = "RESET";
35+
export declare const RETRY_ALL = "RETRY_ALL";
36+
export declare const REMOVE = "REMOVE";
37+
export interface CacheableAction extends AnyAction {
38+
meta: {
39+
[REDUX_ACTION_RETRY]: {
40+
id: string;
41+
};
42+
};
43+
}
44+
export declare type CachedAction<U> = {
45+
[P in keyof U]: U[P];
46+
} & {
47+
action: CacheableAction;
48+
};
49+
export declare type RarAction = {
50+
type: typeof REDUX_ACTION_RETRY;
51+
[RESET]?: true;
52+
[REMOVE]?: CacheableAction;
53+
[RETRY_ALL]?: true;
54+
};
55+
export declare function resetActionCreator(): RarAction;
56+
export declare function removeActionCreator(action: CacheableAction): RarAction;
57+
export declare function retryAllActionCreator(): RarAction;
58+
export declare const cacheLens: {
59+
<T, U>(obj: T): U;
60+
set<T, U, V>(val: T, obj: U): V;
61+
};
62+
export declare function createRetryMechanishm<T, U>(initConfig: Partial<Config<T, U>>): {
63+
stateKeyName: string;
64+
reducer: (state: State<U>, action: AnyAction) => State<U>;
65+
reduxActionRetryMiddleware: ({ getState, dispatch }: {
66+
getState: any;
67+
dispatch: any;
68+
}) => (next: any) => (action: AnyAction) => any;
69+
};

lib/core/index.js

+127
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
"use strict";
2+
Object.defineProperty(exports, "__esModule", { value: true });
3+
const ramda_1 = require("ramda");
4+
exports.INITIAL_STATE = {
5+
cache: []
6+
};
7+
exports.REDUX_ACTION_RETRY = 'REDUX_ACTION_RETRY';
8+
exports.RESET = `RESET`;
9+
exports.RETRY_ALL = `RETRY_ALL`;
10+
exports.REMOVE = `REMOVE`;
11+
function resetActionCreator() {
12+
return {
13+
type: exports.REDUX_ACTION_RETRY,
14+
[exports.RESET]: true
15+
};
16+
}
17+
exports.resetActionCreator = resetActionCreator;
18+
function removeActionCreator(action) {
19+
return {
20+
type: exports.REDUX_ACTION_RETRY,
21+
[exports.REMOVE]: action
22+
};
23+
}
24+
exports.removeActionCreator = removeActionCreator;
25+
function retryAllActionCreator() {
26+
return {
27+
type: exports.REDUX_ACTION_RETRY,
28+
[exports.RETRY_ALL]: true
29+
};
30+
}
31+
exports.retryAllActionCreator = retryAllActionCreator;
32+
exports.cacheLens = ramda_1.lensProp('cache');
33+
function createRetryMechanishm(initConfig) {
34+
const defaultConfig = {
35+
stateKeyName: exports.REDUX_ACTION_RETRY,
36+
cache: {},
37+
extensions: [
38+
upsert,
39+
reset,
40+
remove,
41+
]
42+
};
43+
const config = ramda_1.mergeWithKey((k, l, r) => k === 'extensions' ? [...l, ...r] : r, defaultConfig, initConfig);
44+
const { shouldRetryFunctions, reducers, } = config.extensions.reduce((acc, extension) => {
45+
const c = extension(config);
46+
if (c.shouldRetryAction) {
47+
acc.shouldRetryFunctions.push(c.shouldRetryAction);
48+
}
49+
if (c.reducer) {
50+
acc.reducers.push(c.reducer);
51+
}
52+
return acc;
53+
}, {
54+
shouldRetryFunctions: [],
55+
reducers: [],
56+
});
57+
const shouldRetry = ramda_1.allPass(shouldRetryFunctions);
58+
return {
59+
stateKeyName: config.stateKeyName,
60+
reducer: (state = exports.INITIAL_STATE, action) => {
61+
return reducers.reduce((s, fn) => fn(s, action), state);
62+
},
63+
reduxActionRetryMiddleware: ({ getState, dispatch }) => next => (action) => {
64+
if (action.type === exports.REDUX_ACTION_RETRY && action[exports.RETRY_ALL]) {
65+
const result = next(action);
66+
const cache = ramda_1.path([config.stateKeyName, 'cache'], getState());
67+
cache.forEach(wrap => {
68+
if (shouldRetry(action, wrap)) {
69+
dispatch(wrap.action);
70+
}
71+
});
72+
return result;
73+
}
74+
else {
75+
return next(action);
76+
}
77+
}
78+
};
79+
}
80+
exports.createRetryMechanishm = createRetryMechanishm;
81+
function reset(_) {
82+
return {
83+
reducer: (state = exports.INITIAL_STATE, action) => {
84+
return (action.type === exports.REDUX_ACTION_RETRY && action[exports.RESET])
85+
? exports.INITIAL_STATE
86+
: state;
87+
}
88+
};
89+
}
90+
function remove(_) {
91+
return {
92+
reducer: (state = exports.INITIAL_STATE, action) => {
93+
if (action.type === exports.REDUX_ACTION_RETRY && action[exports.REMOVE]) {
94+
return ramda_1.over(exports.cacheLens, ramda_1.reject(cachedAction => cachedAction.action.meta[exports.REDUX_ACTION_RETRY].id === action[exports.REMOVE].meta[exports.REDUX_ACTION_RETRY].id), state);
95+
}
96+
return state;
97+
}
98+
};
99+
}
100+
function upsert(config) {
101+
const { actionWrapperFuns, } = config.extensions.reduce((acc, extension) => {
102+
if (extension != upsert) {
103+
const c = extension(config);
104+
if (c.actionWrapper) {
105+
acc.actionWrapperFuns.push(c.actionWrapper);
106+
}
107+
}
108+
return acc;
109+
}, {
110+
actionWrapperFuns: [],
111+
});
112+
return {
113+
reducer: (state, action) => {
114+
if (config.cache[action.type]) {
115+
const coincidenceIndex = ramda_1.findIndex((cachedAction => cachedAction.action.meta[exports.REDUX_ACTION_RETRY].id === action.meta[exports.REDUX_ACTION_RETRY].id), state.cache);
116+
const baseActionWrap = (coincidenceIndex < 0)
117+
? { action }
118+
: state.cache[coincidenceIndex];
119+
const actionWrap = ramda_1.converge((...a) => ramda_1.mergeAll(a), [ramda_1.identity, ...actionWrapperFuns])(baseActionWrap);
120+
return ramda_1.over(exports.cacheLens, (cache) => (coincidenceIndex < 0)
121+
? ramda_1.append(actionWrap, cache)
122+
: ramda_1.set(ramda_1.lensIndex(coincidenceIndex), actionWrap)(cache), state);
123+
}
124+
return state;
125+
}
126+
};
127+
}

lib/now.d.ts

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
import moment from 'moment';
2+
export declare function now(): moment.Moment;

lib/now.js

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
"use strict";
2+
var __importDefault = (this && this.__importDefault) || function (mod) {
3+
return (mod && mod.__esModule) ? mod : { "default": mod };
4+
};
5+
Object.defineProperty(exports, "__esModule", { value: true });
6+
const moment_1 = __importDefault(require("moment"));
7+
function now() {
8+
return moment_1.default();
9+
}
10+
exports.now = now;

lib/removeAndRetryAll/index.d.ts

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { CacheableAction, RarAction } from '../core';
2+
export declare const REMOVE_AND_RETRY_ALL = "REMOVE_AND_RETRY_ALL";
3+
export declare type removeAndRetryAllConfg = {};
4+
export declare type removeAndRetryAllWrapAction = {};
5+
export declare function removeAndRetryAllActionCreator(action: CacheableAction): RarAction;

lib/removeAndRetryAll/index.js

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
"use strict";
2+
Object.defineProperty(exports, "__esModule", { value: true });
3+
const core_1 = require("../core");
4+
exports.REMOVE_AND_RETRY_ALL = `REMOVE_AND_RETRY_ALL`;
5+
function removeAndRetryAllActionCreator(action) {
6+
return {
7+
type: core_1.REDUX_ACTION_RETRY,
8+
[core_1.REMOVE]: action,
9+
[core_1.RETRY_ALL]: true
10+
};
11+
}
12+
exports.removeAndRetryAllActionCreator = removeAndRetryAllActionCreator;

lib/timeToLive/index.d.ts

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { Config, VisitorNode, CachedAction } from "../core/index";
2+
import { Duration, Moment } from "moment";
3+
export declare type timeToLiveConfg = {
4+
timeToLive: Duration;
5+
};
6+
export declare const liveUntilKey = "liveUntil";
7+
export declare type timeToLiveWrapAction = {
8+
[liveUntilKey]: Moment;
9+
};
10+
export declare function liveUntil(wrap: CachedAction<unknown>, config: Config<timeToLiveConfg, timeToLiveWrapAction>): Moment;
11+
export declare function TimeToLive(config: Config<timeToLiveConfg, timeToLiveWrapAction>): VisitorNode<timeToLiveWrapAction>;

0 commit comments

Comments
 (0)