| ... | ... | @@ -7,12 +7,14 @@ import type { |
| 7 | 7 | import { |
| 8 | 8 | RelayAgentActivityAggregateState as RelayAgentActivityAggregateStateSchema, |
| 9 | 9 | RelayAgentAwarenessPreferences as RelayAgentAwarenessPreferencesSchema, |
| 10 | RelayDeliveryKind as RelayDeliveryKindSchema, |
| 10 | 11 | } from "@t3tools/contracts/relay"; |
| 11 | 12 | import * as Context from "effect/Context"; |
| 12 | 13 | import * as DateTime from "effect/DateTime"; |
| 13 | 14 | import * as Effect from "effect/Effect"; |
| 14 | 15 | import * as Layer from "effect/Layer"; |
| 15 | 16 | import * as Option from "effect/Option"; |
| 17 | import * as Redacted from "effect/Redacted"; |
| 16 | 18 | import * as Schema from "effect/Schema"; |
| 17 | 19 | |
| 18 | 20 | import { |
| ... | ... | @@ -87,6 +89,28 @@ export class ApnsDeliveryJobClaimInFlight extends Schema.TaggedErrorClass<ApnsDe |
| 87 | 89 | } |
| 88 | 90 | } |
| 89 | 91 | |
| 92 | export class ApnsDeliveryTransportError extends Schema.TaggedErrorClass<ApnsDeliveryTransportError>()( |
| 93 | "ApnsDeliveryTransportError", |
| 94 | { |
| 95 | deviceId: Schema.String, |
| 96 | kind: RelayDeliveryKindSchema, |
| 97 | sourceJobId: Schema.NullOr(Schema.String), |
| 98 | apnsErrorTag: Schema.Literals([ |
| 99 | "ApnsJwtEncodingError", |
| 100 | "ApnsJwtSigningError", |
| 101 | "ApnsHttpRequestError", |
| 102 | ]), |
| 103 | requestStage: Schema.NullOr(Schema.Literals(["send", "read-response"])), |
| 104 | cause: Schema.Defect(), |
| 105 | }, |
| 106 | ) { |
| 107 | override get message(): string { |
| 108 | return `APNs ${this.kind} delivery failed for device ${this.deviceId}.`; |
| 109 | } |
| 110 | } |
| 111 | |
| 112 | export const isApnsDeliveryTransportError = Schema.is(ApnsDeliveryTransportError); |
| 113 | |
| 90 | 114 | const decodeRelayAgentActivityAggregateStateJson = Schema.decodeUnknownOption( |
| 91 | 115 | Schema.fromJsonString(RelayAgentActivityAggregateStateSchema), |
| 92 | 116 | ); |
| ... | ... | @@ -302,6 +326,42 @@ function deliveryAttemptOutcome(result: Apns.ApnsDeliveryResult) { |
| 302 | 326 | }; |
| 303 | 327 | } |
| 304 | 328 | |
| 329 | const recoverApnsDeliveryTransportError = ( |
| 330 | input: { |
| 331 | readonly deviceId: string; |
| 332 | readonly kind: RelayDeliveryKind; |
| 333 | readonly sourceJobId: string | null; |
| 334 | }, |
| 335 | cause: Apns.ApnsError, |
| 336 | ): Effect.Effect<Apns.ApnsDeliveryResult> => { |
| 337 | const error = new ApnsDeliveryTransportError({ |
| 338 | deviceId: input.deviceId, |
| 339 | kind: input.kind, |
| 340 | sourceJobId: input.sourceJobId, |
| 341 | apnsErrorTag: cause._tag, |
| 342 | requestStage: cause._tag === "ApnsHttpRequestError" ? cause.stage : null, |
| 343 | cause, |
| 344 | }); |
| 345 | return Effect.logError(error.message).pipe( |
| 346 | Effect.annotateLogs({ |
| 347 | error: Redacted.make(error, { label: error._tag }), |
| 348 | "error.type": error._tag, |
| 349 | "error.apns_error_tag": error.apnsErrorTag, |
| 350 | ...(error.requestStage === null ? {} : { "error.request_stage": error.requestStage }), |
| 351 | ...(error.stack === undefined ? {} : { "error.stack": error.stack }), |
| 352 | "relay.mobile.device_id": error.deviceId, |
| 353 | "relay.delivery.kind": error.kind, |
| 354 | ...(error.sourceJobId === null ? {} : { "relay.delivery.job_id": error.sourceJobId }), |
| 355 | }), |
| 356 | Effect.as({ |
| 357 | ok: false, |
| 358 | status: 0, |
| 359 | reason: cause.message, |
| 360 | apnsId: null, |
| 361 | }), |
| 362 | ); |
| 363 | }; |
| 364 | |
| 305 | 365 | interface LiveActivityDeliveryTarget { |
| 306 | 366 | readonly user_id: string; |
| 307 | 367 | readonly device_id: string; |
| ... | ... | @@ -440,6 +500,15 @@ export const make = Effect.gen(function* () { |
| 440 | 500 | { ...input, aggregate } as SendLiveActivityDeliveryInput, |
| 441 | 501 | now, |
| 442 | 502 | ); |
| 503 | const recoverTransportError = (cause: Apns.ApnsError) => |
| 504 | recoverApnsDeliveryTransportError( |
| 505 | { |
| 506 | deviceId: input.target.device_id, |
| 507 | kind: input.kind, |
| 508 | sourceJobId: input.sourceJobId ?? null, |
| 509 | }, |
| 510 | cause, |
| 511 | ); |
| 443 | 512 | if (input.sourceJobId) { |
| 444 | 513 | const claim = yield* attempts.claimSourceJob({ |
| 445 | 514 | userId: input.target.user_id, |
| ... | ... | @@ -476,14 +545,11 @@ export const make = Effect.gen(function* () { |
| 476 | 545 | issuedAtUnixSeconds: epochSeconds, |
| 477 | 546 | }) |
| 478 | 547 | .pipe( |
| 479 | | Effect.catch((error) => |
| 480 | | Effect.succeed({ |
| 481 | | ok: false, |
| 482 | | status: 0, |
| 483 | | reason: error.message, |
| 484 | | apnsId: null, |
| 485 | | }), |
| 486 | | ), |
| 548 | Effect.catchTags({ |
| 549 | ApnsJwtEncodingError: recoverTransportError, |
| 550 | ApnsJwtSigningError: recoverTransportError, |
| 551 | ApnsHttpRequestError: recoverTransportError, |
| 552 | }), |
| 487 | 553 | ); |
| 488 | 554 | if (result.ok) { |
| 489 | 555 | yield* liveActivities.markDelivery({ |
| ... | ... | @@ -551,6 +617,15 @@ export const make = Effect.gen(function* () { |
| 551 | 617 | token: input.token, |
| 552 | 618 | notification, |
| 553 | 619 | }); |
| 620 | const recoverTransportError = (cause: Apns.ApnsError) => |
| 621 | recoverApnsDeliveryTransportError( |
| 622 | { |
| 623 | deviceId: input.target.device_id, |
| 624 | kind: "push_notification", |
| 625 | sourceJobId: input.sourceJobId ?? null, |
| 626 | }, |
| 627 | cause, |
| 628 | ); |
| 554 | 629 | if (input.sourceJobId) { |
| 555 | 630 | const claim = yield* attempts.claimSourceJob({ |
| 556 | 631 | userId: input.target.user_id, |
| ... | ... | @@ -593,14 +668,11 @@ export const make = Effect.gen(function* () { |
| 593 | 668 | issuedAtUnixSeconds: epochSeconds, |
| 594 | 669 | }) |
| 595 | 670 | .pipe( |
| 596 | | Effect.catch((error) => |
| 597 | | Effect.succeed({ |
| 598 | | ok: false, |
| 599 | | status: 0, |
| 600 | | reason: error.message, |
| 601 | | apnsId: null, |
| 602 | | }), |
| 603 | | ), |
| 671 | Effect.catchTags({ |
| 672 | ApnsJwtEncodingError: recoverTransportError, |
| 673 | ApnsJwtSigningError: recoverTransportError, |
| 674 | ApnsHttpRequestError: recoverTransportError, |
| 675 | }), |
| 604 | 676 | ); |
| 605 | 677 | if (isPermanentApnsTokenFailure(result)) { |
| 606 | 678 | yield* liveActivities.invalidateDeliveryToken({ |