Restructure chat service, web routes, and add test suite
Split monolithic chat service into modular structure with Matrix client support, extract web server into per-resource route modules, introduce bootstrap/main-loop separation, and add comprehensive vitest coverage across services, repositories, and utilities. Adds project docs and CLAUDE.md agent instructions.
This commit is contained in:
1 parent
dfa0580859
commit
633bb0ec04
84 files changed
+19278
-3888
No files matched your search
@@ -0,0 +1,108 @@
|
||||
import fs from 'fs/promises';
|
||||
import path from 'path';
|
||||
import type { Logger } from '../../utils/logger.js';
|
||||
import type { ActiveChat, ChatCacheFile } from './types.js';
|
||||
|
||||
export interface SyncMarkers {
|
||||
nextBatch: string | null;
|
||||
lastFullSyncAt: number | null;
|
||||
lastDeltaSyncAt: number | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* In-memory + on-disk cache of Reddit chats plus the Matrix delta cursor
|
||||
* so the bot can resume incremental sync across restarts.
|
||||
*/
|
||||
export class ChatCache {
|
||||
private chats: ActiveChat[] = [];
|
||||
private lastFetchTime = 0;
|
||||
readonly cacheDurationMs = 60 * 1000;
|
||||
private readonly filePath: string;
|
||||
|
||||
private nextBatch: string | null = null;
|
||||
private lastFullSyncAt: number | null = null;
|
||||
private lastDeltaSyncAt: number | null = null;
|
||||
|
||||
constructor(private logger: Logger, filePath?: string) {
|
||||
this.filePath = filePath ?? path.join(process.cwd(), 'data', 'chat-cache.json');
|
||||
}
|
||||
|
||||
get(): ActiveChat[] {
|
||||
return this.chats;
|
||||
}
|
||||
|
||||
set(chats: ActiveChat[]): void {
|
||||
this.chats = chats;
|
||||
this.lastFetchTime = Date.now();
|
||||
}
|
||||
|
||||
isFresh(): boolean {
|
||||
return Date.now() - this.lastFetchTime < this.cacheDurationMs && this.chats.length > 0;
|
||||
}
|
||||
|
||||
getMarkers(): SyncMarkers {
|
||||
return {
|
||||
nextBatch: this.nextBatch,
|
||||
lastFullSyncAt: this.lastFullSyncAt,
|
||||
lastDeltaSyncAt: this.lastDeltaSyncAt,
|
||||
};
|
||||
}
|
||||
|
||||
setNextBatch(token: string | null): void {
|
||||
this.nextBatch = token;
|
||||
}
|
||||
|
||||
markFullSync(at: number = Date.now()): void {
|
||||
this.lastFullSyncAt = at;
|
||||
this.lastDeltaSyncAt = at;
|
||||
}
|
||||
|
||||
markDeltaSync(at: number = Date.now()): void {
|
||||
this.lastDeltaSyncAt = at;
|
||||
}
|
||||
|
||||
async load(): Promise<void> {
|
||||
try {
|
||||
const raw = await fs.readFile(this.filePath, 'utf-8');
|
||||
const parsed = JSON.parse(raw) as ChatCacheFile;
|
||||
if (!parsed || !Array.isArray(parsed.chats)) {
|
||||
this.logger.warn('Chat cache file is invalid, ignoring.');
|
||||
return;
|
||||
}
|
||||
this.chats = parsed.chats;
|
||||
this.lastFetchTime = Date.now();
|
||||
this.nextBatch = parsed.nextBatch ?? null;
|
||||
this.lastFullSyncAt = parsed.lastFullSyncAt ?? null;
|
||||
this.lastDeltaSyncAt = parsed.lastDeltaSyncAt ?? null;
|
||||
this.logger.info(
|
||||
`Loaded ${parsed.chats.length} chats from cache` +
|
||||
(this.lastFullSyncAt
|
||||
? ` (last full sync ${new Date(this.lastFullSyncAt).toISOString()})`
|
||||
: ' (no full sync recorded)'),
|
||||
);
|
||||
} catch (error) {
|
||||
if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
|
||||
this.logger.info('No chat cache found to load');
|
||||
return;
|
||||
}
|
||||
this.logger.warn('Failed to load chat cache:', error);
|
||||
}
|
||||
}
|
||||
|
||||
async save(chats: ActiveChat[] = this.chats): Promise<void> {
|
||||
const payload: ChatCacheFile = {
|
||||
version: 1,
|
||||
savedAt: Date.now(),
|
||||
chats,
|
||||
nextBatch: this.nextBatch,
|
||||
lastFullSyncAt: this.lastFullSyncAt,
|
||||
lastDeltaSyncAt: this.lastDeltaSyncAt,
|
||||
};
|
||||
try {
|
||||
await fs.mkdir(path.dirname(this.filePath), { recursive: true });
|
||||
await fs.writeFile(this.filePath, JSON.stringify(payload, null, 2), 'utf-8');
|
||||
} catch (error) {
|
||||
this.logger.warn('Failed to save chat cache:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user