react-native-onyx 3.0.87 → 3.0.89
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/API.md +0 -2
- package/README.md +2 -12
- package/dist/CircuitBreaker/AbstractCircuitBreaker.d.ts +91 -0
- package/dist/CircuitBreaker/AbstractCircuitBreaker.js +166 -0
- package/dist/CircuitBreaker/types.d.ts +38 -0
- package/dist/CircuitBreaker/types.js +24 -0
- package/dist/Onyx.d.ts +0 -2
- package/dist/Onyx.js +0 -2
- package/dist/OnyxConnectionManager.js +18 -15
- package/dist/OnyxUtils.d.ts +11 -5
- package/dist/OnyxUtils.js +92 -82
- package/dist/StateMachine.d.ts +45 -0
- package/dist/StateMachine.js +37 -0
- package/dist/StorageCircuitBreaker.d.ts +66 -0
- package/dist/StorageCircuitBreaker.js +177 -0
- package/dist/storage/__mocks__/index.d.ts +10 -0
- package/dist/storage/__mocks__/index.js +15 -0
- package/dist/storage/errors.d.ts +34 -0
- package/dist/storage/errors.js +41 -0
- package/dist/storage/index.js +5 -0
- package/dist/storage/providers/IDBKeyValProvider/classifyError.d.ts +9 -0
- package/dist/storage/providers/IDBKeyValProvider/classifyError.js +37 -0
- package/dist/storage/providers/IDBKeyValProvider/createStore.js +40 -57
- package/dist/storage/providers/IDBKeyValProvider/index.js +5 -0
- package/dist/storage/providers/MemoryOnlyProvider.js +5 -0
- package/dist/storage/providers/NoopProvider.js +5 -0
- package/dist/storage/providers/SQLiteProvider.js +5 -0
- package/dist/storage/providers/classifySQLiteError.d.ts +13 -0
- package/dist/storage/providers/classifySQLiteError.js +21 -0
- package/dist/storage/providers/types.d.ts +8 -0
- package/dist/types.d.ts +11 -27
- package/dist/useOnyx.js +0 -2
- package/package.json +1 -1
package/API.md
CHANGED
|
@@ -80,7 +80,6 @@ This method will be deprecated soon. Please use `Onyx.connectWithoutView()` inst
|
|
|
80
80
|
| connectOptions | The options object that will define the behavior of the connection. |
|
|
81
81
|
| connectOptions.key | The Onyx key to subscribe to. |
|
|
82
82
|
| connectOptions.callback | A function that will be called when the Onyx data we are subscribed changes. |
|
|
83
|
-
| connectOptions.waitForCollectionCallback | If set to `true`, it will return the entire collection to the callback as a single object. |
|
|
84
83
|
| connectOptions.selector | This will be used to subscribe to a subset of an Onyx key's data. **Only used inside `useOnyx()` hook.** Using this setting on `useOnyx()` can have very positive performance benefits because the component will only re-render when the subset of data changes. Otherwise, any change of data on any property would normally cause the component to re-render (and that can be expensive from a performance standpoint). |
|
|
85
84
|
|
|
86
85
|
**Example**
|
|
@@ -103,7 +102,6 @@ Connects to an Onyx key given the options passed and listens to its changes.
|
|
|
103
102
|
| connectOptions | The options object that will define the behavior of the connection. |
|
|
104
103
|
| connectOptions.key | The Onyx key to subscribe to. |
|
|
105
104
|
| connectOptions.callback | A function that will be called when the Onyx data we are subscribed changes. |
|
|
106
|
-
| connectOptions.waitForCollectionCallback | If set to `true`, it will return the entire collection to the callback as a single object. |
|
|
107
105
|
| connectOptions.selector | This will be used to subscribe to a subset of an Onyx key's data. **Only used inside `useOnyx()` hook.** Using this setting on `useOnyx()` can have very positive performance benefits because the component will only re-render when the subset of data changes. Otherwise, any change of data on any property would normally cause the component to re-render (and that can be expensive from a performance standpoint). |
|
|
108
106
|
|
|
109
107
|
**Example**
|
package/README.md
CHANGED
|
@@ -257,20 +257,10 @@ export default MyComponent;
|
|
|
257
257
|
This will add a prop to the component called `allReports` which is an object of collection member key/values. Changes to the individual member keys will modify the entire object and new props will be passed with each individual key update. The prop doesn't update on the initial rendering of the component until the entire collection has been read out of Onyx.
|
|
258
258
|
|
|
259
259
|
```js
|
|
260
|
-
Onyx.connect({key: ONYXKEYS.COLLECTION.REPORT}, callback: (
|
|
260
|
+
Onyx.connect({key: ONYXKEYS.COLLECTION.REPORT}, callback: (allReports, collectionKey, sourceValue) => {...});
|
|
261
261
|
```
|
|
262
262
|
|
|
263
|
-
This will fire the callback once
|
|
264
|
-
|
|
265
|
-
```js
|
|
266
|
-
Onyx.connect({
|
|
267
|
-
key: ONYXKEYS.COLLECTION.REPORT,
|
|
268
|
-
waitForCollectionCallback: true,
|
|
269
|
-
callback: (allReports, collectionKey, sourceValue) => {...},
|
|
270
|
-
});
|
|
271
|
-
```
|
|
272
|
-
|
|
273
|
-
This final option forces `Onyx.connect()` to behave more like `useOnyx()` and only update the callback once with the entire collection initially and later with an updated version of the collection when individual keys update. The `sourceValue` parameter contains only the specific keys and values that triggered the current update, which can be useful for optimizing your code to only process what changed. This parameter is not available when `waitForCollectionCallback` is false or the key is not a collection key.
|
|
263
|
+
This will fire the callback once with the entire collection initially and later with an updated version of the collection when individual keys update. The `sourceValue` parameter contains only the specific keys and values that triggered the current update, which can be useful for optimizing your code to only process what changed.
|
|
274
264
|
|
|
275
265
|
### Performance Considerations When Using Collections
|
|
276
266
|
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import type { CircuitBreakerOptions, CircuitBreakerState } from './types';
|
|
2
|
+
/**
|
|
3
|
+
* Generic circuit breaker built on {@link StateMachine}.
|
|
4
|
+
*
|
|
5
|
+
* - **closed**: requests are allowed; failures are counted.
|
|
6
|
+
* - **open**: requests are rejected until {@link resetTimeoutMs} elapses.
|
|
7
|
+
* - **half-open**: the recovery-probe state. After the open timeout, the breaker admits exactly ONE
|
|
8
|
+
* probe request: success means the dependency recovered, so the circuit closes. Failure means it's
|
|
9
|
+
* still down, so the circuit reopens. This single-request probe prevents a "thundering herd" where
|
|
10
|
+
* every caller fails loudly when the service hasn't recovered yet.
|
|
11
|
+
*
|
|
12
|
+
* Subclasses implement the failure-counting policy by overriding {@link recordFailureInClosed} (and
|
|
13
|
+
* friends) — e.g. counting consecutive failures, or failures within a rolling time window, or any
|
|
14
|
+
* combination of those.
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* class MyBreaker extends AbstractCircuitBreaker {
|
|
18
|
+
* private failures = 0;
|
|
19
|
+
* protected recordFailureInClosed() {
|
|
20
|
+
* this.failures += 1;
|
|
21
|
+
* return this.failures >= 3 ? `${this.failures} failures` : null;
|
|
22
|
+
* }
|
|
23
|
+
* protected recordSuccessInClosed() { this.failures = 0; }
|
|
24
|
+
* protected resetFailureState() { this.failures = 0; }
|
|
25
|
+
* }
|
|
26
|
+
*
|
|
27
|
+
* const breaker = new MyBreaker({resetTimeoutMs: 30_000});
|
|
28
|
+
* if (breaker.isAllowed()) {
|
|
29
|
+
* try {
|
|
30
|
+
* doWork();
|
|
31
|
+
* breaker.recordSuccess();
|
|
32
|
+
* } catch {
|
|
33
|
+
* breaker.recordFailure();
|
|
34
|
+
* }
|
|
35
|
+
* }
|
|
36
|
+
*/
|
|
37
|
+
declare abstract class AbstractCircuitBreaker {
|
|
38
|
+
private machine;
|
|
39
|
+
private openedAt;
|
|
40
|
+
private isProbeInFlight;
|
|
41
|
+
private readonly resetTimeoutMs;
|
|
42
|
+
private readonly onTrip?;
|
|
43
|
+
private readonly onClose?;
|
|
44
|
+
constructor(options?: CircuitBreakerOptions);
|
|
45
|
+
/** Record a failure while the circuit is closed. Returns a trip reason when the threshold is exceeded. */
|
|
46
|
+
protected abstract recordFailureInClosed(): string | null;
|
|
47
|
+
/** Update failure state after a successful request while the circuit is closed. */
|
|
48
|
+
protected abstract recordSuccessInClosed(): void;
|
|
49
|
+
/** Clear accumulated failure state without changing circuit state. */
|
|
50
|
+
protected abstract resetFailureState(): void;
|
|
51
|
+
/**
|
|
52
|
+
* Whether a request may proceed.
|
|
53
|
+
*
|
|
54
|
+
* Returns `false` while open. In half-open, the FIRST caller is admitted as the recovery probe and
|
|
55
|
+
* `isProbeInFlight` is latched so every subsequent caller is rejected until that probe resolves
|
|
56
|
+
* (via {@link recordSuccess} → close, or {@link recordFailure} → reopen). That single-probe gate is
|
|
57
|
+
* the whole point of half-open: it tests recovery with one request instead of letting a herd of
|
|
58
|
+
* waiting callers stampede a dependency that may still be down.
|
|
59
|
+
*/
|
|
60
|
+
isAllowed(): boolean;
|
|
61
|
+
/**
|
|
62
|
+
* Record a failed request. May open the circuit from closed or half-open.
|
|
63
|
+
* @returns `true` when the circuit is open after recording (the request must not proceed).
|
|
64
|
+
*/
|
|
65
|
+
recordFailure(): boolean;
|
|
66
|
+
/** Record a successful request. Closes the circuit from half-open and clears failure counts. */
|
|
67
|
+
recordSuccess(): void;
|
|
68
|
+
/**
|
|
69
|
+
* The current state WITHOUT advancing recovery — a pure query, safe to call without side effects.
|
|
70
|
+
* The open→half-open transition is applied only at the admission point ({@link isAllowed}); by the
|
|
71
|
+
* time a caller queries state after being admitted, that transition has already happened.
|
|
72
|
+
*/
|
|
73
|
+
peekState(): CircuitBreakerState;
|
|
74
|
+
/**
|
|
75
|
+
* Force the circuit back to closed from ANY state. This is a reset, not a transition, so it
|
|
76
|
+
* deliberately bypasses the transition graph (and does not fire {@link onClose}). Use only to wipe
|
|
77
|
+
* all state — e.g. between tests or sessions.
|
|
78
|
+
*/
|
|
79
|
+
protected hardReset(): void;
|
|
80
|
+
private getCurrentState;
|
|
81
|
+
private trip;
|
|
82
|
+
private close;
|
|
83
|
+
/**
|
|
84
|
+
* Lazily advance open → half-open once the reset timeout has elapsed. This is checked on read
|
|
85
|
+
* (via {@link getCurrentState}) rather than on a timer, so there's nothing to schedule or clean up:
|
|
86
|
+
* the transition simply becomes visible to the next caller after the window. Entering half-open
|
|
87
|
+
* clears `isProbeInFlight` so the next admitted request becomes the recovery probe.
|
|
88
|
+
*/
|
|
89
|
+
private maybeRecover;
|
|
90
|
+
}
|
|
91
|
+
export default AbstractCircuitBreaker;
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const StateMachine_1 = __importDefault(require("../StateMachine"));
|
|
7
|
+
const types_1 = require("./types");
|
|
8
|
+
/**
|
|
9
|
+
* Generic circuit breaker built on {@link StateMachine}.
|
|
10
|
+
*
|
|
11
|
+
* - **closed**: requests are allowed; failures are counted.
|
|
12
|
+
* - **open**: requests are rejected until {@link resetTimeoutMs} elapses.
|
|
13
|
+
* - **half-open**: the recovery-probe state. After the open timeout, the breaker admits exactly ONE
|
|
14
|
+
* probe request: success means the dependency recovered, so the circuit closes. Failure means it's
|
|
15
|
+
* still down, so the circuit reopens. This single-request probe prevents a "thundering herd" where
|
|
16
|
+
* every caller fails loudly when the service hasn't recovered yet.
|
|
17
|
+
*
|
|
18
|
+
* Subclasses implement the failure-counting policy by overriding {@link recordFailureInClosed} (and
|
|
19
|
+
* friends) — e.g. counting consecutive failures, or failures within a rolling time window, or any
|
|
20
|
+
* combination of those.
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* class MyBreaker extends AbstractCircuitBreaker {
|
|
24
|
+
* private failures = 0;
|
|
25
|
+
* protected recordFailureInClosed() {
|
|
26
|
+
* this.failures += 1;
|
|
27
|
+
* return this.failures >= 3 ? `${this.failures} failures` : null;
|
|
28
|
+
* }
|
|
29
|
+
* protected recordSuccessInClosed() { this.failures = 0; }
|
|
30
|
+
* protected resetFailureState() { this.failures = 0; }
|
|
31
|
+
* }
|
|
32
|
+
*
|
|
33
|
+
* const breaker = new MyBreaker({resetTimeoutMs: 30_000});
|
|
34
|
+
* if (breaker.isAllowed()) {
|
|
35
|
+
* try {
|
|
36
|
+
* doWork();
|
|
37
|
+
* breaker.recordSuccess();
|
|
38
|
+
* } catch {
|
|
39
|
+
* breaker.recordFailure();
|
|
40
|
+
* }
|
|
41
|
+
* }
|
|
42
|
+
*/
|
|
43
|
+
class AbstractCircuitBreaker {
|
|
44
|
+
constructor(options = {}) {
|
|
45
|
+
var _a;
|
|
46
|
+
this.openedAt = 0;
|
|
47
|
+
this.isProbeInFlight = false;
|
|
48
|
+
this.resetTimeoutMs = (_a = options.resetTimeoutMs) !== null && _a !== void 0 ? _a : 60000;
|
|
49
|
+
this.onTrip = options.onTrip;
|
|
50
|
+
this.onClose = options.onClose;
|
|
51
|
+
this.machine = new StateMachine_1.default('closed', types_1.CIRCUIT_BREAKER_TRANSITIONS);
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Whether a request may proceed.
|
|
55
|
+
*
|
|
56
|
+
* Returns `false` while open. In half-open, the FIRST caller is admitted as the recovery probe and
|
|
57
|
+
* `isProbeInFlight` is latched so every subsequent caller is rejected until that probe resolves
|
|
58
|
+
* (via {@link recordSuccess} → close, or {@link recordFailure} → reopen). That single-probe gate is
|
|
59
|
+
* the whole point of half-open: it tests recovery with one request instead of letting a herd of
|
|
60
|
+
* waiting callers stampede a dependency that may still be down.
|
|
61
|
+
*/
|
|
62
|
+
isAllowed() {
|
|
63
|
+
const currentState = this.getCurrentState();
|
|
64
|
+
if (currentState === 'open') {
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
if (currentState === 'half-open') {
|
|
68
|
+
if (this.isProbeInFlight) {
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
this.isProbeInFlight = true;
|
|
72
|
+
}
|
|
73
|
+
return true;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Record a failed request. May open the circuit from closed or half-open.
|
|
77
|
+
* @returns `true` when the circuit is open after recording (the request must not proceed).
|
|
78
|
+
*/
|
|
79
|
+
recordFailure() {
|
|
80
|
+
if (this.machine.state === 'open') {
|
|
81
|
+
return true;
|
|
82
|
+
}
|
|
83
|
+
if (this.machine.state === 'half-open') {
|
|
84
|
+
this.trip();
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
87
|
+
const reason = this.recordFailureInClosed();
|
|
88
|
+
if (reason) {
|
|
89
|
+
this.trip(reason);
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
/** Record a successful request. Closes the circuit from half-open and clears failure counts. */
|
|
95
|
+
recordSuccess() {
|
|
96
|
+
if (this.machine.state === 'half-open') {
|
|
97
|
+
this.close();
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
if (this.machine.state === 'closed') {
|
|
101
|
+
this.recordSuccessInClosed();
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* The current state WITHOUT advancing recovery — a pure query, safe to call without side effects.
|
|
106
|
+
* The open→half-open transition is applied only at the admission point ({@link isAllowed}); by the
|
|
107
|
+
* time a caller queries state after being admitted, that transition has already happened.
|
|
108
|
+
*/
|
|
109
|
+
peekState() {
|
|
110
|
+
return this.machine.state;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Force the circuit back to closed from ANY state. This is a reset, not a transition, so it
|
|
114
|
+
* deliberately bypasses the transition graph (and does not fire {@link onClose}). Use only to wipe
|
|
115
|
+
* all state — e.g. between tests or sessions.
|
|
116
|
+
*/
|
|
117
|
+
hardReset() {
|
|
118
|
+
this.machine = new StateMachine_1.default('closed', types_1.CIRCUIT_BREAKER_TRANSITIONS);
|
|
119
|
+
this.openedAt = 0;
|
|
120
|
+
this.isProbeInFlight = false;
|
|
121
|
+
this.resetFailureState();
|
|
122
|
+
}
|
|
123
|
+
getCurrentState() {
|
|
124
|
+
this.maybeRecover();
|
|
125
|
+
return this.machine.state;
|
|
126
|
+
}
|
|
127
|
+
trip(reason = '') {
|
|
128
|
+
var _a;
|
|
129
|
+
if (this.machine.state === 'open') {
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
this.machine = this.machine.transition('open');
|
|
133
|
+
this.openedAt = Date.now();
|
|
134
|
+
this.isProbeInFlight = false;
|
|
135
|
+
this.resetFailureState();
|
|
136
|
+
(_a = this.onTrip) === null || _a === void 0 ? void 0 : _a.call(this, reason);
|
|
137
|
+
}
|
|
138
|
+
close() {
|
|
139
|
+
var _a;
|
|
140
|
+
// close() only ever runs from half-open (see recordSuccess), and half-open → closed is the one
|
|
141
|
+
// legal closing transition — so go through transition() to keep the illegal open → closed jump
|
|
142
|
+
// an error rather than silently constructing a fresh closed machine.
|
|
143
|
+
this.machine = this.machine.transition('closed');
|
|
144
|
+
this.openedAt = 0;
|
|
145
|
+
this.isProbeInFlight = false;
|
|
146
|
+
this.resetFailureState();
|
|
147
|
+
(_a = this.onClose) === null || _a === void 0 ? void 0 : _a.call(this);
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Lazily advance open → half-open once the reset timeout has elapsed. This is checked on read
|
|
151
|
+
* (via {@link getCurrentState}) rather than on a timer, so there's nothing to schedule or clean up:
|
|
152
|
+
* the transition simply becomes visible to the next caller after the window. Entering half-open
|
|
153
|
+
* clears `isProbeInFlight` so the next admitted request becomes the recovery probe.
|
|
154
|
+
*/
|
|
155
|
+
maybeRecover() {
|
|
156
|
+
if (this.machine.state !== 'open') {
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
if (Date.now() - this.openedAt < this.resetTimeoutMs) {
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
this.machine = this.machine.transition('half-open');
|
|
163
|
+
this.isProbeInFlight = false;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
exports.default = AbstractCircuitBreaker;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* States of the circuit breaker.
|
|
3
|
+
*
|
|
4
|
+
* - **closed**: normal operation; requests flow and failures are counted.
|
|
5
|
+
* - **open**: tripped; requests are rejected outright so a known-bad dependency isn't hammered.
|
|
6
|
+
* - **half-open**: a trial state entered after the open timeout — see {@link CIRCUIT_BREAKER_TRANSITIONS}.
|
|
7
|
+
*/
|
|
8
|
+
type CircuitBreakerState = 'closed' | 'open' | 'half-open';
|
|
9
|
+
/**
|
|
10
|
+
* Legal state transitions. The flow is closed → open → half-open → (closed | open).
|
|
11
|
+
*
|
|
12
|
+
* The **half-open** state exists so the breaker can test whether the dependency has recovered WITHOUT
|
|
13
|
+
* flipping straight back to fully closed. Going open → closed blindly would, on a dependency that is
|
|
14
|
+
* still down, immediately re-admit the full load and re-trip — flapping between open and closed every
|
|
15
|
+
* window. Instead, after the open timeout the breaker moves to half-open and admits a single trial
|
|
16
|
+
* ("probe") request:
|
|
17
|
+
* - probe succeeds → the dependency is healthy again → transition to **closed** (resume normal flow).
|
|
18
|
+
* - probe fails → still broken → transition back to **open** for another timeout window.
|
|
19
|
+
*
|
|
20
|
+
* Admitting exactly one probe (rather than reopening the floodgates) is also what prevents the
|
|
21
|
+
* "thundering herd": many callers retrying at once the instant the timeout elapses, re-overwhelming a
|
|
22
|
+
* dependency that was just starting to recover.
|
|
23
|
+
*/
|
|
24
|
+
declare const CIRCUIT_BREAKER_TRANSITIONS: {
|
|
25
|
+
readonly closed: readonly ["open"];
|
|
26
|
+
readonly open: readonly ["half-open"];
|
|
27
|
+
readonly 'half-open': readonly ["closed", "open"];
|
|
28
|
+
};
|
|
29
|
+
type CircuitBreakerOptions = {
|
|
30
|
+
/** Time in milliseconds the circuit stays open before moving to half-open. */
|
|
31
|
+
resetTimeoutMs?: number;
|
|
32
|
+
/** Called once each time the circuit opens. */
|
|
33
|
+
onTrip?: (reason: string) => void;
|
|
34
|
+
/** Called when the circuit closes. */
|
|
35
|
+
onClose?: () => void;
|
|
36
|
+
};
|
|
37
|
+
export type { CircuitBreakerOptions, CircuitBreakerState };
|
|
38
|
+
export { CIRCUIT_BREAKER_TRANSITIONS };
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.CIRCUIT_BREAKER_TRANSITIONS = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* Legal state transitions. The flow is closed → open → half-open → (closed | open).
|
|
6
|
+
*
|
|
7
|
+
* The **half-open** state exists so the breaker can test whether the dependency has recovered WITHOUT
|
|
8
|
+
* flipping straight back to fully closed. Going open → closed blindly would, on a dependency that is
|
|
9
|
+
* still down, immediately re-admit the full load and re-trip — flapping between open and closed every
|
|
10
|
+
* window. Instead, after the open timeout the breaker moves to half-open and admits a single trial
|
|
11
|
+
* ("probe") request:
|
|
12
|
+
* - probe succeeds → the dependency is healthy again → transition to **closed** (resume normal flow).
|
|
13
|
+
* - probe fails → still broken → transition back to **open** for another timeout window.
|
|
14
|
+
*
|
|
15
|
+
* Admitting exactly one probe (rather than reopening the floodgates) is also what prevents the
|
|
16
|
+
* "thundering herd": many callers retrying at once the instant the timeout elapses, re-overwhelming a
|
|
17
|
+
* dependency that was just starting to recover.
|
|
18
|
+
*/
|
|
19
|
+
const CIRCUIT_BREAKER_TRANSITIONS = {
|
|
20
|
+
closed: ['open'],
|
|
21
|
+
open: ['half-open'],
|
|
22
|
+
'half-open': ['closed', 'open'],
|
|
23
|
+
};
|
|
24
|
+
exports.CIRCUIT_BREAKER_TRANSITIONS = CIRCUIT_BREAKER_TRANSITIONS;
|
package/dist/Onyx.d.ts
CHANGED
|
@@ -18,7 +18,6 @@ declare function init({ keys, initialKeyStates, evictableKeys, shouldSyncMultipl
|
|
|
18
18
|
* @param connectOptions The options object that will define the behavior of the connection.
|
|
19
19
|
* @param connectOptions.key The Onyx key to subscribe to.
|
|
20
20
|
* @param connectOptions.callback A function that will be called when the Onyx data we are subscribed changes.
|
|
21
|
-
* @param connectOptions.waitForCollectionCallback If set to `true`, it will return the entire collection to the callback as a single object.
|
|
22
21
|
* @param connectOptions.selector This will be used to subscribe to a subset of an Onyx key's data. **Only used inside `useOnyx()` hook.**
|
|
23
22
|
* Using this setting on `useOnyx()` can have very positive performance benefits because the component will only re-render
|
|
24
23
|
* when the subset of data changes. Otherwise, any change of data on any property would normally
|
|
@@ -40,7 +39,6 @@ declare function connect<TKey extends OnyxKey>(connectOptions: ConnectOptions<TK
|
|
|
40
39
|
* @param connectOptions The options object that will define the behavior of the connection.
|
|
41
40
|
* @param connectOptions.key The Onyx key to subscribe to.
|
|
42
41
|
* @param connectOptions.callback A function that will be called when the Onyx data we are subscribed changes.
|
|
43
|
-
* @param connectOptions.waitForCollectionCallback If set to `true`, it will return the entire collection to the callback as a single object.
|
|
44
42
|
* @param connectOptions.selector This will be used to subscribe to a subset of an Onyx key's data. **Only used inside `useOnyx()` hook.**
|
|
45
43
|
* Using this setting on `useOnyx()` can have very positive performance benefits because the component will only re-render
|
|
46
44
|
* when the subset of data changes. Otherwise, any change of data on any property would normally
|
package/dist/Onyx.js
CHANGED
|
@@ -93,7 +93,6 @@ function init({ keys = {}, initialKeyStates = {}, evictableKeys = [], shouldSync
|
|
|
93
93
|
* @param connectOptions The options object that will define the behavior of the connection.
|
|
94
94
|
* @param connectOptions.key The Onyx key to subscribe to.
|
|
95
95
|
* @param connectOptions.callback A function that will be called when the Onyx data we are subscribed changes.
|
|
96
|
-
* @param connectOptions.waitForCollectionCallback If set to `true`, it will return the entire collection to the callback as a single object.
|
|
97
96
|
* @param connectOptions.selector This will be used to subscribe to a subset of an Onyx key's data. **Only used inside `useOnyx()` hook.**
|
|
98
97
|
* Using this setting on `useOnyx()` can have very positive performance benefits because the component will only re-render
|
|
99
98
|
* when the subset of data changes. Otherwise, any change of data on any property would normally
|
|
@@ -117,7 +116,6 @@ function connect(connectOptions) {
|
|
|
117
116
|
* @param connectOptions The options object that will define the behavior of the connection.
|
|
118
117
|
* @param connectOptions.key The Onyx key to subscribe to.
|
|
119
118
|
* @param connectOptions.callback A function that will be called when the Onyx data we are subscribed changes.
|
|
120
|
-
* @param connectOptions.waitForCollectionCallback If set to `true`, it will return the entire collection to the callback as a single object.
|
|
121
119
|
* @param connectOptions.selector This will be used to subscribe to a subset of an Onyx key's data. **Only used inside `useOnyx()` hook.**
|
|
122
120
|
* Using this setting on `useOnyx()` can have very positive performance benefits because the component will only re-render
|
|
123
121
|
* when the subset of data changes. Otherwise, any change of data on any property would normally
|
|
@@ -60,18 +60,17 @@ class OnyxConnectionManager {
|
|
|
60
60
|
* according to their purpose and effect they produce in the Onyx connection.
|
|
61
61
|
*/
|
|
62
62
|
generateConnectionID(connectOptions) {
|
|
63
|
-
const { key, reuseConnection
|
|
63
|
+
const { key, reuseConnection } = connectOptions;
|
|
64
64
|
// The current session ID is appended to the connection ID so we can have different connections
|
|
65
65
|
// after an `Onyx.clear()` operation.
|
|
66
66
|
let suffix = `,sessionID=${this.sessionID}`;
|
|
67
|
-
// We will generate a unique ID
|
|
68
|
-
//
|
|
69
|
-
//
|
|
70
|
-
|
|
71
|
-
if (reuseConnection === false || (OnyxKeys_1.default.isCollectionKey(key) && (waitForCollectionCallback === undefined || waitForCollectionCallback === false))) {
|
|
67
|
+
// We will generate a unique ID when `reuseConnection` is `false`, which means the subscriber
|
|
68
|
+
// explicitly wants the connection to not be reused. Collection-root subscriptions are now always
|
|
69
|
+
// snapshot mode, so they can be reused like any other connection.
|
|
70
|
+
if (reuseConnection === false) {
|
|
72
71
|
suffix += `,uniqueID=${Str.guid()}`;
|
|
73
72
|
}
|
|
74
|
-
return `onyxKey=${key}
|
|
73
|
+
return `onyxKey=${key}${suffix}`;
|
|
75
74
|
}
|
|
76
75
|
/**
|
|
77
76
|
* Fires all the subscribers callbacks associated with that connection ID.
|
|
@@ -82,11 +81,16 @@ class OnyxConnectionManager {
|
|
|
82
81
|
return;
|
|
83
82
|
}
|
|
84
83
|
for (const callback of connection.callbacks.values()) {
|
|
85
|
-
|
|
86
|
-
|
|
84
|
+
try {
|
|
85
|
+
if (OnyxKeys_1.default.isCollectionKey(connection.onyxKey)) {
|
|
86
|
+
callback(connection.cachedCallbackValue, connection.cachedCallbackKey, connection.sourceValue);
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
callback(connection.cachedCallbackValue, connection.cachedCallbackKey);
|
|
90
|
+
}
|
|
87
91
|
}
|
|
88
|
-
|
|
89
|
-
callback
|
|
92
|
+
catch (error) {
|
|
93
|
+
Logger.logAlert(`[ConnectionManager] Subscriber callback threw an error for key '${connection.onyxKey}': ${error}`);
|
|
90
94
|
}
|
|
91
95
|
}
|
|
92
96
|
}
|
|
@@ -115,13 +119,12 @@ class OnyxConnectionManager {
|
|
|
115
119
|
this.fireCallbacks(connectionID);
|
|
116
120
|
}
|
|
117
121
|
};
|
|
118
|
-
subscriptionID = OnyxUtils_1.default.subscribeToKey(Object.assign(Object.assign({}, connectOptions), { callback
|
|
122
|
+
subscriptionID = OnyxUtils_1.default.subscribeToKey(Object.assign(Object.assign({}, connectOptions), { callback }));
|
|
119
123
|
connectionMetadata = {
|
|
120
124
|
subscriptionID,
|
|
121
125
|
onyxKey: connectOptions.key,
|
|
122
126
|
isConnectionMade: false,
|
|
123
127
|
callbacks: new Map(),
|
|
124
|
-
waitForCollectionCallback: connectOptions.waitForCollectionCallback,
|
|
125
128
|
};
|
|
126
129
|
this.connectionsMap.set(connectionID, connectionMetadata);
|
|
127
130
|
}
|
|
@@ -134,8 +137,8 @@ class OnyxConnectionManager {
|
|
|
134
137
|
// Defer the callback execution to the next tick of the event loop.
|
|
135
138
|
// This ensures that the current execution flow completes and the result connection object is available when the callback fires.
|
|
136
139
|
Promise.resolve().then(() => {
|
|
137
|
-
var _a
|
|
138
|
-
(
|
|
140
|
+
var _a;
|
|
141
|
+
(_a = connectOptions.callback) === null || _a === void 0 ? void 0 : _a.call(connectOptions, connectionMetadata.cachedCallbackValue, connectionMetadata.cachedCallbackKey);
|
|
139
142
|
});
|
|
140
143
|
}
|
|
141
144
|
return { id: connectionID, callbackID };
|
package/dist/OnyxUtils.d.ts
CHANGED
|
@@ -134,11 +134,17 @@ declare function getCollectionDataAndSendAsObject<TKey extends OnyxKey>(matching
|
|
|
134
134
|
declare function remove<TKey extends OnyxKey>(key: TKey, isProcessingCollectionUpdate?: boolean): Promise<void>;
|
|
135
135
|
declare function reportStorageQuota(error?: Error): Promise<void>;
|
|
136
136
|
/**
|
|
137
|
-
* Handles storage operation failures based on the error
|
|
138
|
-
*
|
|
139
|
-
*
|
|
140
|
-
* -
|
|
141
|
-
* -
|
|
137
|
+
* Handles storage operation failures based on the error class (see lib/storage/errors.ts).
|
|
138
|
+
* The connection layer (createStore) owns connection/transport recovery; this operation layer owns
|
|
139
|
+
* capacity recovery (eviction) so that a given failure is retried by exactly one layer:
|
|
140
|
+
* - INVALID_DATA: logs an alert and throws (the same data will always fail).
|
|
141
|
+
* - TRANSIENT / FATAL: the connection layer already retried (transient) or exhausted its heal budget
|
|
142
|
+
* and alerted (fatal). Retrying here would only re-amplify, so we skip the write quietly.
|
|
143
|
+
* - CAPACITY: evicts the least recently accessed evictable key and retries, under a session-level
|
|
144
|
+
* circuit breaker (see lib/StorageCircuitBreaker.ts) that halts the loop once eviction stops making
|
|
145
|
+
* progress or failures storm — the per-operation budget alone cannot stop a session-wide storm.
|
|
146
|
+
* - UNKNOWN: the provider couldn't classify it — log the full error shape (name + message +
|
|
147
|
+
* provider) once so it's visible, then bounded retry without eviction.
|
|
142
148
|
*/
|
|
143
149
|
declare function retryOperation<TMethod extends RetriableOnyxOperation>(error: Error, onyxMethod: TMethod, defaultParams: Parameters<TMethod>[0], retryAttempt: number | undefined, inFlightKeys?: Set<OnyxKey>): Promise<void>;
|
|
144
150
|
/**
|