Skip to content

Commit e93b7df

Browse files
committed
fix(webapp): move routeOperationsToRun into a server module
Restores the unit-testable export (removed to satisfy the Vite route export rules) by extracting the helper out of the route file.
1 parent 8bb45ad commit e93b7df

3 files changed

Lines changed: 119 additions & 114 deletions

File tree

apps/webapp/app/routes/api.v1.runs.$runId.metadata.ts

Lines changed: 2 additions & 113 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,18 @@
11
import type { LoaderFunctionArgs } from "@remix-run/server-runtime";
22
import { json } from "@remix-run/server-runtime";
33
import { tryCatch } from "@trigger.dev/core/utils";
4-
import type { RunMetadataChangeOperation } from "@trigger.dev/core/v3/schemas";
54
import { UpdateMetadataRequestBody } from "@trigger.dev/core/v3";
65
import { z } from "zod";
76
import { $replica } from "~/db.server";
8-
// Aliased to avoid shadowing the local `env: AuthenticatedEnvironment`
9-
// parameter the route handler and `routeOperationsToRun` use.
7+
// Aliased to avoid shadowing the local `env` parameter in the handler.
108
import { env as appEnv } from "~/env.server";
11-
import type { AuthenticatedEnvironment } from "~/services/apiAuth.server";
129
import { authenticateApiRequest } from "~/services/apiAuth.server";
13-
import { logger } from "~/services/logger.server";
1410
import { updateMetadataService } from "~/services/metadata/updateMetadataInstance.server";
1511
import { publishChangeRecord } from "~/services/realtime/runChangeNotifierInstance.server";
1612
import { createActionApiRoute } from "~/services/routeBuilders/apiBuilder.server";
1713
import { ServiceValidationError } from "~/v3/services/common.server";
1814
import { applyMetadataMutationToBufferedRun } from "~/v3/mollifier/applyMetadataMutation.server";
15+
import { routeOperationsToRun } from "~/v3/mollifier/routeOperationsToRun.server";
1916
import { findRunByIdWithMollifierFallback } from "~/v3/mollifier/readFallback.server";
2017
import { runStore } from "~/v3/runStore.server";
2118

@@ -67,114 +64,6 @@ export async function loader({ request, params }: LoaderFunctionArgs) {
6764
return json({ error: "Run not found" }, { status: 404 });
6865
}
6966

70-
// Route parent/root operations to the existing PG service by directly
71-
// invoking it against the parent/root runId. The service ingests via
72-
// its batching worker, which targets PG by id. If the parent/root is
73-
// itself buffered we recurse through our buffered-mutation helper.
74-
// `_ingestion_only` flag: a synthetic body that has the operations
75-
// promoted to top-level `operations` so the service applies them to
76-
// `targetRunId` directly.
77-
// Exported so the silent-failure logging behaviour can be unit-tested.
78-
// The route handler itself isn't an attractive test target (createActionApiRoute
79-
// wraps it in auth + body parsing + error-handler middleware), but the
80-
// fan-out helper carries the load-bearing logic — including the ops-
81-
// visibility branch this change adds.
82-
async function routeOperationsToRun(
83-
targetRunId: string | undefined,
84-
operations: RunMetadataChangeOperation[] | undefined,
85-
env: AuthenticatedEnvironment
86-
): Promise<void> {
87-
if (!targetRunId || !operations || operations.length === 0) return;
88-
89-
// Try PG first via the existing service (this is how parent/root
90-
// operations have always landed; preserve that). Accepts the full
91-
// AuthenticatedEnvironment so we don't have to recover the unsafe
92-
// `as unknown` cast that the previous narrowed `{ id, organizationId }`
93-
// signature forced on us.
94-
//
95-
// Two non-success outcomes from `call`:
96-
// * throws — PG threw (e.g. "Cannot update metadata for a completed
97-
// run", or a transient PG outage).
98-
// * resolves with undefined — PG row didn't exist (the target may be
99-
// buffered, not yet materialised).
100-
// Either way we want to try the buffer fallback below; treating the
101-
// undefined-return as success would make the fallback unreachable.
102-
const [error, result] = await tryCatch(
103-
updateMetadataService.call(targetRunId, { operations }, env)
104-
);
105-
if (!error && result !== undefined) {
106-
// The parent/root run changed too — wake its live feeds (only when something was
107-
// actually written here; buffered writes publish from the flusher).
108-
if (result.updatedAtMs !== undefined) {
109-
publishChangeRecord({
110-
runId: result.runId,
111-
envId: env.id,
112-
tags: result.runTags,
113-
batchId: result.batchId,
114-
updatedAtMs: result.updatedAtMs,
115-
});
116-
}
117-
return;
118-
}
119-
120-
if (error) {
121-
// PG threw — auxiliary op, stay best-effort and don't surface this
122-
// to the caller (the caller's primary mutation already landed). But
123-
// warn so a genuine PG outage on these ops isn't invisible.
124-
logger.warn("metadata route: parent/root PG op failed", {
125-
targetRunId,
126-
error: error instanceof Error ? error.message : String(error),
127-
});
128-
}
129-
130-
// Buffer fallback only makes sense for friendlyId-keyed entries. The
131-
// PG-side parent/root IDs are internal cuids; the buffer keys entries
132-
// by friendlyId, so passing the internal id would silently no-op.
133-
// Skip explicitly — a buffered child's parent is always materialised
134-
// in PG already (a buffered run hasn't executed, so it can't have
135-
// triggered the child), so the buffered-parent branch isn't actually
136-
// reachable. Treating the no-op as intentional rather than incidental.
137-
if (!targetRunId.startsWith("run_")) return;
138-
139-
// Best-effort buffer fallback. Wrap so a transient Redis throw on
140-
// this auxiliary op can't 500 the request after the primary mutation
141-
// already succeeded.
142-
const [bufferError, bufferOutcome] = await tryCatch(
143-
applyMetadataMutationToBufferedRun({
144-
runId: targetRunId,
145-
environmentId: env.id,
146-
organizationId: env.organizationId,
147-
maximumSize: appEnv.TASK_RUN_METADATA_MAXIMUM_SIZE,
148-
maxRetries: appEnv.TRIGGER_MOLLIFIER_METADATA_MAX_RETRIES,
149-
backoffBaseMs: appEnv.TRIGGER_MOLLIFIER_METADATA_BACKOFF_BASE_MS,
150-
backoffStepMs: appEnv.TRIGGER_MOLLIFIER_METADATA_BACKOFF_STEP_MS,
151-
body: { operations },
152-
})
153-
);
154-
if (bufferError) {
155-
logger.warn("metadata route: buffer fallback for parent/root op failed", {
156-
targetRunId,
157-
error: bufferError instanceof Error ? bufferError.message : String(bufferError),
158-
});
159-
return;
160-
}
161-
// `applyMetadataMutationToBufferedRun` reports non-throw failures via
162-
// its returned outcome kind: `not_found`, `busy`, `version_exhausted`,
163-
// `metadata_too_large`. Without inspecting `.kind`, the parent/root
164-
// operation can silently disappear — no PG row landed it (handled
165-
// above) and the buffer rejected it for one of these reasons but the
166-
// helper returned cleanly. Surface a warn log per non-success branch
167-
// so ops can trace why a parent/root op went missing. The customer's
168-
// primary mutation has already succeeded by this point; this remains
169-
// best-effort, so we still don't bubble these to the response.
170-
if (bufferOutcome && bufferOutcome.kind !== "applied") {
171-
logger.warn("metadata route: parent/root buffer op did not apply", {
172-
targetRunId,
173-
kind: bufferOutcome.kind,
174-
});
175-
}
176-
}
177-
17867
const { action } = createActionApiRoute(
17968
{
18069
params: ParamsSchema,
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
import { tryCatch } from "@trigger.dev/core/utils";
2+
import type { RunMetadataChangeOperation } from "@trigger.dev/core/v3/schemas";
3+
import { env as appEnv } from "~/env.server";
4+
import type { AuthenticatedEnvironment } from "~/services/apiAuth.server";
5+
import { logger } from "~/services/logger.server";
6+
import { updateMetadataService } from "~/services/metadata/updateMetadataInstance.server";
7+
import { publishChangeRecord } from "~/services/realtime/runChangeNotifierInstance.server";
8+
import { applyMetadataMutationToBufferedRun } from "./applyMetadataMutation.server";
9+
10+
// Route parent/root operations to the existing PG service by directly
11+
// invoking it against the parent/root runId. The service ingests via
12+
// its batching worker, which targets PG by id. If the parent/root is
13+
// itself buffered we recurse through our buffered-mutation helper.
14+
// `_ingestion_only` flag: a synthetic body that has the operations
15+
// promoted to top-level `operations` so the service applies them to
16+
// `targetRunId` directly.
17+
// Exported so the silent-failure logging behaviour can be unit-tested.
18+
// The route handler itself isn't an attractive test target (createActionApiRoute
19+
// wraps it in auth + body parsing + error-handler middleware), but the
20+
// fan-out helper carries the load-bearing logic — including the ops-
21+
// visibility branch this change adds.
22+
export async function routeOperationsToRun(
23+
targetRunId: string | undefined,
24+
operations: RunMetadataChangeOperation[] | undefined,
25+
env: AuthenticatedEnvironment
26+
): Promise<void> {
27+
if (!targetRunId || !operations || operations.length === 0) return;
28+
29+
// Try PG first via the existing service (this is how parent/root
30+
// operations have always landed; preserve that). Accepts the full
31+
// AuthenticatedEnvironment so we don't have to recover the unsafe
32+
// `as unknown` cast that the previous narrowed `{ id, organizationId }`
33+
// signature forced on us.
34+
//
35+
// Two non-success outcomes from `call`:
36+
// * throws — PG threw (e.g. "Cannot update metadata for a completed
37+
// run", or a transient PG outage).
38+
// * resolves with undefined — PG row didn't exist (the target may be
39+
// buffered, not yet materialised).
40+
// Either way we want to try the buffer fallback below; treating the
41+
// undefined-return as success would make the fallback unreachable.
42+
const [error, result] = await tryCatch(
43+
updateMetadataService.call(targetRunId, { operations }, env)
44+
);
45+
if (!error && result !== undefined) {
46+
// The parent/root run changed too — wake its live feeds (only when something was
47+
// actually written here; buffered writes publish from the flusher).
48+
if (result.updatedAtMs !== undefined) {
49+
publishChangeRecord({
50+
runId: result.runId,
51+
envId: env.id,
52+
tags: result.runTags,
53+
batchId: result.batchId,
54+
updatedAtMs: result.updatedAtMs,
55+
});
56+
}
57+
return;
58+
}
59+
60+
if (error) {
61+
// PG threw — auxiliary op, stay best-effort and don't surface this
62+
// to the caller (the caller's primary mutation already landed). But
63+
// warn so a genuine PG outage on these ops isn't invisible.
64+
logger.warn("metadata route: parent/root PG op failed", {
65+
targetRunId,
66+
error: error instanceof Error ? error.message : String(error),
67+
});
68+
}
69+
70+
// Buffer fallback only makes sense for friendlyId-keyed entries. The
71+
// PG-side parent/root IDs are internal cuids; the buffer keys entries
72+
// by friendlyId, so passing the internal id would silently no-op.
73+
// Skip explicitly — a buffered child's parent is always materialised
74+
// in PG already (a buffered run hasn't executed, so it can't have
75+
// triggered the child), so the buffered-parent branch isn't actually
76+
// reachable. Treating the no-op as intentional rather than incidental.
77+
if (!targetRunId.startsWith("run_")) return;
78+
79+
// Best-effort buffer fallback. Wrap so a transient Redis throw on
80+
// this auxiliary op can't 500 the request after the primary mutation
81+
// already succeeded.
82+
const [bufferError, bufferOutcome] = await tryCatch(
83+
applyMetadataMutationToBufferedRun({
84+
runId: targetRunId,
85+
environmentId: env.id,
86+
organizationId: env.organizationId,
87+
maximumSize: appEnv.TASK_RUN_METADATA_MAXIMUM_SIZE,
88+
maxRetries: appEnv.TRIGGER_MOLLIFIER_METADATA_MAX_RETRIES,
89+
backoffBaseMs: appEnv.TRIGGER_MOLLIFIER_METADATA_BACKOFF_BASE_MS,
90+
backoffStepMs: appEnv.TRIGGER_MOLLIFIER_METADATA_BACKOFF_STEP_MS,
91+
body: { operations },
92+
})
93+
);
94+
if (bufferError) {
95+
logger.warn("metadata route: buffer fallback for parent/root op failed", {
96+
targetRunId,
97+
error: bufferError instanceof Error ? bufferError.message : String(bufferError),
98+
});
99+
return;
100+
}
101+
// `applyMetadataMutationToBufferedRun` reports non-throw failures via
102+
// its returned outcome kind: `not_found`, `busy`, `version_exhausted`,
103+
// `metadata_too_large`. Without inspecting `.kind`, the parent/root
104+
// operation can silently disappear — no PG row landed it (handled
105+
// above) and the buffer rejected it for one of these reasons but the
106+
// helper returned cleanly. Surface a warn log per non-success branch
107+
// so ops can trace why a parent/root op went missing. The customer's
108+
// primary mutation has already succeeded by this point; this remains
109+
// best-effort, so we still don't bubble these to the response.
110+
if (bufferOutcome && bufferOutcome.kind !== "applied") {
111+
logger.warn("metadata route: parent/root buffer op did not apply", {
112+
targetRunId,
113+
kind: bufferOutcome.kind,
114+
});
115+
}
116+
}

apps/webapp/test/metadataRouteOperationsLogging.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ vi.mock("~/services/logger.server", () => ({
5252
},
5353
}));
5454

55-
import { routeOperationsToRun } from "~/routes/api.v1.runs.$runId.metadata";
55+
import { routeOperationsToRun } from "~/v3/mollifier/routeOperationsToRun.server";
5656
import type { AuthenticatedEnvironment } from "~/services/apiAuth.server";
5757

5858
const env = {

0 commit comments

Comments
 (0)