UNPKG

rpc_ts

Version:

Remote Procedure Calls in TypeScript made simple

143 lines 5.69 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); /** * Fault tolerance for a series of `Stream` (the "raw" value returned by an RPC). * * @module ModuleRpcClient * * @license * Copyright (c) Aiden.ai * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ const events = require("events"); const backoff_1 = require("./backoff"); const utils_1 = require("../utils/utils"); /** * Retries a series of streams. * * @typeparam Message The type of the message transmitted by the stream. * @param getStream A stream provider. The stream provider is called until a * stream emits a 'complete' or 'canceled' event, or the maximum number of retries, * as per the exponential backoff schedule, has been reached. * @param backoffOptions Options for the exponential backoff. * @param shouldRetry Determines whether an error should be retried (the * function returns `true`) or the method should fail immediately. * * @return A stream that is in effect a "concatenation" of all the streams initiated * by `getStream`, with special events emitted to inform on the retrying process. * * @example ```Typescript * const stream = retryStream(() => streamFromArray([1, 2, 3], new Error('error))); * stream.on('message', console.log).start(); * ``` */ function retryStream(getStream, backoffOptions, shouldRetry = () => true) { return new RetryingStreamImpl(getStream, Object.assign(Object.assign({}, backoff_1.DEFAULT_BACKOFF_OPTIONS), backoffOptions), shouldRetry); } exports.retryStream = retryStream; /** * All the states of a retrying stream are gathered here for better clarity. */ var RetryingStreamState; (function (RetryingStreamState) { /** Initial state */ RetryingStreamState["initial"] = "initial"; /** The `start()` function was called. */ RetryingStreamState["started"] = "started"; /** The `ready` event was emitted. */ RetryingStreamState["ready"] = "ready"; /** Either the `cancel()` function was called, or the `cancel` event was emitted. */ RetryingStreamState["canceled"] = "canceled"; /** The max number of retries has been reached, the RPC has been abandoned. */ RetryingStreamState["abandoned"] = "abandoned"; /** The RPC successfully completed. */ RetryingStreamState["complete"] = "complete"; })(RetryingStreamState || (RetryingStreamState = {})); class RetryingStreamImpl extends events.EventEmitter { constructor(getStream, options, shouldRetry) { super(); this.getStream = getStream; this.options = options; this.shouldRetry = shouldRetry; /** The state of the retrying stream. We use this to enforce state transitions. */ this.state = RetryingStreamState.initial; /** * The number of retries since a stream emitted the `ready` event. */ this.retriesSinceLastReady = 0; this.attempt = (stream, cb) => { stream .on('ready', () => { this.state = RetryingStreamState.ready; this.emit('ready'); this.retriesSinceLastReady = 0; }) .on('message', message => { if (this.state === RetryingStreamState.ready) { this.emit('message', message); } }) .on('complete', () => { this.state = RetryingStreamState.complete; this.emit('complete'); cb(); }) .on('error', async (err) => { if ((this.options.maxRetries >= 0 && this.retriesSinceLastReady >= this.options.maxRetries) || !this.shouldRetry(err)) { this.state = RetryingStreamState.abandoned; this.emit('retryingError', err, this.retriesSinceLastReady, true); this.emit('error', err); } else { this.state = RetryingStreamState.started; this.emit('retryingError', err, this.retriesSinceLastReady, false); await utils_1.sleep(backoff_1.getBackoffMs(this.options, this.retriesSinceLastReady++)); } cb(); }) .on('canceled', () => { this.state = RetryingStreamState.canceled; this.emit('canceled'); cb(); }) .start(); }; } start() { this.asyncStart(); return this; } cancel() { this.state = RetryingStreamState.canceled; this.cancelCurrentStream && this.cancelCurrentStream(); return this; } async asyncStart() { this.ensureState(RetryingStreamState.initial); this.state = RetryingStreamState.started; // We use async/await to get this nice infinite loop while (![ RetryingStreamState.complete, RetryingStreamState.canceled, RetryingStreamState.abandoned, ].includes(this.state)) { const stream = this.getStream(); this.cancelCurrentStream = stream.cancel.bind(stream); await new Promise(accept => { this.attempt(stream, accept); }); this.cancelCurrentStream = undefined; } } /* istanbul ignore next */ ensureState(state) { if (this.state !== state) { throw new Error(`invalid retrier state: actual: ${this.state}; expected: ${state}`); } } } //# sourceMappingURL=stream_retrier.js.map