authorgravatar for me@t3.ggTheo Browne <me@t3.gg> 2026-09-04 09:01:57-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-09-04 09:01:57-07:00
logc75299ee2085a121bceb6df76796e971fe92b5b6
tree4db8c44b3bf06e3d6cd1b698ecc8ace8a3fc987f
parentdffb4cd3b16dc6f41aced99922950ee3083082c6
signature Signed by PGP key B5690EEEBB952194

perf(relay): avoid repeated activity decoding (#9708)


4 files changed, 44 insertions(+), 25 deletions(-)

infra/relay/src/agentActivity/AgentActivityPublisher.test.ts+2-8
...@@ -517,7 +517,7 @@ describe("AgentActivityPublisher", () => {...@@ -517,7 +517,7 @@ describe("AgentActivityPublisher", () => {
517 });517 });
518518
519 it.effect(519 it.effect(
520 "does not build Live Activity aggregates for links with Live Activities disabled",520 "delivers notifications without querying activity rows when Live Activities are disabled",
521 () => {521 () => {
522 const notificationState: RelayAgentActivityState = {522 const notificationState: RelayAgentActivityState = {
523 ...state,523 ...state,
...@@ -549,13 +549,7 @@ describe("AgentActivityPublisher", () => {...@@ -549,13 +549,7 @@ describe("AgentActivityPublisher", () => {
549 AgentActivityRows.AgentActivityRows,549 AgentActivityRows.AgentActivityRows,
550 makeAgentActivityRows({550 makeAgentActivityRows({
551 listForUser: () =>551 listForUser: () =>
552 Effect.succeed([552 Effect.die("Notification-only delivery must not read rows"),
553 {
554 ...state,
555 environmentId: "other-env" as RelayAgentActivityState["environmentId"],
556 threadId: "other-thread" as RelayAgentActivityState["threadId"],
557 },
558 ]),
559 }),553 }),
560 ),554 ),
561 Layer.succeed(555 Layer.succeed(
infra/relay/src/agentActivity/AgentActivityPublisher.ts+3-1
...@@ -58,7 +58,9 @@ export const make = Effect.gen(function* () {...@@ -58,7 +58,9 @@ export const make = Effect.gen(function* () {
58 readonly state: RelayAgentActivityState | null;58 readonly state: RelayAgentActivityState | null;
59 readonly nowMs: number;59 readonly nowMs: number;
60 }) {60 }) {
61 const activeStates = yield* rows.listForUser({ userId: input.deliveryUser.userId });61 const activeStates = input.deliveryUser.liveActivitiesEnabled
62 ? yield* rows.listForUser({ userId: input.deliveryUser.userId })
63 : [];
62 const liveActivityAggregate = input.deliveryUser.liveActivitiesEnabled64 const liveActivityAggregate = input.deliveryUser.liveActivitiesEnabled
63 ? makeAggregateState({65 ? makeAggregateState({
64 activeStates,66 activeStates,
infra/relay/src/agentActivity/AgentActivityRows.test.ts+36
...@@ -19,6 +19,42 @@ const state: RelayAgentActivityState = {...@@ -19,6 +19,42 @@ const state: RelayAgentActivityState = {
19};19};
2020
21describe("AgentActivityRows", () => {21describe("AgentActivityRows", () => {
22 it.effect("validates database JSON objects and ignores invalid activity rows", () => {
23 const queryRows = [
24 { stateJson: null },
25 { stateJson: JSON.stringify(state) },
26 { stateJson: { ...state, phase: "invalid" } },
27 { stateJson: { ...state, unrelated: "ignored" } },
28 ];
29 const db = {
30 select: () => ({
31 from: () => ({
32 innerJoin: () => ({
33 where: () => ({
34 orderBy: () => Effect.succeed(queryRows),
35 }),
36 }),
37 }),
38 }),
39 } as unknown as RelayDb.RelayDb["Service"];
40
41 return Effect.gen(function* () {
42 const rows = yield* AgentActivityRows.AgentActivityRows;
43 expect(yield* rows.listForUser({ userId: "user-2" })).toEqual([state]);
44 expect(
45 yield* rows.getForUserThread({
46 userId: "user-2",
47 environmentId: "env-1",
48 threadId: "thread-1",
49 }),
50 ).toEqual(state);
51 }).pipe(
52 Effect.provide(
53 AgentActivityRows.layer.pipe(Layer.provide(Layer.succeed(RelayDb.RelayDb, db))),
54 ),
55 );
56 });
57
22 it.effect("preserves activity context on persistence failures", () => {58 it.effect("preserves activity context on persistence failures", () => {
23 const cause = new Error("database unavailable");59 const cause = new Error("database unavailable");
24 const failingDb = {60 const failingDb = {
infra/relay/src/agentActivity/AgentActivityRows.ts+3-16
...@@ -92,15 +92,12 @@ export class AgentActivityRows extends Context.Service<...@@ -92,15 +92,12 @@ export class AgentActivityRows extends Context.Service<
92>()("t3code-relay/agentActivity/AgentActivityRows") {}92>()("t3code-relay/agentActivity/AgentActivityRows") {}
9393
94const decodeJsonString = Schema.decodeEffect(Schema.fromJsonString(Schema.Unknown));94const decodeJsonString = Schema.decodeEffect(Schema.fromJsonString(Schema.Unknown));
95const encodeJsonValue = Schema.encodeEffect(Schema.fromJsonString(Schema.Unknown));
9695
97const encodeRelayAgentActivityStateJson = Schema.encodeEffect(96const encodeRelayAgentActivityStateJson = Schema.encodeEffect(
98 Schema.fromJsonString(RelayAgentActivityStateSchema),97 Schema.fromJsonString(RelayAgentActivityStateSchema),
99);98);
10099
101const decodeRelayAgentActivityStateJson = Schema.decodeUnknownOption(100const decodeRelayAgentActivityState = Schema.decodeUnknownOption(RelayAgentActivityStateSchema);
102 Schema.fromJsonString(RelayAgentActivityStateSchema),
103);
104101
105export const make = Effect.gen(function* () {102export const make = Effect.gen(function* () {
106 const db = yield* RelayDb.RelayDb;103 const db = yield* RelayDb.RelayDb;
...@@ -229,13 +226,8 @@ export const make = Effect.gen(function* () {...@@ -229,13 +226,8 @@ export const make = Effect.gen(function* () {
229 )226 )
230 .orderBy(desc(relayAgentActivityRows.updatedAt))227 .orderBy(desc(relayAgentActivityRows.updatedAt))
231 .pipe(228 .pipe(
232 Effect.flatMap((rows) =>
233 Effect.forEach(rows, (row) => encodeJsonValue(row.stateJson), {
234 concurrency: "unbounded",
235 }),
236 ),
237 Effect.map((rows) =>229 Effect.map((rows) =>
238 rows.flatMap((row) => Option.toArray(decodeRelayAgentActivityStateJson(row))),230 rows.flatMap((row) => Option.toArray(decodeRelayAgentActivityState(row.stateJson))),
239 ),231 ),
240 Effect.mapError(232 Effect.mapError(
241 (cause) =>233 (cause) =>
...@@ -271,14 +263,9 @@ export const make = Effect.gen(function* () {...@@ -271,14 +263,9 @@ export const make = Effect.gen(function* () {
271 )263 )
272 .orderBy(desc(relayAgentActivityRows.updatedAt))264 .orderBy(desc(relayAgentActivityRows.updatedAt))
273 .pipe(265 .pipe(
274 Effect.flatMap((rows) =>
275 Effect.forEach(rows, (row) => encodeJsonValue(row.stateJson), {
276 concurrency: "unbounded",
277 }),
278 ),
279 Effect.map((rows) => {266 Effect.map((rows) => {
280 for (const row of rows) {267 for (const row of rows) {
281 const decoded = decodeRelayAgentActivityStateJson(row);268 const decoded = decodeRelayAgentActivityState(row.stateJson);
282 if (Option.isSome(decoded)) {269 if (Option.isSome(decoded)) {
283 return decoded.value;270 return decoded.value;
284 }271 }