feat(cleanup): hide and prune stale mailboxes
Mailbox listings grew unbounded as old sessions ended without unregistering. This adds two layers of cleanup, configurable via mailbox.json or `serve` flags: - Lazy filter: list responses (REST /v1/list, MCP list_mailboxes) drop mailboxes idle longer than hideAfterMinutes (default 24h), while always keeping the caller and any sender with messages pending for them. - Background sweep: startServer runs an initial prune on boot and schedules an unref'd interval timer that hard-deletes mailboxes idle longer than deleteAfterMinutes (default 7d) which have no pending messages, and wipes their delivered history.
This commit is contained in:
@@ -93,11 +93,13 @@ export async function buildServer(cfg: DaemonConfig, store: MailboxStore): Promi
|
||||
|
||||
app.get("/v1/list", async (req) => {
|
||||
const name = req.mailboxName;
|
||||
return store.listMailboxes(name).map((m) => ({
|
||||
name: m.name,
|
||||
lastSeenAt: m.lastSeenAt.toISOString(),
|
||||
pendingForYou: m.pendingForYou,
|
||||
}));
|
||||
return store
|
||||
.listMailboxes(name, { hideAfterMinutes: cfg.hideAfterMinutes })
|
||||
.map((m) => ({
|
||||
name: m.name,
|
||||
lastSeenAt: m.lastSeenAt.toISOString(),
|
||||
pendingForYou: m.pendingForYou,
|
||||
}));
|
||||
});
|
||||
|
||||
app.post<{ Body: { to?: string } }>("/v1/rename", async (req, reply) => {
|
||||
@@ -119,14 +121,45 @@ export async function buildServer(cfg: DaemonConfig, store: MailboxStore): Promi
|
||||
}
|
||||
});
|
||||
|
||||
await registerMcp(app, store);
|
||||
await registerMcp(app, store, cfg.hideAfterMinutes);
|
||||
|
||||
return app;
|
||||
}
|
||||
|
||||
export async function startServer(cfg: DaemonConfig): Promise<{ app: FastifyInstance; store: MailboxStore }> {
|
||||
function startSweep(
|
||||
store: MailboxStore,
|
||||
cfg: DaemonConfig,
|
||||
log: FastifyInstance["log"],
|
||||
): NodeJS.Timeout | null {
|
||||
if (cfg.sweepIntervalMinutes <= 0 || cfg.deleteAfterMinutes <= 0) return null;
|
||||
const runOnce = (): void => {
|
||||
try {
|
||||
const r = store.pruneStale(cfg.deleteAfterMinutes);
|
||||
if (r.deletedMailboxes > 0 || r.deletedMessages > 0) {
|
||||
log.info(
|
||||
r,
|
||||
`Pruned ${r.deletedMailboxes} stale mailbox(es) and ${r.deletedMessages} delivered message(s)`,
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
log.error({ err }, "Stale-mailbox sweep failed");
|
||||
}
|
||||
};
|
||||
runOnce();
|
||||
const timer = setInterval(runOnce, cfg.sweepIntervalMinutes * 60_000);
|
||||
timer.unref?.();
|
||||
return timer;
|
||||
}
|
||||
|
||||
export async function startServer(
|
||||
cfg: DaemonConfig,
|
||||
): Promise<{ app: FastifyInstance; store: MailboxStore; sweepTimer: NodeJS.Timeout | null }> {
|
||||
const store = new MailboxStore(cfg.dbPath);
|
||||
const app = await buildServer(cfg, store);
|
||||
await app.listen({ host: cfg.bind, port: cfg.port });
|
||||
return { app, store };
|
||||
const sweepTimer = startSweep(store, cfg, app.log);
|
||||
app.addHook("onClose", async () => {
|
||||
if (sweepTimer) clearInterval(sweepTimer);
|
||||
});
|
||||
return { app, store, sweepTimer };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user