@stackmemoryai/stackmemory
Version:
Lossless, project-scoped memory for AI coding tools. Durable context across sessions with 56 MCP tools, FTS5 search, conductor orchestrator, loop/watch monitoring, snapshot capture, pre-flight overlap checks, Claude/Codex/OpenCode wrappers, Linear sync, a
269 lines (268 loc) • 7.33 kB
JavaScript
import { fileURLToPath as __fileURLToPath } from 'url';
import { dirname as __pathDirname } from 'path';
const __filename = __fileURLToPath(import.meta.url);
const __dirname = __pathDirname(__filename);
import { logger } from "../monitoring/logger.js";
import {
isRetryableError,
getErrorMessage
} from "./index.js";
var CircuitState = /* @__PURE__ */ ((CircuitState2) => {
CircuitState2["CLOSED"] = "closed";
CircuitState2["OPEN"] = "open";
CircuitState2["HALF_OPEN"] = "half_open";
return CircuitState2;
})(CircuitState || {});
function calculateBackoff(attempt, initialDelay, maxDelay, factor) {
const exponentialDelay = Math.min(
initialDelay * Math.pow(factor, attempt - 1),
maxDelay
);
const jitter = exponentialDelay * Math.random() * 0.25;
return Math.floor(exponentialDelay + jitter);
}
async function retry(fn, options = {}) {
const {
maxAttempts = 3,
initialDelay = 1e3,
maxDelay = 3e4,
backoffFactor = 2,
timeout,
onRetry
} = options;
let lastError;
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
if (timeout) {
return await Promise.race([
fn(),
new Promise(
(_, reject) => setTimeout(
() => reject(new Error(`Operation timed out after ${timeout}ms`)),
timeout
)
)
]);
}
return await fn();
} catch (error) {
lastError = error;
if (!isRetryableError(error) || attempt === maxAttempts) {
throw error;
}
const delay = calculateBackoff(
attempt,
initialDelay,
maxDelay,
backoffFactor
);
logger.warn(`Retry attempt ${attempt}/${maxAttempts} after ${delay}ms`, {
error: getErrorMessage(error),
attempt,
delay
});
if (onRetry) {
onRetry(attempt, error);
}
await new Promise((resolve) => setTimeout(resolve, delay));
}
}
throw lastError;
}
class CircuitBreaker {
constructor(name, options = {}) {
this.name = name;
this.options = {
failureThreshold: options.failureThreshold ?? 5,
resetTimeout: options.resetTimeout ?? 6e4,
halfOpenRequests: options.halfOpenRequests ?? 3
};
}
state = "closed" /* CLOSED */;
failures = 0;
successCount = 0;
lastFailTime;
options;
async execute(fn) {
if (this.state === "open" /* OPEN */) {
const timeSinceLastFailure = this.lastFailTime ? Date.now() - this.lastFailTime.getTime() : 0;
if (timeSinceLastFailure >= this.options.resetTimeout) {
this.state = "half_open" /* HALF_OPEN */;
this.successCount = 0;
logger.info(`Circuit breaker ${this.name} entering half-open state`);
} else {
throw new Error(
`Circuit breaker ${this.name} is OPEN. Retry after ${this.options.resetTimeout - timeSinceLastFailure}ms`
);
}
}
try {
const result = await fn();
if (this.state === "half_open" /* HALF_OPEN */) {
this.successCount++;
if (this.successCount >= this.options.halfOpenRequests) {
this.state = "closed" /* CLOSED */;
this.failures = 0;
logger.info(`Circuit breaker ${this.name} is now CLOSED`);
}
} else {
this.failures = 0;
}
return result;
} catch (error) {
this.handleFailure(error);
throw error;
}
}
handleFailure(_error) {
this.failures++;
this.lastFailTime = /* @__PURE__ */ new Date();
if (this.state === "half_open" /* HALF_OPEN */) {
this.state = "open" /* OPEN */;
logger.error(
`Circuit breaker ${this.name} reopened due to failure in half-open state`
);
} else if (this.state === "closed" /* CLOSED */ && this.failures >= this.options.failureThreshold) {
this.state = "open" /* OPEN */;
logger.error(
`Circuit breaker ${this.name} opened after ${this.failures} failures`
);
}
}
getState() {
return this.state;
}
reset() {
this.state = "closed" /* CLOSED */;
this.failures = 0;
this.successCount = 0;
this.lastFailTime = void 0;
logger.info(`Circuit breaker ${this.name} manually reset`);
}
}
async function withFallback(primary, fallbacks, context) {
const errors = [];
try {
return await primary();
} catch (error) {
errors.push(error);
logger.warn("Primary operation failed, trying fallbacks", {
error: getErrorMessage(error),
context
});
}
for (let i = 0; i < fallbacks.length; i++) {
try {
const result = await fallbacks[i]();
logger.info(`Fallback ${i + 1} succeeded`, { context });
return result;
} catch (error) {
errors.push(error);
if (i < fallbacks.length - 1) {
logger.warn(`Fallback ${i + 1} failed, trying next`, {
error: getErrorMessage(error),
context
});
}
}
}
throw new Error(
`All attempts failed. Errors: ${errors.map(getErrorMessage).join(", ")}`
);
}
class Bulkhead {
constructor(name, maxConcurrent) {
this.name = name;
this.maxConcurrent = maxConcurrent;
}
running = 0;
queue = [];
async execute(fn) {
if (this.running >= this.maxConcurrent) {
await new Promise((resolve) => {
this.queue.push(resolve);
});
}
this.running++;
try {
return await fn();
} finally {
this.running--;
const next = this.queue.shift();
if (next) {
next();
}
}
}
getStats() {
return {
running: this.running,
queued: this.queue.length,
maxConcurrent: this.maxConcurrent
};
}
}
async function withTimeout(fn, timeoutMs, timeoutMessage) {
return Promise.race([
fn(),
new Promise(
(_, reject) => setTimeout(
() => reject(
new Error(
timeoutMessage ?? `Operation timed out after ${timeoutMs}ms`
)
),
timeoutMs
)
)
]);
}
async function gracefulDegrade(fn, defaultValue, logContext) {
try {
return await fn();
} catch (error) {
logger.warn("Operation failed, using default value", {
error: getErrorMessage(error),
...logContext
});
return defaultValue;
}
}
function createResilientOperation(name, options = {}) {
const circuitBreaker = options.circuitBreaker ? new CircuitBreaker(name, options.circuitBreaker) : null;
const bulkhead = options.bulkhead ? new Bulkhead(name, options.bulkhead) : null;
return async (fn) => {
let currentFn = fn;
if (bulkhead) {
const wrapped = currentFn;
currentFn = () => bulkhead.execute(wrapped);
}
if (options.timeout) {
const wrapped = currentFn;
currentFn = () => withTimeout(wrapped, options.timeout);
}
if (options.retry) {
const wrapped = currentFn;
currentFn = () => retry(wrapped, options.retry);
}
if (circuitBreaker) {
const wrapped = currentFn;
currentFn = () => circuitBreaker.execute(wrapped);
}
if (options.fallback) {
return withFallback(currentFn, [options.fallback]);
}
return currentFn();
};
}
export {
Bulkhead,
CircuitBreaker,
CircuitState,
calculateBackoff,
createResilientOperation,
gracefulDegrade,
retry,
withFallback,
withTimeout
};