Skip to content

Instantly share code, notes, and snippets.

@eloytoro
Created November 30, 2019 15:27
Show Gist options
  • Save eloytoro/067b8b20339faa2c0429942eead1298e to your computer and use it in GitHub Desktop.
Save eloytoro/067b8b20339faa2c0429942eead1298e to your computer and use it in GitHub Desktop.
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