Created
November 30, 2019 15:27
-
-
Save eloytoro/067b8b20339faa2c0429942eead1298e to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import _ from 'lodash'; | |
import { connect } from 'react-redux'; | |
import { runSaga } from 'redux-saga'; | |
import { call, put } from 'redux-saga/effects'; | |
import { defer } from './async'; | |
import { Result, Enum } from './enums'; | |
/** | |
* latestAction(saga) | |
* Creates an action creator that will fire the saga every time its dispatched, the action creator | |
* will return a promise that resolves to the returned value of the saga. | |
* Its important to note latestAction only allows for one thread to run, and will cancel any | |
* ongoing executions every time its called again. | |
* For examples on how the sagas are cancelled on multiple executions see the test cases | |
*/ | |
export const latestAction = saga => { | |
let deferred; | |
let currentTask; | |
return (...args) => (dispatch, getState) => { | |
if (currentTask) { | |
currentTask.cancel(); | |
} else { | |
deferred = defer(); | |
} | |
const options = { | |
dispatch, | |
getState, | |
onError: err => { | |
currentTask = null; | |
deferred.reject(err); | |
} | |
}; | |
const task = (currentTask = runSaga(options, saga, ...args)); | |
task.toPromise().then(result => { | |
if (currentTask === task) { | |
currentTask = null; | |
deferred.resolve(result); | |
} | |
// for some reason errors are propagated twice, catch them with noop to stop this. | |
}, _.noop); | |
return deferred.promise; | |
}; | |
}; | |
/** | |
* everyAction(saga) | |
* Creates an action creator that will fire the saga every time its dispatched, the action creator | |
* will return a promise that resolves to the returned value of the saga. | |
*/ | |
export const everyAction = saga => { | |
return (...args) => (dispatch, getState) => { | |
const deferred = defer(); | |
const options = { | |
dispatch, | |
getState, | |
onError: deferred.reject | |
}; | |
const task = runSaga(options, saga, ...args); | |
task | |
.toPromise() | |
// for some reason errors are propagated twice, catch them with noop to stop this. | |
.then(deferred.resolve, _.noop); | |
return deferred.promise; | |
}; | |
}; | |
/** | |
* singleAction(saga) | |
* Creates an action creator that will fire the saga when its dispatched, but all following | |
* dispatches while the saga is running will be ignored until the saga finishes. Useful preventing | |
* firing the same saga twice at the same time. | |
* The action creator will return a promise that resolves to the returned value of the saga. | |
*/ | |
export const singleAction = saga => { | |
let deferred; | |
let running = false; | |
return (...args) => (dispatch, getState) => { | |
if (!running) { | |
running = true; | |
deferred = defer(); | |
const options = { | |
dispatch, | |
getState, | |
onError: err => { | |
running = false; | |
deferred.reject(err); | |
} | |
}; | |
const task = runSaga(options, saga, ...args); | |
task.toPromise().then(result => { | |
running = false; | |
deferred.resolve(result); | |
}, _.noop); | |
} | |
return deferred.promise; | |
}; | |
}; | |
export const createAction = (type, payloadCreator = _.identity) => { | |
return (...args) => ({ | |
type, | |
payload: payloadCreator(...args) | |
}); | |
}; | |
export const createReducer = (initialState, map) => ( | |
state = initialState, | |
action | |
) => { | |
if (action.type in map) { | |
return map[action.type](state, action.payload); | |
} | |
return state; | |
}; | |
export const Connect = connect((state, props) => props.mapStateToProps(state))( | |
({ children, ...props }) => children(props) | |
); | |
export const mapToList = map => | |
Object.keys(map).reduce((list, key) => list.concat(map[key]), []); | |
export const batch = actions => { | |
const map = {}; | |
for (const action of actions) { | |
if (action.type in map) { | |
map[action.type] += 1; | |
} else { | |
map[action.type] = 1; | |
} | |
} | |
return { | |
type: `@@BATCH[${Object.keys(map) | |
.map(type => `${type}(x${map[type]})`) | |
.join(',')}]`, | |
payload: actions | |
}; | |
}; | |
/** | |
* withBatch(reducer): reducer | |
* Enhances the reducer to accept action batches. These allow you to reduce the state with several actions | |
* with a single dispatch. | |
*/ | |
export const withBatch = reducer => (state, action) => { | |
if (/^@@BATCH/.test(action.type)) { | |
return action.payload.reduce(reducer, state); | |
} | |
return reducer(state, action); | |
}; | |
/** | |
* bufferFormActions | |
* Useful middleware for batching all of those REGISTER_FIELD field actions that redux-form fires. | |
* Increases performance significantly for large numbers of fields | |
*/ | |
export const bufferFormActions = store => { | |
let actionQueue = []; | |
const debounceBatch = _.debounce(() => { | |
store.dispatch(batch(actionQueue)); | |
actionQueue = []; | |
}); | |
return next => action => { | |
if (/^@@redux-form\/(UN)?REGISTER_FIELD/.test(action.type)) { | |
actionQueue.push(action); | |
debounceBatch(); | |
} else { | |
return next(action); | |
} | |
}; | |
}; | |
/** | |
* match(value, map): any | |
* Pattern matches the given `value` to its corresponding enum type name in the `map` object. | |
* Use the special case `_` for handling any other case. | |
* E.g. | |
* ```js | |
* const msg = match(Result.Ok('this result is'), { | |
* Ok(msg) { | |
* return `${msg} Ok`; | |
* }, | |
* _() { | |
* return `${msg) Unknown`; | |
* } | |
* }); | |
* ``` | |
*/ | |
export const match = (value, map) => { | |
if (value instanceof Enum) { | |
return call(map[value.constructor.match] || map._, value.val); | |
} | |
throw new TypeError(`Can't match non enum value ${JSON.stringify(value)}`); | |
}; | |
let bufferList = []; | |
export const buffer = action => { | |
if (bufferList.length === 0) { | |
throw new TypeError('Trying to buffer actions outside of transaction'); | |
} | |
bufferList[bufferList.length - 1].push(action); | |
}; | |
/** | |
* op(fn, ...args): Result | |
* Effect that has the same use as `call` but will always return a `Result`, never throws. | |
*/ | |
export const op = (fn, ...args) => | |
call(function*() { | |
try { | |
return Result.Ok(yield call(fn, ...args)); | |
} catch (err) { | |
return Result.Err(err); | |
} | |
}); | |
/** | |
* tx(saga): saga | |
* Enhances the saga with transactions. These allow you to `buffer` actions instead of using `put`. | |
* Buffered actions will only be dispatched _after_ the saga completes succesfully. | |
*/ | |
export const tx = saga => | |
function*(...args) { | |
bufferList.push([]); | |
const result = yield op(saga, ...args); | |
const actions = bufferList.pop(); | |
if (result.isOk()) { | |
if (actions.length === 1) { | |
yield put(actions[0]); | |
} else if (actions.length > 1) { | |
yield put(batch(actions)); | |
} | |
return result.ok().unwrap(); | |
} else { | |
throw result.err().unwrap(); | |
} | |
}; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment