| ... | @@ -7,12 +7,14 @@ import type { | ... | @@ -7,12 +7,14 @@ import type { |
| 7 | import { | 7 | import { |
| 8 | RelayAgentActivityAggregateState as RelayAgentActivityAggregateStateSchema, | 8 | RelayAgentActivityAggregateState as RelayAgentActivityAggregateStateSchema, |
| 9 | RelayAgentAwarenessPreferences as RelayAgentAwarenessPreferencesSchema, | 9 | RelayAgentAwarenessPreferences as RelayAgentAwarenessPreferencesSchema, |
| | 10 | RelayDeliveryKind as RelayDeliveryKindSchema, |
| 10 | } from "@t3tools/contracts/relay"; | 11 | } from "@t3tools/contracts/relay"; |
| 11 | import * as Context from "effect/Context"; | 12 | import * as Context from "effect/Context"; |
| 12 | import * as DateTime from "effect/DateTime"; | 13 | import * as DateTime from "effect/DateTime"; |
| 13 | import * as Effect from "effect/Effect"; | 14 | import * as Effect from "effect/Effect"; |
| 14 | import * as Layer from "effect/Layer"; | 15 | import * as Layer from "effect/Layer"; |
| 15 | import * as Option from "effect/Option"; | 16 | import * as Option from "effect/Option"; |
| | 17 | import * as Redacted from "effect/Redacted"; |
| 16 | import * as Schema from "effect/Schema"; | 18 | import * as Schema from "effect/Schema"; |
| 17 | | 19 | |
| 18 | import { | 20 | import { |
| ... | @@ -87,6 +89,28 @@ export class ApnsDeliveryJobClaimInFlight extends Schema.TaggedErrorClass<ApnsDe | ... | @@ -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 | const decodeRelayAgentActivityAggregateStateJson = Schema.decodeUnknownOption( | 114 | const decodeRelayAgentActivityAggregateStateJson = Schema.decodeUnknownOption( |
| 91 | Schema.fromJsonString(RelayAgentActivityAggregateStateSchema), | 115 | Schema.fromJsonString(RelayAgentActivityAggregateStateSchema), |
| 92 | ); | 116 | ); |
| ... | @@ -302,6 +326,42 @@ function deliveryAttemptOutcome(result: Apns.ApnsDeliveryResult) { | ... | @@ -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 | interface LiveActivityDeliveryTarget { | 365 | interface LiveActivityDeliveryTarget { |
| 306 | readonly user_id: string; | 366 | readonly user_id: string; |
| 307 | readonly device_id: string; | 367 | readonly device_id: string; |
| ... | @@ -440,6 +500,15 @@ export const make = Effect.gen(function* () { | ... | @@ -440,6 +500,15 @@ export const make = Effect.gen(function* () { |
| 440 | { ...input, aggregate } as SendLiveActivityDeliveryInput, | 500 | { ...input, aggregate } as SendLiveActivityDeliveryInput, |
| 441 | now, | 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 | if (input.sourceJobId) { | 512 | if (input.sourceJobId) { |
| 444 | const claim = yield* attempts.claimSourceJob({ | 513 | const claim = yield* attempts.claimSourceJob({ |
| 445 | userId: input.target.user_id, | 514 | userId: input.target.user_id, |
| ... | @@ -476,14 +545,11 @@ export const make = Effect.gen(function* () { | ... | @@ -476,14 +545,11 @@ export const make = Effect.gen(function* () { |
| 476 | issuedAtUnixSeconds: epochSeconds, | 545 | issuedAtUnixSeconds: epochSeconds, |
| 477 | }) | 546 | }) |
| 478 | .pipe( | 547 | .pipe( |
| 479 | Effect.catch((error) => | 548 | Effect.catchTags({ |
| 480 | Effect.succeed({ | 549 | ApnsJwtEncodingError: recoverTransportError, |
| 481 | ok: false, | 550 | ApnsJwtSigningError: recoverTransportError, |
| 482 | status: 0, | 551 | ApnsHttpRequestError: recoverTransportError, |
| 483 | reason: error.message, | 552 | }), |
| 484 | apnsId: null, | | |
| 485 | }), | | |
| 486 | ), | | |
| 487 | ); | 553 | ); |
| 488 | if (result.ok) { | 554 | if (result.ok) { |
| 489 | yield* liveActivities.markDelivery({ | 555 | yield* liveActivities.markDelivery({ |
| ... | @@ -551,6 +617,15 @@ export const make = Effect.gen(function* () { | ... | @@ -551,6 +617,15 @@ export const make = Effect.gen(function* () { |
| 551 | token: input.token, | 617 | token: input.token, |
| 552 | notification, | 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 | if (input.sourceJobId) { | 629 | if (input.sourceJobId) { |
| 555 | const claim = yield* attempts.claimSourceJob({ | 630 | const claim = yield* attempts.claimSourceJob({ |
| 556 | userId: input.target.user_id, | 631 | userId: input.target.user_id, |
| ... | @@ -593,14 +668,11 @@ export const make = Effect.gen(function* () { | ... | @@ -593,14 +668,11 @@ export const make = Effect.gen(function* () { |
| 593 | issuedAtUnixSeconds: epochSeconds, | 668 | issuedAtUnixSeconds: epochSeconds, |
| 594 | }) | 669 | }) |
| 595 | .pipe( | 670 | .pipe( |
| 596 | Effect.catch((error) => | 671 | Effect.catchTags({ |
| 597 | Effect.succeed({ | 672 | ApnsJwtEncodingError: recoverTransportError, |
| 598 | ok: false, | 673 | ApnsJwtSigningError: recoverTransportError, |
| 599 | status: 0, | 674 | ApnsHttpRequestError: recoverTransportError, |
| 600 | reason: error.message, | 675 | }), |
| 601 | apnsId: null, | | |
| 602 | }), | | |
| 603 | ), | | |
| 604 | ); | 676 | ); |
| 605 | if (isPermanentApnsTokenFailure(result)) { | 677 | if (isPermanentApnsTokenFailure(result)) { |
| 606 | yield* liveActivities.invalidateDeliveryToken({ | 678 | yield* liveActivities.invalidateDeliveryToken({ |