raiden-ts
Version:
Raiden Light Client Typescript/Javascript SDK
340 lines • 17.1 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.partitionMap = exports.dispatchRequestAndGetResponse = exports.catchAndLog = exports.withMergeFrom = exports.concatBuffer = exports.timeoutFirst = exports.lastMap = exports.completeWith = exports.takeIf = exports.retryAsync$ = exports.retryWhile = exports.repeatUntil = exports.distinctRecordValues = exports.pluckDistinct = void 0;
const constant_1 = __importDefault(require("lodash/constant"));
const rxjs_1 = require("rxjs");
const operators_1 = require("rxjs/operators");
const actions_1 = require("./actions");
const error_1 = require("./error");
const types_1 = require("./types");
/**
* Maps each source value (an object) to its specified nested property,
* and emits only if the value changed since last emission
*
* It's a combination of `pluck` and `distinctUntilChanged` operators.
*
* @param properties - The nested properties to pluck from each source value (an object).
* @returns A new Observable of property values from the source values.
*/
const pluckDistinct = (...properties) => (0, rxjs_1.pipe)((0, operators_1.pluck)(...properties), (0, operators_1.distinctUntilChanged)());
exports.pluckDistinct = pluckDistinct;
/**
* Creates an operator to output changed values unique by key ([key, value] tuples)
* It's equivalent to (from(Object.entries) + distinct), but uses key to prevent memory leak
*
* @param compareFn - Function to compare equality between two values, default to === (reference)
* @returns Operator to map from a record to changed values (all on first)
*/
function distinctRecordValues(compareFn = (prev, new_) => prev === new_) {
return (0, rxjs_1.pipe)((0, operators_1.distinctUntilChanged)(), (0, operators_1.mergeMap)((map) => Object.entries(map)),
/* this scan stores a reference to each [key,value] in 'acc', and emit as 'changed' iff it
* changes from last time seen. It relies on value references changing only if needed */
(0, operators_1.scan)(({ acc }, [key, value]) =>
// if ref didn't change, emit previous accumulator, without 'changed' value
compareFn(acc[key], value)
? { acc }
: // else, update ref in 'acc' and emit value in 'changed' prop
{ acc: { ...acc, [key]: value }, changed: [key, value] }, { acc: {} }), (0, operators_1.pluck)('changed'), (0, operators_1.filter)(types_1.isntNil));
}
exports.distinctRecordValues = distinctRecordValues;
/**
* Operator to repeat-subscribe an input observable until a notifier emits
*
* @param notifier - Notifier observable to stop repeating
* @param delayMs - Delay between retries or an Iterator of delays; in milliseconds
* @returns Monotype operator function
*/
function repeatUntil(notifier, delayMs = 30e3) {
// Resubscribe/retry every 30s or yielded ms after messageSend succeeds
// Notice first (or any) messageSend.request can wait for a long time before succeeding, as it
// waits for address's user in transport to be online and joined room before actually
// sending the message. That's why repeatWhen emits/resubscribe only some time after
// sendOnceAndWaitSent$ completes, instead of a plain 'interval'
return (0, rxjs_1.pipe)((0, operators_1.repeatWhen)((completed$) => completed$.pipe((0, operators_1.map)(() => {
if (typeof delayMs === 'number')
return delayMs;
const next = delayMs.next();
return !next.done ? next.value : -1;
}), (0, operators_1.takeWhile)((value) => value >= 0), // stop repeatWhen when done
(0, operators_1.switchMap)((value) => (0, rxjs_1.timer)(value)))), (0, operators_1.takeUntil)(notifier));
}
exports.repeatUntil = repeatUntil;
// guard an Iterable between an iterable and iterator union
function isIterable(interval) {
return Symbol.iterator in interval;
}
/**
* Operator to retry/re-subscribe input$ until a stopPredicate returns truthy or delayMs iterator
* completes, waiting delayMs milliseconds between retries.
* Input observable must be re-subscribable/retriable.
*
* @param interval - Interval, iterable or iterator of intervals to wait between retries;
* if it's an iterable, it resets (iterator recreated) if input$ emits
* @param options - shouldRetryError options, conditions are ANDed
* @returns Operator function to retry if stopPredicate not truthy waiting between retries
*/
function retryWhile(interval, options = {}) {
let iter;
if (options.log)
options = { ...options, log: options.log.bind(null, 'retryWhile') };
let shouldRetry;
return (input$) => (0, rxjs_1.defer)(() => {
iter = undefined;
shouldRetry = (0, error_1.shouldRetryError)(options);
return (0, rxjs_1.from)(input$).pipe(
// if input$ emits, reset iter (only useful if delayMs is an Iterable) and shouldRetry func
(0, operators_1.tap)(() => {
iter = undefined;
shouldRetry = (0, error_1.shouldRetryError)(options);
}), (0, operators_1.retryWhen)((error$) => error$.pipe((0, operators_1.mergeMap)((error) => {
let delayMs;
if (typeof interval === 'number')
delayMs = interval;
else {
if (!iter) {
if (isIterable(interval))
iter = interval[Symbol.iterator]();
else
iter = interval;
}
const next = iter.next();
delayMs = !next.done ? next.value : -1;
}
if (delayMs <= 0 || !shouldRetry(error))
throw error;
return (0, rxjs_1.timer)(delayMs);
}))));
});
}
exports.retryWhile = retryWhile;
/**
* Receives an async function and returns an observable which will retry it every interval until it
* resolves, or throw if it can't succeed after 10 retries.
* It is needed e.g. on provider methods which perform RPC requests directly, as they can fail
* temporarily due to network errors, so they need to be retried for a while.
* JsonRpcProvider._doPoll also catches, suppresses & retry
*
* @param func - An async function (e.g. a Promise factory, like a defer callback)
* @param retryWhileArgs - Rest arguments as received by [[retryWhile]] operator
* @returns Observable version of async function, with retries
*/
function retryAsync$(func, ...retryWhileArgs) {
return (0, rxjs_1.defer)(func).pipe(retryWhile(...retryWhileArgs));
}
exports.retryAsync$ = retryAsync$;
/**
* RxJS operator to keep subscribed to input$ if condition is truty (or falsy, if negated),
* unsubscribes from source if cond$ becomes falsy, and re-subscribes if it becomes truty again
* (input$ must be re-subscribable). While subscribed to source$, completes when source$ completes,
* otherwise, when cond$ completes (since source$ isn't subscribed then), so make sure cond$
* completes too when desired, or the output observable may hang until unsubscribed.
*
* @param cond$ - Condition observable
* @param negate - Whether to negate condition
* (i.e. keep subscribed while falsy, unsubscribe when truty)
* @returns monotype operator to unsubscribe and re-subscribe to source/input based on confition
*/
function takeIf(cond$, negate = false) {
return (source$) => {
const completed$ = new rxjs_1.Subject();
return cond$.pipe((0, operators_1.map)((cond) => (negate ? !cond : !!cond)), (0, operators_1.distinctUntilChanged)(), (0, operators_1.takeUntil)(completed$), (0, operators_1.switchMap)((cond) => {
if (!cond)
return rxjs_1.EMPTY;
return source$.pipe((0, operators_1.tap)({ complete: () => completed$.next(true) }));
}));
};
}
exports.takeIf = takeIf;
/**
* Complete an input when another observable completes
*
* @param complete$ - Observable which will complete input when completed
* @param delayMs - Delay unsubscribing source by some time after complete$ completes
* @returns Operator returning observable mirroring input, but completes when complete$ completes
*/
function completeWith(complete$, delayMs) {
return (0, rxjs_1.pipe)((0, operators_1.takeUntil)(complete$.pipe(lastMap((0, constant_1.default)(delayMs === undefined ? (0, rxjs_1.of)(0) : (0, rxjs_1.timer)(delayMs))))));
}
exports.completeWith = completeWith;
/**
* Like a mergeMap which only subscribes to the inner observable once the input completes;
* Intermediary values are ignored; project receives optionally the last value emitted by input,
* or null if no value was emitted
*
* @param project - callback to generate the inner observable, receives last emitted value or null
* @returns Operator returning observable mirroring inner observable
*/
function lastMap(project) {
return (0, rxjs_1.pipe)((0, operators_1.last)(undefined, null), (0, operators_1.mergeMap)(project));
}
exports.lastMap = lastMap;
/**
* Like timeout rxjs operator, but applies only on first emition
*
* @param timeout - Timeout to wait for an item flow through input
* @returns Operator function
*/
function timeoutFirst(timeout) {
return (input$) => (0, rxjs_1.race)((0, rxjs_1.timer)(timeout).pipe((0, operators_1.mergeMapTo)((0, rxjs_1.throwError)(() => new Error('timeout waiting first')))), input$);
}
exports.timeoutFirst = timeoutFirst;
/**
* Like a concatMap, but input values emitted while a subscription is active are buffered and
* passed to project callback as an array when previous subscription completes.
* This means a value emitted by input while there's no active subscription will cause project to
* be called with a single-element array (value), while multiple values going through will get
* buffered and project called with all of them only once previous completes.
*
* @param project - Callback to generate the inner ObservableInput
* @param maxBatchSize - Limit emitted batches to this size; non-emitted values will stay in queue
* and be passed on next project call and subscription
* @returns Observable of values emitted by inner subscription
*/
function concatBuffer(project, maxBatchSize) {
return (input$) => {
const buffer = [];
return input$.pipe((0, operators_1.tap)((value) => buffer.push(value)), (0, operators_1.concatMap)(() => (0, rxjs_1.defer)(() => buffer.length
? project(buffer.splice(0, maxBatchSize ?? buffer.length))
: rxjs_1.EMPTY)));
};
}
exports.concatBuffer = concatBuffer;
/**
* Flatten the merging of higher-order observables but preserving previous value
*
* It's like [[withLatestFrom]], but don't lose outter values and merges all inner emitted ones.
* Instead of the callback-hell of:
* obs1.pipe(
* mergeMap((v1) =>
* obs2(v1).pipe( // obs2 uses v1
* mergeMap((v2) =>
* obs3(v1, v2).pipe( // obs3 uses v1, v2
* map(({ v3_a, v3_b }) => { v1, v2, v3: v3_a + v3_b }), // map uses v1, v2, v3
* ),
* ),
* ),
* ),
* );
*
* You can now:
* obs1.pipe(
* withMergeFrom((v1) => obs2(v1, 123)),
* withMergeFrom(([v1, v2]) => obs3(v1, v2, true)),
* // you can use tuple-destructuring on values, and obj-destructuring on objects
* map(([[v1, v2], { v3_a, v3_b }]) => ({ v1, v2, v3: v3_a + v3_b })),
* );
*
* @param project - Project function passed to mergeMap
* @param mapFunc - Funtion to merge result with, like mergeMap or switchMap
* @returns Observable mirroring project's return, but prepending emitted values from this inner
* observable in a tuple with the value from the outter observable which generated the inner.
*/
function withMergeFrom(project, mapFunc = operators_1.mergeMap) {
return (0, rxjs_1.pipe)(mapFunc((value, index) => (0, rxjs_1.from)(project(value, index)).pipe((0, operators_1.map)((res) => [value, res]))));
}
exports.withMergeFrom = withMergeFrom;
/**
* Operator to catch, log and suppress observable errors
*
* @param opts - shouldRetryError parameters
* @param logParams - Additional log parameters, message and details to bind to opts.log
* @returns Operator to catch errors, log and suppress if it matches the opts conditions,
* Re-throws otherwise
*/
function catchAndLog(opts, ...logParams) {
if (opts.log && logParams.length)
opts = { ...opts, log: opts.log.bind(null, ...logParams) };
const shouldSuppress = (0, error_1.shouldRetryError)(opts);
return (0, rxjs_1.pipe)((0, operators_1.catchError)((err) => {
if (!shouldSuppress(err))
throw err;
return rxjs_1.EMPTY;
}));
}
exports.catchAndLog = catchAndLog;
/**
* Custom operator providing a project function which is mirrored in the output, but provides a
* parameter function which allows submitting requests directly to the output as well, and returns
* with an observable which filters input$ for success|failures, errors on failures and completes
* on successes. In case 'confirmed' is true, this observable also emits intermediate unconfirmed
* successes and only completes upon the confirmed one is seen.
* Example:
* output$: Observable<anotherAction.success | messageSend.request> = action$.pipe(
* dispatchRequestAndGetResponse(messageSend, (dispatchRequest) =>
* // this observable will be mirrored to output, plus requests sent to dispatchRequest
* action$.pipe(
* filter(anotherAction.request.is),
* mergeMap((action) =>
* dispatchRequest(messageSend.request('test')).pipe(
* map((sentAction) => anotherAction.success({ sent: msgSendSucAction })),
* ),
* ),
* ),
* ),
* )
*
* @param aac - AsyncActionCreator type to wait for response; can be an array of action creators,
* in which case, dispatchRequest function will accept one request and return an observable
* for the corresponding response
* @param project - Function to be merged to output; called with a function which allows to
* dispatch requests directly to output and returns an observable which will emit the success
* coming in input and complete, or error if a failure goes through
* @param confirmed - Keep emitting success to dispatchRequest's returned observable while it isn't
* confirmed yet
* @param dedupKey - Function to calculate keys to deduplicate requests (returns the same
observable as result if a request with similar key is performed while one is still pending)
* @returns Custom operator which mirrors projected observable plus requests called in the
* project's function parameter
*/
function dispatchRequestAndGetResponse(aac, project, confirmed = false, dedupKey = (value) => value) {
return (input$) => (0, rxjs_1.defer)(() => {
const requestOutput$ = new rxjs_1.Subject();
const pending = new Map();
const projectOutput$ = (0, rxjs_1.defer)(() => project((request) => {
const key = dedupKey(request);
const pending$ = pending.get(key);
if (pending$)
return pending$;
const result$ = new rxjs_1.ReplaySubject(1);
const sub = input$
.pipe((0, operators_1.filter)((0, actions_1.isResponseOf)(aac, request.meta)), (0, operators_1.map)((response) => {
if (aac.failure.is(response))
throw response.payload;
return response;
}), (0, operators_1.takeWhile)((response) => confirmed &&
'confirmed' in response.payload &&
response.payload.confirmed === undefined, true))
.subscribe(result$);
requestOutput$.next(request);
const res = result$.pipe((0, operators_1.finalize)(() => {
sub.unsubscribe();
pending.delete(key);
}));
pending.set(key, res);
return res;
})).pipe((0, operators_1.finalize)(() => requestOutput$.complete()));
return (0, rxjs_1.merge)(requestOutput$, projectOutput$);
});
}
exports.dispatchRequestAndGetResponse = dispatchRequestAndGetResponse;
/**
* A custom operator to apply an inner operator only to a partitioned (filtered) view of the input,
* matching a given predicate, and merging the output with the values which doesn't match it
*
* @param predicate - Test input values if they should be projected
* @param operator - Receives observable of input values which matches predicate and return an
* observable input to be merged in the output together with values which don't
* @returns Observable of values which doesn't pass the predicate merged with the projected
* observables returned on the values which pass
*/
function partitionMap(predicate, operator) {
return (source$) => {
const [true$, false$] = (0, rxjs_1.partition)(source$.pipe((0, operators_1.share)()), predicate);
return (0, rxjs_1.merge)(operator(true$), false$);
};
}
exports.partitionMap = partitionMap;
//# sourceMappingURL=rx.js.map