@zerothrow/resilience
Version:
Production-grade resilience patterns for ZeroThrow
373 lines (362 loc) • 9.93 kB
JavaScript
"use strict";
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// src/index.ts
var index_exports = {};
__export(index_exports, {
CircuitBreakerPolicy: () => CircuitBreakerPolicy,
CircuitOpenError: () => CircuitOpenError,
PolicyFactory: () => Policy,
RetryExhaustedError: () => RetryExhaustedError,
RetryPolicy: () => RetryPolicy,
SystemClock: () => SystemClock,
TestClock: () => TestClock,
TimeoutError: () => TimeoutError,
TimeoutPolicy: () => TimeoutPolicy,
compose: () => compose,
wrap: () => wrap
});
module.exports = __toCommonJS(index_exports);
// src/types.ts
var RetryExhaustedError = class extends Error {
constructor(policyName, attempts, lastError, context) {
super(`Retry exhausted after ${attempts} attempts`);
this.policyName = policyName;
this.attempts = attempts;
this.lastError = lastError;
this.context = context;
this.name = "RetryExhaustedError";
}
type = "retry-exhausted";
};
var CircuitOpenError = class extends Error {
constructor(policyName, openedAt, failureCount, context) {
super(`Circuit breaker is open`);
this.policyName = policyName;
this.openedAt = openedAt;
this.failureCount = failureCount;
this.context = context;
this.name = "CircuitOpenError";
}
type = "circuit-open";
};
var TimeoutError = class extends Error {
constructor(policyName, timeout, elapsed, context) {
super(`Operation timed out after ${elapsed}ms (limit: ${timeout}ms)`);
this.policyName = policyName;
this.timeout = timeout;
this.elapsed = elapsed;
this.context = context;
this.name = "TimeoutError";
}
type = "timeout";
};
// src/policies/retry.ts
var import_core2 = require("@zerothrow/core");
// src/policy.ts
var import_core = require("@zerothrow/core");
// src/clock.ts
var SystemClock = class {
now() {
return /* @__PURE__ */ new Date();
}
async sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
};
var TestClock = class {
currentTime = Date.now();
sleepers = [];
now() {
return new Date(this.currentTime);
}
async sleep(ms) {
const wakeTime = this.currentTime + ms;
return new Promise((resolve) => {
this.sleepers.push({ wakeTime, resolve });
this.sleepers.sort((a, b) => a.wakeTime - b.wakeTime);
});
}
advance(ms) {
this.currentTime += ms;
while (this.sleepers.length > 0) {
const nextSleeper = this.sleepers[0];
if (nextSleeper && nextSleeper.wakeTime <= this.currentTime) {
this.sleepers.shift();
nextSleeper.resolve();
} else {
break;
}
}
}
setTime(time) {
this.currentTime = typeof time === "number" ? time : time.getTime();
}
};
// src/policy.ts
var BasePolicy = class {
constructor(name, clock = new SystemClock()) {
this.name = name;
this.clock = clock;
}
async runOperation(operation) {
return import_core.ZT.tryAsync(operation);
}
};
// src/policies/retry.ts
var RetryPolicy = class extends BasePolicy {
constructor(count, options = {}, clock) {
super("retry", clock);
this.count = count;
this.options = options;
}
retryCallback;
async execute(operation) {
let lastError;
for (let attempt = 0; attempt <= this.count; attempt++) {
const result = await this.runOperation(operation);
if (result.ok) {
return result;
}
lastError = result.error;
if (this.options.handle && !this.options.handle(result.error)) {
return result;
}
if (attempt < this.count) {
const delayTime = this.calculateDelay(attempt + 1);
if (this.retryCallback) {
this.retryCallback(attempt + 1, lastError, delayTime);
}
await this.clock.sleep(delayTime);
}
}
return import_core2.ZT.err(new RetryExhaustedError(
this.name,
this.count + 1,
lastError
));
}
onRetry(callback) {
this.retryCallback = callback;
return this;
}
calculateDelay(attempt) {
const { backoff = "constant", delay = 1e3, maxDelay = 3e4 } = this.options;
let calculatedDelay;
switch (backoff) {
case "constant":
calculatedDelay = delay;
break;
case "linear":
calculatedDelay = delay * attempt;
break;
case "exponential":
calculatedDelay = delay * Math.pow(2, attempt - 1);
break;
default:
calculatedDelay = delay;
}
return Math.min(calculatedDelay, maxDelay);
}
};
// src/policies/circuit.ts
var import_core3 = require("@zerothrow/core");
var CircuitBreakerPolicy = class extends BasePolicy {
constructor(options, clock) {
super("circuit-breaker", clock);
this.options = options;
}
state = "closed";
failures = 0;
lastFailureTime;
nextAllowedTime;
stateChangeCallback;
async execute(operation) {
if (this.state === "open") {
const now = this.clock.now().getTime();
if (this.nextAllowedTime && now >= this.nextAllowedTime) {
this.setState("half-open");
} else {
return import_core3.ZT.err(new CircuitOpenError(
this.name,
new Date(this.lastFailureTime || now),
this.failures
));
}
}
const result = await this.runOperation(operation);
if (result.ok) {
this.onSuccess();
return result;
} else {
this.onFailure();
const currentState = this.state;
if (currentState === "open") {
return import_core3.ZT.err(new CircuitOpenError(
this.name,
new Date(this.lastFailureTime || Date.now()),
this.failures
));
}
return result;
}
}
onSuccess() {
if (this.state === "half-open") {
this.reset();
this.options.onClose?.();
}
}
onFailure() {
this.failures++;
this.lastFailureTime = this.clock.now().getTime();
if (this.state === "half-open") {
this.open();
} else if (this.failures >= this.options.threshold) {
this.open();
}
}
open() {
this.setState("open");
this.nextAllowedTime = this.clock.now().getTime() + this.options.duration;
this.options.onOpen?.();
}
reset() {
this.setState("closed");
this.failures = 0;
delete this.lastFailureTime;
delete this.nextAllowedTime;
}
setState(newState) {
if (this.state !== newState) {
this.state = newState;
this.stateChangeCallback?.(newState);
}
}
onCircuitStateChange(callback) {
this.stateChangeCallback = callback;
return this;
}
};
// src/policies/timeout.ts
var import_core4 = require("@zerothrow/core");
var TimeoutPolicy = class extends BasePolicy {
constructor(options, clock) {
super("timeout", clock);
this.options = options;
}
async execute(operation) {
const startTime = this.clock.now().getTime();
const timeoutPromise = new Promise((_, reject) => {
setTimeout(() => {
const elapsed = this.clock.now().getTime() - startTime;
reject(new TimeoutError(
this.name,
this.options.timeout,
elapsed
));
}, this.options.timeout);
});
try {
const result = await Promise.race([
operation(),
timeoutPromise
]);
return import_core4.ZT.ok(result);
} catch (error) {
if (error instanceof TimeoutError) {
return import_core4.ZT.err(error);
}
return import_core4.ZT.err(error instanceof Error ? error : new Error(String(error)));
}
}
};
// src/compose.ts
function wrap(outer, inner) {
return {
async execute(operation) {
return outer.execute(
() => (
// The inner policy executes the actual operation
inner.execute(operation).then((result) => {
if (!result.ok) {
throw result.error;
}
return result.value;
})
)
);
}
};
}
function compose(...policies) {
if (policies.length === 0) {
throw new Error("compose requires at least one policy");
}
if (policies.length === 1) {
return policies[0];
}
return policies.reduce((acc, policy) => wrap(policy, acc));
}
// src/policy-factory.ts
var Policy = {
/**
* Creates a retry policy
*/
retry(count, options, clock) {
return new RetryPolicy(count, options, clock);
},
/**
* Creates a circuit breaker policy
*/
circuitBreaker(options, clock) {
return new CircuitBreakerPolicy(options, clock);
},
/**
* Creates a timeout policy
*/
timeout(options, clock) {
const opts = typeof options === "number" ? { timeout: options } : options;
return new TimeoutPolicy(opts, clock);
},
/**
* Wraps one policy with another
* The outer policy executes first
*/
wrap,
/**
* Composes multiple policies from left to right
* The leftmost policy is the outermost wrapper
*/
compose
};
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
CircuitBreakerPolicy,
CircuitOpenError,
PolicyFactory,
RetryExhaustedError,
RetryPolicy,
SystemClock,
TestClock,
TimeoutError,
TimeoutPolicy,
compose,
wrap
});
//# sourceMappingURL=index.cjs.map