raiden-ts
Version:
Raiden Light Client Typescript/Javascript SDK
206 lines • 8.6 kB
JavaScript
/* eslint-disable @typescript-eslint/no-explicit-any */
import * as t from 'io-ts';
import isMatchWith from 'lodash/isMatchWith';
import { firstValueFrom } from 'rxjs';
import { filter, map, take } from 'rxjs/operators';
import { assert } from '../utils';
import { ErrorCodes, RaidenError } from '../utils/error';
import { BigNumberC } from './types';
/**
* @param args - Args params
* @returns action creator
*/
export function createAction(...args) {
const [type, payloadCodec, metaCodec, error] = args;
const codec = t.readonly(t.type({
type: t.literal(type),
...(payloadCodec ? { payload: payloadCodec } : null),
...(metaCodec ? { meta: metaCodec } : null),
...(error ? { error: t.literal(true) } : null),
}));
const is = process.env['NODE_ENV'] === 'development'
? (action) => codec.is(action)
: (action) => action?.['type'] === type;
return Object.assign(function actionFactory(payload, meta) {
return {
type,
...(payloadCodec ? { payload } : {}),
...(metaCodec ? { meta } : {}),
...(error ? { error } : {}),
};
}, { type, is, codec, error });
}
/**
* Curried typeguard function (arity=2) which validates 2nd param is of type of some ActionCreators
*
* @param ac - Single or array of ActionCreators
* @param args - if an object is passed, verify it, else returns a function which does
* @returns boolean indicating object is of type of action, if passing 2nd argument,
* or typeguard function
*/
export function isActionOf(ac, ...args) {
function _isActionOf(action) {
if (typeof this === 'function')
return this.is(action);
if (Array.isArray(this))
return this.some((a) => _isActionOf.call(a, action));
if (typeof this === 'object')
return _isActionOf.call(Object.values(this), action);
return false;
}
if (args.length > 0)
return _isActionOf.call(ac, args[0]);
return _isActionOf.bind(ac);
}
/**
* Create a set of async actions
*
* Here, meta is first class citizen, as it's required and what links a request with its responses
* (success or failure).
*
* @param args - Arguments tuple; [meta, type] are required, while [request, success an failure]
* are codecs to be used as payloads for the respective ActionCreators
* @returns Async actions
*/
export function createAsyncAction(...args) {
return {
request: createAction(`${args[1]}/request`, args[2], args[0]),
success: createAction(`${args[1]}/success`, args[3], args[0]),
failure: createAction(`${args[1]}/failure`, (args[4] ?? t.unknown), args[0], true),
};
}
/**
* Match a passed meta with an action if returns true if metas are from corresponding actions
*
* curried (arity=2) for action passed as 2nd param.
*
* @param meta - meta base for comparison
* @param args - curried args array
* @returns true if metas are compatible, false otherwise
*/
function matchMeta(meta, ...args) {
const _match = (action) =>
// like isEqual, but for BigNumbers, use .eq
isMatchWith(action.meta, meta, (objVal, othVal) =>
// any is to avoid lodash's issue with undefined-returning isMatchWithCustomizer cb type
BigNumberC.is(objVal) && BigNumberC.is(othVal) ? objVal.eq(othVal) : undefined);
if (args.length)
return _match(args[0]);
return _match;
}
/**
* Given an AsyncActionCreator and a respective 'meta' object, returns a type guard function for
* responses actions (success|failure) matching given 'meta'
*
* This function receives 2-3 params. If it receives 2, it returns the type guard function, to be
* used for filtering. Otherwise, it performs the check on the 3rd param.
*
* @param asyncAction - AsyncActionCreator object
* @param meta - meta object to filter matching actions
* @param args - curried last param
* @returns type guard function to filter deep-equal meta success|failure actions
*/
export function isResponseOf(asyncAction, meta, ...args) {
const _isResponseOf = (action) => isActionOf([asyncAction.success, asyncAction.failure], action) && matchMeta(meta, action);
if (args.length)
return _isResponseOf(args[0]);
return _isResponseOf;
}
/**
* Like isResponseOf, but ignores non-confirmed (or removed by a reorg) success action
*
* Confirmable success actions are emitted twice: first with payload.confirmed=undefined, then with
* either confirmed=true, if tx still present after confirmation blocks, or confirmed=false, if tx
* was removed from blockchain by a reorg.
* This curied helper filter function ensures only one of the later causes a positive filter.
*
* @param asyncAction - AsyncActionCreator object
* @param meta - meta object to filter matching actions
* @param args - curried last param
* @returns type guard function to filter deep-equal meta success|failure actions
*/
export function isConfirmationResponseOf(asyncAction, meta, ...args) {
/**
* @param action - action to check
* @returns boolean indicating whether object is confirmation
*/
function _isConfirmation(action) {
return typeof action?.['payload']?.['confirmed'] === 'boolean';
}
const _isResponseOf = (action) => isResponseOf(asyncAction, meta, action) &&
(asyncAction.failure.is(action) || _isConfirmation(action));
if (args.length)
return _isResponseOf(args[0]);
return _isResponseOf;
}
/**
* Watch a stream of actions and resolves on meta-matching success or rejects on failure
*
* @param asyncAction - async actions object to wait for
* @param meta - meta object of a request to wait for the respective response
* @param action$ - actions stream to watch for responses
* @param confirmed - undefined for any response action, false to filter confirmable actions,
* true for confirmed ones
* @returns Promise which rejects with payload in case of failure, or resolves payload otherwise
*/
export async function asyncActionToPromise(asyncAction, meta, action$, confirmed) {
return firstValueFrom(action$.pipe(filter(confirmed
? isConfirmationResponseOf(asyncAction, meta)
: isResponseOf(asyncAction, meta)), filter((action) => confirmed === undefined ||
!asyncAction.success.is(action) ||
'confirmed' in action.payload), take(1), map((action) => {
if (asyncAction.failure.is(action))
throw action.payload;
else if (action.payload?.confirmed === false)
throw new RaidenError(ErrorCodes.RDN_TRANSACTION_REORG, {
transactionHash: action.payload.txHash,
});
return action.payload;
})), { defaultValue: undefined });
}
// createReducer
/**
* Create a reducer which can be extended with additional actions handlers
*
* Usage:
* const reducer = createReducer(State)
* .handle(action, (s, a): State => ...)
* .handle(...)
* .handle(...);
*
* @param initialState - state for initialization (if no state is passed on reducer call)
* @returns A reducer function, extended with a handle method to extend it
*/
export function createReducer(initialState) {
/**
* Make a reducer function for given handlers
*
* @param handlers - handlers to put into the reducer
* @returns reducer function for given handlers
*/
function makeReducer(handlers) {
const reducer = (state = initialState, action) => {
const handler = handlers[action.type];
if (handler && handler[0].is(action))
return handler[1](state, action); // calls registered handler
return state; // fallback returns unchanged state
};
/**
* Circular dependency on generic params forbids an already handled action from being accepted
*
* @param ac - Single or array of ActionCreators
* @param handler - handler to use
* @returns reducer with the action created incorporated
*/
function handle(ac, handler) {
const arr = Array.isArray(ac) ? ac : [ac];
assert(!arr.some((a) => a.type in handlers), 'Already handled');
return makeReducer(Object.assign({}, handlers, ...arr.map((ac) => ({ [ac.type]: [ac, handler] }))));
}
// grow reducer function with our `handle` extender
return Object.assign(reducer, { handle });
}
// initially makes a reducer which doesn't handle anything (just returns unchanged state)
return makeReducer({});
}
//# sourceMappingURL=actions.js.map