rpc_ts
Version:
Remote Procedure Calls in TypeScript made simple
143 lines • 5.69 kB
JavaScript
"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