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.
Files changed (33) hide show
  1. package/API.md +0 -2
  2. package/README.md +2 -12
  3. package/dist/CircuitBreaker/AbstractCircuitBreaker.d.ts +91 -0
  4. package/dist/CircuitBreaker/AbstractCircuitBreaker.js +166 -0
  5. package/dist/CircuitBreaker/types.d.ts +38 -0
  6. package/dist/CircuitBreaker/types.js +24 -0
  7. package/dist/Onyx.d.ts +0 -2
  8. package/dist/Onyx.js +0 -2
  9. package/dist/OnyxConnectionManager.js +18 -15
  10. package/dist/OnyxUtils.d.ts +11 -5
  11. package/dist/OnyxUtils.js +92 -82
  12. package/dist/StateMachine.d.ts +45 -0
  13. package/dist/StateMachine.js +37 -0
  14. package/dist/StorageCircuitBreaker.d.ts +66 -0
  15. package/dist/StorageCircuitBreaker.js +177 -0
  16. package/dist/storage/__mocks__/index.d.ts +10 -0
  17. package/dist/storage/__mocks__/index.js +15 -0
  18. package/dist/storage/errors.d.ts +34 -0
  19. package/dist/storage/errors.js +41 -0
  20. package/dist/storage/index.js +5 -0
  21. package/dist/storage/providers/IDBKeyValProvider/classifyError.d.ts +9 -0
  22. package/dist/storage/providers/IDBKeyValProvider/classifyError.js +37 -0
  23. package/dist/storage/providers/IDBKeyValProvider/createStore.js +40 -57
  24. package/dist/storage/providers/IDBKeyValProvider/index.js +5 -0
  25. package/dist/storage/providers/MemoryOnlyProvider.js +5 -0
  26. package/dist/storage/providers/NoopProvider.js +5 -0
  27. package/dist/storage/providers/SQLiteProvider.js +5 -0
  28. package/dist/storage/providers/classifySQLiteError.d.ts +13 -0
  29. package/dist/storage/providers/classifySQLiteError.js +21 -0
  30. package/dist/storage/providers/types.d.ts +8 -0
  31. package/dist/types.d.ts +11 -27
  32. package/dist/useOnyx.js +0 -2
  33. 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: (memberValue, memberKey) => {...});
260
+ Onyx.connect({key: ONYXKEYS.COLLECTION.REPORT}, callback: (allReports, collectionKey, sourceValue) => {...});
261
261
  ```
262
262
 
263
- This will fire the callback once per member key depending on how many collection member keys are currently stored. Changes to those keys after the initial callbacks fire will occur when each individual key is updated.
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, waitForCollectionCallback } = connectOptions;
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 in any of the following situations:
68
- // - `reuseConnection` is `false`. That means the subscriber explicitly wants the connection to not be reused.
69
- // - `key` is a collection key AND `waitForCollectionCallback` is `undefined/false`. This combination needs a new connection at every subscription
70
- // in order to send all the collection entries, so the connection can't be reused.
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},waitForCollectionCallback=${waitForCollectionCallback !== null && waitForCollectionCallback !== void 0 ? waitForCollectionCallback : false}${suffix}`;
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
- if (connection.waitForCollectionCallback) {
86
- callback(connection.cachedCallbackValue, connection.cachedCallbackKey, connection.sourceValue);
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
- else {
89
- callback(connection.cachedCallbackValue, connection.cachedCallbackKey);
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: 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, _b;
138
- (_b = (_a = connectOptions).callback) === null || _b === void 0 ? void 0 : _b.call(_a, connectionMetadata.cachedCallbackValue, connectionMetadata.cachedCallbackKey);
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 };
@@ -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 type:
138
- * - Storage capacity errors: evicts data and retries the operation
139
- * - Invalid data errors: logs an alert and throws an error
140
- * - Non-retriable errors: logs an alert and resolves without retrying
141
- * - Other errors: retries the operation
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
  /**