@chipmobilesdk/rn-ads 0.4.0 → 0.5.1

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/gates.ts CHANGED
@@ -1,91 +1,94 @@
1
- /**
2
- * The one place that decides why a request was refused (FR-140, FR-141).
3
- *
4
- * After 0.2.0 there are three independent ways to be blocked — platform
5
- * availability, the app's three-state enable flag, and consent — and they can be
6
- * closed in any combination. A precedence spread across four call sites is not a
7
- * precedence; it is four coincidences that currently agree. So every refusal
8
- * path in the package routes through `evaluateGates` and nothing else compares
9
- * gate state.
10
- *
11
- * **The order is availability, then enablement, then consent — most immovable
12
- * first.** Two reasons, and they agree:
13
- *
14
- * 1. It is what 0.1.0 already did. `initMachine`'s BLOCK transition is refused
15
- * outright when the status is `disabled` or `unavailable`, so consent was
16
- * already the weakest of the three. Any other order would change observable
17
- * behavior for a consumer who configured nothing new.
18
- * 2. It is the most useful order for whoever is debugging. An unsupported
19
- * platform makes the other two unfixable, so reporting "consent required" on
20
- * a platform that can never serve an ad sends a developer off to build a flow
21
- * that will never help. Reporting `unavailable` to an app that has also
22
- * disabled ads costs nothing — that app was not going to show an ad either
23
- * way.
24
- *
25
- * The accepted cost is that an app which *deliberately* disabled ads on an
26
- * unsupported platform is told about the platform rather than about its own
27
- * switch. That is fine: neither answer changes what the app does next.
28
- *
29
- * Pure. No provider, no React, no I/O — which is what lets the full combination
30
- * table be a unit test rather than an integration one.
31
- */
32
-
33
- import type { Enablement, PrivacyBlockReason, UnavailableReason } from '../config/types';
34
-
35
- /** What the consent flow contributes, or `null` when the app never opted in. */
36
- export type ConsentGateInput =
37
- | { readonly permitted: true }
38
- | { readonly permitted: false; readonly reason: 'consent_flow_required' | 'consent_flow_error' }
39
- | null;
40
-
41
- export interface GateInput {
42
- readonly available: { readonly ok: true } | { readonly ok: false; readonly reason: UnavailableReason };
43
- readonly enablement: Enablement;
44
- /** The app's own gate, from `PrivacyOptions.canRequestAds`. */
45
- readonly appGate: { readonly permitted: true } | { readonly permitted: false; readonly reason: PrivacyBlockReason };
46
- /** The package's consent flow. `null` when unused — then the app's gate alone decides. */
47
- readonly consentGate: ConsentGateInput;
48
- }
49
-
50
- export type GateDecision =
51
- | { readonly allowed: true }
52
- | { readonly allowed: false; readonly kind: 'unavailable'; readonly reason: UnavailableReason }
53
- | { readonly allowed: false; readonly kind: 'disabled' }
54
- | { readonly allowed: false; readonly kind: 'undecided' }
55
- | { readonly allowed: false; readonly kind: 'blocked'; readonly reason: PrivacyBlockReason };
56
-
57
- export function evaluateGates(input: GateInput): GateDecision {
58
- // 1. Availability. Nothing downstream can be fixed while this is closed.
59
- if (!input.available.ok) {
60
- return { allowed: false, kind: 'unavailable', reason: input.available.reason };
61
- }
62
-
63
- // 2. Enablement. `undecided` is reported separately from `disabled` because
64
- // that distinction is the entire reason the third state exists (FR-116).
65
- if (input.enablement === 'disabled') {
66
- return { allowed: false, kind: 'disabled' };
67
- }
68
- if (input.enablement === 'undecided') {
69
- return { allowed: false, kind: 'undecided' };
70
- }
71
-
72
- // 3. Consent, whose two inputs combine as most-restrictive-wins (FR-105a).
73
- //
74
- // Requesting is permitted only when both permit it, and neither can widen
75
- // what the other allows. The case this protects is an app that obtained
76
- // consent successfully but independently blocked ads for a reason of its own
77
- // — a reason FR-120 forbids the package from knowing, and therefore one it
78
- // must never overrule.
79
- //
80
- // When both are closed the app's own gate is reported: it is the input the
81
- // app controls directly, and the flow's state is separately readable through
82
- // `consent.getState()` at any time.
83
- if (!input.appGate.permitted) {
84
- return { allowed: false, kind: 'blocked', reason: input.appGate.reason };
85
- }
86
- if (input.consentGate && !input.consentGate.permitted) {
87
- return { allowed: false, kind: 'blocked', reason: input.consentGate.reason };
88
- }
89
-
90
- return { allowed: true };
91
- }
1
+ /**
2
+ * The one place that decides why a request was refused (FR-140, FR-141).
3
+ *
4
+ * After 0.2.0 there are three independent ways to be blocked — platform
5
+ * availability, the app's three-state enable flag, and consent — and they can be
6
+ * closed in any combination. A precedence spread across four call sites is not a
7
+ * precedence; it is four coincidences that currently agree. So every refusal
8
+ * path in the package routes through `evaluateGates` and nothing else compares
9
+ * gate state.
10
+ *
11
+ * **The order is availability, then enablement, then consent — most immovable
12
+ * first.** Two reasons, and they agree:
13
+ *
14
+ * 1. It is what 0.1.0 already did. `initMachine`'s BLOCK transition is refused
15
+ * outright when the status is `disabled` or `unavailable`, so consent was
16
+ * already the weakest of the three. Any other order would change observable
17
+ * behavior for a consumer who configured nothing new.
18
+ * 2. It is the most useful order for whoever is debugging. An unsupported
19
+ * platform makes the other two unfixable, so reporting "consent required" on
20
+ * a platform that can never serve an ad sends a developer off to build a flow
21
+ * that will never help. Reporting `unavailable` to an app that has also
22
+ * disabled ads costs nothing — that app was not going to show an ad either
23
+ * way.
24
+ *
25
+ * The accepted cost is that an app which *deliberately* disabled ads on an
26
+ * unsupported platform is told about the platform rather than about its own
27
+ * switch. That is fine: neither answer changes what the app does next.
28
+ *
29
+ * Pure. No provider, no React, no I/O — which is what lets the full combination
30
+ * table be a unit test rather than an integration one.
31
+ */
32
+
33
+ import type { Enablement, PrivacyBlockReason, UnavailableReason } from '../config/types';
34
+
35
+ /** What the consent flow contributes, or `null` when the app never opted in. */
36
+ export type ConsentGateInput =
37
+ | { readonly permitted: true }
38
+ | { readonly permitted: false; readonly reason: 'consent_flow_required' | 'consent_flow_error' }
39
+ | null;
40
+
41
+ export interface GateInput {
42
+ readonly available:
43
+ { readonly ok: true } | { readonly ok: false; readonly reason: UnavailableReason };
44
+ readonly enablement: Enablement;
45
+ /** The app's own gate, from `PrivacyOptions.canRequestAds`. */
46
+ readonly appGate:
47
+ | { readonly permitted: true }
48
+ | { readonly permitted: false; readonly reason: PrivacyBlockReason };
49
+ /** The package's consent flow. `null` when unused — then the app's gate alone decides. */
50
+ readonly consentGate: ConsentGateInput;
51
+ }
52
+
53
+ export type GateDecision =
54
+ | { readonly allowed: true }
55
+ | { readonly allowed: false; readonly kind: 'unavailable'; readonly reason: UnavailableReason }
56
+ | { readonly allowed: false; readonly kind: 'disabled' }
57
+ | { readonly allowed: false; readonly kind: 'undecided' }
58
+ | { readonly allowed: false; readonly kind: 'blocked'; readonly reason: PrivacyBlockReason };
59
+
60
+ export function evaluateGates(input: GateInput): GateDecision {
61
+ // 1. Availability. Nothing downstream can be fixed while this is closed.
62
+ if (!input.available.ok) {
63
+ return { allowed: false, kind: 'unavailable', reason: input.available.reason };
64
+ }
65
+
66
+ // 2. Enablement. `undecided` is reported separately from `disabled` because
67
+ // that distinction is the entire reason the third state exists (FR-116).
68
+ if (input.enablement === 'disabled') {
69
+ return { allowed: false, kind: 'disabled' };
70
+ }
71
+ if (input.enablement === 'undecided') {
72
+ return { allowed: false, kind: 'undecided' };
73
+ }
74
+
75
+ // 3. Consent, whose two inputs combine as most-restrictive-wins (FR-105a).
76
+ //
77
+ // Requesting is permitted only when both permit it, and neither can widen
78
+ // what the other allows. The case this protects is an app that obtained
79
+ // consent successfully but independently blocked ads for a reason of its own
80
+ // — a reason FR-120 forbids the package from knowing, and therefore one it
81
+ // must never overrule.
82
+ //
83
+ // When both are closed the app's own gate is reported: it is the input the
84
+ // app controls directly, and the flow's state is separately readable through
85
+ // `consent.getState()` at any time.
86
+ if (!input.appGate.permitted) {
87
+ return { allowed: false, kind: 'blocked', reason: input.appGate.reason };
88
+ }
89
+ if (input.consentGate && !input.consentGate.permitted) {
90
+ return { allowed: false, kind: 'blocked', reason: input.consentGate.reason };
91
+ }
92
+
93
+ return { allowed: true };
94
+ }
@@ -1,187 +1,187 @@
1
- /**
2
- * Initialization state machine (FR-022 – FR-025).
3
- *
4
- * Pure and synchronous: it decides what a transition means; the instance
5
- * performs the I/O. That split is what makes every rule in data-model.md §3.1
6
- * testable without a provider.
7
- *
8
- * The load-bearing invariant is idempotence. Three consumers calling
9
- * `initialize()` at once must produce exactly one native initialization and
10
- * three identical results (SC-005) — the instance holds one in-flight promise,
11
- * and this machine refuses to leave `initializing` for anything but a terminal
12
- * outcome.
13
- */
14
-
15
- import type { InitStatus, PrivacyBlockReason, UnavailableReason } from '../config/types';
16
- import type { AdsError } from '../diagnostics/errors';
17
-
18
- export type InitEvent =
19
- | { readonly type: 'START' }
20
- | { readonly type: 'BLOCK'; readonly reason: PrivacyBlockReason }
21
- | { readonly type: 'UNAVAILABLE'; readonly reason: UnavailableReason }
22
- | { readonly type: 'DISABLE' }
23
- | { readonly type: 'UNDECIDE' }
24
- | { readonly type: 'ENABLE' }
25
- | { readonly type: 'SUCCEED' }
26
- /**
27
- * Back to `ready` with no native call, because the provider was initialized
28
- * earlier and nothing has torn it down since (FR-154). The instance decides
29
- * whether that holds; this machine only refuses to resume from a state a
30
- * fresh start could not leave either.
31
- */
32
- | { readonly type: 'RESUME' }
33
- | { readonly type: 'FAIL'; readonly error: AdsError };
34
-
35
- export interface InitSnapshot {
36
- readonly status: InitStatus;
37
- readonly blockReason: PrivacyBlockReason | null;
38
- readonly unavailableReason: UnavailableReason | null;
39
- readonly error: AdsError | null;
40
- /**
41
- * The status held when ads were switched off, so re-enabling can return to
42
- * it instead of silently dropping to `not_started`.
43
- *
44
- * 0.1.0's `ENABLE` sent a disabled instance to `not_started`, which meant the
45
- * app had to call `initialize()` again or every later load and show was
46
- * refused — and nothing said so. Remembering one value is the whole fix
47
- * (FR-121).
48
- */
49
- readonly resumeTo: InitStatus | null;
50
- }
51
-
52
- export const initialInit: InitSnapshot = {
53
- status: 'not_started',
54
- blockReason: null,
55
- unavailableReason: null,
56
- error: null,
57
- resumeTo: null,
58
- };
59
-
60
- function at(status: InitStatus, patch: Partial<InitSnapshot> = {}): InitSnapshot {
61
- return { ...initialInit, status, ...patch };
62
- }
63
-
64
- /**
65
- * States that behave as "switched off" for everything downstream.
66
- *
67
- * `undecided` is grouped with `disabled` for request handling — neither may
68
- * reach the provider — while staying a distinct reported status, which is the
69
- * only reason the third state is worth having (FR-115, FR-116).
70
- */
71
- function isSwitchedOff(status: InitStatus): boolean {
72
- return status === 'disabled' || status === 'undecided';
73
- }
74
-
75
- /**
76
- * States a fresh start may leave from.
77
- *
78
- * `blocked` is included because resolving consent and trying again is the
79
- * documented, expected flow (FR-019). `initializing` is excluded because that is
80
- * exactly the race idempotence exists to prevent. `ready` is excluded because
81
- * re-initializing an initialized provider is what FR-022 forbids.
82
- */
83
- const STARTABLE: ReadonlySet<InitStatus> = new Set<InitStatus>([
84
- 'not_started',
85
- 'blocked',
86
- 'failed',
87
- ]);
88
-
89
- export function canStart(status: InitStatus): boolean {
90
- return STARTABLE.has(status);
91
- }
92
-
93
- /** Only `ready` may issue requests. Everything else is a refusal with a reason. */
94
- export function canRequest(status: InitStatus): boolean {
95
- return status === 'ready';
96
- }
97
-
98
- /**
99
- * What to remember when switching off.
100
- *
101
- * Only a completed initialization is worth keeping. A `blocked` or `failed`
102
- * instance has nothing to resume to, and remembering `initializing` would mean
103
- * resuming into a state whose in-flight operation was already abandoned.
104
- */
105
- function rememberFor(state: InitSnapshot): InitStatus | null {
106
- if (state.status === 'ready') {
107
- return 'ready';
108
- }
109
- // Preserve an earlier memory across an off -> off transition, so
110
- // disable -> undecided -> enable still resumes.
111
- return isSwitchedOff(state.status) ? state.resumeTo : null;
112
- }
113
-
114
- export function initReducer(state: InitSnapshot, event: InitEvent): InitSnapshot {
115
- switch (event.type) {
116
- case 'START':
117
- // `disabled`, `undecided`, and `unavailable` all refuse a start:
118
- // nothing may reach the provider while any of them holds.
119
- if (isSwitchedOff(state.status) || state.status === 'unavailable') {
120
- return state;
121
- }
122
- return canStart(state.status) ? at('initializing') : state;
123
-
124
- case 'BLOCK':
125
- // Consent is the weakest of the three gates, so it never overwrites a
126
- // stronger one. This guard is the 0.1.0 behavior that settled the
127
- // precedence question: availability and enablement already outranked
128
- // consent, and FR-140 makes that explicit rather than inventing it.
129
- if (isSwitchedOff(state.status) || state.status === 'unavailable') {
130
- return state;
131
- }
132
- // A gate withdrawn after initialization does not un-initialize the
133
- // provider — it stops requests. Reporting `blocked` is what the app
134
- // acts on; the native SDK stays as it is.
135
- return at('blocked', { blockReason: event.reason });
136
-
137
- case 'UNAVAILABLE':
138
- return at('unavailable', { unavailableReason: event.reason });
139
-
140
- case 'DISABLE':
141
- // Availability outranks enablement (FR-140a). 0.1.0 left these two
142
- // unguarded against each other, so whichever arrived last won and the
143
- // reported reason depended on timing — exactly the incidental ordering
144
- // FR-140 forbids. Availability winning is deterministic and is the
145
- // more useful answer: an unsupported platform cannot be fixed by
146
- // turning ads back on.
147
- if (state.status === 'unavailable') {
148
- return state;
149
- }
150
- return at('disabled', { resumeTo: rememberFor(state) });
151
-
152
- case 'UNDECIDE':
153
- if (state.status === 'unavailable') {
154
- return state;
155
- }
156
- return at('undecided', { resumeTo: rememberFor(state) });
157
-
158
- case 'ENABLE': {
159
- if (!isSwitchedOff(state.status)) {
160
- return state;
161
- }
162
- // Resume only from a completed initialization. Anything else has no
163
- // ready state to return to, and claiming one would put the instance in
164
- // a state it never actually reached — so the caller is told to
165
- // initialize instead, explicitly (FR-121).
166
- return state.resumeTo === 'ready' ? at('ready') : at('not_started');
167
- }
168
-
169
- case 'SUCCEED':
170
- // Only a transition out of `initializing` counts. A `SUCCEED` arriving
171
- // after a disable or a block is a late callback, and honoring it would
172
- // resurrect a state the app deliberately left.
173
- return state.status === 'initializing' ? at('ready') : state;
174
-
175
- case 'RESUME':
176
- // Same guard as START, for the same reason: `disabled`, `undecided` and
177
- // `unavailable` are not left by a gate reopening. `canStart` excludes
178
- // them, and `initializing`, whose own outcome is still on its way.
179
- return canStart(state.status) ? at('ready') : state;
180
-
181
- case 'FAIL':
182
- return state.status === 'initializing' ? at('failed', { error: event.error }) : state;
183
-
184
- default:
185
- return state;
186
- }
187
- }
1
+ /**
2
+ * Initialization state machine (FR-022 – FR-025).
3
+ *
4
+ * Pure and synchronous: it decides what a transition means; the instance
5
+ * performs the I/O. That split is what makes every rule in data-model.md §3.1
6
+ * testable without a provider.
7
+ *
8
+ * The load-bearing invariant is idempotence. Three consumers calling
9
+ * `initialize()` at once must produce exactly one native initialization and
10
+ * three identical results (SC-005) — the instance holds one in-flight promise,
11
+ * and this machine refuses to leave `initializing` for anything but a terminal
12
+ * outcome.
13
+ */
14
+
15
+ import type { InitStatus, PrivacyBlockReason, UnavailableReason } from '../config/types';
16
+ import type { AdsError } from '../diagnostics/errors';
17
+
18
+ export type InitEvent =
19
+ | { readonly type: 'START' }
20
+ | { readonly type: 'BLOCK'; readonly reason: PrivacyBlockReason }
21
+ | { readonly type: 'UNAVAILABLE'; readonly reason: UnavailableReason }
22
+ | { readonly type: 'DISABLE' }
23
+ | { readonly type: 'UNDECIDE' }
24
+ | { readonly type: 'ENABLE' }
25
+ | { readonly type: 'SUCCEED' }
26
+ /**
27
+ * Back to `ready` with no native call, because the provider was initialized
28
+ * earlier and nothing has torn it down since (FR-154). The instance decides
29
+ * whether that holds; this machine only refuses to resume from a state a
30
+ * fresh start could not leave either.
31
+ */
32
+ | { readonly type: 'RESUME' }
33
+ | { readonly type: 'FAIL'; readonly error: AdsError };
34
+
35
+ export interface InitSnapshot {
36
+ readonly status: InitStatus;
37
+ readonly blockReason: PrivacyBlockReason | null;
38
+ readonly unavailableReason: UnavailableReason | null;
39
+ readonly error: AdsError | null;
40
+ /**
41
+ * The status held when ads were switched off, so re-enabling can return to
42
+ * it instead of silently dropping to `not_started`.
43
+ *
44
+ * 0.1.0's `ENABLE` sent a disabled instance to `not_started`, which meant the
45
+ * app had to call `initialize()` again or every later load and show was
46
+ * refused — and nothing said so. Remembering one value is the whole fix
47
+ * (FR-121).
48
+ */
49
+ readonly resumeTo: InitStatus | null;
50
+ }
51
+
52
+ export const initialInit: InitSnapshot = {
53
+ status: 'not_started',
54
+ blockReason: null,
55
+ unavailableReason: null,
56
+ error: null,
57
+ resumeTo: null,
58
+ };
59
+
60
+ function at(status: InitStatus, patch: Partial<InitSnapshot> = {}): InitSnapshot {
61
+ return { ...initialInit, status, ...patch };
62
+ }
63
+
64
+ /**
65
+ * States that behave as "switched off" for everything downstream.
66
+ *
67
+ * `undecided` is grouped with `disabled` for request handling — neither may
68
+ * reach the provider — while staying a distinct reported status, which is the
69
+ * only reason the third state is worth having (FR-115, FR-116).
70
+ */
71
+ function isSwitchedOff(status: InitStatus): boolean {
72
+ return status === 'disabled' || status === 'undecided';
73
+ }
74
+
75
+ /**
76
+ * States a fresh start may leave from.
77
+ *
78
+ * `blocked` is included because resolving consent and trying again is the
79
+ * documented, expected flow (FR-019). `initializing` is excluded because that is
80
+ * exactly the race idempotence exists to prevent. `ready` is excluded because
81
+ * re-initializing an initialized provider is what FR-022 forbids.
82
+ */
83
+ const STARTABLE: ReadonlySet<InitStatus> = new Set<InitStatus>([
84
+ 'not_started',
85
+ 'blocked',
86
+ 'failed',
87
+ ]);
88
+
89
+ export function canStart(status: InitStatus): boolean {
90
+ return STARTABLE.has(status);
91
+ }
92
+
93
+ /** Only `ready` may issue requests. Everything else is a refusal with a reason. */
94
+ export function canRequest(status: InitStatus): boolean {
95
+ return status === 'ready';
96
+ }
97
+
98
+ /**
99
+ * What to remember when switching off.
100
+ *
101
+ * Only a completed initialization is worth keeping. A `blocked` or `failed`
102
+ * instance has nothing to resume to, and remembering `initializing` would mean
103
+ * resuming into a state whose in-flight operation was already abandoned.
104
+ */
105
+ function rememberFor(state: InitSnapshot): InitStatus | null {
106
+ if (state.status === 'ready') {
107
+ return 'ready';
108
+ }
109
+ // Preserve an earlier memory across an off -> off transition, so
110
+ // disable -> undecided -> enable still resumes.
111
+ return isSwitchedOff(state.status) ? state.resumeTo : null;
112
+ }
113
+
114
+ export function initReducer(state: InitSnapshot, event: InitEvent): InitSnapshot {
115
+ switch (event.type) {
116
+ case 'START':
117
+ // `disabled`, `undecided`, and `unavailable` all refuse a start:
118
+ // nothing may reach the provider while any of them holds.
119
+ if (isSwitchedOff(state.status) || state.status === 'unavailable') {
120
+ return state;
121
+ }
122
+ return canStart(state.status) ? at('initializing') : state;
123
+
124
+ case 'BLOCK':
125
+ // Consent is the weakest of the three gates, so it never overwrites a
126
+ // stronger one. This guard is the 0.1.0 behavior that settled the
127
+ // precedence question: availability and enablement already outranked
128
+ // consent, and FR-140 makes that explicit rather than inventing it.
129
+ if (isSwitchedOff(state.status) || state.status === 'unavailable') {
130
+ return state;
131
+ }
132
+ // A gate withdrawn after initialization does not un-initialize the
133
+ // provider — it stops requests. Reporting `blocked` is what the app
134
+ // acts on; the native SDK stays as it is.
135
+ return at('blocked', { blockReason: event.reason });
136
+
137
+ case 'UNAVAILABLE':
138
+ return at('unavailable', { unavailableReason: event.reason });
139
+
140
+ case 'DISABLE':
141
+ // Availability outranks enablement (FR-140a). 0.1.0 left these two
142
+ // unguarded against each other, so whichever arrived last won and the
143
+ // reported reason depended on timing — exactly the incidental ordering
144
+ // FR-140 forbids. Availability winning is deterministic and is the
145
+ // more useful answer: an unsupported platform cannot be fixed by
146
+ // turning ads back on.
147
+ if (state.status === 'unavailable') {
148
+ return state;
149
+ }
150
+ return at('disabled', { resumeTo: rememberFor(state) });
151
+
152
+ case 'UNDECIDE':
153
+ if (state.status === 'unavailable') {
154
+ return state;
155
+ }
156
+ return at('undecided', { resumeTo: rememberFor(state) });
157
+
158
+ case 'ENABLE': {
159
+ if (!isSwitchedOff(state.status)) {
160
+ return state;
161
+ }
162
+ // Resume only from a completed initialization. Anything else has no
163
+ // ready state to return to, and claiming one would put the instance in
164
+ // a state it never actually reached — so the caller is told to
165
+ // initialize instead, explicitly (FR-121).
166
+ return state.resumeTo === 'ready' ? at('ready') : at('not_started');
167
+ }
168
+
169
+ case 'SUCCEED':
170
+ // Only a transition out of `initializing` counts. A `SUCCEED` arriving
171
+ // after a disable or a block is a late callback, and honoring it would
172
+ // resurrect a state the app deliberately left.
173
+ return state.status === 'initializing' ? at('ready') : state;
174
+
175
+ case 'RESUME':
176
+ // Same guard as START, for the same reason: `disabled`, `undecided` and
177
+ // `unavailable` are not left by a gate reopening. `canStart` excludes
178
+ // them, and `initializing`, whose own outcome is still on its way.
179
+ return canStart(state.status) ? at('ready') : state;
180
+
181
+ case 'FAIL':
182
+ return state.status === 'initializing' ? at('failed', { error: event.error }) : state;
183
+
184
+ default:
185
+ return state;
186
+ }
187
+ }