chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,211 @@
|
||||
import { EngineServiceValidationError } from "@internal/run-engine";
|
||||
import { json } from "@remix-run/server-runtime";
|
||||
import {
|
||||
generateJWT as internal_generateJWT,
|
||||
RunEngineVersionSchema,
|
||||
TriggerTaskRequestBody,
|
||||
} from "@trigger.dev/core/v3";
|
||||
import type { TaskRun } from "@trigger.dev/database";
|
||||
import { z } from "zod";
|
||||
import { prisma } from "~/db.server";
|
||||
import { env } from "~/env.server";
|
||||
import type { ApiAuthenticationResultSuccess } from "~/services/apiAuth.server";
|
||||
import { getOneTimeUseToken } from "~/services/apiAuth.server";
|
||||
import { logger } from "~/services/logger.server";
|
||||
import { extractJwtSigningSecretKey } from "~/services/realtime/jwtAuth.server";
|
||||
import { determineRealtimeStreamsVersion } from "~/services/realtime/v1StreamsGlobal.server";
|
||||
import { createActionApiRoute } from "~/services/routeBuilders/apiBuilder.server";
|
||||
import { resolveIdempotencyKeyTTL } from "~/utils/idempotencyKeys.server";
|
||||
import {
|
||||
handleRequestIdempotency,
|
||||
saveRequestIdempotency,
|
||||
} from "~/utils/requestIdempotency.server";
|
||||
import { sanitizeTriggerSource } from "~/utils/triggerSource";
|
||||
import { runStore } from "~/v3/runStore.server";
|
||||
import { ServiceValidationError } from "~/v3/services/baseService.server";
|
||||
import { OutOfEntitlementError, TriggerTaskService } from "~/v3/services/triggerTask.server";
|
||||
|
||||
const ParamsSchema = z.object({
|
||||
taskId: z.string(),
|
||||
});
|
||||
|
||||
export const HeadersSchema = z.object({
|
||||
"idempotency-key": z
|
||||
.string()
|
||||
.max(2048, "idempotency-key must be 2048 characters or less")
|
||||
.nullish(),
|
||||
"idempotency-key-ttl": z.string().nullish(),
|
||||
"trigger-version": z.string().nullish(),
|
||||
"x-trigger-span-parent-as-link": z.coerce.number().nullish(),
|
||||
"x-trigger-worker": z.string().nullish(),
|
||||
"x-trigger-client": z.string().nullish(),
|
||||
"x-trigger-engine-version": RunEngineVersionSchema.nullish(),
|
||||
"x-trigger-request-idempotency-key": z.string().nullish(),
|
||||
"x-trigger-realtime-streams-version": z.string().nullish(),
|
||||
"x-trigger-source": z.string().nullish(),
|
||||
traceparent: z.string().optional(),
|
||||
tracestate: z.string().optional(),
|
||||
});
|
||||
|
||||
const { action, loader } = createActionApiRoute(
|
||||
{
|
||||
headers: HeadersSchema,
|
||||
params: ParamsSchema,
|
||||
body: TriggerTaskRequestBody,
|
||||
allowJWT: true,
|
||||
maxContentLength: env.TASK_PAYLOAD_MAXIMUM_SIZE,
|
||||
authorization: {
|
||||
action: "trigger",
|
||||
resource: (params) => ({ type: "tasks", id: params.taskId }),
|
||||
},
|
||||
corsStrategy: "all",
|
||||
},
|
||||
async ({ body, headers, params, authentication }) => {
|
||||
const {
|
||||
"idempotency-key": idempotencyKey,
|
||||
"idempotency-key-ttl": idempotencyKeyTTL,
|
||||
"trigger-version": triggerVersion,
|
||||
"x-trigger-span-parent-as-link": spanParentAsLink,
|
||||
traceparent,
|
||||
tracestate,
|
||||
"x-trigger-worker": isFromWorker,
|
||||
"x-trigger-client": _triggerClient,
|
||||
"x-trigger-engine-version": engineVersion,
|
||||
"x-trigger-request-idempotency-key": requestIdempotencyKey,
|
||||
"x-trigger-realtime-streams-version": realtimeStreamsVersion,
|
||||
"x-trigger-source": triggerSourceHeader,
|
||||
} = headers;
|
||||
|
||||
const cachedResponse = await handleRequestIdempotency(requestIdempotencyKey, {
|
||||
requestType: "trigger",
|
||||
findCachedEntity: async (cachedRequestId) => {
|
||||
return await runStore.findRun(
|
||||
{
|
||||
id: cachedRequestId,
|
||||
},
|
||||
{
|
||||
select: {
|
||||
friendlyId: true,
|
||||
},
|
||||
},
|
||||
prisma
|
||||
);
|
||||
},
|
||||
buildResponse: (cachedRun) => ({
|
||||
id: cachedRun.friendlyId,
|
||||
isCached: false,
|
||||
}),
|
||||
buildResponseHeaders: async (responseBody, cachedEntity) => {
|
||||
return await responseHeaders(cachedEntity, authentication);
|
||||
},
|
||||
});
|
||||
|
||||
if (cachedResponse) {
|
||||
return cachedResponse;
|
||||
}
|
||||
|
||||
const service = new TriggerTaskService();
|
||||
|
||||
try {
|
||||
const traceContext = isFromWorker
|
||||
? { traceparent, tracestate }
|
||||
: { external: { traceparent, tracestate } };
|
||||
|
||||
const oneTimeUseToken = await getOneTimeUseToken(authentication);
|
||||
|
||||
const idempotencyKeyExpiresAt = resolveIdempotencyKeyTTL(idempotencyKeyTTL);
|
||||
|
||||
const result = await service.call(
|
||||
params.taskId,
|
||||
authentication.environment,
|
||||
body,
|
||||
{
|
||||
idempotencyKey: idempotencyKey ?? undefined,
|
||||
idempotencyKeyExpiresAt: idempotencyKeyExpiresAt,
|
||||
triggerVersion: triggerVersion ?? undefined,
|
||||
traceContext,
|
||||
spanParentAsLink: spanParentAsLink === 1,
|
||||
oneTimeUseToken,
|
||||
realtimeStreamsVersion: determineRealtimeStreamsVersion(
|
||||
realtimeStreamsVersion ?? undefined
|
||||
),
|
||||
triggerSource: isFromWorker
|
||||
? "sdk"
|
||||
: (sanitizeTriggerSource(triggerSourceHeader) ?? "api"),
|
||||
triggerAction: "trigger",
|
||||
},
|
||||
engineVersion ?? undefined
|
||||
);
|
||||
|
||||
if (!result) {
|
||||
return json({ error: "Task not found" }, { status: 404 });
|
||||
}
|
||||
|
||||
// Skip request-idempotency caching when the gate diverted to the
|
||||
// mollifier buffer. `result.run.id` is a synthesised cuid with no
|
||||
// corresponding PG row, so a lost-response SDK retry that reaches
|
||||
// `handleRequestIdempotency` would lookup that id, miss in PG, and
|
||||
// fall through to a fresh trigger — producing a duplicate buffer
|
||||
// entry for triggers without a task-level idempotency key (the
|
||||
// task-level path still dedupes via the buffer's SETNX in
|
||||
// `findBufferedRunWithIdempotency`). Accepting the retry-as-fresh-
|
||||
// trigger semantics here is bounded by the drainer's eventual
|
||||
// materialisation: once the run lands in PG, normal request-
|
||||
// idempotency from that point forward works as usual.
|
||||
if (!result.isMollified) {
|
||||
await saveRequestIdempotency(requestIdempotencyKey, "trigger", result.run.id);
|
||||
}
|
||||
|
||||
const $responseHeaders = await responseHeaders(result.run, authentication);
|
||||
|
||||
return json(
|
||||
{
|
||||
id: result.run.friendlyId,
|
||||
isCached: result.isCached,
|
||||
},
|
||||
{
|
||||
headers: $responseHeaders,
|
||||
status: 200,
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
if (error instanceof ServiceValidationError) {
|
||||
return json({ error: error.message }, { status: error.status ?? 422 });
|
||||
} else if (error instanceof EngineServiceValidationError) {
|
||||
return json({ error: error.message }, { status: error.status ?? 422 });
|
||||
} else if (error instanceof OutOfEntitlementError) {
|
||||
return json({ error: error.message }, { status: 422 });
|
||||
}
|
||||
|
||||
logger.error("Trigger task failed", { error });
|
||||
return json({ error: "Something went wrong" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
async function responseHeaders(
|
||||
run: Pick<TaskRun, "friendlyId">,
|
||||
authentication: ApiAuthenticationResultSuccess
|
||||
): Promise<Record<string, string>> {
|
||||
const { environment, realtime } = authentication;
|
||||
|
||||
const claims = {
|
||||
sub: environment.id,
|
||||
pub: true,
|
||||
scopes: [`read:runs:${run.friendlyId}`, `write:inputStreams:${run.friendlyId}`],
|
||||
realtime,
|
||||
};
|
||||
|
||||
const jwt = await internal_generateJWT({
|
||||
secretKey: extractJwtSigningSecretKey(environment),
|
||||
payload: claims,
|
||||
expirationTime: "1h",
|
||||
});
|
||||
|
||||
return {
|
||||
"x-trigger-jwt-claims": JSON.stringify(claims),
|
||||
"x-trigger-jwt": jwt,
|
||||
};
|
||||
}
|
||||
|
||||
export { action, loader };
|
||||
Reference in New Issue
Block a user