UNPKG

react-native-onyx

Version:

State management for React Native

167 lines (166 loc) 6.63 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const StateMachine_1 = __importDefault(require("../StateMachine")); const types_1 = require("./types"); /** * Generic circuit breaker built on {@link StateMachine}. * * - **closed**: requests are allowed; failures are counted. * - **open**: requests are rejected until {@link resetTimeoutMs} elapses. * - **half-open**: the recovery-probe state. After the open timeout, the breaker admits exactly ONE * probe request: success means the dependency recovered, so the circuit closes. Failure means it's * still down, so the circuit reopens. This single-request probe prevents a "thundering herd" where * every caller fails loudly when the service hasn't recovered yet. * * Subclasses implement the failure-counting policy by overriding {@link recordFailureInClosed} (and * friends) — e.g. counting consecutive failures, or failures within a rolling time window, or any * combination of those. * * @example * class MyBreaker extends AbstractCircuitBreaker { * private failures = 0; * protected recordFailureInClosed() { * this.failures += 1; * return this.failures >= 3 ? `${this.failures} failures` : null; * } * protected recordSuccessInClosed() { this.failures = 0; } * protected resetFailureState() { this.failures = 0; } * } * * const breaker = new MyBreaker({resetTimeoutMs: 30_000}); * if (breaker.isAllowed()) { * try { * doWork(); * breaker.recordSuccess(); * } catch { * breaker.recordFailure(); * } * } */ class AbstractCircuitBreaker { constructor(options = {}) { var _a; this.openedAt = 0; this.isProbeInFlight = false; this.resetTimeoutMs = (_a = options.resetTimeoutMs) !== null && _a !== void 0 ? _a : 60000; this.onTrip = options.onTrip; this.onClose = options.onClose; this.machine = new StateMachine_1.default('closed', types_1.CIRCUIT_BREAKER_TRANSITIONS); } /** * Whether a request may proceed. * * Returns `false` while open. In half-open, the FIRST caller is admitted as the recovery probe and * `isProbeInFlight` is latched so every subsequent caller is rejected until that probe resolves * (via {@link recordSuccess} → close, or {@link recordFailure} → reopen). That single-probe gate is * the whole point of half-open: it tests recovery with one request instead of letting a herd of * waiting callers stampede a dependency that may still be down. */ isAllowed() { const currentState = this.getCurrentState(); if (currentState === 'open') { return false; } if (currentState === 'half-open') { if (this.isProbeInFlight) { return false; } this.isProbeInFlight = true; } return true; } /** * Record a failed request. May open the circuit from closed or half-open. * @returns `true` when the circuit is open after recording (the request must not proceed). */ recordFailure() { if (this.machine.state === 'open') { return true; } if (this.machine.state === 'half-open') { this.trip(); return true; } const reason = this.recordFailureInClosed(); if (reason) { this.trip(reason); return true; } return false; } /** Record a successful request. Closes the circuit from half-open and clears failure counts. */ recordSuccess() { if (this.machine.state === 'half-open') { this.close(); return; } if (this.machine.state === 'closed') { this.recordSuccessInClosed(); } } /** * The current state WITHOUT advancing recovery — a pure query, safe to call without side effects. * The open→half-open transition is applied only at the admission point ({@link isAllowed}); by the * time a caller queries state after being admitted, that transition has already happened. */ peekState() { return this.machine.state; } /** * Force the circuit back to closed from ANY state. This is a reset, not a transition, so it * deliberately bypasses the transition graph (and does not fire {@link onClose}). Use only to wipe * all state — e.g. between tests or sessions. */ hardReset() { this.machine = new StateMachine_1.default('closed', types_1.CIRCUIT_BREAKER_TRANSITIONS); this.openedAt = 0; this.isProbeInFlight = false; this.resetFailureState(); } getCurrentState() { this.maybeRecover(); return this.machine.state; } trip(reason = '') { var _a; if (this.machine.state === 'open') { return; } this.machine = this.machine.transition('open'); this.openedAt = Date.now(); this.isProbeInFlight = false; this.resetFailureState(); (_a = this.onTrip) === null || _a === void 0 ? void 0 : _a.call(this, reason); } close() { var _a; // close() only ever runs from half-open (see recordSuccess), and half-open → closed is the one // legal closing transition — so go through transition() to keep the illegal open → closed jump // an error rather than silently constructing a fresh closed machine. this.machine = this.machine.transition('closed'); this.openedAt = 0; this.isProbeInFlight = false; this.resetFailureState(); (_a = this.onClose) === null || _a === void 0 ? void 0 : _a.call(this); } /** * Lazily advance open → half-open once the reset timeout has elapsed. This is checked on read * (via {@link getCurrentState}) rather than on a timer, so there's nothing to schedule or clean up: * the transition simply becomes visible to the next caller after the window. Entering half-open * clears `isProbeInFlight` so the next admitted request becomes the recovery probe. */ maybeRecover() { if (this.machine.state !== 'open') { return; } if (Date.now() - this.openedAt < this.resetTimeoutMs) { return; } this.machine = this.machine.transition('half-open'); this.isProbeInFlight = false; } } exports.default = AbstractCircuitBreaker;