Files
triggerdotdev--trigger.dev/apps/webapp/app/presenters/v3/BatchPresenter.server.ts
T
2026-07-13 13:32:57 +08:00

156 lines
5.6 KiB
TypeScript

import { type BatchTaskRunStatus, type Prisma } from "@trigger.dev/database";
import { type PrismaClientOrTransaction, type PrismaReplicaClient } from "~/db.server";
import { findDisplayableEnvironment } from "~/models/runtimeEnvironment.server";
import { engine } from "~/v3/runEngine.server";
import { readThroughRun } from "~/v3/runOpsMigration/readThrough.server";
import { BasePresenter } from "./basePresenter.server";
type BatchPresenterOptions = {
environmentId: string;
batchId: string;
userId?: string;
};
// Shared by the read-through closures and the passthrough so every store path returns
// a byte-identical row shape.
const BATCH_SELECT = {
id: true,
friendlyId: true,
status: true,
runCount: true,
batchVersion: true,
createdAt: true,
updatedAt: true,
completedAt: true,
processingStartedAt: true,
processingCompletedAt: true,
successfulRunCount: true,
failedRunCount: true,
idempotencyKey: true,
errors: {
select: {
id: true,
index: true,
taskIdentifier: true,
error: true,
errorCode: true,
createdAt: true,
},
orderBy: {
index: "asc",
},
},
} satisfies Prisma.BatchTaskRunSelect;
type BatchRow = Prisma.BatchTaskRunGetPayload<{ select: typeof BATCH_SELECT }>;
type BatchPresenterDeps = {
/** Resolved boot constant; never awaited per-request when supplied. */
splitEnabled?: boolean;
newClient?: PrismaReplicaClient;
legacyReplica?: PrismaReplicaClient;
readThrough?: typeof readThroughRun;
resolveDisplayableEnvironment?: typeof findDisplayableEnvironment;
};
export type BatchPresenterData = Awaited<ReturnType<BatchPresenter["call"]>>;
export class BatchPresenter extends BasePresenter {
constructor(
_prisma?: PrismaClientOrTransaction,
_replica?: PrismaClientOrTransaction,
private readonly deps: BatchPresenterDeps = {}
) {
super(_prisma, _replica);
}
public async call({ environmentId, batchId, userId }: BatchPresenterOptions) {
// Reads the BatchTaskRun (run-ops) via the read-through layer: split on -> new run-ops
// first, then the LEGACY RUN-OPS READ REPLICA only for not-yet-migrated batches (never the
// legacy primary); split off (single-DB / self-host) -> one plain batchTaskRun.findFirst
// (passthrough). The runtimeEnvironment (control-plane) is resolved separately because its
// FK is physically dropped on cloud, so a batch row on the new run-ops DB cannot single-SQL
// join to control-plane RuntimeEnvironment.
const where = { runtimeEnvironmentId: environmentId, friendlyId: batchId } as const;
const readBatch = (client: PrismaReplicaClient): Promise<BatchRow | null> =>
client.batchTaskRun.findFirst({ select: BATCH_SELECT, where });
const readThrough = this.deps.readThrough ?? readThroughRun;
const batchResult = await readThrough<BatchRow>({
// The read-through key; here it is the batch friendlyId. A cuid-shaped batch friendlyId
// classifies as LEGACY and the read-through probes both stores (new first, then legacy
// replica); a run-ops-shaped one (cut-over orgs) classifies as NEW and reads only the new
// store — either way the row is found on the DB that owns it.
runId: batchId,
environmentId,
readNew: readBatch,
readLegacy: readBatch,
deps: {
splitEnabled: this.deps.splitEnabled,
newClient: this.deps.newClient,
legacyReplica: this.deps.legacyReplica,
},
});
const batch =
batchResult.source === "new" || batchResult.source === "legacy-replica"
? batchResult.value
: null; // not-found / past-retention => normal not-found surface
if (!batch) {
throw new Error("Batch not found");
}
const hasFinished = batch.status !== "PENDING" && batch.status !== "PROCESSING";
const isV2 = batch.batchVersion === "runengine:v2";
// For v2 batches in PROCESSING state, get live progress from Redis
// This provides real-time updates without waiting for the batch to complete
let liveSuccessCount = batch.successfulRunCount ?? 0;
let liveFailureCount = batch.failedRunCount ?? 0;
if (isV2 && batch.status === "PROCESSING") {
const liveProgress = await engine.getBatchQueueProgress(batch.id);
if (liveProgress) {
liveSuccessCount = liveProgress.successCount;
liveFailureCount = liveProgress.failureCount;
}
}
// Control-plane env resolved separately from the run-ops batch row (cross-seam FK dropped).
const resolveEnv = this.deps.resolveDisplayableEnvironment ?? findDisplayableEnvironment;
return {
id: batch.id,
friendlyId: batch.friendlyId,
status: batch.status as BatchTaskRunStatus,
runCount: batch.runCount,
batchVersion: batch.batchVersion,
isV2,
createdAt: batch.createdAt.toISOString(),
updatedAt: batch.updatedAt.toISOString(),
completedAt: batch.completedAt?.toISOString(),
processingStartedAt: batch.processingStartedAt?.toISOString(),
processingCompletedAt: batch.processingCompletedAt?.toISOString(),
finishedAt: batch.completedAt
? batch.completedAt.toISOString()
: hasFinished
? batch.updatedAt.toISOString()
: undefined,
hasFinished,
successfulRunCount: liveSuccessCount,
failedRunCount: liveFailureCount,
idempotencyKey: batch.idempotencyKey,
environment: await resolveEnv(environmentId, userId),
errors: batch.errors.map((error) => ({
id: error.id,
index: error.index,
taskIdentifier: error.taskIdentifier,
error: error.error,
errorCode: error.errorCode,
createdAt: error.createdAt.toISOString(),
})),
};
}
}