Implement events api routes

This commit is contained in:
Dominic Ferrando
2026-07-16 00:55:04 -04:00
parent ee9b03796d
commit 48765b4b89
6 changed files with 225 additions and 97 deletions
+134
View File
@@ -0,0 +1,134 @@
import type { Static } from "@sinclair/typebox";
import { t } from "elysia";
import { sql, type Selectable, type Transaction } from "kysely";
import type { DB } from "../database/out/db";
import { db } from "../database/db";
import { log } from "../util";
import { minToMs } from "@blade-and-brawn/domain";
export const EventStatusSchema = t.Union([
t.Literal("waiting"),
t.Literal("available"),
t.Literal("processing"),
t.Literal("failed"),
t.Literal("fulfilled"),
]);
export type EventStatus = Static<typeof EventStatusSchema>;
export type EventSource = "portal" | "printful" | "webflow";
export class EventsService {
static readonly CONCURRENCY_TIMEOUT_MS = minToMs(10);
private _lastCleanDate: Date = new Date(0);
get lastCleanDate() { return this._lastCleanDate; };
async get(id: string, trx?: Transaction<DB>) {
return await (trx ?? db).selectFrom("events")
.selectAll()
.where("id", "=", id)
.executeTakeFirst();
}
async list(opt: {
filter?: { status?: EventStatus, type?: string, group?: string },
limit?: number,
offset?: number,
} = {}, trx?: Transaction<DB>) {
return await (trx ?? db).selectFrom("events")
.select([
"id", "type", "source", "started_at", "ended_at", "available_at",
"errors", "rate_limits", "has_failed", "last_error", "created_at",
])
.$if(opt.filter?.group !== undefined, (qb) => qb.where("group", "=", opt.filter!.group!))
.$if(opt.filter?.type !== undefined, (qb) => qb.where("type", "=", opt.filter!.type!))
.$if(opt.filter?.status === "waiting", (qb) => qb
.where("started_at", "is", null)
.where("available_at", ">", new Date())
)
.$if(opt.filter?.status === "available", (qb) => qb
.where("started_at", "is", null)
.where("available_at", "<=", new Date())
)
.$if(opt.filter?.status === "processing", (qb) => qb
.where("started_at", "is not", null)
.where("ended_at", "is", null)
)
.$if(opt.filter?.status === "failed", (qb) => qb
.where("started_at", "is not", null)
.where("ended_at", "is not", null)
.where("has_failed", "is", true)
)
.$if(opt.filter?.status === "fulfilled", (qb) => qb
.where("started_at", "is not", null)
.where("ended_at", "is not", null)
.where("has_failed", "is", false)
)
.orderBy(sql`(started_at is not null and ended_at is null)`, "desc")
.orderBy("created_at", "desc")
.orderBy("id", "desc")
.$if(opt.limit !== undefined, (qb) => qb.limit(opt.limit!))
.$if(opt.offset !== undefined, (qb) => qb.offset(opt.offset!))
.execute();
}
static status(event: Pick<Selectable<DB["events"]>, "available_at" | "started_at" | "ended_at" | "has_failed">): EventStatus {
if (!event.started_at) {
if (Date.now() < event.available_at.getTime()) return "waiting";
return "available";
};
if (!event.ended_at) return "processing";
return event.has_failed ? "failed" : "fulfilled";
}
async fail(id: string, trx?: Transaction<DB>): Promise<void> {
await (trx ?? db).updateTable("events")
.set((eb) => ({
state: null,
ended_at: new Date(),
has_failed: true,
errors: eb("errors", "+", 1)
}))
.where("id", "=", id)
.execute();
}
async fulfill(id: string, trx?: Transaction<DB>): Promise<void> {
await (trx ?? db).updateTable("events")
.set({ state: null, ended_at: new Date(), last_error: null })
.where("id", "=", id)
.execute();
}
async replay(id: string): Promise<boolean> {
const result = await db.updateTable("events")
.set({
started_at: null,
ended_at: null,
has_failed: false,
available_at: new Date(),
})
.where("id", "=", id)
.where("has_failed", "=", true)
.executeTakeFirst();
return result.numUpdatedRows > 0n;
}
async clean(opt: { filter?: { type?: string, group?: string } } = {}) {
try {
// Find and fail any timed out processing events
const processingEvents = await this.list({ filter: { status: "processing", ...opt.filter } });
for (const processingEvent of processingEvents) {
const processingEventTimedOut = (Date.now() - (processingEvent.started_at?.getTime() ?? 0) > EventsService.CONCURRENCY_TIMEOUT_MS);
if (processingEventTimedOut) {
log.warn({ name: processingEvent.id }, "processing event has timed out. failing it and continuing...");
await this.fail(processingEvent.id);
}
}
}
finally {
this._lastCleanDate = new Date();
}
}
}