refactor: atomic, DRY, and logging improvements across codebase
- Split llmModerationClient.ts (2170 lines) into 5 focused sub-modules - Split aiAnalyzer.ts (1282 lines) into 4 modular pipelines - Split messages.db.ts (826 lines) into 5 domain-specific modules - Moved shared schema to @bete/shared, eliminated backend duplication - Added createChildLogger to all voice-recording and AI moderation modules - Extracted tryCommandThenFallback, normalizeMediaState, DEFAULT_VOICE_STATUS - Created shared pagination.ts utility, eliminated 5+ cursor-pagination duplications - Created shared messageMapper.ts for row mapping - Standardized backend error handling with asyncHandler - Added frontend createLogger utility and useAsyncAction hook - Added structured logging to frontend hooks, socket, and API client Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
b68789fffc
commit
07032ab521
@@ -4,6 +4,8 @@ export function delay(ms: number): Promise<void> {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
export * from "./pagination.js";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Retry with exponential backoff
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
// Shared cursor-based pagination utilities
|
||||
|
||||
export interface CursorData {
|
||||
created_at: number;
|
||||
id: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Encode a cursor to a base64 string.
|
||||
*/
|
||||
export function encodeCursor(data: CursorData): string {
|
||||
return Buffer.from(JSON.stringify(data)).toString("base64");
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode a cursor from a base64 string. Returns null on invalid input.
|
||||
*/
|
||||
export function decodeCursor(cursor?: string): CursorData | null {
|
||||
if (!cursor) return null;
|
||||
try {
|
||||
const data = JSON.parse(Buffer.from(cursor, "base64").toString("utf-8"));
|
||||
if (typeof data.created_at === "number" && typeof data.id === "string") {
|
||||
return data;
|
||||
}
|
||||
return null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a `PageResult` from a slice of rows (limit + 1) using cursor-based pagination.
|
||||
*/
|
||||
export function pageResult<T extends { created_at: number; id: string }>(
|
||||
rows: unknown[],
|
||||
limit: number,
|
||||
): { data: T[]; nextCursor: string | null } {
|
||||
const hasMore = rows.length > limit;
|
||||
const data = rows.slice(0, limit) as T[];
|
||||
const lastItem = data[data.length - 1];
|
||||
const nextCursor =
|
||||
hasMore && lastItem
|
||||
? encodeCursor({ created_at: lastItem.created_at, id: lastItem.id })
|
||||
: null;
|
||||
|
||||
return { data, nextCursor };
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a Drizzle cursor condition expression.
|
||||
* Used in WHERE clauses: `(created_at < cursor.created_at OR (created_at = cursor.created_at AND id < cursor.id))`
|
||||
*
|
||||
* Returns the SQL expression or undefined when cursor is absent.
|
||||
*/
|
||||
import { type SQL, sql } from "drizzle-orm";
|
||||
|
||||
export function buildCursorCondition(
|
||||
created_at_col: SQL | unknown,
|
||||
id_col: SQL | unknown,
|
||||
cursor?: string,
|
||||
): SQL | undefined {
|
||||
const data = decodeCursor(cursor);
|
||||
if (!data) return undefined;
|
||||
return sql`(${created_at_col} < ${data.created_at} or (${created_at_col} = ${data.created_at} and ${id_col} < ${data.id}))`;
|
||||
}
|
||||
Reference in New Issue
Block a user