UNPKG

@comic-vine/sqlite-store

Version:

SQLite store implementations for Comic Vine client caching, deduplication, and rate limiting using Drizzle ORM

1,216 lines (1,210 loc) 41.3 kB
'use strict'; var Database = require('better-sqlite3'); var drizzleOrm = require('drizzle-orm'); var betterSqlite3 = require('drizzle-orm/better-sqlite3'); var sqliteCore = require('drizzle-orm/sqlite-core'); var crypto = require('crypto'); var client = require('@comic-vine/client'); function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; } var Database__default = /*#__PURE__*/_interopDefault(Database); var __async = (__this, __arguments, generator) => { return new Promise((resolve, reject) => { var fulfilled = (value) => { try { step(generator.next(value)); } catch (e) { reject(e); } }; var rejected = (value) => { try { step(generator.throw(value)); } catch (e) { reject(e); } }; var step = (x) => x.done ? resolve(x.value) : Promise.resolve(x.value).then(fulfilled, rejected); step((generator = generator.apply(__this, __arguments)).next()); }); }; var cacheTable = sqliteCore.sqliteTable("cache", { hash: sqliteCore.text("hash").primaryKey(), value: sqliteCore.blob("value", { mode: "json" }).notNull(), expiresAt: sqliteCore.integer("expires_at").notNull(), createdAt: sqliteCore.integer("created_at").notNull() }); var dedupeTable = sqliteCore.sqliteTable("dedupe_jobs", { hash: sqliteCore.text("hash").primaryKey(), jobId: sqliteCore.text("job_id").notNull(), status: sqliteCore.text("status").notNull(), // 'pending', 'completed', 'failed' result: sqliteCore.blob("result", { mode: "json" }), error: sqliteCore.text("error"), createdAt: sqliteCore.integer("created_at").notNull(), updatedAt: sqliteCore.integer("updated_at").notNull() }); var rateLimitTable = sqliteCore.sqliteTable("rate_limits", { resource: sqliteCore.text("resource").notNull(), timestamp: sqliteCore.integer("timestamp").notNull(), id: sqliteCore.integer("id").primaryKey({ autoIncrement: true }) }); // src/sqlite-cache-store.ts var SQLiteCacheStore = class { constructor({ /** File path or existing `better-sqlite3` connection. Defaults to `':memory:'`. */ database = ":memory:", /** Cleanup interval in milliseconds. Defaults to 1 minute. */ cleanupIntervalMs = 6e4, /** Maximum allowed size (in bytes) for a single cache entry. Defaults to 5 MiB. */ maxEntrySizeBytes = 5 * 1024 * 1024 } = {}) { /** Indicates whether this store is responsible for managing (and therefore closing) the SQLite connection */ this.isConnectionManaged = false; this.isDestroyed = false; let sqliteInstance; let isConnectionManaged = false; if (typeof database === "string") { sqliteInstance = new Database__default.default(database); isConnectionManaged = true; } else { sqliteInstance = database; } this.sqlite = sqliteInstance; this.isConnectionManaged = isConnectionManaged; this.db = betterSqlite3.drizzle(sqliteInstance); this.cleanupIntervalMs = cleanupIntervalMs; this.maxEntrySizeBytes = maxEntrySizeBytes; this.initializeDatabase(); this.startCleanupInterval(); } get(hash) { return __async(this, null, function* () { if (this.isDestroyed) { throw new Error("Cache store has been destroyed"); } const result = yield this.db.select().from(cacheTable).where(drizzleOrm.eq(cacheTable.hash, hash)).limit(1); if (result.length === 0) { return void 0; } const item = result[0]; if (!item) { return void 0; } const now = Date.now(); if (now >= item.expiresAt) { yield this.db.delete(cacheTable).where(drizzleOrm.eq(cacheTable.hash, hash)); return void 0; } try { if (item.value === "__UNDEFINED__") { return void 0; } return JSON.parse(item.value); } catch (e) { yield this.db.delete(cacheTable).where(drizzleOrm.eq(cacheTable.hash, hash)); return void 0; } }); } set(hash, value, ttlSeconds) { return __async(this, null, function* () { if (this.isDestroyed) { throw new Error("Cache store has been destroyed"); } const now = Date.now(); const expiresAt = ttlSeconds <= 0 ? now : now + ttlSeconds * 1e3; let serializedValue; try { if (value === void 0) { serializedValue = "__UNDEFINED__"; } else { serializedValue = JSON.stringify(value); } } catch (error) { throw new Error( `Failed to serialize value: ${error instanceof Error ? error.message : String(error)}` ); } if (Buffer.byteLength(serializedValue, "utf8") > this.maxEntrySizeBytes) { return; } yield this.db.insert(cacheTable).values({ hash, value: serializedValue, expiresAt, createdAt: now }).onConflictDoUpdate({ target: cacheTable.hash, set: { value: serializedValue, expiresAt, createdAt: now } }); }); } delete(hash) { return __async(this, null, function* () { if (this.isDestroyed) { throw new Error("Cache store has been destroyed"); } yield this.db.delete(cacheTable).where(drizzleOrm.eq(cacheTable.hash, hash)); }); } clear() { return __async(this, null, function* () { if (this.isDestroyed) { throw new Error("Cache store has been destroyed"); } yield this.db.delete(cacheTable); }); } /** * Get cache statistics */ getStats() { return __async(this, null, function* () { var _a, _b, _c, _d; const now = Date.now(); const totalResult = yield this.db.select({ count: drizzleOrm.count() }).from(cacheTable); const expiredResult = yield this.db.select({ count: drizzleOrm.count() }).from(cacheTable).where(drizzleOrm.lt(cacheTable.expiresAt, now)); const pageCount = Number( this.sqlite.pragma("page_count", { simple: true }) ); const pageSize = Number(this.sqlite.pragma("page_size", { simple: true })); const safePageCount = Number.isFinite(pageCount) ? pageCount : 0; const safePageSize = Number.isFinite(pageSize) ? pageSize : 0; const databaseSizeKB = Math.round(safePageCount * safePageSize / 1024); return { databaseSizeKB, expiredItems: (_b = (_a = expiredResult[0]) == null ? void 0 : _a.count) != null ? _b : 0, totalItems: (_d = (_c = totalResult[0]) == null ? void 0 : _c.count) != null ? _d : 0 }; }); } /** * Manually trigger cleanup of expired items */ cleanup() { return __async(this, null, function* () { const now = Date.now(); yield this.db.delete(cacheTable).where(drizzleOrm.lt(cacheTable.expiresAt, now)); }); } /** * Close the database connection */ close() { return __async(this, null, function* () { if (this.cleanupInterval) { clearInterval(this.cleanupInterval); this.cleanupInterval = void 0; } this.isDestroyed = true; if (this.isConnectionManaged && typeof this.sqlite.close === "function") { this.sqlite.close(); } }); } /** * Alias for close() to match test expectations */ destroy() { this.close(); } initializeDatabase() { this.db.run(drizzleOrm.sql` CREATE TABLE IF NOT EXISTS cache ( hash TEXT PRIMARY KEY, value BLOB NOT NULL, expires_at INTEGER NOT NULL, created_at INTEGER NOT NULL ) `); this.db.run(drizzleOrm.sql` CREATE INDEX IF NOT EXISTS idx_cache_expires_at ON cache(expires_at) `); } startCleanupInterval() { this.cleanupInterval = setInterval(() => __async(this, null, function* () { yield this.cleanup(); }), this.cleanupIntervalMs); if (typeof this.cleanupInterval.unref === "function") { this.cleanupInterval.unref(); } } cleanupExpiredItems() { return __async(this, null, function* () { const now = Date.now(); yield this.db.delete(cacheTable).where(drizzleOrm.lt(cacheTable.expiresAt, now)); }); } }; var SQLiteDedupeStore = class { constructor({ /** File path or existing `better-sqlite3` Database instance. Defaults to `':memory:'`. */ database = ":memory:", /** Job timeout in milliseconds. Preferred over timeoutMs. */ jobTimeoutMs, /** Legacy alias for jobTimeoutMs. */ timeoutMs, /** Cleanup interval in milliseconds. Defaults to 1 minute. */ cleanupIntervalMs = 6e4 } = {}) { /** Indicates whether this store manages (and should close) the SQLite connection */ this.isConnectionManaged = false; this.jobPromises = /* @__PURE__ */ new Map(); this.jobResolvers = /* @__PURE__ */ new Map(); this.isDestroyed = false; var _a; let sqliteInstance; let isConnectionManaged = false; if (typeof database === "string") { sqliteInstance = new Database__default.default(database); isConnectionManaged = true; } else { sqliteInstance = database; } this.sqlite = sqliteInstance; this.isConnectionManaged = isConnectionManaged; this.db = betterSqlite3.drizzle(sqliteInstance); this.jobTimeoutMs = (_a = timeoutMs != null ? timeoutMs : jobTimeoutMs) != null ? _a : 3e5; this.cleanupIntervalMs = cleanupIntervalMs; this.initializeDatabase(); this.startCleanupInterval(); } startCleanupInterval() { if (this.cleanupIntervalMs > 0) { this.cleanupInterval = setInterval(() => { this.cleanupExpiredJobs().catch(() => { }); }, this.cleanupIntervalMs); if (typeof this.cleanupInterval.unref === "function") { this.cleanupInterval.unref(); } } } cleanupExpiredJobs() { return __async(this, null, function* () { const noTimeoutConfigured = this.jobTimeoutMs <= 0; if (noTimeoutConfigured) { return; } const now = Date.now(); const expiredThreshold = now - this.jobTimeoutMs; yield this.db.delete(dedupeTable).where( drizzleOrm.and( drizzleOrm.eq(dedupeTable.status, "pending"), drizzleOrm.lt(dedupeTable.createdAt, expiredThreshold) ) ); }); } waitFor(hash) { return __async(this, null, function* () { if (this.isDestroyed) { throw new Error("Dedupe store has been destroyed"); } const existingPromise = this.jobPromises.get(hash); if (existingPromise) { return existingPromise; } const result = yield this.db.select().from(dedupeTable).where(drizzleOrm.eq(dedupeTable.hash, hash)).limit(1); if (result.length === 0) { return void 0; } const job = result[0]; if (!job) { return void 0; } if (job.status === "completed") { try { if (job.result === "__UNDEFINED__" || job.result === "__NULL__") { return void 0; } else if (job.result) { return JSON.parse(job.result); } return void 0; } catch (e) { return void 0; } } if (job.status === "failed") { return void 0; } const promise = new Promise((resolve, reject) => { this.jobResolvers.set(hash, { resolve, reject }); if (this.jobTimeoutMs > 0) { setTimeout(() => { const resolver = this.jobResolvers.get(hash); if (resolver) { this.jobResolvers.delete(hash); this.jobPromises.delete(hash); this.db.update(dedupeTable).set({ status: "failed", error: "Job timed out", updatedAt: Date.now() }).where(drizzleOrm.eq(dedupeTable.hash, hash)).then(() => { resolve(void 0); }).catch(() => { resolve(void 0); }); } }, this.jobTimeoutMs); } }); this.jobPromises.set(hash, promise); return promise; }); } register(hash) { return __async(this, null, function* () { if (this.isDestroyed) { throw new Error("Dedupe store has been destroyed"); } const existingJob = yield this.db.select().from(dedupeTable).where(drizzleOrm.eq(dedupeTable.hash, hash)).limit(1); if (existingJob.length > 0) { const job = existingJob[0]; if (job && job.status === "pending") { return job.jobId; } } const jobId = crypto.randomUUID(); const now = Date.now(); yield this.db.insert(dedupeTable).values({ hash, jobId, status: "pending", createdAt: now, updatedAt: now }).onConflictDoUpdate({ target: dedupeTable.hash, set: { jobId, status: "pending", updatedAt: now } }); return jobId; }); } complete(hash, value) { return __async(this, null, function* () { var _a; if (this.isDestroyed) { throw new Error("Dedupe store has been destroyed"); } let serializedResult; if (value === void 0) { serializedResult = "__UNDEFINED__"; } else if (value === null) { serializedResult = "__NULL__"; } else { try { serializedResult = JSON.stringify(value); } catch (error) { throw new Error( `Failed to serialize result: ${error instanceof Error ? error.message : String(error)}` ); } } const now = Date.now(); const existingJob = yield this.db.select().from(dedupeTable).where(drizzleOrm.eq(dedupeTable.hash, hash)).limit(1); if (existingJob.length > 0 && ((_a = existingJob[0]) == null ? void 0 : _a.status) === "completed") { return; } yield this.db.update(dedupeTable).set({ status: "completed", result: serializedResult, updatedAt: now }).where(drizzleOrm.eq(dedupeTable.hash, hash)); const resolver = this.jobResolvers.get(hash); if (resolver) { resolver.resolve(value); this.jobResolvers.delete(hash); this.jobPromises.delete(hash); } }); } fail(hash, error) { return __async(this, null, function* () { if (this.isDestroyed) { throw new Error("Dedupe store has been destroyed"); } const now = Date.now(); yield this.db.update(dedupeTable).set({ status: "failed", error: error.message, updatedAt: now }).where(drizzleOrm.eq(dedupeTable.hash, hash)); const resolver = this.jobResolvers.get(hash); if (resolver) { resolver.reject(error); this.jobResolvers.delete(hash); this.jobPromises.delete(hash); } }); } isInProgress(hash) { return __async(this, null, function* () { if (this.isDestroyed) { throw new Error("Dedupe store has been destroyed"); } const result = yield this.db.select().from(dedupeTable).where(drizzleOrm.eq(dedupeTable.hash, hash)).limit(1); if (result.length === 0) { return false; } const job = result[0]; if (!job) { return false; } const jobExpired = this.jobTimeoutMs > 0 && Date.now() - job.createdAt >= this.jobTimeoutMs; if (jobExpired) { yield this.db.delete(dedupeTable).where(drizzleOrm.eq(dedupeTable.hash, hash)); return false; } return job.status === "pending"; }); } getResult(hash) { return __async(this, null, function* () { const result = yield this.db.select().from(dedupeTable).where(drizzleOrm.eq(dedupeTable.hash, hash)).limit(1); if (result.length === 0) { return void 0; } const job = result[0]; if (!job) { return void 0; } const now = Date.now(); const isExpired = now - job.createdAt > this.jobTimeoutMs; if (isExpired) { yield this.db.delete(dedupeTable).where(drizzleOrm.eq(dedupeTable.hash, hash)); return void 0; } if (job.status === "completed") { try { if (job.result === "__UNDEFINED__") { return void 0; } else if (job.result === "__NULL__") { return null; } else if (job.result) { return JSON.parse(job.result); } return void 0; } catch (e) { return void 0; } } return void 0; }); } /** * Get statistics about dedupe jobs */ getStats() { return __async(this, null, function* () { var _a, _b, _c, _d, _e; const now = Date.now(); const expiredTime = now - this.jobTimeoutMs; const totalResult = yield this.db.select({ count: drizzleOrm.count() }).from(dedupeTable); const pendingResult = yield this.db.select({ count: drizzleOrm.count() }).from(dedupeTable).where(drizzleOrm.eq(dedupeTable.status, "pending")); const completedResult = yield this.db.select({ count: drizzleOrm.count() }).from(dedupeTable).where(drizzleOrm.eq(dedupeTable.status, "completed")); const failedResult = yield this.db.select({ count: drizzleOrm.count() }).from(dedupeTable).where(drizzleOrm.eq(dedupeTable.status, "failed")); const expiredResult = yield this.db.select({ count: drizzleOrm.count() }).from(dedupeTable).where(drizzleOrm.lt(dedupeTable.createdAt, expiredTime)); return { totalJobs: ((_a = totalResult[0]) == null ? void 0 : _a.count) || 0, pendingJobs: ((_b = pendingResult[0]) == null ? void 0 : _b.count) || 0, completedJobs: ((_c = completedResult[0]) == null ? void 0 : _c.count) || 0, failedJobs: ((_d = failedResult[0]) == null ? void 0 : _d.count) || 0, expiredJobs: ((_e = expiredResult[0]) == null ? void 0 : _e.count) || 0 }; }); } /** * Clean up expired jobs */ cleanup() { return __async(this, null, function* () { const now = Date.now(); const expiredTime = now - this.jobTimeoutMs; yield this.db.delete(dedupeTable).where(drizzleOrm.lt(dedupeTable.createdAt, expiredTime)); }); } /** * Clear all jobs */ clear() { return __async(this, null, function* () { yield this.db.delete(dedupeTable); this.jobPromises.clear(); this.jobResolvers.clear(); }); } /** * Close the database connection */ close() { return __async(this, null, function* () { if (this.cleanupInterval) { clearInterval(this.cleanupInterval); this.cleanupInterval = void 0; } this.jobPromises.clear(); this.jobResolvers.clear(); this.isDestroyed = true; if (this.isConnectionManaged && typeof this.sqlite.close === "function") { this.sqlite.close(); } }); } /** * Alias for close() to match test expectations */ destroy() { this.close(); } initializeDatabase() { this.db.run(drizzleOrm.sql` CREATE TABLE IF NOT EXISTS dedupe_jobs ( hash TEXT PRIMARY KEY, job_id TEXT NOT NULL, status TEXT NOT NULL, result BLOB, error TEXT, created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL ) `); this.db.run(drizzleOrm.sql` CREATE INDEX IF NOT EXISTS idx_dedupe_status ON dedupe_jobs(status) `); } }; var SQLiteRateLimitStore = class { constructor({ /** File path or existing `better-sqlite3` Database instance. Defaults to `':memory:'`. */ database = ":memory:", /** Global/default rate-limit config applied when a resource-specific override is not provided. */ defaultConfig = client.DEFAULT_RATE_LIMIT, /** Optional per-resource overrides. */ resourceConfigs = /* @__PURE__ */ new Map() } = {}) { /** Indicates whether this store manages (and should close) the SQLite connection */ this.isConnectionManaged = false; this.resourceConfigs = /* @__PURE__ */ new Map(); this.isDestroyed = false; let sqliteInstance; let isConnectionManaged = false; if (typeof database === "string") { sqliteInstance = new Database__default.default(database); isConnectionManaged = true; } else { sqliteInstance = database; } this.sqlite = sqliteInstance; this.isConnectionManaged = isConnectionManaged; this.db = betterSqlite3.drizzle(sqliteInstance); this.defaultConfig = defaultConfig; this.resourceConfigs = resourceConfigs; this.initializeDatabase(); } canProceed(resource) { return __async(this, null, function* () { var _a; if (this.isDestroyed) { throw new Error("Rate limit store has been destroyed"); } const config = this.resourceConfigs.get(resource) || this.defaultConfig; const now = Date.now(); const windowStart = now - config.windowMs; yield this.cleanupExpiredRequests(resource, windowStart); const result = yield this.db.select({ count: drizzleOrm.count() }).from(rateLimitTable).where( drizzleOrm.and( drizzleOrm.eq(rateLimitTable.resource, resource), drizzleOrm.gte(rateLimitTable.timestamp, windowStart) ) ); const currentCount = ((_a = result[0]) == null ? void 0 : _a.count) || 0; return currentCount < config.limit; }); } record(resource) { return __async(this, null, function* () { if (this.isDestroyed) { throw new Error("Rate limit store has been destroyed"); } const now = Date.now(); yield this.db.insert(rateLimitTable).values({ resource, timestamp: now }); }); } getStatus(resource) { return __async(this, null, function* () { var _a; if (this.isDestroyed) { throw new Error("Rate limit store has been destroyed"); } const config = this.resourceConfigs.get(resource) || this.defaultConfig; const now = Date.now(); const windowStart = now - config.windowMs; yield this.cleanupExpiredRequests(resource, windowStart); const result = yield this.db.select({ count: drizzleOrm.count() }).from(rateLimitTable).where( drizzleOrm.and( drizzleOrm.eq(rateLimitTable.resource, resource), drizzleOrm.gte(rateLimitTable.timestamp, windowStart) ) ); const currentRequests = ((_a = result[0]) == null ? void 0 : _a.count) || 0; const remaining = Math.max(0, config.limit - currentRequests); const resetTime = new Date(now + config.windowMs); return { remaining, resetTime, limit: config.limit }; }); } reset(resource) { return __async(this, null, function* () { if (this.isDestroyed) { throw new Error("Rate limit store has been destroyed"); } yield this.db.delete(rateLimitTable).where(drizzleOrm.eq(rateLimitTable.resource, resource)); }); } getWaitTime(resource) { return __async(this, null, function* () { var _a, _b; if (this.isDestroyed) { throw new Error("Rate limit store has been destroyed"); } const config = this.resourceConfigs.get(resource) || this.defaultConfig; if (config.limit === 0) { return config.windowMs; } const now = Date.now(); const windowStart = now - config.windowMs; yield this.cleanupExpiredRequests(resource, windowStart); const countResult = yield this.db.select({ count: drizzleOrm.count() }).from(rateLimitTable).where( drizzleOrm.and( drizzleOrm.eq(rateLimitTable.resource, resource), drizzleOrm.gte(rateLimitTable.timestamp, windowStart) ) ); const currentRequests = ((_a = countResult[0]) == null ? void 0 : _a.count) || 0; if (currentRequests < config.limit) { return 0; } const oldestResult = yield this.db.select({ timestamp: rateLimitTable.timestamp }).from(rateLimitTable).where( drizzleOrm.and( drizzleOrm.eq(rateLimitTable.resource, resource), drizzleOrm.gte(rateLimitTable.timestamp, windowStart) ) ).orderBy(rateLimitTable.timestamp).limit(1); if (oldestResult.length === 0) { return 0; } const oldestTimestamp = (_b = oldestResult[0]) == null ? void 0 : _b.timestamp; if (oldestTimestamp === void 0) { return 0; } const timeUntilOldestExpires = oldestTimestamp + config.windowMs - now; return Math.max(0, timeUntilOldestExpires); }); } /** * Set rate limit configuration for a specific resource */ setResourceConfig(resource, config) { this.resourceConfigs.set(resource, config); } /** * Get rate limit configuration for a resource */ getResourceConfig(resource) { return this.resourceConfigs.get(resource) || this.defaultConfig; } /** * Get statistics for all resources */ getStats() { return __async(this, null, function* () { var _a; if (this.isDestroyed) { throw new Error("Rate limit store has been destroyed"); } const totalResult = yield this.db.select({ count: drizzleOrm.count() }).from(rateLimitTable); const resourcesResult = yield this.db.select({ resource: rateLimitTable.resource }).from(rateLimitTable).groupBy(rateLimitTable.resource); const uniqueResources = resourcesResult.length; const rateLimitedResources = []; for (const { resource } of resourcesResult) { const canProceed = yield this.canProceed(resource); if (!canProceed) { rateLimitedResources.push(resource); } } return { totalRequests: ((_a = totalResult[0]) == null ? void 0 : _a.count) || 0, uniqueResources, rateLimitedResources }; }); } /** * Clean up all rate limit data */ clear() { return __async(this, null, function* () { if (this.isDestroyed) { throw new Error("Rate limit store has been destroyed"); } yield this.db.delete(rateLimitTable); }); } /** * Clean up expired requests for all resources */ cleanup() { return __async(this, null, function* () { const now = Date.now(); const resources = yield this.db.select({ resource: rateLimitTable.resource }).from(rateLimitTable).groupBy(rateLimitTable.resource); for (const { resource } of resources) { const config = this.resourceConfigs.get(resource) || this.defaultConfig; const windowStart = now - config.windowMs; yield this.cleanupExpiredRequests(resource, windowStart); } }); } /** * Close the database connection */ close() { return __async(this, null, function* () { this.isDestroyed = true; if (this.isConnectionManaged && typeof this.sqlite.close === "function") { this.sqlite.close(); } }); } /** * Alias for close() to match test expectations */ destroy() { this.close(); } cleanupExpiredRequests(resource, windowStart) { return __async(this, null, function* () { yield this.db.delete(rateLimitTable).where( drizzleOrm.and( drizzleOrm.eq(rateLimitTable.resource, resource), drizzleOrm.lt(rateLimitTable.timestamp, windowStart) ) ); }); } initializeDatabase() { this.db.run(drizzleOrm.sql` CREATE TABLE IF NOT EXISTS rate_limits ( id INTEGER PRIMARY KEY AUTOINCREMENT, resource TEXT NOT NULL, timestamp INTEGER NOT NULL ) `); this.db.run(drizzleOrm.sql` CREATE INDEX IF NOT EXISTS idx_rate_limit_resource ON rate_limits(resource) `); this.db.run(drizzleOrm.sql` CREATE INDEX IF NOT EXISTS idx_rate_limit_timestamp ON rate_limits(timestamp) `); } }; var DEFAULT_RATE_LIMIT2 = { limit: 200, windowMs: 36e5 // 1 hour }; var SqliteAdaptiveRateLimitStore = class { constructor({ database = ":memory:", defaultConfig = DEFAULT_RATE_LIMIT2, resourceConfigs = /* @__PURE__ */ new Map(), adaptiveConfig = {} } = {}) { /** Indicates whether this store manages (and should close) the SQLite connection */ this.isConnectionManaged = false; this.resourceConfigs = /* @__PURE__ */ new Map(); this.isDestroyed = false; this.activityMetrics = /* @__PURE__ */ new Map(); this.lastCapacityUpdate = /* @__PURE__ */ new Map(); this.cachedCapacity = /* @__PURE__ */ new Map(); let sqliteInstance; let isConnectionManaged = false; if (typeof database === "string") { sqliteInstance = new Database__default.default(database); isConnectionManaged = true; } else { sqliteInstance = database; } this.sqlite = sqliteInstance; this.isConnectionManaged = isConnectionManaged; this.db = betterSqlite3.drizzle(sqliteInstance); this.defaultConfig = defaultConfig; this.resourceConfigs = resourceConfigs; this.capacityCalculator = new client.AdaptiveCapacityCalculator(adaptiveConfig); this.initializeDatabase(); } canProceed(resource, priority = "background") { return __async(this, null, function* () { if (this.isDestroyed) { throw new Error("Rate limit store has been destroyed"); } yield this.ensureActivityMetrics(resource); const metrics = this.getOrCreateActivityMetrics(resource); const capacity = this.calculateCurrentCapacity(resource, metrics); if (priority === "background" && capacity.backgroundPaused) { return false; } const currentUserRequests = yield this.getCurrentUsage(resource, "user"); const currentBackgroundRequests = yield this.getCurrentUsage( resource, "background" ); if (priority === "user") { return currentUserRequests < capacity.userReserved; } else { return currentBackgroundRequests < capacity.backgroundMax; } }); } record(resource, priority = "background") { return __async(this, null, function* () { if (this.isDestroyed) { throw new Error("Rate limit store has been destroyed"); } const now = Date.now(); this.db.run(drizzleOrm.sql` INSERT INTO rate_limits (resource, timestamp, priority) VALUES (${resource}, ${now}, ${priority}) `); const metrics = this.getOrCreateActivityMetrics(resource); if (priority === "user") { metrics.recentUserRequests.push(now); this.cleanupOldRequests(metrics.recentUserRequests); } else { metrics.recentBackgroundRequests.push(now); this.cleanupOldRequests(metrics.recentBackgroundRequests); } metrics.userActivityTrend = this.capacityCalculator.calculateActivityTrend( metrics.recentUserRequests ); }); } getStatus(resource) { return __async(this, null, function* () { if (this.isDestroyed) { throw new Error("Rate limit store has been destroyed"); } yield this.ensureActivityMetrics(resource); const metrics = this.getOrCreateActivityMetrics(resource); const capacity = this.calculateCurrentCapacity(resource, metrics); const currentUserUsage = yield this.getCurrentUsage(resource, "user"); const currentBackgroundUsage = yield this.getCurrentUsage( resource, "background" ); const config = this.resourceConfigs.get(resource) || this.defaultConfig; return { remaining: capacity.userReserved - currentUserUsage + (capacity.backgroundMax - currentBackgroundUsage), resetTime: new Date(Date.now() + config.windowMs), limit: this.getResourceLimit(resource), adaptive: { userReserved: capacity.userReserved, backgroundMax: capacity.backgroundMax, backgroundPaused: capacity.backgroundPaused, recentUserActivity: this.capacityCalculator.getRecentActivity( metrics.recentUserRequests ), reason: capacity.reason } }; }); } reset(resource) { return __async(this, null, function* () { if (this.isDestroyed) { throw new Error("Rate limit store has been destroyed"); } yield this.db.delete(rateLimitTable).where(drizzleOrm.eq(rateLimitTable.resource, resource)); this.activityMetrics.delete(resource); this.cachedCapacity.delete(resource); this.lastCapacityUpdate.delete(resource); }); } getWaitTime(resource, priority = "background") { return __async(this, null, function* () { if (this.isDestroyed) { throw new Error("Rate limit store has been destroyed"); } const config = this.resourceConfigs.get(resource) || this.defaultConfig; if (config.limit === 0) { return config.windowMs; } const canProceed = yield this.canProceed(resource, priority); if (canProceed) { return 0; } yield this.ensureActivityMetrics(resource); const metrics = this.getOrCreateActivityMetrics(resource); const capacity = this.calculateCurrentCapacity(resource, metrics); if (priority === "background" && capacity.backgroundPaused) { return this.capacityCalculator.config.recalculationIntervalMs; } const now = Date.now(); const windowStart = now - config.windowMs; const oldestResult = this.sqlite.prepare( ` SELECT timestamp FROM rate_limits WHERE resource = ? AND COALESCE(priority, 'background') = ? AND timestamp >= ? ORDER BY timestamp LIMIT 1 ` ).get(resource, priority, windowStart); if (!oldestResult) { return 0; } const oldestTimestamp = oldestResult.timestamp; if (!oldestTimestamp) { return 0; } const timeUntilOldestExpires = oldestTimestamp + config.windowMs - now; return Math.max(0, timeUntilOldestExpires); }); } /** * Set rate limit configuration for a specific resource */ setResourceConfig(resource, config) { this.resourceConfigs.set(resource, config); } /** * Get rate limit configuration for a resource */ getResourceConfig(resource) { return this.resourceConfigs.get(resource) || this.defaultConfig; } /** * Get statistics for all resources */ getStats() { return __async(this, null, function* () { var _a; if (this.isDestroyed) { throw new Error("Rate limit store has been destroyed"); } const totalResult = yield this.db.select({ count: drizzleOrm.count() }).from(rateLimitTable); const resourcesResult = yield this.db.select({ resource: rateLimitTable.resource }).from(rateLimitTable).groupBy(rateLimitTable.resource); const uniqueResources = resourcesResult.length; const rateLimitedResources = []; for (const { resource } of resourcesResult) { const canProceed = yield this.canProceed(resource); if (!canProceed) { rateLimitedResources.push(resource); } } return { totalRequests: ((_a = totalResult[0]) == null ? void 0 : _a.count) || 0, uniqueResources, rateLimitedResources }; }); } /** * Clean up all rate limit data */ clear() { return __async(this, null, function* () { if (this.isDestroyed) { throw new Error("Rate limit store has been destroyed"); } yield this.db.delete(rateLimitTable); this.activityMetrics.clear(); this.cachedCapacity.clear(); this.lastCapacityUpdate.clear(); }); } /** * Clean up expired requests for all resources */ cleanup() { return __async(this, null, function* () { const now = Date.now(); const resources = yield this.db.select({ resource: rateLimitTable.resource }).from(rateLimitTable).groupBy(rateLimitTable.resource); for (const { resource } of resources) { const config = this.resourceConfigs.get(resource) || this.defaultConfig; const windowStart = now - config.windowMs; yield this.cleanupExpiredRequests(resource, windowStart); } }); } /** * Close the database connection */ close() { return __async(this, null, function* () { this.isDestroyed = true; if (this.isConnectionManaged && typeof this.sqlite.close === "function") { this.sqlite.close(); } }); } /** * Alias for close() to match test expectations */ destroy() { this.close(); } // Private helper methods for adaptive functionality calculateCurrentCapacity(resource, metrics) { const lastUpdate = this.lastCapacityUpdate.get(resource) || 0; const recalcInterval = this.capacityCalculator.config.recalculationIntervalMs; if (Date.now() - lastUpdate < recalcInterval) { return this.cachedCapacity.get(resource) || this.getDefaultCapacity(resource); } const totalLimit = this.getResourceLimit(resource); const capacity = this.capacityCalculator.calculateDynamicCapacity( resource, totalLimit, metrics ); this.cachedCapacity.set(resource, capacity); this.lastCapacityUpdate.set(resource, Date.now()); return capacity; } getOrCreateActivityMetrics(resource) { if (!this.activityMetrics.has(resource)) { this.activityMetrics.set(resource, { recentUserRequests: [], recentBackgroundRequests: [], userActivityTrend: "none" }); } return this.activityMetrics.get(resource); } ensureActivityMetrics(resource) { return __async(this, null, function* () { if (this.activityMetrics.has(resource)) { return; } const now = Date.now(); const windowStart = now - this.capacityCalculator.config.monitoringWindowMs; const recentRequests = this.sqlite.prepare( ` SELECT timestamp, COALESCE(priority, 'background') as priority FROM rate_limits WHERE resource = ? AND timestamp >= ? ORDER BY timestamp ` ).all(resource, windowStart); const metrics = { recentUserRequests: [], recentBackgroundRequests: [], userActivityTrend: "none" }; for (const request of recentRequests) { if (request.priority === "user") { metrics.recentUserRequests.push(request.timestamp); } else { metrics.recentBackgroundRequests.push(request.timestamp); } } metrics.userActivityTrend = this.capacityCalculator.calculateActivityTrend( metrics.recentUserRequests ); this.activityMetrics.set(resource, metrics); }); } getCurrentUsage(resource, priority) { return __async(this, null, function* () { const config = this.resourceConfigs.get(resource) || this.defaultConfig; const now = Date.now(); const windowStart = now - config.windowMs; yield this.cleanupExpiredRequests(resource, windowStart); const result = this.sqlite.prepare( ` SELECT COUNT(*) as count FROM rate_limits WHERE resource = ? AND priority = ? AND timestamp >= ? ` ).get(resource, priority, windowStart); return result.count || 0; }); } cleanupOldRequests(requests) { const cutoff = Date.now() - this.capacityCalculator.config.monitoringWindowMs; while (requests.length > 0 && requests[0] < cutoff) { requests.shift(); } } getResourceLimit(resource) { const config = this.resourceConfigs.get(resource) || this.defaultConfig; return config.limit; } getDefaultCapacity(resource) { const limit = this.getResourceLimit(resource); return { userReserved: Math.floor(limit * 0.3), backgroundMax: Math.floor(limit * 0.7), backgroundPaused: false, reason: "Default capacity allocation" }; } cleanupExpiredRequests(resource, windowStart) { return __async(this, null, function* () { yield this.db.delete(rateLimitTable).where( drizzleOrm.and( drizzleOrm.eq(rateLimitTable.resource, resource), drizzleOrm.lt(rateLimitTable.timestamp, windowStart) ) ); }); } initializeDatabase() { this.db.run(drizzleOrm.sql` CREATE TABLE IF NOT EXISTS rate_limits ( id INTEGER PRIMARY KEY AUTOINCREMENT, resource TEXT NOT NULL, timestamp INTEGER NOT NULL, priority TEXT NOT NULL DEFAULT 'background' ) `); try { this.db.run(drizzleOrm.sql` ALTER TABLE rate_limits ADD COLUMN priority TEXT DEFAULT 'background' `); } catch (e) { } this.db.run(drizzleOrm.sql` CREATE INDEX IF NOT EXISTS idx_rate_limit_resource ON rate_limits(resource) `); this.db.run(drizzleOrm.sql` CREATE INDEX IF NOT EXISTS idx_rate_limit_timestamp ON rate_limits(timestamp) `); this.db.run(drizzleOrm.sql` CREATE INDEX IF NOT EXISTS idx_rate_limit_resource_priority_timestamp ON rate_limits(resource, priority, timestamp) `); } }; exports.SQLiteCacheStore = SQLiteCacheStore; exports.SQLiteDedupeStore = SQLiteDedupeStore; exports.SQLiteRateLimitStore = SQLiteRateLimitStore; exports.SqliteAdaptiveRateLimitStore = SqliteAdaptiveRateLimitStore; exports.cacheTable = cacheTable; exports.dedupeTable = dedupeTable; exports.rateLimitTable = rateLimitTable; //# sourceMappingURL=index.cjs.map //# sourceMappingURL=index.cjs.map