mirror of
https://github.com/tiennm99/miti99bot.git
synced 2026-04-17 13:21:31 +00:00
- trading_trades table (migration 0001) persists every buy/sell via optional onTrade callback - /history [n] command shows caller's last N trades (default 10, max 50), HTML-escaped - daily cron at 0 17 * * * trims to 1000/user + 10000/global via FIFO delete - persistence failure logs but does not fail the trade reply
95 lines
3.4 KiB
JavaScript
95 lines
3.4 KiB
JavaScript
/**
|
|
* @file retention — daily cron handler that trims trading_trades to enforce row caps.
|
|
*
|
|
* Strategy (two-pass):
|
|
* 1. Per-user pass: for each distinct user_id, delete rows beyond PER_USER_CAP
|
|
* (keeps the newest N rows per user).
|
|
* 2. Global FIFO pass: delete any rows beyond GLOBAL_CAP across all users
|
|
* (keeps the newest N rows globally).
|
|
*
|
|
* Uses a hybrid SELECT-then-DELETE approach so it works with both the real
|
|
* Cloudflare D1 binding and the in-memory fake-d1 used in unit tests.
|
|
*
|
|
* @typedef {import("../../db/sql-store-interface.js").SqlStore} SqlStore
|
|
*/
|
|
|
|
const TABLE = "trading_trades";
|
|
|
|
/** Default per-user row cap. Exported for testability. */
|
|
export const PER_USER_CAP = 1000;
|
|
|
|
/** Default global row cap across all users. Exported for testability. */
|
|
export const GLOBAL_CAP = 10000;
|
|
|
|
/**
|
|
* Build a dynamic `DELETE FROM <table> WHERE id IN (?, ?, ...)` query.
|
|
* Returns [query, ids] tuple — ids passed as spread binds.
|
|
*
|
|
* @param {number[]} ids
|
|
* @returns {[string, number[]]}
|
|
*/
|
|
function buildDeleteByIds(ids) {
|
|
const placeholders = ids.map(() => "?").join(", ");
|
|
return [`DELETE FROM ${TABLE} WHERE id IN (${placeholders})`, ids];
|
|
}
|
|
|
|
/**
|
|
* Daily cron handler — trims trading_trades to enforce per-user and global caps.
|
|
*
|
|
* @param {any} _event — Cloudflare ScheduledEvent (unused; present for handler contract).
|
|
* @param {{ sql: SqlStore | null }} ctx
|
|
* @param {{ perUserCap?: number, globalCap?: number }} [caps] — override caps (for tests).
|
|
* @returns {Promise<void>}
|
|
*/
|
|
export async function trimTradesHandler(_event, { sql }, caps = {}) {
|
|
if (sql === null) {
|
|
console.log("[trim-trades] no D1 binding — skipping");
|
|
return;
|
|
}
|
|
|
|
const perUserCap = caps.perUserCap ?? PER_USER_CAP;
|
|
const globalCap = caps.globalCap ?? GLOBAL_CAP;
|
|
|
|
let perUserDeleted = 0;
|
|
|
|
// ── Pass 1: per-user trim ──────────────────────────────────────────────────
|
|
const userRows = await sql.all(`SELECT DISTINCT user_id FROM ${TABLE}`);
|
|
|
|
for (const row of userRows) {
|
|
const userId = row.user_id;
|
|
|
|
// Fetch IDs of rows that exceed the per-user cap (oldest rows = beyond OFFSET perUserCap).
|
|
const excessRows = await sql.all(
|
|
`SELECT id FROM ${TABLE} WHERE user_id = ? ORDER BY ts DESC LIMIT -1 OFFSET ?`,
|
|
userId,
|
|
perUserCap,
|
|
);
|
|
|
|
if (excessRows.length === 0) continue;
|
|
|
|
const ids = excessRows.map((r) => r.id);
|
|
const [query, binds] = buildDeleteByIds(ids);
|
|
const result = await sql.run(query, ...binds);
|
|
perUserDeleted += result.changes ?? ids.length;
|
|
}
|
|
|
|
console.log(`[trim-trades] per-user pass: deleted ${perUserDeleted} rows`);
|
|
|
|
// ── Pass 2: global FIFO trim ───────────────────────────────────────────────
|
|
const globalExcess = await sql.all(
|
|
`SELECT id FROM ${TABLE} ORDER BY ts DESC LIMIT -1 OFFSET ?`,
|
|
globalCap,
|
|
);
|
|
|
|
let globalDeleted = 0;
|
|
if (globalExcess.length > 0) {
|
|
const ids = globalExcess.map((r) => r.id);
|
|
const [query, binds] = buildDeleteByIds(ids);
|
|
const result = await sql.run(query, ...binds);
|
|
globalDeleted = result.changes ?? ids.length;
|
|
}
|
|
|
|
console.log(`[trim-trades] global pass: deleted ${globalDeleted} rows`);
|
|
console.log(`[trim-trades] total deleted: ${perUserDeleted + globalDeleted} rows`);
|
|
}
|