@chipmobilesdk/rn-payment 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/core/retry.ts CHANGED
@@ -1,133 +1,95 @@
1
- /**
2
- * Bounded backoff.
3
- *
4
- * "Bounded" is the whole point (FR-020, FR-045): an unbounded reconnect loop
5
- * against a billing service that is down drains battery and hammers a system
6
- * service, and an unbounded finalize loop hides a real failure behind noise.
7
- *
8
- * Delay computation is pure and separated from waiting so tests can assert the
9
- * schedule without burning real time.
10
- */
11
-
12
- import type { BackoffConfig } from '../config/types';
13
-
14
- /** Opaque timer handle: a Node Timeout and a browser numeric id are both valid. */
15
- type TimerHandle = ReturnType<typeof setTimeout> | number;
16
-
17
- /**
18
- * Delay before `attempt` (1-based), capped and optionally jittered.
19
- *
20
- * Jitter is full jitter over [0, capped]: several devices that lost the billing
21
- * connection at the same moment should not retry in lockstep.
22
- */
23
- export function backoffDelayMs(
24
- attempt: number,
25
- config: Required<BackoffConfig>,
26
- random: () => number = Math.random,
27
- ): number {
28
- const raw = config.initialDelayMs * Math.pow(config.factor, attempt - 1);
29
- const capped = Math.min(raw, config.maxDelayMs);
30
- return config.jitter ? Math.floor(capped * random()) : capped;
31
- }
32
-
33
- /** The full schedule, for asserting a retry policy without waiting for it. */
34
- export function backoffSchedule(config: Required<BackoffConfig>): number[] {
35
- const delays: number[] = [];
36
- for (let attempt = 1; attempt <= config.maxAttempts; attempt += 1) {
37
- delays.push(backoffDelayMs(attempt, config, () => 1));
38
- }
39
- return delays;
40
- }
41
-
42
- export interface RetryHandle {
43
- /** Cancels any pending attempt. Idempotent, so cleanup can call it freely (FR-082). */
44
- cancel(): void;
45
- /** Attempts consumed so far, so a retry storm is observable rather than inferred. */
46
- attempts(): number;
47
- /** True once the per-session budget is spent. */
48
- exhausted(): boolean;
49
- }
50
-
51
- export interface RetryRunnerOptions {
52
- config: Required<BackoffConfig>;
53
- /** Return true when the attempt succeeded and retrying should stop. */
54
- attempt: (attemptNumber: number) => Promise<boolean>;
55
- /** Called once the per-session budget is spent without success. */
56
- onExhausted?: (attempts: number) => void;
57
- /**
58
- * Injected so a test can run the whole schedule synchronously instead of
59
- * waiting minutes of real backoff. The handle is opaque, because a Node
60
- * `Timeout` and a browser numeric id are both valid here.
61
- */
62
- setTimeoutFn?: (fn: () => void, ms: number) => TimerHandle;
63
- clearTimeoutFn?: (handle: TimerHandle) => void;
64
- random?: () => number;
65
- }
66
-
67
- /**
68
- * Runs `attempt` under the bounded schedule.
69
- *
70
- * Exhaustion is reported rather than thrown: for finalization, a spent session
71
- * budget is not a failure but a handoff to the next launch, and the caller is
72
- * the only one that knows which of the two it is.
73
- */
74
- export function runWithRetry(options: RetryRunnerOptions): RetryHandle {
75
- const {
76
- config,
77
- attempt,
78
- onExhausted,
79
- setTimeoutFn = setTimeout,
80
- clearTimeoutFn = clearTimeout,
81
- random = Math.random,
82
- } = options;
83
-
84
- let attemptCount = 0;
85
- let cancelled = false;
86
- let pending: TimerHandle | undefined;
87
-
88
- const schedule = (delayMs: number): void => {
89
- pending = setTimeoutFn(() => {
90
- pending = undefined;
91
- run().catch(() => undefined);
92
- }, delayMs);
93
- };
94
-
95
- const run = async (): Promise<void> => {
96
- if (cancelled) {
97
- return;
98
- }
99
- attemptCount += 1;
100
- let succeeded = false;
101
- try {
102
- succeeded = await attempt(attemptCount);
103
- } catch {
104
- // A throwing attempt is a failed attempt, nothing more. Letting it escape
105
- // would abandon the schedule and, for finalization, silently drop a paid
106
- // transaction.
107
- succeeded = false;
108
- }
109
- if (cancelled || succeeded) {
110
- return;
111
- }
112
- if (attemptCount >= config.maxAttempts) {
113
- onExhausted?.(attemptCount);
114
- return;
115
- }
116
- schedule(backoffDelayMs(attemptCount + 1, config, random));
117
- };
118
-
119
- // Fire the first attempt immediately; the schedule owns everything after it.
120
- run().catch(() => undefined);
121
-
122
- return {
123
- cancel(): void {
124
- cancelled = true;
125
- if (pending !== undefined) {
126
- clearTimeoutFn(pending);
127
- pending = undefined;
128
- }
129
- },
130
- attempts: () => attemptCount,
131
- exhausted: () => attemptCount >= config.maxAttempts,
132
- };
133
- }
1
+ /**
2
+ * Bounded backoff.
3
+ *
4
+ * "Bounded" is the whole point (FR-020, FR-045): an unbounded reconnect loop
5
+ * against a billing service that is down drains battery and hammers a system
6
+ * service, and an unbounded finalize loop hides a real failure behind noise.
7
+ *
8
+ * Delay computation is pure and separated from waiting so tests can assert the
9
+ * schedule without burning real time.
10
+ */
11
+
12
+ import {
13
+ computeBackoff,
14
+ runWithRetry as runCoreRetry,
15
+ type RetryHandle,
16
+ } from '@chipmobilesdk/rn-core';
17
+
18
+ import type { BackoffConfig } from '../config/types';
19
+
20
+ /** Opaque timer handle: a Node Timeout and a browser numeric id are both valid. */
21
+ type TimerHandle = ReturnType<typeof setTimeout> | number;
22
+
23
+ /**
24
+ * Delay before `attempt` (1-based), capped and optionally jittered.
25
+ *
26
+ * Jitter is full jitter over [0, capped]: several devices that lost the billing
27
+ * connection at the same moment should not retry in lockstep.
28
+ */
29
+ export function backoffDelayMs(
30
+ attempt: number,
31
+ config: Required<BackoffConfig>,
32
+ random: () => number = Math.random,
33
+ ): number {
34
+ return computeBackoff(attempt, toSpec(config), random);
35
+ }
36
+
37
+ function toSpec(config: Required<BackoffConfig>) {
38
+ return {
39
+ initialDelayMs: config.initialDelayMs,
40
+ maxDelayMs: config.maxDelayMs,
41
+ factor: config.factor,
42
+ jitter: config.jitter ? ('full' as const) : ('none' as const),
43
+ maxAttempts: config.maxAttempts,
44
+ };
45
+ }
46
+
47
+ /** The full schedule, for asserting a retry policy without waiting for it. */
48
+ export function backoffSchedule(config: Required<BackoffConfig>): number[] {
49
+ const delays: number[] = [];
50
+ for (let attempt = 1; attempt <= config.maxAttempts; attempt += 1) {
51
+ delays.push(backoffDelayMs(attempt, config, () => 1));
52
+ }
53
+ return delays;
54
+ }
55
+
56
+ export type { RetryHandle } from '@chipmobilesdk/rn-core';
57
+
58
+ export interface RetryRunnerOptions {
59
+ config: Required<BackoffConfig>;
60
+ /** Return true when the attempt succeeded and retrying should stop. */
61
+ attempt: (attemptNumber: number) => Promise<boolean>;
62
+ /** Called once the per-session budget is spent without success. */
63
+ onExhausted?: (attempts: number) => void;
64
+ /**
65
+ * Injected so a test can run the whole schedule synchronously instead of
66
+ * waiting minutes of real backoff. The handle is opaque, because a Node
67
+ * `Timeout` and a browser numeric id are both valid here.
68
+ */
69
+ setTimeoutFn?: (fn: () => void, ms: number) => TimerHandle;
70
+ clearTimeoutFn?: (handle: TimerHandle) => void;
71
+ random?: () => number;
72
+ }
73
+
74
+ /**
75
+ * Runs `attempt` under the bounded schedule (rn-core's runner).
76
+ *
77
+ * Exhaustion is reported rather than thrown: for finalization, a spent session
78
+ * budget is not a failure but a handoff to the next launch, and the caller is
79
+ * the only one that knows which of the two it is. A throwing attempt is a failed
80
+ * attempt, nothing more: letting it escape would silently drop a paid transaction.
81
+ */
82
+ export function runWithRetry(options: RetryRunnerOptions): RetryHandle {
83
+ const setTimeoutFn = options.setTimeoutFn ?? setTimeout;
84
+ const clearTimeoutFn = options.clearTimeoutFn ?? clearTimeout;
85
+ return runCoreRetry({
86
+ spec: toSpec(options.config),
87
+ attempt: options.attempt,
88
+ onExhausted: options.onExhausted,
89
+ random: options.random,
90
+ scheduler: {
91
+ setTimeout: (callback, ms) => setTimeoutFn(callback, ms),
92
+ clearTimeout: handle => clearTimeoutFn(handle as TimerHandle),
93
+ },
94
+ });
95
+ }