Skip to content

Commit 32c7f90

Browse files
[codex] Structure APNs delivery queue errors (#3326)
Co-authored-by: codex <codex@users.noreply.github.com>
1 parent 2c16edd commit 32c7f90

2 files changed

Lines changed: 144 additions & 12 deletions

File tree

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import * as NodeCryptoLayer from "@effect/platform-node/NodeCrypto";
2+
import { describe, expect, it } from "@effect/vitest";
3+
import * as Cloudflare from "alchemy/Cloudflare";
4+
import * as Effect from "effect/Effect";
5+
import * as Layer from "effect/Layer";
6+
import * as Redacted from "effect/Redacted";
7+
8+
import * as RelayConfiguration from "../Config.ts";
9+
import * as ApnsDeliveryQueue from "./ApnsDeliveryQueue.ts";
10+
11+
const config: RelayConfiguration.RelayConfiguration["Service"] = {
12+
relayIssuer: "https://relay.example.com",
13+
apns: {
14+
teamId: "team-1",
15+
keyId: "key-1",
16+
privateKey: Redacted.make("apns-private-key"),
17+
bundleId: "com.t3tools.test",
18+
environment: "sandbox",
19+
},
20+
clerkSecretKey: Redacted.make("clerk-secret"),
21+
clerkPublishableKey: "pk_test_test",
22+
clerkJwtAudience: "t3-code-relay",
23+
apnsDeliveryJobSigningSecret: Redacted.make("apns-job-secret"),
24+
cloudMintPrivateKey: Redacted.make("cloud-private-key"),
25+
cloudMintPublicKey: "cloud-public-key",
26+
managedEndpointBaseDomain: undefined,
27+
managedEndpointNamespace: undefined,
28+
};
29+
30+
describe("ApnsDeliveryQueue", () => {
31+
it.effect("preserves job identity and the queue sender cause", () => {
32+
const cause = new Error("queue unavailable");
33+
const senderCause = new Cloudflare.QueueSendError({
34+
message: cause.message,
35+
cause,
36+
});
37+
const layer = ApnsDeliveryQueue.layer.pipe(
38+
Layer.provide(NodeCryptoLayer.layer),
39+
Layer.provide(RelayConfiguration.layer(config)),
40+
Layer.provide(
41+
Layer.succeed(ApnsDeliveryQueue.ApnsDeliveryQueueSender, {
42+
send: () => Effect.fail(senderCause),
43+
}),
44+
),
45+
);
46+
47+
return Effect.gen(function* () {
48+
const queue = yield* ApnsDeliveryQueue.ApnsDeliveryQueue;
49+
const error = yield* Effect.flip(
50+
queue.enqueuePushNotification({
51+
userId: "user-1",
52+
deviceId: "device-1",
53+
token: "push-token",
54+
notification: {
55+
title: "Thread",
56+
body: "Input: Project",
57+
environmentId: "env-1",
58+
threadId: "thread-1",
59+
deepLink: "/threads/env-1/thread-1",
60+
},
61+
}),
62+
);
63+
64+
expect(error).toMatchObject({
65+
_tag: "ApnsDeliveryQueueSendError",
66+
operation: "send",
67+
jobId: expect.any(String),
68+
kind: "push_notification",
69+
userId: "user-1",
70+
deviceId: "device-1",
71+
cause: senderCause,
72+
});
73+
expect(senderCause.cause).toBe(cause);
74+
expect(error.message).toBe(
75+
"Failed to enqueue APNs push notification delivery during send for device device-1.",
76+
);
77+
}).pipe(Effect.provide(layer));
78+
});
79+
});

infra/relay/src/agentActivity/ApnsDeliveryQueue.ts

Lines changed: 65 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,10 @@ import * as Effect from "effect/Effect";
77
import * as Layer from "effect/Layer";
88
import * as Schema from "effect/Schema";
99

10-
import type { RelayDeliveryResult } from "@t3tools/contracts/relay";
10+
import {
11+
RelayDeliveryKind as RelayDeliveryKindSchema,
12+
type RelayDeliveryResult,
13+
} from "@t3tools/contracts/relay";
1114

1215
import {
1316
sanitizeAgentActivityAggregateState,
@@ -24,10 +27,17 @@ import * as RelayConfiguration from "../Config.ts";
2427

2528
export class ApnsDeliveryQueueSendError extends Schema.TaggedErrorClass<ApnsDeliveryQueueSendError>()(
2629
"ApnsDeliveryQueueSendError",
27-
{ cause: Schema.Defect() },
30+
{
31+
operation: Schema.Literals(["generate-job-id", "send"]),
32+
jobId: Schema.NullOr(Schema.String),
33+
kind: RelayDeliveryKindSchema,
34+
userId: Schema.String,
35+
deviceId: Schema.String,
36+
cause: Schema.Defect(),
37+
},
2838
) {
2939
override get message(): string {
30-
return "Failed to enqueue APNs delivery";
40+
return `Failed to enqueue APNs ${this.kind.replaceAll("_", " ")} delivery during ${this.operation} for device ${this.deviceId}.`;
3141
}
3242
}
3343

@@ -36,7 +46,7 @@ export type ApnsDeliveryQueueError = ApnsDeliveryQueueSendError;
3646
export class ApnsDeliveryQueueSender extends Context.Service<
3747
ApnsDeliveryQueueSender,
3848
{
39-
readonly send: (body: SignedApnsDeliveryJob) => Effect.Effect<void, ApnsDeliveryQueueSendError>;
49+
readonly send: (body: SignedApnsDeliveryJob) => Effect.Effect<void, Cloudflare.QueueSendError>;
4050
}
4151
>()("t3code-relay/agentActivity/ApnsDeliveryQueue/ApnsDeliveryQueueSender") {}
4252

@@ -73,7 +83,17 @@ export const make = Effect.gen(function* () {
7383
});
7484
const now = yield* DateTime.now;
7585
const jobId = yield* crypto.randomUUIDv4.pipe(
76-
Effect.mapError((cause) => new ApnsDeliveryQueueSendError({ cause })),
86+
Effect.mapError(
87+
(cause) =>
88+
new ApnsDeliveryQueueSendError({
89+
operation: "generate-job-id",
90+
jobId: null,
91+
kind: input.kind,
92+
userId: input.userId,
93+
deviceId: input.deviceId,
94+
cause,
95+
}),
96+
),
7797
);
7898
yield* Effect.annotateCurrentSpan({ "relay.delivery.job_id": jobId });
7999
const payload = makeApnsDeliveryJobPayload({
@@ -88,7 +108,19 @@ export const make = Effect.gen(function* () {
88108
secret: config.apnsDeliveryJobSigningSecret,
89109
payload,
90110
});
91-
yield* sender.send(signed);
111+
yield* sender.send(signed).pipe(
112+
Effect.mapError(
113+
(cause) =>
114+
new ApnsDeliveryQueueSendError({
115+
operation: "send",
116+
jobId,
117+
kind: input.kind,
118+
userId: input.userId,
119+
deviceId: input.deviceId,
120+
cause,
121+
}),
122+
),
123+
);
92124
return {
93125
deviceId: input.deviceId,
94126
kind: input.kind,
@@ -110,7 +142,17 @@ export const make = Effect.gen(function* () {
110142
});
111143
const now = yield* DateTime.now;
112144
const jobId = yield* crypto.randomUUIDv4.pipe(
113-
Effect.mapError((cause) => new ApnsDeliveryQueueSendError({ cause })),
145+
Effect.mapError(
146+
(cause) =>
147+
new ApnsDeliveryQueueSendError({
148+
operation: "generate-job-id",
149+
jobId: null,
150+
kind: "push_notification",
151+
userId: input.userId,
152+
deviceId: input.deviceId,
153+
cause,
154+
}),
155+
),
114156
);
115157
yield* Effect.annotateCurrentSpan({ "relay.delivery.job_id": jobId });
116158
const payload = makeApnsDeliveryJobPayload({
@@ -128,7 +170,19 @@ export const make = Effect.gen(function* () {
128170
secret: config.apnsDeliveryJobSigningSecret,
129171
payload,
130172
});
131-
yield* sender.send(signed);
173+
yield* sender.send(signed).pipe(
174+
Effect.mapError(
175+
(cause) =>
176+
new ApnsDeliveryQueueSendError({
177+
operation: "send",
178+
jobId,
179+
kind: "push_notification",
180+
userId: input.userId,
181+
deviceId: input.deviceId,
182+
cause,
183+
}),
184+
),
185+
);
132186
return {
133187
deviceId: input.deviceId,
134188
kind: "push_notification" as const,
@@ -155,10 +209,9 @@ export const layerCloudflareQueues = (
155209
ApnsDeliveryQueueSender,
156210
ApnsDeliveryQueueSender.of({
157211
send: (body) =>
158-
sender.send(body).pipe(
159-
Effect.mapError((cause) => new ApnsDeliveryQueueSendError({ cause })),
160-
Effect.provideService(Alchemy.RuntimeContext, alchemyRuntimeContext),
161-
),
212+
sender
213+
.send(body)
214+
.pipe(Effect.provideService(Alchemy.RuntimeContext, alchemyRuntimeContext)),
162215
}),
163216
),
164217
),

0 commit comments

Comments
 (0)