| @@ -7,12 +7,14 @@ import type { · 6 unmodified lines |
| 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 · 68 unmodified lines |
| 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) { · 209 unmodified lines |
| 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* () { · 132 unmodified lines |
| 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* () { · 30 unmodified lines |
| 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* () { · 61 unmodified lines |
| 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* () { · 36 unmodified lines |
| 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({ |