File size: 2,266 Bytes
f76c374
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
import { refusalPolicy } from './billing-policy.js'
import type { BillingChargeStatusResponse } from './billing-types.js'

export interface SettlementDeps {
  fetchStatus(): Promise<BillingChargeStatusResponse>
  sleep(ms: number): Promise<void>
  isCancelled(): boolean
  now(): number
}

export type SettlementOutcome =
  | { kind: 'settled'; status: BillingChargeStatusResponse }
  | { kind: 'failed'; status: BillingChargeStatusResponse }
  | { kind: 'refused'; error: string; status: BillingChargeStatusResponse }
  | {
      kind: 'ambiguous'
      error: string
      status?: BillingChargeStatusResponse
      cause?: unknown
    }
  | { kind: 'timed_out' }
  | { kind: 'cancelled' }

export const SETTLEMENT_POLL_INTERVAL_MS = 2000
export const SETTLEMENT_POLL_CAP_MS = 5 * 60 * 1000
export const SETTLEMENT_MAX_RETRY_AFTER_MS = 30000

export async function driveChargeSettlement(deps: SettlementDeps): Promise<SettlementOutcome> {
  const start = deps.now()
  const timedOut = (): boolean => deps.now() - start >= SETTLEMENT_POLL_CAP_MS

  while (true) {
    if (deps.isCancelled()) {
      return { kind: 'cancelled' }
    }

    let status: BillingChargeStatusResponse

    try {
      status = await deps.fetchStatus()
    } catch (cause) {
      return { kind: 'ambiguous', error: 'transport', cause }
    }

    if (!status.ok) {
      const error = status.error ?? ''
      const policy = refusalPolicy(error)

      if (policy.ambiguousMidPoll) {
        return { kind: 'ambiguous', error: error || 'unknown', status }
      }

      if (policy.recovery === 'retry') {
        if (timedOut()) {
          return { kind: 'timed_out' }
        }

        const wait = Math.min(
          (status.retry_after ?? 5) * 1000,
          SETTLEMENT_MAX_RETRY_AFTER_MS
        )

        await deps.sleep(wait)

        continue
      }

      return {
        kind: 'refused',
        error: status.error ?? status.message ?? 'error',
        status
      }
    }

    if (status.status === 'settled') {
      return { kind: 'settled', status }
    }

    if (status.status === 'failed') {
      return { kind: 'failed', status }
    }

    if (timedOut()) {
      return { kind: 'timed_out' }
    }

    await deps.sleep(SETTLEMENT_POLL_INTERVAL_MS)
  }
}