feat(storage): add StorageManager and cache subsystem; integrate storage into ConnectionManager and GitopsApp, migrate legacy connections, and add tests
This commit is contained in:
68
ts/cache/classes.cache.cleaner.ts
vendored
Normal file
68
ts/cache/classes.cache.cleaner.ts
vendored
Normal file
@@ -0,0 +1,68 @@
|
||||
import { logger } from '../logging.ts';
|
||||
import type { CacheDb } from './classes.cachedb.ts';
|
||||
|
||||
// deno-lint-ignore no-explicit-any
|
||||
type DocumentClass = { getInstances: (filter: any) => Promise<{ delete: () => Promise<void> }[]> };
|
||||
|
||||
const DEFAULT_INTERVAL_MS = 60 * 60 * 1000; // 1 hour
|
||||
|
||||
/**
|
||||
* Periodically cleans up expired cached documents.
|
||||
*/
|
||||
export class CacheCleaner {
|
||||
private intervalId: number | null = null;
|
||||
private intervalMs: number;
|
||||
private documentClasses: DocumentClass[] = [];
|
||||
private cacheDb: CacheDb;
|
||||
|
||||
constructor(cacheDb: CacheDb, intervalMs = DEFAULT_INTERVAL_MS) {
|
||||
this.cacheDb = cacheDb;
|
||||
this.intervalMs = intervalMs;
|
||||
}
|
||||
|
||||
/** Register a document class for cleanup */
|
||||
registerClass(cls: DocumentClass): void {
|
||||
this.documentClasses.push(cls);
|
||||
}
|
||||
|
||||
start(): void {
|
||||
if (this.intervalId !== null) return;
|
||||
this.intervalId = setInterval(() => {
|
||||
this.clean().catch((err) => {
|
||||
logger.error(`CacheCleaner error: ${err}`);
|
||||
});
|
||||
}, this.intervalMs);
|
||||
// Unref so the interval doesn't prevent process exit
|
||||
Deno.unrefTimer(this.intervalId);
|
||||
logger.debug(`CacheCleaner started (interval: ${this.intervalMs}ms)`);
|
||||
}
|
||||
|
||||
stop(): void {
|
||||
if (this.intervalId !== null) {
|
||||
clearInterval(this.intervalId);
|
||||
this.intervalId = null;
|
||||
logger.debug('CacheCleaner stopped');
|
||||
}
|
||||
}
|
||||
|
||||
/** Run a single cleanup pass */
|
||||
async clean(): Promise<number> {
|
||||
const now = Date.now();
|
||||
let totalDeleted = 0;
|
||||
for (const cls of this.documentClasses) {
|
||||
try {
|
||||
const expired = await cls.getInstances({ expiresAt: { $lt: now } });
|
||||
for (const doc of expired) {
|
||||
await doc.delete();
|
||||
totalDeleted++;
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error(`CacheCleaner: failed to clean class: ${err}`);
|
||||
}
|
||||
}
|
||||
if (totalDeleted > 0) {
|
||||
logger.debug(`CacheCleaner: deleted ${totalDeleted} expired document(s)`);
|
||||
}
|
||||
return totalDeleted;
|
||||
}
|
||||
}
|
||||
57
ts/cache/classes.cached.document.ts
vendored
Normal file
57
ts/cache/classes.cached.document.ts
vendored
Normal file
@@ -0,0 +1,57 @@
|
||||
import * as plugins from '../plugins.ts';
|
||||
|
||||
/** TTL duration constants in milliseconds */
|
||||
export const TTL = {
|
||||
MINUTES_5: 5 * 60 * 1000,
|
||||
HOURS_1: 60 * 60 * 1000,
|
||||
HOURS_24: 24 * 60 * 60 * 1000,
|
||||
DAYS_7: 7 * 24 * 60 * 60 * 1000,
|
||||
DAYS_30: 30 * 24 * 60 * 60 * 1000,
|
||||
DAYS_90: 90 * 24 * 60 * 60 * 1000,
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* Abstract base class for cached documents with TTL support.
|
||||
* Extend this class and add @Collection decorator pointing to your CacheDb.
|
||||
*/
|
||||
export abstract class CachedDocument<
|
||||
T extends CachedDocument<T>,
|
||||
> extends plugins.smartdata.SmartDataDbDoc<T, T> {
|
||||
@plugins.smartdata.svDb()
|
||||
public createdAt: number = Date.now();
|
||||
|
||||
@plugins.smartdata.svDb()
|
||||
public expiresAt: number = Date.now() + TTL.HOURS_1;
|
||||
|
||||
@plugins.smartdata.svDb()
|
||||
public lastAccessedAt: number = Date.now();
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
/** Set TTL in milliseconds from now */
|
||||
setTTL(ms: number): void {
|
||||
this.expiresAt = Date.now() + ms;
|
||||
}
|
||||
|
||||
/** Set TTL in days from now */
|
||||
setTTLDays(days: number): void {
|
||||
this.setTTL(days * 24 * 60 * 60 * 1000);
|
||||
}
|
||||
|
||||
/** Set TTL in hours from now */
|
||||
setTTLHours(hours: number): void {
|
||||
this.setTTL(hours * 60 * 60 * 1000);
|
||||
}
|
||||
|
||||
/** Check if this document has expired */
|
||||
isExpired(): boolean {
|
||||
return Date.now() > this.expiresAt;
|
||||
}
|
||||
|
||||
/** Update last accessed timestamp */
|
||||
touch(): void {
|
||||
this.lastAccessedAt = Date.now();
|
||||
}
|
||||
}
|
||||
82
ts/cache/classes.cachedb.ts
vendored
Normal file
82
ts/cache/classes.cachedb.ts
vendored
Normal file
@@ -0,0 +1,82 @@
|
||||
import * as plugins from '../plugins.ts';
|
||||
import { logger } from '../logging.ts';
|
||||
|
||||
export interface ICacheDbOptions {
|
||||
storagePath?: string;
|
||||
dbName?: string;
|
||||
debug?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Singleton wrapper around LocalTsmDb + SmartdataDb.
|
||||
* Provides a managed MongoDB-compatible cache database.
|
||||
*/
|
||||
export class CacheDb {
|
||||
private static instance: CacheDb | null = null;
|
||||
|
||||
private localTsmDb: InstanceType<typeof plugins.smartmongo.LocalTsmDb> | null = null;
|
||||
private smartdataDb: InstanceType<typeof plugins.smartdata.SmartdataDb> | null = null;
|
||||
private options: Required<ICacheDbOptions>;
|
||||
|
||||
private constructor(options: ICacheDbOptions = {}) {
|
||||
this.options = {
|
||||
storagePath: options.storagePath ?? './.nogit/cachedb',
|
||||
dbName: options.dbName ?? 'gitops_cache',
|
||||
debug: options.debug ?? false,
|
||||
};
|
||||
}
|
||||
|
||||
static getInstance(options?: ICacheDbOptions): CacheDb {
|
||||
if (!CacheDb.instance) {
|
||||
CacheDb.instance = new CacheDb(options);
|
||||
}
|
||||
return CacheDb.instance;
|
||||
}
|
||||
|
||||
static resetInstance(): void {
|
||||
CacheDb.instance = null;
|
||||
}
|
||||
|
||||
async start(): Promise<void> {
|
||||
logger.info('Starting CacheDb...');
|
||||
this.localTsmDb = new plugins.smartmongo.LocalTsmDb({
|
||||
folderPath: this.options.storagePath,
|
||||
});
|
||||
const { connectionUri } = await this.localTsmDb.start();
|
||||
|
||||
this.smartdataDb = new plugins.smartdata.SmartdataDb({
|
||||
mongoDbUrl: connectionUri,
|
||||
mongoDbName: this.options.dbName,
|
||||
});
|
||||
await this.smartdataDb.init();
|
||||
logger.success(`CacheDb started (db: ${this.options.dbName})`);
|
||||
}
|
||||
|
||||
async stop(): Promise<void> {
|
||||
logger.info('Stopping CacheDb...');
|
||||
if (this.smartdataDb) {
|
||||
await this.smartdataDb.close();
|
||||
this.smartdataDb = null;
|
||||
}
|
||||
if (this.localTsmDb) {
|
||||
// localDb.stop() may hang under Deno — fire-and-forget with timeout
|
||||
const stopPromise = this.localTsmDb.stop().catch(() => {});
|
||||
await Promise.race([
|
||||
stopPromise,
|
||||
new Promise<void>((resolve) => {
|
||||
const id = setTimeout(resolve, 3000);
|
||||
Deno.unrefTimer(id);
|
||||
}),
|
||||
]);
|
||||
this.localTsmDb = null;
|
||||
}
|
||||
logger.success('CacheDb stopped');
|
||||
}
|
||||
|
||||
getDb(): InstanceType<typeof plugins.smartdata.SmartdataDb> {
|
||||
if (!this.smartdataDb) {
|
||||
throw new Error('CacheDb not started. Call start() first.');
|
||||
}
|
||||
return this.smartdataDb;
|
||||
}
|
||||
}
|
||||
32
ts/cache/documents/classes.cached.project.ts
vendored
Normal file
32
ts/cache/documents/classes.cached.project.ts
vendored
Normal file
@@ -0,0 +1,32 @@
|
||||
import * as plugins from '../../plugins.ts';
|
||||
import { CacheDb } from '../classes.cachedb.ts';
|
||||
import { CachedDocument, TTL } from '../classes.cached.document.ts';
|
||||
|
||||
/**
|
||||
* Cached project data from git providers. TTL: 5 minutes.
|
||||
*/
|
||||
@plugins.smartdata.Collection(() => CacheDb.getInstance().getDb())
|
||||
export class CachedProject extends CachedDocument<CachedProject> {
|
||||
@plugins.smartdata.unI()
|
||||
public id: string = '';
|
||||
|
||||
@plugins.smartdata.svDb()
|
||||
public connectionId: string = '';
|
||||
|
||||
@plugins.smartdata.svDb()
|
||||
public projectName: string = '';
|
||||
|
||||
@plugins.smartdata.svDb()
|
||||
public projectUrl: string = '';
|
||||
|
||||
@plugins.smartdata.svDb()
|
||||
public description: string = '';
|
||||
|
||||
@plugins.smartdata.svDb()
|
||||
public defaultBranch: string = '';
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.setTTL(TTL.MINUTES_5);
|
||||
}
|
||||
}
|
||||
1
ts/cache/documents/index.ts
vendored
Normal file
1
ts/cache/documents/index.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
export { CachedProject } from './classes.cached.project.ts';
|
||||
5
ts/cache/index.ts
vendored
Normal file
5
ts/cache/index.ts
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
export { CacheDb } from './classes.cachedb.ts';
|
||||
export type { ICacheDbOptions } from './classes.cachedb.ts';
|
||||
export { CachedDocument, TTL } from './classes.cached.document.ts';
|
||||
export { CacheCleaner } from './classes.cache.cleaner.ts';
|
||||
export * from './documents/index.ts';
|
||||
Reference in New Issue
Block a user