d3ro-voice/server/supabase/functions/send-push/dispatch-response.ts

74 lines
2.2 KiB
TypeScript

// server/supabase/functions/send-push/dispatch-response.ts
// Pure presenter: turns a processed push dispatch into the HTTP body/status.
//
// Provider transport codes (webpush_send_timeout, fcm_send_failed, …) and
// their distinct HTTP statuses describe what happened on the function's own
// network. Echoing them to an end-user caller turns the dispatch endpoint into
// a reachability oracle, so user callers only ever see one generic failure.
// Trusted system callers (service role, drain worker) keep the detailed code
// for operations.
export type DispatchAudience = 'user' | 'system'
export interface DispatchOutcome {
status: 'pending' | 'processing' | 'succeeded' | 'partial' | 'failed'
complete: boolean
attempted: number
delivered: number
stale: number
retryableFailed: number
permanentFailed: number
nextRetryAt: string | null
transientError: string | null
transientStatus: number | null
}
export interface DispatchResponse {
body: Record<string, unknown>
status: number
}
export const DELIVERY_FAILURE_CODE = 'push_delivery_failed'
const DELIVERY_FAILURE_STATUS = 502
export function presentDispatchOutcome(
audience: DispatchAudience,
eventType: string,
resourceId: string,
outcome: DispatchOutcome,
): DispatchResponse {
const body: Record<string, unknown> = {
event_type: eventType,
resource_id: resourceId,
duplicate: false,
status: outcome.status,
complete: outcome.complete,
attempted: outcome.attempted,
sent: outcome.delivered,
stale_removed: outcome.stale,
retryable_failed: outcome.retryableFailed,
permanent_failed: outcome.permanentFailed,
next_retry_at: outcome.nextRetryAt,
}
const failed = outcome.status === 'partial' || outcome.status === 'failed'
if (outcome.transientError) {
if (audience === 'user') {
return {
body: { error: DELIVERY_FAILURE_CODE, ...body },
status: DELIVERY_FAILURE_STATUS,
}
}
return {
body: { error: outcome.transientError, ...body },
status: outcome.transientStatus ?? DELIVERY_FAILURE_STATUS,
}
}
if (failed) {
return {
body: { error: DELIVERY_FAILURE_CODE, ...body },
status: DELIVERY_FAILURE_STATUS,
}
}
return { body, status: outcome.complete ? 200 : 202 }
}