chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,194 @@
|
||||
import type { PrismaClient, Session } from "@trigger.dev/database";
|
||||
import type { SessionItem } from "@trigger.dev/core/v3";
|
||||
import type { RunStore } from "@internal/run-store";
|
||||
import { $replica, prisma } from "~/db.server";
|
||||
import { runStore as defaultRunStore } from "~/v3/runStore.server";
|
||||
|
||||
/**
|
||||
* Prefix that {@link SessionId.generate} attaches to every Session friendlyId.
|
||||
* Used to distinguish friendlyId lookups (`session_abc...`) from externalId
|
||||
* lookups on the public `GET /api/v1/sessions/:session` route.
|
||||
*/
|
||||
const SESSION_FRIENDLY_ID_PREFIX = "session_";
|
||||
|
||||
/**
|
||||
* Resolve a session from a URL path parameter that may contain either a
|
||||
* friendlyId (`session_abc...`) or a user-supplied externalId.
|
||||
*
|
||||
* Disambiguated by prefix: values starting with `session_` are treated as
|
||||
* friendlyIds, anything else is looked up against `externalId` scoped to
|
||||
* the caller's environment.
|
||||
*/
|
||||
// CONTROL-PLANE: `Session` lives on the control-plane DB; these reads are NOT
|
||||
// routed to run-ops read-through — only the `TaskRun` currentRunId resolves in
|
||||
// this file are run-ops read-through routed.
|
||||
export async function resolveSessionByIdOrExternalId(
|
||||
prisma: Pick<PrismaClient, "session">,
|
||||
runtimeEnvironmentId: string,
|
||||
idOrExternalId: string
|
||||
): Promise<Session | null> {
|
||||
if (isSessionFriendlyIdForm(idOrExternalId)) {
|
||||
return prisma.session.findFirst({
|
||||
where: { friendlyId: idOrExternalId, runtimeEnvironmentId },
|
||||
});
|
||||
}
|
||||
|
||||
// `findFirst` rather than `findUnique` per the repo rule — `findUnique`'s
|
||||
// implicit DataLoader has open correctness bugs in Prisma 6.x that bite
|
||||
// hot-path lookups exactly like this one.
|
||||
return prisma.session.findFirst({
|
||||
where: { runtimeEnvironmentId, externalId: idOrExternalId },
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Replica-first session resolution with a writer fallback on miss. For the
|
||||
* hot realtime routes (append / SSE subscribe / end-and-continue): a fresh
|
||||
* session's first append or subscribe can arrive inside the replica's apply
|
||||
* window, and a bare replica miss there surfaces as a 404 (or a subscribe
|
||||
* that never finds its session) for a session that exists on the writer.
|
||||
*/
|
||||
export async function resolveSessionWithWriterFallback(
|
||||
runtimeEnvironmentId: string,
|
||||
idOrExternalId: string
|
||||
): Promise<Session | null> {
|
||||
const row = await resolveSessionByIdOrExternalId($replica, runtimeEnvironmentId, idOrExternalId);
|
||||
if (row) return row;
|
||||
return resolveSessionByIdOrExternalId(prisma, runtimeEnvironmentId, idOrExternalId);
|
||||
}
|
||||
|
||||
/** True for `session_*` friendlyId form, false for everything else. */
|
||||
export function isSessionFriendlyIdForm(value: string): boolean {
|
||||
return value.startsWith(SESSION_FRIENDLY_ID_PREFIX);
|
||||
}
|
||||
|
||||
/**
|
||||
* Canonicalise the addressing key used for everything stream-level: the
|
||||
* S2 stream path and the run-engine waitpoint cache key. `chat.agent`
|
||||
* and the rest of the operational surface always pass `externalId`, but
|
||||
* a public-API caller may legitimately address by `friendlyId` — and a
|
||||
* session created without an `externalId` only has a friendlyId at all.
|
||||
*
|
||||
* Rule:
|
||||
* - If we have a Session row, the canonical key is `externalId` if
|
||||
* set, else `friendlyId`. This way two callers addressing the same
|
||||
* row via different forms always converge to the same S2 stream.
|
||||
* - If we have no row (yet — chat.agent's transport may subscribe
|
||||
* before the agent's bind-time upsert lands), the canonical key is
|
||||
* whatever the URL had. Operationally that's always an externalId.
|
||||
* Friendlyid-form callers without a matching row are rejected by
|
||||
* the route handler before this is reached.
|
||||
*/
|
||||
export function canonicalSessionAddressingKey(row: Session | null, paramSession: string): string {
|
||||
if (row) {
|
||||
return row.externalId ?? row.friendlyId;
|
||||
}
|
||||
return paramSession;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a Prisma `Session` row to the public {@link SessionItem} wire format.
|
||||
* Strips internal columns (project/environment/organization ids) and narrows
|
||||
* the `metadata` JSON to a record.
|
||||
*
|
||||
* Note: `currentRunId` is left as-is — Prisma stores the internal run id
|
||||
* (cuid), but `SessionItem.currentRunId` is the *friendly* form. Routes
|
||||
* that emit `SessionItem`s must translate: single-row endpoints via
|
||||
* {@link serializeSessionWithFriendlyRunId}, list endpoints via the
|
||||
* batched {@link serializeSessionsWithFriendlyRunIds}. Never put this
|
||||
* raw form on the wire directly.
|
||||
*/
|
||||
export function serializeSession(session: Session): SessionItem {
|
||||
return {
|
||||
id: session.friendlyId,
|
||||
externalId: session.externalId,
|
||||
type: session.type,
|
||||
taskIdentifier: session.taskIdentifier,
|
||||
triggerConfig: session.triggerConfig as SessionItem["triggerConfig"],
|
||||
currentRunId: session.currentRunId,
|
||||
tags: session.tags,
|
||||
metadata: (session.metadata ?? null) as SessionItem["metadata"],
|
||||
closedAt: session.closedAt,
|
||||
closedReason: session.closedReason,
|
||||
expiresAt: session.expiresAt,
|
||||
createdAt: session.createdAt,
|
||||
updatedAt: session.updatedAt,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Same as {@link serializeSession} but resolves `currentRunId` from the
|
||||
* internal cuid to the public `run_*` friendlyId via a TaskRun lookup.
|
||||
* Single-row endpoints (`POST/GET/PATCH/close /api/v1/sessions/:s`) use
|
||||
* this so the wire-side `currentRunId` is consistent with the rest of
|
||||
* the public API (which only accepts friendlyIds for run lookups).
|
||||
*
|
||||
* Skips the lookup when `currentRunId` is null.
|
||||
*
|
||||
* Resolves `currentRunId` -> `friendlyId` through `runStore.findRun` so a
|
||||
* run-ops id (NEW-DB) session run resolves from its owning store rather than the
|
||||
* control-plane replica. Mirrors `sessionRunManager.server.ts`.
|
||||
* Tenant-scoped because `Session.currentRunId` is a no-FK pointer.
|
||||
*/
|
||||
export async function serializeSessionWithFriendlyRunId(
|
||||
session: Session,
|
||||
runStore: RunStore = defaultRunStore
|
||||
): Promise<SessionItem> {
|
||||
const base = serializeSession(session);
|
||||
if (!session.currentRunId) return base;
|
||||
|
||||
const run = await runStore.findRun(
|
||||
{
|
||||
id: session.currentRunId,
|
||||
projectId: session.projectId,
|
||||
runtimeEnvironmentId: session.runtimeEnvironmentId,
|
||||
},
|
||||
{ select: { friendlyId: true } }
|
||||
);
|
||||
|
||||
return {
|
||||
...base,
|
||||
currentRunId: run?.friendlyId ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Batched form of {@link serializeSessionWithFriendlyRunId} for list
|
||||
* endpoints: one `IN` lookup per page instead of N+1. `currentRunId` on
|
||||
* the wire is always the public `run_*` friendlyId — the raw
|
||||
* {@link serializeSession} form leaks the internal cuid, which customers
|
||||
* can't use with `runs.retrieve(...)`.
|
||||
*/
|
||||
export async function serializeSessionsWithFriendlyRunIds(
|
||||
sessions: Session[],
|
||||
scope: { projectId: string; runtimeEnvironmentId: string },
|
||||
runStore: RunStore = defaultRunStore
|
||||
): Promise<SessionItem[]> {
|
||||
const runIds = [
|
||||
...new Set(sessions.map((s) => s.currentRunId).filter((id): id is string => !!id)),
|
||||
];
|
||||
|
||||
// `runStore.findRuns` fans out across both stores under split (NEW + LEGACY
|
||||
// replica merge) and is a plain `$replica` find when split is off. Tenant-
|
||||
// scoped: `Session.currentRunId` is a no-FK pointer, so a stale id must never
|
||||
// resolve a run in another env.
|
||||
const runs =
|
||||
runIds.length > 0
|
||||
? await runStore.findRuns({
|
||||
where: {
|
||||
id: { in: runIds },
|
||||
projectId: scope.projectId,
|
||||
runtimeEnvironmentId: scope.runtimeEnvironmentId,
|
||||
},
|
||||
select: { id: true, friendlyId: true },
|
||||
})
|
||||
: [];
|
||||
const friendlyIdByRunId = new Map(runs.map((run) => [run.id, run.friendlyId]));
|
||||
|
||||
return sessions.map((session) => ({
|
||||
...serializeSession(session),
|
||||
currentRunId: session.currentRunId
|
||||
? (friendlyIdByRunId.get(session.currentRunId) ?? null)
|
||||
: null,
|
||||
}));
|
||||
}
|
||||
Reference in New Issue
Block a user