Files
ai_platform/lib/thumbnail-events-store.js

186 lines
5.5 KiB
JavaScript

/**
* PPT 썸네일 작업 이벤트 로그 — PostgreSQL 우선, PG 없을 때만 data/thumbnail-events.json 폴백
*/
const fs = require("fs/promises");
const { v4: uuidv4 } = require("uuid");
/**
* @param {import("pg").QueryResultRow} row
*/
function mapRowToEvent(row) {
if (!row) return null;
return {
id: row.id,
at: row.occurred_at ? new Date(row.occurred_at).toISOString() : null,
type: row.event_type,
lectureId: row.lecture_id || undefined,
lectureTitle: row.lecture_title || undefined,
reason: row.reason || undefined,
force: row.force_flag === true,
queueSizeAfter: row.queue_size_after != null ? Number(row.queue_size_after) : undefined,
retryCount: row.retry_count != null ? Number(row.retry_count) : undefined,
durationMs: row.duration_ms != null ? Number(row.duration_ms) : undefined,
error: row.error_text || undefined,
};
}
/**
* @param {import("pg").Pool | null} pgPool
* @param {string} jsonPath
*/
async function readThumbnailEvents(pgPool, jsonPath) {
if (pgPool) {
const r = await pgPool.query(
`SELECT id, occurred_at, event_type, lecture_id, lecture_title, reason, force_flag,
queue_size_after, retry_count, duration_ms, error_text
FROM lecture_thumbnail_events
ORDER BY occurred_at ASC`
);
return (r.rows || []).map(mapRowToEvent);
}
try {
const raw = await fs.readFile(jsonPath, "utf-8");
const parsed = JSON.parse(raw);
return Array.isArray(parsed) ? parsed : [];
} catch {
return [];
}
}
/**
* @param {import("pg").Pool} pgPool
* @param {number} keep
*/
async function pruneThumbnailEvents(pgPool, keep) {
const k = Math.max(Number(keep) || 200, 20);
await pgPool.query(
`DELETE FROM lecture_thumbnail_events a
USING (
SELECT id FROM (
SELECT id, ROW_NUMBER() OVER (ORDER BY occurred_at DESC) AS rn
FROM lecture_thumbnail_events
) t WHERE t.rn > $1
) d
WHERE a.id = d.id`,
[k]
);
}
/**
* @param {import("pg").Pool | null} pgPool
* @param {string} jsonPath
* @param {number} keep
* @param {object} payload
*/
async function appendThumbnailEvent(pgPool, jsonPath, keep, payload) {
const id = uuidv4();
const at = new Date().toISOString();
if (pgPool) {
await pgPool.query(
`INSERT INTO lecture_thumbnail_events (
id, occurred_at, event_type, lecture_id, lecture_title, reason, force_flag,
queue_size_after, retry_count, duration_ms, error_text
) VALUES ($1, $2::timestamptz, $3, $4::uuid, $5, $6, $7, $8, $9, $10, $11)`,
[
id,
at,
payload.type || "unknown",
payload.lectureId || null,
payload.lectureTitle || null,
payload.reason || null,
payload.force === true,
payload.queueSizeAfter != null ? payload.queueSizeAfter : null,
payload.retryCount != null ? payload.retryCount : null,
payload.durationMs != null ? payload.durationMs : null,
payload.error || null,
]
);
await pruneThumbnailEvents(pgPool, keep);
return;
}
const events = await readThumbnailEvents(null, jsonPath);
events.push({ id, at, ...payload });
const sliced = events.slice(-Math.max(keep, 20));
await fs.writeFile(jsonPath, JSON.stringify(sliced, null, 2), "utf-8");
}
/**
* @param {import("pg").Pool | null} pgPool
* @param {string} jsonPath
*/
async function clearThumbnailEvents(pgPool, jsonPath) {
if (pgPool) {
await pgPool.query(`DELETE FROM lecture_thumbnail_events`);
return;
}
await fs.writeFile(jsonPath, "[]", "utf-8");
}
/**
* 기존 JSON 파일을 한 번 DB로 옮긴 뒤 파일을 백업 이름으로 변경 (PG 사용 시)
* @param {import("pg").Pool | null} pgPool
* @param {string} jsonPath
*/
/**
* @param {import("pg").Pool | null} pgPool
* @param {string} jsonPath
* @param {number} [keep]
*/
async function migrateThumbnailEventsFromJson(pgPool, jsonPath, keep) {
if (!pgPool) return;
const keepN = Math.max(Number(keep) || 200, 20);
let rows;
try {
const raw = await fs.readFile(jsonPath, "utf-8");
const parsed = JSON.parse(raw);
rows = Array.isArray(parsed) ? parsed : [];
} catch {
return;
}
if (rows.length === 0) return;
for (const evt of rows) {
if (!evt || !evt.id || !evt.type) continue;
try {
await pgPool.query(
`INSERT INTO lecture_thumbnail_events (
id, occurred_at, event_type, lecture_id, lecture_title, reason, force_flag,
queue_size_after, retry_count, duration_ms, error_text
) VALUES ($1::uuid, $2::timestamptz, $3, $4::uuid, $5, $6, $7, $8, $9, $10, $11)
ON CONFLICT (id) DO NOTHING`,
[
evt.id,
evt.at || new Date().toISOString(),
evt.type,
evt.lectureId || null,
evt.lectureTitle || null,
evt.reason || null,
evt.force === true,
evt.queueSizeAfter != null ? evt.queueSizeAfter : null,
evt.retryCount != null ? evt.retryCount : null,
evt.durationMs != null ? evt.durationMs : null,
evt.error || null,
]
);
} catch {
/* ignore row */
}
}
await pruneThumbnailEvents(pgPool, keepN);
try {
const fsSync = require("fs");
if (fsSync.existsSync(jsonPath)) {
fsSync.renameSync(jsonPath, `${jsonPath}.migrated.bak`);
}
} catch (e) {
console.warn("[thumbnail-events] JSON migration rename failed:", e?.message || e);
}
}
module.exports = {
readThumbnailEvents,
appendThumbnailEvent,
clearThumbnailEvents,
migrateThumbnailEventsFromJson,
};