@stackmemoryai/stackmemory
Version:
Project-scoped memory for AI coding tools. Durable context across sessions with MCP integration, frames, smart retrieval, Claude Code skills, and automatic hooks.
431 lines (430 loc) • 13.2 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 { DatabaseError, ErrorCode } from "../errors/index.js";
import { EventEmitter } from "events";
class QueryRouter extends EventEmitter {
tiers = /* @__PURE__ */ new Map();
metrics;
decisionCache = /* @__PURE__ */ new Map();
cacheExpiration = 6e4;
// 1 minute
maxCacheSize = 1e3;
constructor() {
super();
this.metrics = {
totalQueries: 0,
queriesByTier: /* @__PURE__ */ new Map(),
queriesByType: /* @__PURE__ */ new Map(),
averageLatency: 0,
latencyByTier: /* @__PURE__ */ new Map(),
errorsByTier: /* @__PURE__ */ new Map(),
cacheHitRate: 0,
routingDecisions: 0
};
}
/**
* Register a storage tier with the router
*/
registerTier(tier) {
this.tiers.set(tier.name, tier);
logger.info(
`Registered storage tier: ${tier.name} (priority: ${tier.priority})`
);
this.emit("tierRegistered", tier);
}
/**
* Remove a storage tier from the router
*/
unregisterTier(tierName) {
const tier = this.tiers.get(tierName);
if (tier) {
this.tiers.delete(tierName);
logger.info(`Unregistered storage tier: ${tierName}`);
this.emit("tierUnregistered", tier);
}
}
/**
* Route a query to the most appropriate storage tier
*/
async route(operation, context, executor) {
const startTime = Date.now();
this.metrics.totalQueries++;
this.metrics.queriesByType.set(
context.queryType,
(this.metrics.queriesByType.get(context.queryType) || 0) + 1
);
try {
const decision = await this.makeRoutingDecision(operation, context);
try {
const result = await this.executeOnTier(decision.primaryTier, executor);
this.updateMetrics(decision.primaryTier.name, startTime, true);
return result;
} catch (error) {
logger.warn(
`Query failed on primary tier ${decision.primaryTier.name}:`,
error
);
this.updateMetrics(decision.primaryTier.name, startTime, false);
for (const fallbackTier of decision.fallbackTiers) {
try {
logger.info(`Attempting fallback to tier: ${fallbackTier.name}`);
const result = await this.executeOnTier(fallbackTier, executor);
this.updateMetrics(fallbackTier.name, startTime, true);
return result;
} catch (fallbackError) {
logger.warn(
`Query failed on fallback tier ${fallbackTier.name}:`,
fallbackError
);
this.updateMetrics(fallbackTier.name, startTime, false);
}
}
throw error;
}
} catch (error) {
logger.error("Query routing failed:", error);
this.emit("routingError", { operation, context, error });
throw error;
}
}
/**
* Make routing decision based on query context
*/
async makeRoutingDecision(operation, context) {
const cacheKey = this.generateCacheKey(operation, context);
const cached = this.decisionCache.get(cacheKey);
if (cached && Date.now() - cached.estimatedLatency < this.cacheExpiration) {
this.metrics.cacheHitRate = (this.metrics.cacheHitRate * this.metrics.routingDecisions + 1) / (this.metrics.routingDecisions + 1);
return cached;
}
this.metrics.routingDecisions++;
const evaluations = [];
for (const tier of this.tiers.values()) {
const score = await this.evaluateTier(tier, operation, context);
const rationale = this.generateRationale(tier, operation, context, score);
evaluations.push({ tier, score, rationale });
}
evaluations.sort((a, b) => b.score - a.score);
if (evaluations.length === 0) {
throw new DatabaseError(
"No storage tiers available for routing",
ErrorCode.DB_CONNECTION_FAILED,
{ query: context.query, tiersConfigured: this.tiers.length }
);
}
const primaryEval = evaluations[0];
const fallbackTiers = evaluations.slice(1).map((evaluation) => evaluation.tier);
const decision = {
primaryTier: primaryEval.tier,
fallbackTiers,
rationale: primaryEval.rationale,
confidence: primaryEval.score,
estimatedLatency: this.estimateLatency(
primaryEval.tier,
operation,
context
),
cacheRecommendation: this.recommendCacheStrategy(
primaryEval.tier,
context
)
};
this.cacheDecision(cacheKey, decision);
logger.debug(
`Routing decision: ${decision.primaryTier.name} (confidence: ${decision.confidence.toFixed(2)})`
);
this.emit("routingDecision", { operation, context, decision });
return decision;
}
/**
* Evaluate how well a tier fits the query requirements
*/
async evaluateTier(tier, operation, context) {
let score = 0;
let maxScore = 0;
for (const rule of tier.config.routingRules) {
maxScore += rule.weight;
if (this.evaluateRule(rule, operation, context, tier)) {
score += rule.weight;
}
}
if (tier.config.preferredOperations.includes(context.queryType)) {
score += 0.2;
maxScore += 0.2;
}
if (context.requiredFeatures) {
const supportedFeatures = context.requiredFeatures.filter(
(feature) => tier.config.supportedFeatures.includes(feature)
);
if (supportedFeatures.length === context.requiredFeatures.length) {
score += 0.3;
}
maxScore += 0.3;
}
const currentLoad = await this.getCurrentLoad(tier);
if (tier.config.maxThroughput && currentLoad < tier.config.maxThroughput * 0.8) {
score += 0.1;
}
maxScore += 0.1;
if (await this.isWithinCapacity(tier)) {
score += 0.1;
}
maxScore += 0.1;
return maxScore > 0 ? score / maxScore : 0;
}
/**
* Evaluate a single routing rule
*/
evaluateRule(rule, _operation, context, _tier) {
let actualValue;
switch (rule.condition) {
case "age":
if (context.frames && context.frames.length > 0) {
const avgAge = context.frames.reduce(
(sum, frame) => sum + (Date.now() - frame.created_at),
0
) / context.frames.length;
actualValue = avgAge;
} else if (context.timeRange) {
actualValue = Date.now() - context.timeRange.end.getTime();
} else {
return false;
}
break;
case "query_type":
actualValue = context.queryType;
break;
case "feature":
actualValue = context.requiredFeatures || [];
break;
case "priority":
actualValue = context.priority || "medium";
break;
case "size":
actualValue = context.frames ? context.frames.length : 0;
break;
default:
return false;
}
return this.compareValues(actualValue, rule.operator, rule.value);
}
/**
* Compare values based on operator
*/
compareValues(actual, operator, expected) {
switch (operator) {
case ">":
return actual > expected;
case "<":
return actual < expected;
case "=":
case "==":
return actual === expected;
case "!=":
return actual !== expected;
case "in":
return Array.isArray(expected) && expected.includes(actual);
case "not_in":
return Array.isArray(expected) && !expected.includes(actual);
case "contains":
return Array.isArray(actual) && actual.some((item) => expected.includes(item));
default:
return false;
}
}
/**
* Execute query on specific tier
*/
async executeOnTier(tier, executor) {
const startTime = Date.now();
try {
const result = await executor(tier.adapter);
const duration = Date.now() - startTime;
logger.debug(`Query executed on tier ${tier.name} in ${duration}ms`);
this.emit("queryExecuted", {
tierName: tier.name,
duration,
success: true
});
return result;
} catch (error) {
const duration = Date.now() - startTime;
logger.error(
`Query failed on tier ${tier.name} after ${duration}ms:`,
error
);
this.emit("queryExecuted", {
tierName: tier.name,
duration,
success: false,
error
});
throw error;
}
}
/**
* Generate cache key for routing decisions
*/
generateCacheKey(operation, context) {
const keyParts = [
operation,
context.queryType,
context.priority || "medium",
(context.requiredFeatures || []).sort().join(","),
context.timeRange ? `${context.timeRange.start.getTime()}-${context.timeRange.end.getTime()}` : ""
];
return keyParts.join("|");
}
/**
* Cache routing decision
*/
cacheDecision(key, decision) {
if (this.decisionCache.size >= this.maxCacheSize) {
const firstKey = this.decisionCache.keys().next().value;
this.decisionCache.delete(firstKey);
}
this.decisionCache.set(key, decision);
}
/**
* Estimate query latency for a tier
*/
estimateLatency(tier, operation, context) {
const baseLatency = this.metrics.latencyByTier.get(tier.name) || tier.config.maxLatency || 100;
let multiplier = 1;
switch (context.queryType) {
case "search":
multiplier = 1.5;
break;
case "analytics":
multiplier = 2;
break;
case "bulk":
multiplier = 3;
break;
default:
multiplier = 1;
}
return baseLatency * multiplier;
}
/**
* Recommend cache strategy for the context
*/
recommendCacheStrategy(tier, context) {
if (context.cacheStrategy && context.cacheStrategy !== "none") {
return context.cacheStrategy;
}
if (tier.name === "hot" || tier.name === "memory") {
return "read_write";
} else if (context.queryType === "read") {
return "read";
}
return "none";
}
/**
* Generate human-readable rationale for routing decision
*/
generateRationale(tier, operation, context, score) {
const reasons = [];
if (tier.config.preferredOperations.includes(context.queryType)) {
reasons.push(`optimized for ${context.queryType} operations`);
}
if (context.requiredFeatures?.every(
(feature) => tier.config.supportedFeatures.includes(feature)
)) {
reasons.push(
`supports all required features (${context.requiredFeatures.join(", ")})`
);
}
if (score > 0.8) {
reasons.push("high confidence match");
} else if (score > 0.6) {
reasons.push("good match");
} else if (score > 0.4) {
reasons.push("acceptable match");
}
return reasons.length > 0 ? reasons.join(", ") : "default tier selection";
}
/**
* Get current load for a tier
*/
async getCurrentLoad(tier) {
return this.metrics.queriesByTier.get(tier.name) || 0;
}
/**
* Check if tier is within capacity limits
*/
async isWithinCapacity(tier) {
try {
const stats = await tier.adapter.getStats();
if (tier.config.maxFrames && stats.totalFrames >= tier.config.maxFrames) {
return false;
}
if (tier.config.maxSizeMB && stats.diskUsage >= tier.config.maxSizeMB * 1024 * 1024) {
return false;
}
return true;
} catch (error) {
logger.warn(`Failed to check capacity for tier ${tier.name}:`, error);
return true;
}
}
/**
* Update routing metrics
*/
updateMetrics(tierName, startTime, success) {
const duration = Date.now() - startTime;
this.metrics.queriesByTier.set(
tierName,
(this.metrics.queriesByTier.get(tierName) || 0) + 1
);
if (success) {
const currentAvg = this.metrics.latencyByTier.get(tierName) || 0;
const count = this.metrics.queriesByTier.get(tierName) || 1;
const newAvg = (currentAvg * (count - 1) + duration) / count;
this.metrics.latencyByTier.set(tierName, newAvg);
this.metrics.averageLatency = (this.metrics.averageLatency * (this.metrics.totalQueries - 1) + duration) / this.metrics.totalQueries;
} else {
this.metrics.errorsByTier.set(
tierName,
(this.metrics.errorsByTier.get(tierName) || 0) + 1
);
}
}
/**
* Get current routing metrics
*/
getMetrics() {
const cacheRequests = this.metrics.routingDecisions;
const cacheHits = cacheRequests - this.decisionCache.size;
this.metrics.cacheHitRate = cacheRequests > 0 ? cacheHits / cacheRequests : 0;
return { ...this.metrics };
}
/**
* Get registered tiers
*/
getTiers() {
return Array.from(this.tiers.values()).sort(
(a, b) => b.priority - a.priority
);
}
/**
* Clear routing decision cache
*/
clearCache() {
this.decisionCache.clear();
logger.info("Routing decision cache cleared");
}
/**
* Get tier by name
*/
getTier(name) {
return this.tiers.get(name);
}
}
export {
QueryRouter
};
//# sourceMappingURL=query-router.js.map