chat-dev / server /chatTrashStore.js
incognitolm
Update
0f84d64
import crypto from 'crypto';
import path from 'path';
import { loadEncryptedJson, saveEncryptedJson } from './cryptoUtils.js';
const DATA_ROOT = '/data/deleted_chats';
const INDEX_FILE = path.join(DATA_ROOT, 'index.json');
const RETENTION_MS = 30 * 24 * 60 * 60 * 1000;
const state = {
loaded: false,
index: {
deletedChats: {},
},
};
function nowIso() {
return new Date().toISOString();
}
function ensureOwner(owner) {
if (!owner?.type || !owner?.id) throw new Error('Invalid deleted chat owner');
return owner;
}
async function ensureLoaded() {
if (state.loaded) return;
const stored = await loadEncryptedJson(INDEX_FILE);
state.index = {
deletedChats: stored?.deletedChats || {},
};
state.loaded = true;
await thisStore.purgeExpired();
}
async function saveIndex() {
await saveEncryptedJson(INDEX_FILE, state.index);
}
function matchesOwner(record, owner) {
return record.ownerType === owner.type && record.ownerId === owner.id;
}
function sanitize(record) {
return {
id: record.id,
originalSessionId: record.originalSessionId,
name: record.name,
deletedAt: record.deletedAt,
purgeAt: record.purgeAt,
created: record.created,
};
}
const thisStore = {
async add(owner, sessionSnapshot) {
ensureOwner(owner);
await ensureLoaded();
const sessionId = sessionSnapshot?.id;
if (!sessionId) return null;
for (const record of Object.values(state.index.deletedChats)) {
if (
record.originalSessionId === sessionId &&
record.ownerType === owner.type &&
record.ownerId === owner.id
) {
record.sessionSnapshot = sessionSnapshot;
record.name = sessionSnapshot.name || 'Deleted Chat';
record.created = sessionSnapshot.created || Date.now();
record.deletedAt = nowIso();
record.purgeAt = new Date(Date.now() + RETENTION_MS).toISOString();
await saveIndex();
return sanitize(record);
}
}
const deleted = {
id: crypto.randomUUID(),
originalSessionId: sessionId,
ownerType: owner.type,
ownerId: owner.id,
name: sessionSnapshot.name || 'Deleted Chat',
created: sessionSnapshot.created || Date.now(),
sessionSnapshot,
deletedAt: nowIso(),
purgeAt: new Date(Date.now() + RETENTION_MS).toISOString(),
};
state.index.deletedChats[deleted.id] = deleted;
await saveIndex();
return sanitize(deleted);
},
async list(owner) {
ensureOwner(owner);
await ensureLoaded();
return Object.values(state.index.deletedChats)
.filter((record) => matchesOwner(record, owner))
.sort((a, b) => new Date(b.deletedAt).getTime() - new Date(a.deletedAt).getTime())
.map(sanitize);
},
async restore(owner, ids) {
ensureOwner(owner);
await ensureLoaded();
const restored = [];
for (const id of ids || []) {
const record = state.index.deletedChats[id];
if (!record || !matchesOwner(record, owner)) continue;
restored.push(record.sessionSnapshot);
delete state.index.deletedChats[id];
}
if (restored.length) await saveIndex();
return restored;
},
async deleteForever(owner, ids) {
ensureOwner(owner);
await ensureLoaded();
const removed = [];
for (const id of ids || []) {
const record = state.index.deletedChats[id];
if (!record || !matchesOwner(record, owner)) continue;
delete state.index.deletedChats[id];
removed.push(id);
}
if (removed.length) await saveIndex();
return removed;
},
async purgeExpired() {
if (!state.loaded) return;
const now = Date.now();
let changed = false;
for (const [id, record] of Object.entries(state.index.deletedChats)) {
if (new Date(record.purgeAt).getTime() <= now) {
delete state.index.deletedChats[id];
changed = true;
}
}
if (changed) await saveIndex();
},
};
setInterval(() => {
thisStore.purgeExpired().catch((err) => console.error('chatTrashStore cleanup failed:', err));
}, 6 * 60 * 60 * 1000);
export const chatTrashStore = thisStore;