175 lines
5.8 KiB
TypeScript
175 lines
5.8 KiB
TypeScript
import {
|
|
type Prisma,
|
|
type PrismaClient,
|
|
type PrismaClientOrTransaction,
|
|
} from "@trigger.dev/database";
|
|
import type { RunStore } from "@internal/run-store";
|
|
import { BoundedTtlCache } from "./boundedTtlCache";
|
|
import { RESERVED_COLUMNS, type RealtimeRunRow } from "./electricStreamProtocol.server";
|
|
|
|
/**
|
|
* RunReader — the pluggable read half of the native-backend realtime feed: ClickHouse is filter-only
|
|
* (resolves ids), Postgres always hydrates row columns. Owns the `RunHydrator` (by-id) and the
|
|
* `RunListResolver` interface (the tag/list filter -> id-set seam, implemented over ClickHouse).
|
|
*/
|
|
|
|
/** The TaskRun columns the realtime feed projects (mirrors DEFAULT_ELECTRIC_COLUMNS). */
|
|
export const RUN_HYDRATOR_SELECT = {
|
|
id: true,
|
|
taskIdentifier: true,
|
|
createdAt: true,
|
|
updatedAt: true,
|
|
startedAt: true,
|
|
delayUntil: true,
|
|
queuedAt: true,
|
|
expiredAt: true,
|
|
completedAt: true,
|
|
friendlyId: true,
|
|
number: true,
|
|
isTest: true,
|
|
status: true,
|
|
usageDurationMs: true,
|
|
costInCents: true,
|
|
baseCostInCents: true,
|
|
ttl: true,
|
|
payload: true,
|
|
payloadType: true,
|
|
metadata: true,
|
|
metadataType: true,
|
|
output: true,
|
|
outputType: true,
|
|
runTags: true,
|
|
error: true,
|
|
realtimeStreams: true,
|
|
} satisfies Prisma.TaskRunSelect;
|
|
|
|
/** Columns hydrated regardless of `skipColumns`: `id` keys the row, `updatedAt` drives the offset and working-set diff. */
|
|
const ALWAYS_HYDRATED_COLUMNS = new Set<string>(["id", "updatedAt", ...RESERVED_COLUMNS]);
|
|
|
|
/** Project `RUN_HYDRATOR_SELECT` down to the columns the client didn't skip (plus
|
|
* the always-needed ones). An empty skip set returns the full select unchanged. */
|
|
export function buildHydratorSelect(skipColumns: string[] = []): Prisma.TaskRunSelect {
|
|
if (skipColumns.length === 0) {
|
|
return RUN_HYDRATOR_SELECT;
|
|
}
|
|
const skip = new Set(skipColumns);
|
|
const select: Record<string, boolean> = {};
|
|
for (const column of Object.keys(RUN_HYDRATOR_SELECT)) {
|
|
if (ALWAYS_HYDRATED_COLUMNS.has(column) || !skip.has(column)) {
|
|
select[column] = true;
|
|
}
|
|
}
|
|
return select as Prisma.TaskRunSelect;
|
|
}
|
|
|
|
export type RunListFilter = {
|
|
organizationId: string;
|
|
projectId: string;
|
|
environmentId: string;
|
|
/** Contains-ANY tag match (OR). Omit/empty for non-tag feeds. */
|
|
tags?: string[];
|
|
/** Restrict to a single batch (internal batch id) — the batch feed. */
|
|
batchId?: string;
|
|
/** Lower bound on createdAt (the tag-list feed pins this; batch omits it). */
|
|
createdAtAfter?: Date;
|
|
/** Hard cap on the result set so a broad filter can't unbound the snapshot. */
|
|
limit: number;
|
|
};
|
|
|
|
/** Resolves a tag/list filter into the matching run id-set, filter-only (rows hydrated from Postgres by id afterward). ClickHouse impl in `clickHouseRunListResolver.server.ts`. */
|
|
export interface RunListResolver {
|
|
resolveMatchingRunIds(filter: RunListFilter): Promise<string[]>;
|
|
}
|
|
|
|
export type RunHydratorOptions = {
|
|
/** A read-replica Prisma client (`$replica`). Always Postgres. */
|
|
replica: Pick<PrismaClient, "taskRun">;
|
|
/** RunStore the reads are routed through; `replica` is passed as the read client. */
|
|
runStore: RunStore;
|
|
/** Read-through cache TTL (ms) collapsing duplicate refetches for the same run. Set 0 to disable. Defaults to 250ms. */
|
|
cacheTtlMs?: number;
|
|
/** Hard cap on cache entries before expired entries are swept. */
|
|
maxCacheEntries?: number;
|
|
};
|
|
|
|
const DEFAULT_CACHE_TTL_MS = 250;
|
|
const DEFAULT_MAX_CACHE_ENTRIES = 5_000;
|
|
|
|
/** Hydrates runs by id through the runStore seam (split routing lives in the store, below this file), projected to the realtime columns; concurrent same-run refetches are single-flighted + short-TTL cached. */
|
|
export class RunHydrator {
|
|
readonly #inflight = new Map<string, Promise<RealtimeRunRow | null>>();
|
|
readonly #cache: BoundedTtlCache<RealtimeRunRow | null>;
|
|
readonly #cacheTtlMs: number;
|
|
|
|
constructor(private readonly options: RunHydratorOptions) {
|
|
this.#cacheTtlMs = options.cacheTtlMs ?? DEFAULT_CACHE_TTL_MS;
|
|
this.#cache = new BoundedTtlCache(
|
|
this.#cacheTtlMs,
|
|
options.maxCacheEntries ?? DEFAULT_MAX_CACHE_ENTRIES
|
|
);
|
|
}
|
|
|
|
async getRunById(environmentId: string, runId: string): Promise<RealtimeRunRow | null> {
|
|
const key = `${environmentId}:${runId}`;
|
|
|
|
if (this.#cacheTtlMs > 0) {
|
|
// A cached null is a valid "run not found" hit; only undefined is a miss.
|
|
const cached = this.#cache.get(key);
|
|
if (cached !== undefined) {
|
|
return cached;
|
|
}
|
|
}
|
|
|
|
const existing = this.#inflight.get(key);
|
|
if (existing) {
|
|
return existing;
|
|
}
|
|
|
|
const promise = this.#fetch(environmentId, runId).finally(() => this.#inflight.delete(key));
|
|
this.#inflight.set(key, promise);
|
|
|
|
const row = await promise;
|
|
|
|
if (this.#cacheTtlMs > 0) {
|
|
this.#cache.set(key, row);
|
|
}
|
|
|
|
return row;
|
|
}
|
|
|
|
/** Hydrate many runs by id in one query (order not guaranteed); `skipColumns` projects the SELECT so dropped columns aren't shipped. */
|
|
async hydrateByIds(
|
|
environmentId: string,
|
|
ids: string[],
|
|
skipColumns: string[] = []
|
|
): Promise<RealtimeRunRow[]> {
|
|
if (ids.length === 0) {
|
|
return [];
|
|
}
|
|
const rows = await this.options.runStore.findRuns(
|
|
{
|
|
where: {
|
|
runtimeEnvironmentId: environmentId,
|
|
id: { in: ids },
|
|
},
|
|
select: buildHydratorSelect(skipColumns),
|
|
},
|
|
this.options.replica as PrismaClientOrTransaction
|
|
);
|
|
return rows as unknown as RealtimeRunRow[];
|
|
}
|
|
|
|
async #fetch(environmentId: string, runId: string): Promise<RealtimeRunRow | null> {
|
|
const run = await this.options.runStore.findRun(
|
|
{
|
|
id: runId,
|
|
runtimeEnvironmentId: environmentId,
|
|
},
|
|
{ select: RUN_HYDRATOR_SELECT },
|
|
this.options.replica as PrismaClientOrTransaction
|
|
);
|
|
|
|
return (run ?? null) as RealtimeRunRow | null;
|
|
}
|
|
}
|