UNPKG

opossum

Version:

A fail-fast circuit breaker for promises and callbacks

1,327 lines (1,197 loc) 77.9 kB
(function webpackUniversalModuleDefinition(root, factory) { if(typeof exports === 'object' && typeof module === 'object') module.exports = factory(); else if(typeof define === 'function' && define.amd) define([], factory); else if(typeof exports === 'object') exports["circuitBreaker"] = factory(); else root["circuitBreaker"] = factory(); })(self, () => { return /******/ (() => { // webpackBootstrap /******/ var __webpack_modules__ = ({ /***/ "./node_modules/events/events.js": /*!***************************************!*\ !*** ./node_modules/events/events.js ***! \***************************************/ /***/ ((module) => { "use strict"; // Copyright Joyent, Inc. and other Node contributors. // // Permission is hereby granted, free of charge, to any person obtaining a // copy of this software and associated documentation files (the // "Software"), to deal in the Software without restriction, including // without limitation the rights to use, copy, modify, merge, publish, // distribute, sublicense, and/or sell copies of the Software, and to permit // persons to whom the Software is furnished to do so, subject to the // following conditions: // // The above copyright notice and this permission notice shall be included // in all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS // OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN // NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, // DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE // USE OR OTHER DEALINGS IN THE SOFTWARE. var R = typeof Reflect === 'object' ? Reflect : null var ReflectApply = R && typeof R.apply === 'function' ? R.apply : function ReflectApply(target, receiver, args) { return Function.prototype.apply.call(target, receiver, args); } var ReflectOwnKeys if (R && typeof R.ownKeys === 'function') { ReflectOwnKeys = R.ownKeys } else if (Object.getOwnPropertySymbols) { ReflectOwnKeys = function ReflectOwnKeys(target) { return Object.getOwnPropertyNames(target) .concat(Object.getOwnPropertySymbols(target)); }; } else { ReflectOwnKeys = function ReflectOwnKeys(target) { return Object.getOwnPropertyNames(target); }; } function ProcessEmitWarning(warning) { if (console && console.warn) console.warn(warning); } var NumberIsNaN = Number.isNaN || function NumberIsNaN(value) { return value !== value; } function EventEmitter() { EventEmitter.init.call(this); } module.exports = EventEmitter; module.exports.once = once; // Backwards-compat with node 0.10.x EventEmitter.EventEmitter = EventEmitter; EventEmitter.prototype._events = undefined; EventEmitter.prototype._eventsCount = 0; EventEmitter.prototype._maxListeners = undefined; // By default EventEmitters will print a warning if more than 10 listeners are // added to it. This is a useful default which helps finding memory leaks. var defaultMaxListeners = 10; function checkListener(listener) { if (typeof listener !== 'function') { throw new TypeError('The "listener" argument must be of type Function. Received type ' + typeof listener); } } Object.defineProperty(EventEmitter, 'defaultMaxListeners', { enumerable: true, get: function() { return defaultMaxListeners; }, set: function(arg) { if (typeof arg !== 'number' || arg < 0 || NumberIsNaN(arg)) { throw new RangeError('The value of "defaultMaxListeners" is out of range. It must be a non-negative number. Received ' + arg + '.'); } defaultMaxListeners = arg; } }); EventEmitter.init = function() { if (this._events === undefined || this._events === Object.getPrototypeOf(this)._events) { this._events = Object.create(null); this._eventsCount = 0; } this._maxListeners = this._maxListeners || undefined; }; // Obviously not all Emitters should be limited to 10. This function allows // that to be increased. Set to zero for unlimited. EventEmitter.prototype.setMaxListeners = function setMaxListeners(n) { if (typeof n !== 'number' || n < 0 || NumberIsNaN(n)) { throw new RangeError('The value of "n" is out of range. It must be a non-negative number. Received ' + n + '.'); } this._maxListeners = n; return this; }; function _getMaxListeners(that) { if (that._maxListeners === undefined) return EventEmitter.defaultMaxListeners; return that._maxListeners; } EventEmitter.prototype.getMaxListeners = function getMaxListeners() { return _getMaxListeners(this); }; EventEmitter.prototype.emit = function emit(type) { var args = []; for (var i = 1; i < arguments.length; i++) args.push(arguments[i]); var doError = (type === 'error'); var events = this._events; if (events !== undefined) doError = (doError && events.error === undefined); else if (!doError) return false; // If there is no 'error' event listener then throw. if (doError) { var er; if (args.length > 0) er = args[0]; if (er instanceof Error) { // Note: The comments on the `throw` lines are intentional, they show // up in Node's output if this results in an unhandled exception. throw er; // Unhandled 'error' event } // At least give some kind of context to the user var err = new Error('Unhandled error.' + (er ? ' (' + er.message + ')' : '')); err.context = er; throw err; // Unhandled 'error' event } var handler = events[type]; if (handler === undefined) return false; if (typeof handler === 'function') { ReflectApply(handler, this, args); } else { var len = handler.length; var listeners = arrayClone(handler, len); for (var i = 0; i < len; ++i) ReflectApply(listeners[i], this, args); } return true; }; function _addListener(target, type, listener, prepend) { var m; var events; var existing; checkListener(listener); events = target._events; if (events === undefined) { events = target._events = Object.create(null); target._eventsCount = 0; } else { // To avoid recursion in the case that type === "newListener"! Before // adding it to the listeners, first emit "newListener". if (events.newListener !== undefined) { target.emit('newListener', type, listener.listener ? listener.listener : listener); // Re-assign `events` because a newListener handler could have caused the // this._events to be assigned to a new object events = target._events; } existing = events[type]; } if (existing === undefined) { // Optimize the case of one listener. Don't need the extra array object. existing = events[type] = listener; ++target._eventsCount; } else { if (typeof existing === 'function') { // Adding the second element, need to change to array. existing = events[type] = prepend ? [listener, existing] : [existing, listener]; // If we've already got an array, just append. } else if (prepend) { existing.unshift(listener); } else { existing.push(listener); } // Check for listener leak m = _getMaxListeners(target); if (m > 0 && existing.length > m && !existing.warned) { existing.warned = true; // No error code for this since it is a Warning // eslint-disable-next-line no-restricted-syntax var w = new Error('Possible EventEmitter memory leak detected. ' + existing.length + ' ' + String(type) + ' listeners ' + 'added. Use emitter.setMaxListeners() to ' + 'increase limit'); w.name = 'MaxListenersExceededWarning'; w.emitter = target; w.type = type; w.count = existing.length; ProcessEmitWarning(w); } } return target; } EventEmitter.prototype.addListener = function addListener(type, listener) { return _addListener(this, type, listener, false); }; EventEmitter.prototype.on = EventEmitter.prototype.addListener; EventEmitter.prototype.prependListener = function prependListener(type, listener) { return _addListener(this, type, listener, true); }; function onceWrapper() { if (!this.fired) { this.target.removeListener(this.type, this.wrapFn); this.fired = true; if (arguments.length === 0) return this.listener.call(this.target); return this.listener.apply(this.target, arguments); } } function _onceWrap(target, type, listener) { var state = { fired: false, wrapFn: undefined, target: target, type: type, listener: listener }; var wrapped = onceWrapper.bind(state); wrapped.listener = listener; state.wrapFn = wrapped; return wrapped; } EventEmitter.prototype.once = function once(type, listener) { checkListener(listener); this.on(type, _onceWrap(this, type, listener)); return this; }; EventEmitter.prototype.prependOnceListener = function prependOnceListener(type, listener) { checkListener(listener); this.prependListener(type, _onceWrap(this, type, listener)); return this; }; // Emits a 'removeListener' event if and only if the listener was removed. EventEmitter.prototype.removeListener = function removeListener(type, listener) { var list, events, position, i, originalListener; checkListener(listener); events = this._events; if (events === undefined) return this; list = events[type]; if (list === undefined) return this; if (list === listener || list.listener === listener) { if (--this._eventsCount === 0) this._events = Object.create(null); else { delete events[type]; if (events.removeListener) this.emit('removeListener', type, list.listener || listener); } } else if (typeof list !== 'function') { position = -1; for (i = list.length - 1; i >= 0; i--) { if (list[i] === listener || list[i].listener === listener) { originalListener = list[i].listener; position = i; break; } } if (position < 0) return this; if (position === 0) list.shift(); else { spliceOne(list, position); } if (list.length === 1) events[type] = list[0]; if (events.removeListener !== undefined) this.emit('removeListener', type, originalListener || listener); } return this; }; EventEmitter.prototype.off = EventEmitter.prototype.removeListener; EventEmitter.prototype.removeAllListeners = function removeAllListeners(type) { var listeners, events, i; events = this._events; if (events === undefined) return this; // not listening for removeListener, no need to emit if (events.removeListener === undefined) { if (arguments.length === 0) { this._events = Object.create(null); this._eventsCount = 0; } else if (events[type] !== undefined) { if (--this._eventsCount === 0) this._events = Object.create(null); else delete events[type]; } return this; } // emit removeListener for all listeners on all events if (arguments.length === 0) { var keys = Object.keys(events); var key; for (i = 0; i < keys.length; ++i) { key = keys[i]; if (key === 'removeListener') continue; this.removeAllListeners(key); } this.removeAllListeners('removeListener'); this._events = Object.create(null); this._eventsCount = 0; return this; } listeners = events[type]; if (typeof listeners === 'function') { this.removeListener(type, listeners); } else if (listeners !== undefined) { // LIFO order for (i = listeners.length - 1; i >= 0; i--) { this.removeListener(type, listeners[i]); } } return this; }; function _listeners(target, type, unwrap) { var events = target._events; if (events === undefined) return []; var evlistener = events[type]; if (evlistener === undefined) return []; if (typeof evlistener === 'function') return unwrap ? [evlistener.listener || evlistener] : [evlistener]; return unwrap ? unwrapListeners(evlistener) : arrayClone(evlistener, evlistener.length); } EventEmitter.prototype.listeners = function listeners(type) { return _listeners(this, type, true); }; EventEmitter.prototype.rawListeners = function rawListeners(type) { return _listeners(this, type, false); }; EventEmitter.listenerCount = function(emitter, type) { if (typeof emitter.listenerCount === 'function') { return emitter.listenerCount(type); } else { return listenerCount.call(emitter, type); } }; EventEmitter.prototype.listenerCount = listenerCount; function listenerCount(type) { var events = this._events; if (events !== undefined) { var evlistener = events[type]; if (typeof evlistener === 'function') { return 1; } else if (evlistener !== undefined) { return evlistener.length; } } return 0; } EventEmitter.prototype.eventNames = function eventNames() { return this._eventsCount > 0 ? ReflectOwnKeys(this._events) : []; }; function arrayClone(arr, n) { var copy = new Array(n); for (var i = 0; i < n; ++i) copy[i] = arr[i]; return copy; } function spliceOne(list, index) { for (; index + 1 < list.length; index++) list[index] = list[index + 1]; list.pop(); } function unwrapListeners(arr) { var ret = new Array(arr.length); for (var i = 0; i < ret.length; ++i) { ret[i] = arr[i].listener || arr[i]; } return ret; } function once(emitter, name) { return new Promise(function (resolve, reject) { function errorListener(err) { emitter.removeListener(name, resolver); reject(err); } function resolver() { if (typeof emitter.removeListener === 'function') { emitter.removeListener('error', errorListener); } resolve([].slice.call(arguments)); }; eventTargetAgnosticAddListener(emitter, name, resolver, { once: true }); if (name !== 'error') { addErrorHandlerIfEventEmitter(emitter, errorListener, { once: true }); } }); } function addErrorHandlerIfEventEmitter(emitter, handler, flags) { if (typeof emitter.on === 'function') { eventTargetAgnosticAddListener(emitter, 'error', handler, flags); } } function eventTargetAgnosticAddListener(emitter, name, listener, flags) { if (typeof emitter.on === 'function') { if (flags.once) { emitter.once(name, listener); } else { emitter.on(name, listener); } } else if (typeof emitter.addEventListener === 'function') { // EventTarget does not have `error` event semantics like Node // EventEmitters, we do not listen for `error` events here. emitter.addEventListener(name, function wrapListener(arg) { // IE does not have builtin `{ once: true }` support so we // have to do it manually. if (flags.once) { emitter.removeEventListener(name, wrapListener); } listener(arg); }); } else { throw new TypeError('The "emitter" argument must be of type EventEmitter. Received type ' + typeof emitter); } } /***/ }), /***/ "./index.js": /*!******************!*\ !*** ./index.js ***! \******************/ /***/ ((module, exports, __webpack_require__) => { "use strict"; module.exports = exports = __webpack_require__(/*! ./lib/circuit */ "./lib/circuit.js"); /***/ }), /***/ "./lib/cache.js": /*!**********************!*\ !*** ./lib/cache.js ***! \**********************/ /***/ ((module, exports) => { function _typeof(o) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) { return typeof o; } : function (o) { return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o; }, _typeof(o); } function _classCallCheck(a, n) { if (!(a instanceof n)) throw new TypeError("Cannot call a class as a function"); } function _defineProperties(e, r) { for (var t = 0; t < r.length; t++) { var o = r[t]; o.enumerable = o.enumerable || !1, o.configurable = !0, "value" in o && (o.writable = !0), Object.defineProperty(e, _toPropertyKey(o.key), o); } } function _createClass(e, r, t) { return r && _defineProperties(e.prototype, r), t && _defineProperties(e, t), Object.defineProperty(e, "prototype", { writable: !1 }), e; } function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == _typeof(i) ? i : i + ""; } function _toPrimitive(t, r) { if ("object" != _typeof(t) || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != _typeof(i)) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); } /** * Simple in-memory cache implementation * @class MemoryCache * @property {Map} cache Cache map */ var MemoryCache = /*#__PURE__*/function () { function MemoryCache(maxEntries) { _classCallCheck(this, MemoryCache); this.cache = new Map(); this.maxEntries = maxEntries !== null && maxEntries !== void 0 ? maxEntries : Math.pow(2, 24) - 1; // Max size for Map is 2^24. } /** * Get cache value by key * @param {string} key Cache key * @return {any} Response from cache */ _createClass(MemoryCache, [{ key: "get", value: function get(key) { var cached = this.cache.get(key); if (cached) { if (cached.expiresAt > Date.now() || cached.expiresAt === 0) { return cached.value; } this.cache["delete"](key); } return undefined; } /** * Set cache key with value and ttl * @param {string} key Cache key * @param {any} value Value to cache * @param {number} ttl Time to live in milliseconds * @return {void} */ }, { key: "set", value: function set(key, value, ttl) { // Evict first entry when at capacity - only when it's a new key. if (this.cache.size === this.maxEntries && this.get(key) === undefined) { this.cache["delete"](this.cache.keys().next().value); } this.cache.set(key, { expiresAt: ttl, value: value }); } /** * Delete cache key * @param {string} key Cache key * @return {void} */ }, { key: "delete", value: function _delete(key) { this.cache["delete"](key); } /** * Clear cache * @returns {void} */ }, { key: "flush", value: function flush() { this.cache.clear(); } }]); return MemoryCache; }(); module.exports = exports = MemoryCache; /***/ }), /***/ "./lib/circuit.js": /*!************************!*\ !*** ./lib/circuit.js ***! \************************/ /***/ ((module, exports, __webpack_require__) => { "use strict"; function _toConsumableArray(r) { return _arrayWithoutHoles(r) || _iterableToArray(r) || _unsupportedIterableToArray(r) || _nonIterableSpread(); } function _nonIterableSpread() { throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } function _unsupportedIterableToArray(r, a) { if (r) { if ("string" == typeof r) return _arrayLikeToArray(r, a); var t = {}.toString.call(r).slice(8, -1); return "Object" === t && r.constructor && (t = r.constructor.name), "Map" === t || "Set" === t ? Array.from(r) : "Arguments" === t || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t) ? _arrayLikeToArray(r, a) : void 0; } } function _iterableToArray(r) { if ("undefined" != typeof Symbol && null != r[Symbol.iterator] || null != r["@@iterator"]) return Array.from(r); } function _arrayWithoutHoles(r) { if (Array.isArray(r)) return _arrayLikeToArray(r); } function _arrayLikeToArray(r, a) { (null == a || a > r.length) && (a = r.length); for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e]; return n; } function _typeof(o) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) { return typeof o; } : function (o) { return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o; }, _typeof(o); } function _classCallCheck(a, n) { if (!(a instanceof n)) throw new TypeError("Cannot call a class as a function"); } function _defineProperties(e, r) { for (var t = 0; t < r.length; t++) { var o = r[t]; o.enumerable = o.enumerable || !1, o.configurable = !0, "value" in o && (o.writable = !0), Object.defineProperty(e, _toPropertyKey(o.key), o); } } function _createClass(e, r, t) { return r && _defineProperties(e.prototype, r), t && _defineProperties(e, t), Object.defineProperty(e, "prototype", { writable: !1 }), e; } function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == _typeof(i) ? i : i + ""; } function _toPrimitive(t, r) { if ("object" != _typeof(t) || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != _typeof(i)) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); } function _inherits(t, e) { if ("function" != typeof e && null !== e) throw new TypeError("Super expression must either be null or a function"); t.prototype = Object.create(e && e.prototype, { constructor: { value: t, writable: !0, configurable: !0 } }), Object.defineProperty(t, "prototype", { writable: !1 }), e && _setPrototypeOf(t, e); } function _setPrototypeOf(t, e) { return _setPrototypeOf = Object.setPrototypeOf ? Object.setPrototypeOf.bind() : function (t, e) { return t.__proto__ = e, t; }, _setPrototypeOf(t, e); } function _createSuper(t) { var r = _isNativeReflectConstruct(); return function () { var e, o = _getPrototypeOf(t); if (r) { var s = _getPrototypeOf(this).constructor; e = Reflect.construct(o, arguments, s); } else e = o.apply(this, arguments); return _possibleConstructorReturn(this, e); }; } function _possibleConstructorReturn(t, e) { if (e && ("object" == _typeof(e) || "function" == typeof e)) return e; if (void 0 !== e) throw new TypeError("Derived constructors may only return object or undefined"); return _assertThisInitialized(t); } function _assertThisInitialized(e) { if (void 0 === e) throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); return e; } function _isNativeReflectConstruct() { try { var t = !Boolean.prototype.valueOf.call(Reflect.construct(Boolean, [], function () {})); } catch (t) {} return (_isNativeReflectConstruct = function _isNativeReflectConstruct() { return !!t; })(); } function _getPrototypeOf(t) { return _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf.bind() : function (t) { return t.__proto__ || Object.getPrototypeOf(t); }, _getPrototypeOf(t); } var EventEmitter = __webpack_require__(/*! events */ "./node_modules/events/events.js"); var Status = __webpack_require__(/*! ./status */ "./lib/status.js"); var Semaphore = __webpack_require__(/*! ./semaphore */ "./lib/semaphore.js"); var MemoryCache = __webpack_require__(/*! ./cache */ "./lib/cache.js"); var STATE = Symbol('state'); var OPEN = Symbol('open'); var CLOSED = Symbol('closed'); var HALF_OPEN = Symbol('half-open'); var PENDING_CLOSE = Symbol('pending-close'); var SHUTDOWN = Symbol('shutdown'); var FALLBACK_FUNCTION = Symbol('fallback'); var STATUS = Symbol('status'); var NAME = Symbol('name'); var GROUP = Symbol('group'); var ENABLED = Symbol('Enabled'); var WARMING_UP = Symbol('warming-up'); var VOLUME_THRESHOLD = Symbol('volume-threshold'); var OUR_ERROR = Symbol('our-error'); var RESET_TIMEOUT = Symbol('reset-timeout'); var WARMUP_TIMEOUT = Symbol('warmup-timeout'); var LAST_TIMER_AT = Symbol('last-timer-at'); var deprecation = "options.maxFailures is deprecated. Please use options.errorThresholdPercentage"; /** * Constructs a {@link CircuitBreaker}. * * @class CircuitBreaker * @extends EventEmitter * @param {Function} action The action to fire for this {@link CircuitBreaker} * @param {Object} options Options for the {@link CircuitBreaker} * @param {Status} options.status A {@link Status} object that might * have pre-prime stats * @param {Number} options.timeout The time in milliseconds that action should * be allowed to execute before timing out. Timeout can be disabled by setting * this to `false`. Default 10000 (10 seconds) * @param {Number} options.maxFailures (Deprecated) The number of times the * circuit can fail before opening. Default 10. * @param {Number} options.resetTimeout The time in milliseconds to wait before * setting the breaker to `halfOpen` state, and trying the action again. * Default: 30000 (30 seconds) * @param {Number} options.rollingCountTimeout Sets the duration of the * statistical rolling window, in milliseconds. This is how long Opossum keeps * metrics for the circuit breaker to use and for publishing. Default: 10000 * @param {Number} options.rollingCountBuckets Sets the number of buckets the * rolling statistical window is divided into. So, if * options.rollingCountTimeout is 10000, and options.rollingCountBuckets is 10, * then the statistical window will be 1000/1 second snapshots in the * statistical window. Default: 10 * @param {String} options.name the circuit name to use when reporting stats. * Default: the name of the function this circuit controls. * @param {boolean} options.rollingPercentilesEnabled This property indicates * whether execution latencies should be tracked and calculated as percentiles. * If they are disabled, all summary statistics (mean, percentiles) are * returned as -1. Default: true * @param {Number} options.capacity the number of concurrent requests allowed. * If the number currently executing function calls is equal to * options.capacity, further calls to `fire()` are rejected until at least one * of the current requests completes. Default: `Number.MAX_SAFE_INTEGER`. * @param {Number} options.errorThresholdPercentage the error percentage at * which to open the circuit and start short-circuiting requests to fallback. * Default: 50 * @param {boolean} options.enabled whether this circuit is enabled upon * construction. Default: true * @param {boolean} options.allowWarmUp determines whether to allow failures * without opening the circuit during a brief warmup period (this is the * `rollingCountTimeout` property). Default: false * This can help in situations where no matter what your * `errorThresholdPercentage` is, if the first execution times out or fails, * the circuit immediately opens. * @param {Number} options.volumeThreshold the minimum number of requests within * the rolling statistical window that must exist before the circuit breaker * can open. This is similar to `options.allowWarmUp` in that no matter how many * failures there are, if the number of requests within the statistical window * does not exceed this threshold, the circuit will remain closed. Default: 0 * @param {Function} options.errorFilter an optional function that will be * called when the circuit's function fails (returns a rejected Promise). If * this function returns truthy, the circuit's failPure statistics will not be * incremented. This is useful, for example, when you don't want HTTP 404 to * trip the circuit, but still want to handle it as a failure case. * @param {boolean} options.cache whether the return value of the first * successful execution of the circuit's function will be cached. Once a value * has been cached that value will be returned for every subsequent execution: * the cache can be cleared using `clearCache`. (The metrics `cacheHit` and * `cacheMiss` reflect cache activity.) Default: false * @param {Number} options.cacheTTL the time to live for the cache * in milliseconds. Set 0 for infinity cache. Default: 0 (no TTL) * @param {Number} options.cacheSize the max amount of entries in the internal * cache. Only used when cacheTransport is not defined. * Default: max size of JS map (2^24). * @param {Function} options.cacheGetKey function that returns the key to use * when caching the result of the circuit's fire. * Better to use custom one, because `JSON.stringify` is not good * from performance perspective. * Default: `(...args) => JSON.stringify(args)` * @param {CacheTransport} options.cacheTransport custom cache transport * should implement `get`, `set` and `flush` methods. * @param {boolean} options.coalesce If true, this provides coalescing of * requests to this breaker, in other words: the promise will be cached. * Only one action (with same cache key) is executed at a time, and the other * pending actions wait for the result. Performance will improve when rapidly * firing the circuitbreaker with the same request, especially on a slower * action (e.g. multiple end-users fetching same data from remote). * Will use internal cache only. Can be used in combination with options.cache. * The metrics `coalesceCacheHit` and `coalesceCacheMiss` are available. * Default: false * @param {Number} options.coalesceTTL the time to live for the coalescing * in milliseconds. Set 0 for infinity cache. Default: same as options.timeout * @param {Number} options.coalesceSize the max amount of entries in the * coalescing cache. Default: max size of JS map (2^24). * @param {string[]} options.coalesceResetOn when to reset the coalesce cache. * Options: `error`, `success`, `timeout`. Default: not set, reset using TTL. * @param {AbortController} options.abortController this allows Opossum to * signal upon timeout and properly abort your on going requests instead of * leaving it in the background * @param {boolean} options.enableSnapshots whether to enable the rolling * stats snapshots that opossum emits at the bucketInterval. Disable this * as an optimization if you don't listen to the 'snapshot' event to reduce * the number of timers opossum initiates. * @param {EventEmitter} options.rotateBucketController if you have multiple * breakers in your app, the number of timers across breakers can get costly. * This option allows you to provide an EventEmitter that rotates the buckets * so you can have one global timer in your app. Make sure that you are * emitting a 'rotate' event from this EventEmitter * @param {boolean} options.autoRenewAbortController Automatically recreates * the instance of AbortController whenever the circuit transitions to * 'halfOpen' or 'closed' state. This ensures that new requests are not * impacted by previous signals that were triggered when the circuit was 'open'. * Default: false * * * @fires CircuitBreaker#halfOpen * @fires CircuitBreaker#close * @fires CircuitBreaker#open * @fires CircuitBreaker#fire * @fires CircuitBreaker#cacheHit * @fires CircuitBreaker#cacheMiss * @fires CircuitBreaker#coalesceCacheHit * @fires CircuitBreaker#coalesceCacheMiss * @fires CircuitBreaker#reject * @fires CircuitBreaker#timeout * @fires CircuitBreaker#success * @fires CircuitBreaker#semaphoreLocked * @fires CircuitBreaker#healthCheckFailed * @fires CircuitBreaker#fallback * @fires CircuitBreaker#failure */ var CircuitBreaker = /*#__PURE__*/function (_EventEmitter) { _inherits(CircuitBreaker, _EventEmitter); var _super = _createSuper(CircuitBreaker); function CircuitBreaker(action) { var _options$timeout, _options$resetTimeout, _options$errorThresho, _options$rollingCount, _options$rollingCount2, _options$cacheTTL, _options$cacheGetKey, _options$coalesceTTL, _options$coalesceRese; var _this; var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; _classCallCheck(this, CircuitBreaker); _this = _super.call(this); _this.options = options; _this.options.timeout = (_options$timeout = options.timeout) !== null && _options$timeout !== void 0 ? _options$timeout : 10000; _this.options.resetTimeout = (_options$resetTimeout = options.resetTimeout) !== null && _options$resetTimeout !== void 0 ? _options$resetTimeout : 30000; _this.options.errorThresholdPercentage = (_options$errorThresho = options.errorThresholdPercentage) !== null && _options$errorThresho !== void 0 ? _options$errorThresho : 50; _this.options.rollingCountTimeout = (_options$rollingCount = options.rollingCountTimeout) !== null && _options$rollingCount !== void 0 ? _options$rollingCount : 10000; _this.options.rollingCountBuckets = (_options$rollingCount2 = options.rollingCountBuckets) !== null && _options$rollingCount2 !== void 0 ? _options$rollingCount2 : 10; _this.options.rollingPercentilesEnabled = options.rollingPercentilesEnabled !== false; _this.options.capacity = Number.isInteger(options.capacity) ? options.capacity : Number.MAX_SAFE_INTEGER; _this.options.errorFilter = options.errorFilter || function (_) { return false; }; _this.options.cacheTTL = (_options$cacheTTL = options.cacheTTL) !== null && _options$cacheTTL !== void 0 ? _options$cacheTTL : 0; _this.options.cacheGetKey = (_options$cacheGetKey = options.cacheGetKey) !== null && _options$cacheGetKey !== void 0 ? _options$cacheGetKey : function () { for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) { args[_key] = arguments[_key]; } return JSON.stringify(args); }; _this.options.enableSnapshots = options.enableSnapshots !== false; _this.options.rotateBucketController = options.rotateBucketController; _this.options.coalesce = !!options.coalesce; _this.options.coalesceTTL = (_options$coalesceTTL = options.coalesceTTL) !== null && _options$coalesceTTL !== void 0 ? _options$coalesceTTL : _this.options.timeout; _this.options.coalesceResetOn = ((_options$coalesceRese = options.coalesceResetOn) === null || _options$coalesceRese === void 0 ? void 0 : _options$coalesceRese.filter(function (o) { return ['error', 'success', 'timeout'].includes(o); })) || []; // Set default cache transport if not provided if (_this.options.cache) { if (_this.options.cacheTransport === undefined) { _this.options.cacheTransport = new MemoryCache(options.cacheSize); } else if (_typeof(_this.options.cacheTransport) !== 'object' || !_this.options.cacheTransport.get || !_this.options.cacheTransport.set || !_this.options.cacheTransport.flush) { throw new TypeError('options.cacheTransport should be an object with `get`, `set` and `flush` methods'); } } if (_this.options.coalesce) { _this.options.coalesceCache = new MemoryCache(options.coalesceSize); } _this.semaphore = new Semaphore(_this.options.capacity); // check if action is defined if (!action) { throw new TypeError('No action provided. Cannot construct a CircuitBreaker without an invocable action.'); } if (options.autoRenewAbortController && !options.abortController) { options.abortController = new AbortController(); } if (options.abortController && typeof options.abortController.abort !== 'function') { throw new TypeError('AbortController does not contain `abort()` method'); } _this[VOLUME_THRESHOLD] = Number.isInteger(options.volumeThreshold) ? options.volumeThreshold : 0; _this[WARMING_UP] = options.allowWarmUp === true; // The user can pass in a Status object to initialize the Status/stats if (_this.options.status) { // Do a check that this is a Status Object, if (_this.options.status instanceof Status) { _this[STATUS] = _this.options.status; } else { _this[STATUS] = new Status({ stats: _this.options.status }); } } else { _this[STATUS] = new Status(_this.options); } _this[STATE] = CLOSED; if (options.state) { _this[ENABLED] = options.state.enabled !== false; _this[WARMING_UP] = options.state.warmUp || _this[WARMING_UP]; // Closed if nothing is passed in _this[CLOSED] = options.state.closed !== false; // These should be in sync _this[HALF_OPEN] = _this[PENDING_CLOSE] = options.state.halfOpen || false; // Open should be the opposite of closed, // but also the opposite of half_open _this[OPEN] = !_this[CLOSED] && !_this[HALF_OPEN]; _this[SHUTDOWN] = options.state.shutdown || false; } else { _this[PENDING_CLOSE] = false; _this[ENABLED] = options.enabled !== false; } _this[FALLBACK_FUNCTION] = null; _this[NAME] = options.name || action.name || nextName(); _this[GROUP] = options.group || _this[NAME]; if (_this[WARMING_UP]) { var timer = _this[WARMUP_TIMEOUT] = setTimeout(function (_) { return _this[WARMING_UP] = false; }, _this.options.rollingCountTimeout); if (typeof timer.unref === 'function') { timer.unref(); } } if (typeof action !== 'function') { _this.action = function (_) { return Promise.resolve(action); }; } else _this.action = action; if (options.maxFailures) console.error(deprecation); var increment = function increment(property) { return function (result, runTime) { return _this[STATUS].increment(property, runTime); }; }; _this.on('success', increment('successes')); _this.on('failure', increment('failures')); _this.on('fallback', increment('fallbacks')); _this.on('timeout', increment('timeouts')); _this.on('fire', increment('fires')); _this.on('reject', increment('rejects')); _this.on('cacheHit', increment('cacheHits')); _this.on('cacheMiss', increment('cacheMisses')); _this.on('coalesceCacheHit', increment('coalesceCacheHits')); _this.on('coalesceCacheMiss', increment('coalesceCacheMisses')); _this.on('open', function (_) { return _this[STATUS].open(); }); _this.on('close', function (_) { return _this[STATUS].close(); }); _this.on('semaphoreLocked', increment('semaphoreRejections')); /** * @param {CircuitBreaker} circuit This current circuit * @returns {function(): void} A bound reset callback * @private */ function _startTimer(circuit) { circuit[LAST_TIMER_AT] = Date.now(); return function (_) { var timer = circuit[RESET_TIMEOUT] = setTimeout(function () { _halfOpen(circuit); }, circuit.options.resetTimeout); if (typeof timer.unref === 'function') { timer.unref(); } }; } /** * Sets the circuit breaker to half open * @private * @param {CircuitBreaker} circuit The current circuit breaker * @returns {void} */ function _halfOpen(circuit) { circuit[STATE] = HALF_OPEN; circuit[PENDING_CLOSE] = true; circuit._renewAbortControllerIfNeeded(); /** * Emitted after `options.resetTimeout` has elapsed, allowing for * a single attempt to call the service again. If that attempt is * successful, the circuit will be closed. Otherwise it remains open. * * @event CircuitBreaker#halfOpen * @type {Number} how long the circuit remained open */ circuit.emit('halfOpen', circuit.options.resetTimeout); } _this.on('open', _startTimer(_assertThisInitialized(_this))); _this.on('success', function (_) { if (_this.halfOpen) { _this.close(); } }); // Prepopulate the State of the Breaker if (_this[SHUTDOWN]) { _this[STATE] = SHUTDOWN; _this.shutdown(); } else if (_this[CLOSED]) { _this.close(); } else if (_this[OPEN]) { // If the state being passed in is OPEN but more time has elapsed // than the resetTimeout, then we should be in halfOpen state if (_this.options.state.lastTimerAt !== undefined && Date.now() - _this.options.state.lastTimerAt > _this.options.resetTimeout) { _halfOpen(_assertThisInitialized(_this)); } else { _this.open(); } } else if (_this[HALF_OPEN]) { // Not sure if anything needs to be done here _this[STATE] = HALF_OPEN; } return _this; } /** * Renews the abort controller if needed * @private * @returns {void} */ _createClass(CircuitBreaker, [{ key: "_renewAbortControllerIfNeeded", value: function _renewAbortControllerIfNeeded() { if (this.options.autoRenewAbortController && this.options.abortController && this.options.abortController.signal.aborted) { this.options.abortController = new AbortController(); } } /** * Closes the breaker, allowing the action to execute again * @fires CircuitBreaker#close * @returns {void} */ }, { key: "close", value: function close() { if (this[STATE] !== CLOSED) { if (this[RESET_TIMEOUT]) { clearTimeout(this[RESET_TIMEOUT]); } this[STATE] = CLOSED; this[PENDING_CLOSE] = false; this._renewAbortControllerIfNeeded(); /** * Emitted when the breaker is reset allowing the action to execute again * @event CircuitBreaker#close */ this.emit('close'); } } /** * Opens the breaker. Each time the breaker is fired while the circuit is * opened, a failed Promise is returned, or if any fallback function * has been provided, it is invoked. * * If the breaker is already open this call does nothing. * @fires CircuitBreaker#open * @returns {void} */ }, { key: "open", value: function open() { if (this[STATE] !== OPEN) { this[STATE] = OPEN; this[PENDING_CLOSE] = false; /** * Emitted when the breaker opens because the action has * failure percentage greater than `options.errorThresholdPercentage`. * @event CircuitBreaker#open */ this.emit('open'); } } /** * Shuts down this circuit breaker. All subsequent calls to the * circuit will fail, returning a rejected promise. * @returns {void} */ }, { key: "shutdown", value: function shutdown() { /** * Emitted when the circuit breaker has been shut down. * @event CircuitBreaker#shutdown */ this.emit('shutdown'); this.disable(); this.removeAllListeners(); if (this[RESET_TIMEOUT]) { clearTimeout(this[RESET_TIMEOUT]); } if (this[WARMUP_TIMEOUT]) { clearTimeout(this[WARMUP_TIMEOUT]); } this.status.shutdown(); this[STATE] = SHUTDOWN; // clear cache on shutdown this.clearCache(); } /** * Determines if the circuit has been shutdown. * @type {Boolean} */ }, { key: "isShutdown", get: function get() { return this[STATE] === SHUTDOWN; } /** * Gets the name of this circuit * @type {String} */ }, { key: "name", get: function get() { return this[NAME]; } /** * Gets the name of this circuit group * @type {String} */ }, { key: "group", get: function get() { return this[GROUP]; } /** * Gets whether this circuit is in the `pendingClosed` state * @type {Boolean} */ }, { key: "pendingClose", get: function get() { return this[PENDING_CLOSE]; } /** * True if the circuit is currently closed. False otherwise. * @type {Boolean} */ }, { key: "closed", get: function get() { return this[STATE] === CLOSED; } /** * True if the circuit is currently opened. False otherwise. * @type {Boolean} */ }, { key: "opened", get: function get() { return this[STATE] === OPEN; } /** * True if the circuit is currently half opened. False otherwise. * @type {Boolean} */ }, { key: "halfOpen", get: function get() { return this[STATE] === HALF_OPEN; } /** * The current {@link Status} of this {@link CircuitBreaker} * @type {Status} */ }, { key: "status", get: function get() { return this[STATUS]; } /** * Get the current stats for the circuit. * @see Status#stats * @type {Object} */ }, { key: "stats", get: function get() { return this[STATUS].stats; } }, { key: "toJSON", value: function toJSON() { return { state: { name: this.name, enabled: this.enabled, closed: this.closed, open: this.opened, halfOpen: this.halfOpen, warmUp: this.warmUp, shutdown: this.isShutdown, lastTimerAt: this[LAST_TIMER_AT] }, status: this.status.stats }; } /** * Gets whether the circuit is enabled or not * @type {Boolean} */ }, { key: "enabled", get: function get() { return this[ENABLED]; } /** * Gets whether the circuit is currently in warm up phase * @type {Boolean} */ }, { key: "warmUp", get: function get() { return this[WARMING_UP]; } /** * Gets the volume threshold for this circuit * @type {Boolean} */ }, { key: "volumeThreshold", get: function get() { return this[VOLUME_THRESHOLD]; } /** * Provide a fallback function for this {@link CircuitBreaker}. This * function will be executed when the circuit is `fire`d and fails. * It will always be preceded by a `failure` event, and `breaker.fire` returns * a rejected Promise. * @param {Function | CircuitBreaker} func the fallback function to execute * when the breaker has opened or when a timeout or error occurs. * @return {CircuitBreaker} this */ }, { key: "fallback", value: function fallback(func) { var fb = func; if (func instanceof CircuitBreaker) { fb = function fb() { return func.fire.apply(func, arguments); }; } this[FALLBACK_FUNCTION] = fb; return this; } /** * Execute the action for this circuit. If the action fails or times out, the * returned promise will be rejected. If the action succeeds, the promise will * resolve with the resolved value from action. If a fallback function was * provided, it will be invoked in the event of any failure or timeout. * * Any parameters passed to this function will be proxied to the circuit * function. * * @return {Promise<any>} promise resolves with the circuit function's return * value on success or is rejected on failure of the action. Use isOurError() * to determine if a rejection was a result of the circuit breaker or the * action. * * @fires CircuitBreaker#failure * @fires CircuitBreaker#fallback * @fires CircuitBreaker#fire * @fires CircuitBreaker#reject * @fires CircuitBreaker#success * @fires CircuitBreaker#timeout * @fires CircuitBreaker#semaphoreLocked */ }, { key: "fire", value: function fire() { for (var _len2 = arguments.length, args = new Array(_len2), _key2 = 0; _key2 < _len2; _key2++) { args[_key2] = arguments[_key2]; } return this.call.apply(this, [this.action].concat(args)); } /** * Execute the action for this circuit using `context` as `this`. * If the action fails or times out, the * returned promise will be rejected. If the action succeeds, the promise will * resolve with the resolved value from action. If a fallback function was * provided, it will be invoked in the event of any failure or timeout. * * Any parameters in addition to `context will be passed to the * circuit function. * * @param {any} context the `this` context used for function execution * @param {any} rest the arguments passed to the action * * @return {Promise<any>} promise resolves with the circuit function's return * value on success or is rejected on failure of the action. * * @fires CircuitBreaker#failure * @fires CircuitBreaker#fallback * @fires CircuitBreaker#fire * @fires CircuitBreaker#reject * @fires CircuitBreaker#success * @fires CircuitBreaker#timeout * @fires CircuitBreaker#semaphoreLocked */ }, { key: "call", value: function call(context) { var _this2 = this; if (this.isShutdown) { var err = buildError('The circuit has been shutdown.', 'ESHUTDOWN'); return Promise.reject(err); } for (var _len3 = arguments.length, rest = new Array(_len3 > 1 ? _len3 - 1 : 0), _key3 = 1; _key3 < _len3; _key3++) { rest[_key3 - 1] = arguments[_key3]; } var args = rest.slice(); /** * Emitted when the circuit breaker action is executed * @event CircuitBreaker#fire * @type {any} the arguments passed to the fired function */ this.emit('fire', args); // Protection, caches and coalesce disabled. if (!this[ENABLED]) { var result = this.action.apply(context, args); return typeof result.then === 'function' ? result : Promise.resolve(result); } // Generate cachekey only when cache and/or coalesce is enabled. var cacheKey = this.options.cache || this.options.coalesce ? this.options.cacheGetKey.apply(this, rest) : ''; // If cache is enabled, check if we have a cached value if (this.options.cache) { var