react-native-onyx 3.0.116 → 3.0.118
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 +15 -5
- package/README.md +8 -8
- package/dist/Onyx.d.ts +17 -14
- package/dist/Onyx.js +125 -38
- package/dist/OnyxCache.d.ts +2 -2
- package/dist/OnyxCache.js +4 -9
- package/dist/OnyxUtils.d.ts +17 -55
- package/dist/OnyxUtils.js +63 -352
- package/dist/PendingWrites.d.ts +35 -0
- package/dist/PendingWrites.js +103 -0
- package/dist/index.d.ts +1 -2
- package/dist/storage/providers/IDBKeyValProvider/classifyError.js +4 -0
- package/dist/storage/providers/IDBKeyValProvider/createStore.js +1 -0
- package/dist/types.d.ts +10 -13
- package/dist/useOnyx.d.ts +11 -10
- package/dist/useOnyx.js +68 -193
- package/package.json +4 -2
- package/dist/OnyxConnectionManager.d.ts +0 -78
- package/dist/OnyxConnectionManager.js +0 -189
- package/dist/OnyxSnapshotCache.d.ts +0 -75
- package/dist/OnyxSnapshotCache.js +0 -125
- package/dist/createMemoizedSelector.d.ts +0 -18
- package/dist/createMemoizedSelector.js +0 -37
- package/dist/memoizedShallowEqual.d.ts +0 -5
- package/dist/memoizedShallowEqual.js +0 -34
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { OnyxKey } from './types';
|
|
2
|
+
/**
|
|
3
|
+
* Registers an in-flight write under each key it can change, so `scheduleInitialSubscriberNotification`
|
|
4
|
+
* waits only for the writes relevant to a connecting key. Returns the same promise so callers can wrap a
|
|
5
|
+
* write's return value inline. The write is deregistered once it settles (success or failure).
|
|
6
|
+
*/
|
|
7
|
+
declare function trackPendingWrite<T>(keys: OnyxKey | OnyxKey[], pendingWrite: Promise<T>): Promise<T>;
|
|
8
|
+
/**
|
|
9
|
+
* Registers an in-flight write that affects every key (Onyx.clear). Deregistered once it settles.
|
|
10
|
+
*/
|
|
11
|
+
declare function trackPendingGlobalWrite<T>(pendingGlobalWrite: Promise<T>): Promise<T>;
|
|
12
|
+
/**
|
|
13
|
+
* In-flight writes that can change the value delivered to a subscriber of `key`: writes to the key
|
|
14
|
+
* itself, writes to any member when `key` is a collection root, and global writes (clear).
|
|
15
|
+
*/
|
|
16
|
+
declare function getPendingWritesForKey(key: OnyxKey): Array<Promise<unknown>>;
|
|
17
|
+
/**
|
|
18
|
+
* Defer a subscriber's initial notification until the writes relevant to `key` that are in flight this
|
|
19
|
+
* tick have applied, so it reads post-write cache and dedups against their notifications. The wait is
|
|
20
|
+
* scoped to `key` and snapshotted after one microtask, so an unrelated or slow write elsewhere cannot
|
|
21
|
+
* block or postpone this delivery, and writes issued after it do not either.
|
|
22
|
+
*/
|
|
23
|
+
declare function scheduleInitialSubscriberNotification(key: OnyxKey, notify: () => void): void;
|
|
24
|
+
/**
|
|
25
|
+
* Drop all tracked writes, useful in test environments.
|
|
26
|
+
*/
|
|
27
|
+
declare function clearPendingWrites(): void;
|
|
28
|
+
declare const _default: {
|
|
29
|
+
trackPendingWrite: typeof trackPendingWrite;
|
|
30
|
+
trackPendingGlobalWrite: typeof trackPendingGlobalWrite;
|
|
31
|
+
getPendingWritesForKey: typeof getPendingWritesForKey;
|
|
32
|
+
scheduleInitialSubscriberNotification: typeof scheduleInitialSubscriberNotification;
|
|
33
|
+
clearPendingWrites: typeof clearPendingWrites;
|
|
34
|
+
};
|
|
35
|
+
export default _default;
|
|
@@ -0,0 +1,103 @@
|
|
|
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 OnyxKeys_1 = __importDefault(require("./OnyxKeys"));
|
|
7
|
+
// In-flight writes tracked per affected key, so a subscriber's initial notification waits only for
|
|
8
|
+
// writes that can change its own value, never for unrelated (or slow) writes elsewhere.
|
|
9
|
+
const pendingWritesByKey = new Map();
|
|
10
|
+
// In-flight writes that affect every key (Onyx.clear). Any initial notification waits for these.
|
|
11
|
+
const pendingGlobalWrites = new Set();
|
|
12
|
+
/**
|
|
13
|
+
* Registers an in-flight write under each key it can change, so `scheduleInitialSubscriberNotification`
|
|
14
|
+
* waits only for the writes relevant to a connecting key. Returns the same promise so callers can wrap a
|
|
15
|
+
* write's return value inline. The write is deregistered once it settles (success or failure).
|
|
16
|
+
*/
|
|
17
|
+
function trackPendingWrite(keys, pendingWrite) {
|
|
18
|
+
// Drop nullish keys (e.g. a keyless `clear` item) so they never reach `getPendingWritesForKey`'s scan.
|
|
19
|
+
const keyList = (Array.isArray(keys) ? keys : [keys]).filter((key) => typeof key === 'string');
|
|
20
|
+
for (const key of keyList) {
|
|
21
|
+
let pendingWritesForKey = pendingWritesByKey.get(key);
|
|
22
|
+
if (!pendingWritesForKey) {
|
|
23
|
+
pendingWritesForKey = new Set();
|
|
24
|
+
pendingWritesByKey.set(key, pendingWritesForKey);
|
|
25
|
+
}
|
|
26
|
+
pendingWritesForKey.add(pendingWrite);
|
|
27
|
+
}
|
|
28
|
+
const deregister = () => {
|
|
29
|
+
for (const key of keyList) {
|
|
30
|
+
const pendingWritesForKey = pendingWritesByKey.get(key);
|
|
31
|
+
if (!pendingWritesForKey) {
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
pendingWritesForKey.delete(pendingWrite);
|
|
35
|
+
if (pendingWritesForKey.size === 0) {
|
|
36
|
+
pendingWritesByKey.delete(key);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
pendingWrite.then(deregister, deregister);
|
|
41
|
+
return pendingWrite;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Registers an in-flight write that affects every key (Onyx.clear). Deregistered once it settles.
|
|
45
|
+
*/
|
|
46
|
+
function trackPendingGlobalWrite(pendingGlobalWrite) {
|
|
47
|
+
pendingGlobalWrites.add(pendingGlobalWrite);
|
|
48
|
+
const deregister = () => pendingGlobalWrites.delete(pendingGlobalWrite);
|
|
49
|
+
pendingGlobalWrite.then(deregister, deregister);
|
|
50
|
+
return pendingGlobalWrite;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* In-flight writes that can change the value delivered to a subscriber of `key`: writes to the key
|
|
54
|
+
* itself, writes to any member when `key` is a collection root, and global writes (clear).
|
|
55
|
+
*/
|
|
56
|
+
function getPendingWritesForKey(key) {
|
|
57
|
+
const promises = [...pendingGlobalWrites];
|
|
58
|
+
const own = pendingWritesByKey.get(key);
|
|
59
|
+
if (own) {
|
|
60
|
+
promises.push(...own);
|
|
61
|
+
}
|
|
62
|
+
if (OnyxKeys_1.default.isCollectionKey(key)) {
|
|
63
|
+
for (const [writeKey, pendingWritesForKey] of pendingWritesByKey) {
|
|
64
|
+
// Membership resolves by longest match, as `notifyKey`/`notifyCollection` do, so a write to a
|
|
65
|
+
// more specific collection (`test_level_last_1`) is not treated as a member of a shorter one
|
|
66
|
+
// (`test_level_`) and cannot delay its subscribers.
|
|
67
|
+
if (writeKey !== key && OnyxKeys_1.default.getCollectionKey(writeKey) === key) {
|
|
68
|
+
promises.push(...pendingWritesForKey);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return promises;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Defer a subscriber's initial notification until the writes relevant to `key` that are in flight this
|
|
76
|
+
* tick have applied, so it reads post-write cache and dedups against their notifications. The wait is
|
|
77
|
+
* scoped to `key` and snapshotted after one microtask, so an unrelated or slow write elsewhere cannot
|
|
78
|
+
* block or postpone this delivery, and writes issued after it do not either.
|
|
79
|
+
*/
|
|
80
|
+
function scheduleInitialSubscriberNotification(key, notify) {
|
|
81
|
+
Promise.resolve().then(() => {
|
|
82
|
+
const relevant = getPendingWritesForKey(key);
|
|
83
|
+
if (relevant.length === 0) {
|
|
84
|
+
notify();
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
Promise.all(relevant.map((pendingWrite) => pendingWrite.catch(() => undefined))).then(notify);
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Drop all tracked writes, useful in test environments.
|
|
92
|
+
*/
|
|
93
|
+
function clearPendingWrites() {
|
|
94
|
+
pendingWritesByKey.clear();
|
|
95
|
+
pendingGlobalWrites.clear();
|
|
96
|
+
}
|
|
97
|
+
exports.default = {
|
|
98
|
+
trackPendingWrite,
|
|
99
|
+
trackPendingGlobalWrite,
|
|
100
|
+
getPendingWritesForKey,
|
|
101
|
+
scheduleInitialSubscriberNotification,
|
|
102
|
+
clearPendingWrites,
|
|
103
|
+
};
|
package/dist/index.d.ts
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
import type { ConnectOptions, OnyxUpdate } from './Onyx';
|
|
1
|
+
import type { Connection, ConnectOptions, OnyxUpdate } from './Onyx';
|
|
2
2
|
import Onyx from './Onyx';
|
|
3
3
|
import type { CustomTypeOptions, KeyValueMapping, NullishDeep, OnyxCollection, OnyxEntry, OnyxKey, OnyxValue, Selector, OnyxInputValue, OnyxCollectionInputValue, OnyxInput, OnyxSetInput, OnyxMultiSetInput, OnyxMergeInput, OnyxMergeCollectionInput, OnyxSetCollectionInput } from './types';
|
|
4
4
|
import type { FetchStatus, ResultMetadata, UseOnyxResult, UseOnyxOptions } from './useOnyx';
|
|
5
|
-
import type { Connection } from './OnyxConnectionManager';
|
|
6
5
|
import useOnyx from './useOnyx';
|
|
7
6
|
import type { OnyxSQLiteKeyValuePair } from './storage/providers/SQLiteProvider';
|
|
8
7
|
export default Onyx;
|
|
@@ -31,6 +31,10 @@ function classifyIDBError(error) {
|
|
|
31
31
|
if (message.includes('internal error opening backing store')) {
|
|
32
32
|
return errors_1.StorageErrorClass.FATAL;
|
|
33
33
|
}
|
|
34
|
+
// Chromium persistence failure with no cause attached, recovered by a fresh connection.
|
|
35
|
+
if (name === 'unknownerror' && message === 'internal error.') {
|
|
36
|
+
return errors_1.StorageErrorClass.FATAL;
|
|
37
|
+
}
|
|
34
38
|
// Transient connection/transport failures — the cached connection is stale and a reopen fixes it:
|
|
35
39
|
// - InvalidStateError: connection closed between getDB() resolving and db.transaction().
|
|
36
40
|
// - AbortError: write transaction aborted (connection close / versionchange / sibling abort).
|
|
@@ -209,6 +209,7 @@ function createStore(dbName, storeName) {
|
|
|
209
209
|
Logger.logInfo(`IDB heal: backing store error detected — dropping cached connection and reopening (${healAttemptsRemaining} attempts left)`, {
|
|
210
210
|
dbName,
|
|
211
211
|
storeName,
|
|
212
|
+
errorMessage: error instanceof Error ? error.message : String(error),
|
|
212
213
|
});
|
|
213
214
|
dbp = undefined;
|
|
214
215
|
return executeTransaction(txMode, callback).then((result) => {
|
package/dist/types.d.ts
CHANGED
|
@@ -176,14 +176,6 @@ type NullishObjectDeep<ObjectType extends object> = {
|
|
|
176
176
|
* the object of collection keys/values specified in the second parameter of the method.
|
|
177
177
|
*/
|
|
178
178
|
type Collection<TKey extends CollectionKeyBase, TValue> = Record<`${TKey}${string}`, TValue>;
|
|
179
|
-
/** Represents the base options used in `Onyx.connect()` method. */
|
|
180
|
-
type BaseConnectOptions = {
|
|
181
|
-
/**
|
|
182
|
-
* If set to `false`, the connection won't be reused between other subscribers that are listening to the same Onyx key
|
|
183
|
-
* with the same connect configurations.
|
|
184
|
-
*/
|
|
185
|
-
reuseConnection?: boolean;
|
|
186
|
-
};
|
|
187
179
|
/** Represents the callback function used in `Onyx.connect()` method with a regular key. */
|
|
188
180
|
type DefaultConnectCallback<TKey extends OnyxKey> = (value: OnyxEntry<KeyValueMapping[TKey]>, key: TKey) => void;
|
|
189
181
|
/** Represents the callback function used in `Onyx.connect()` method with a collection key. */
|
|
@@ -196,15 +188,12 @@ type CollectionConnectCallback<TKey extends OnyxKey> = (value: NonUndefined<Onyx
|
|
|
196
188
|
* `(collection, key)`). For any other key, the callback fires with the value at
|
|
197
189
|
* that key (signature `(value, key)`).
|
|
198
190
|
*/
|
|
199
|
-
type ConnectOptions<TKey extends OnyxKey> =
|
|
191
|
+
type ConnectOptions<TKey extends OnyxKey> = {
|
|
200
192
|
/** The Onyx key to subscribe to. */
|
|
201
193
|
key: TKey;
|
|
202
194
|
/** A function that will be called when the Onyx data we are subscribed changes. */
|
|
203
195
|
callback?: (value: TKey extends CollectionKeyBase ? NonUndefined<OnyxCollection<KeyValueMapping[TKey]>> : OnyxEntry<KeyValueMapping[TKey]>, key: TKey) => void;
|
|
204
196
|
};
|
|
205
|
-
type CallbackToStateMapping<TKey extends OnyxKey> = ConnectOptions<TKey> & {
|
|
206
|
-
subscriptionID: number;
|
|
207
|
-
};
|
|
208
197
|
/**
|
|
209
198
|
* Represents a single Onyx input value, that can be either `TOnyxValue` or `null` if the key should be deleted.
|
|
210
199
|
* This type is used for data passed to Onyx e.g. in `Onyx.merge` and `Onyx.set`.
|
|
@@ -362,4 +351,12 @@ type MixedOperationsQueue = {
|
|
|
362
351
|
mergeReplaceNullPatches: MultiMergeReplaceNullPatches;
|
|
363
352
|
set: OnyxInputKeyValueMapping;
|
|
364
353
|
};
|
|
365
|
-
|
|
354
|
+
/**
|
|
355
|
+
* Represents a connection to an Onyx key, returned by `Onyx.connect()`/`Onyx.connectWithoutView()`.
|
|
356
|
+
* Pass it to `Onyx.disconnect()` to stop receiving callbacks for this subscription.
|
|
357
|
+
*/
|
|
358
|
+
type Connection = {
|
|
359
|
+
/** Unsubscribe this connection. Idempotent. */
|
|
360
|
+
unsubscribe: () => void;
|
|
361
|
+
};
|
|
362
|
+
export type { Connection, Collection, CollectionConnectCallback, CollectionKey, CollectionKeyBase, ConnectOptions, CustomTypeOptions, DeepRecord, DefaultConnectCallback, ExtractOnyxCollectionValue, GenericFunction, InitOptions, Key, KeyValueMapping, NonNull, NonUndefined, OnyxInputKeyValueMapping, NullishDeep, OnyxCollection, OnyxEntry, OnyxKey, OnyxInputValue, OnyxCollectionInputValue, OnyxInput, OnyxSetInput, OnyxMultiSetInput, OnyxMergeInput, OnyxMergeCollectionInput, OnyxSetCollectionInput, OnyxMethod, OnyxMethodMap, OnyxUpdate, OnyxValue, Selector, SetOptions, SetParams, SetCollectionParams, MergeCollectionWithPatchesParams, MultiMergeReplaceNullPatches, MixedOperationsQueue, RetriableOnyxOperation, };
|
package/dist/useOnyx.d.ts
CHANGED
|
@@ -2,24 +2,25 @@ import type { OnyxKey, OnyxValue } from './types';
|
|
|
2
2
|
type UseOnyxSelector<TKey extends OnyxKey, TReturnValue = OnyxValue<TKey>> = (data: OnyxValue<TKey> | undefined) => TReturnValue;
|
|
3
3
|
type UseOnyxOptions<TKey extends OnyxKey, TReturnValue> = {
|
|
4
4
|
/**
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*/
|
|
8
|
-
reuseConnection?: boolean;
|
|
9
|
-
/**
|
|
10
|
-
* This will be used to subscribe to a subset of an Onyx key's data.
|
|
11
|
-
* Using this setting on `useOnyx` can have very positive performance benefits because the component will only re-render
|
|
12
|
-
* when the subset of data changes. Otherwise, any change of data on any property would normally
|
|
13
|
-
* cause the component to re-render (and that can be expensive from a performance standpoint).
|
|
14
|
-
* @see `useOnyx` cannot return `null` and so selector will replace `null` with `undefined` to maintain compatibility.
|
|
5
|
+
* Select a subset of the key's data. Re-renders only when the selector's output changes by deep
|
|
6
|
+
* equality, so an inline selector that allocates fresh objects/arrays each render is safe.
|
|
15
7
|
*/
|
|
16
8
|
selector?: UseOnyxSelector<TKey, TReturnValue>;
|
|
17
9
|
};
|
|
10
|
+
/**
|
|
11
|
+
* `loading` while the key's first value is still on its way: `Onyx.init` hasn't hydrated the cache yet,
|
|
12
|
+
* or nothing is cached and a merge is in flight. `loaded` otherwise.
|
|
13
|
+
*/
|
|
18
14
|
type FetchStatus = 'loading' | 'loaded';
|
|
19
15
|
type ResultMetadata = {
|
|
20
16
|
status: FetchStatus;
|
|
21
17
|
};
|
|
22
18
|
type UseOnyxResult<TValue> = [NonNullable<TValue> | undefined, ResultMetadata];
|
|
19
|
+
/**
|
|
20
|
+
* Subscribes a component to an Onyx key, re-rendering when the value changes (for a collection key,
|
|
21
|
+
* when any member changes; the value is the frozen collection object). Returns `[value, {status}]`,
|
|
22
|
+
* `status` `loading` until the key's first value can be known.
|
|
23
|
+
*/
|
|
23
24
|
declare function useOnyx<TKey extends OnyxKey, TReturnValue = OnyxValue<TKey>>(key: TKey, options?: UseOnyxOptions<TKey, TReturnValue>): UseOnyxResult<TReturnValue>;
|
|
24
25
|
export default useOnyx;
|
|
25
26
|
export type { FetchStatus, ResultMetadata, UseOnyxResult, UseOnyxOptions, UseOnyxSelector };
|
package/dist/useOnyx.js
CHANGED
|
@@ -1,209 +1,84 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
-
if (k2 === undefined) k2 = k;
|
|
4
|
-
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
-
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
-
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
-
}
|
|
8
|
-
Object.defineProperty(o, k2, desc);
|
|
9
|
-
}) : (function(o, m, k, k2) {
|
|
10
|
-
if (k2 === undefined) k2 = k;
|
|
11
|
-
o[k2] = m[k];
|
|
12
|
-
}));
|
|
13
|
-
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
-
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
-
}) : function(o, v) {
|
|
16
|
-
o["default"] = v;
|
|
17
|
-
});
|
|
18
|
-
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
-
var ownKeys = function(o) {
|
|
20
|
-
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
-
var ar = [];
|
|
22
|
-
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
-
return ar;
|
|
24
|
-
};
|
|
25
|
-
return ownKeys(o);
|
|
26
|
-
};
|
|
27
|
-
return function (mod) {
|
|
28
|
-
if (mod && mod.__esModule) return mod;
|
|
29
|
-
var result = {};
|
|
30
|
-
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
-
__setModuleDefault(result, mod);
|
|
32
|
-
return result;
|
|
33
|
-
};
|
|
34
|
-
})();
|
|
35
2
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
4
|
};
|
|
38
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const fast_equals_1 = require("fast-equals");
|
|
39
7
|
const react_1 = require("react");
|
|
40
|
-
const
|
|
41
|
-
const OnyxCache_1 =
|
|
42
|
-
const
|
|
8
|
+
const with_selector_1 = require("use-sync-external-store/with-selector");
|
|
9
|
+
const OnyxCache_1 = __importDefault(require("./OnyxCache"));
|
|
10
|
+
const OnyxSubscriptionManager_1 = __importDefault(require("./OnyxSubscriptionManager"));
|
|
43
11
|
const OnyxUtils_1 = __importDefault(require("./OnyxUtils"));
|
|
44
|
-
|
|
45
|
-
|
|
12
|
+
function isOnyxInitialised() {
|
|
13
|
+
return OnyxUtils_1.default.getDeferredInitTask().isResolved;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* What can be said about a key's value, as a single value so that init finishing re-renders a
|
|
17
|
+
* subscriber even when its key has no value and nothing else about it changed.
|
|
18
|
+
*/
|
|
19
|
+
const AVAILABILITY = {
|
|
20
|
+
/** `Onyx.init` hasn't hydrated the cache yet, so nothing can be said about the key. */
|
|
21
|
+
UNKNOWN: 0,
|
|
22
|
+
/** The cache is hydrated and the key has no value. */
|
|
23
|
+
ABSENT: 1,
|
|
24
|
+
/** The cache is hydrated and the key has a value. */
|
|
25
|
+
PRESENT: 2,
|
|
26
|
+
};
|
|
27
|
+
function getAvailability(key) {
|
|
28
|
+
if (!isOnyxInitialised()) {
|
|
29
|
+
return AVAILABILITY.UNKNOWN;
|
|
30
|
+
}
|
|
31
|
+
return OnyxCache_1.default.hasCacheForKey(key) ? AVAILABILITY.PRESENT : AVAILABILITY.ABSENT;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Subscribes a component to an Onyx key, re-rendering when the value changes (for a collection key,
|
|
35
|
+
* when any member changes; the value is the frozen collection object). Returns `[value, {status}]`,
|
|
36
|
+
* `status` `loading` until the key's first value can be known.
|
|
37
|
+
*/
|
|
46
38
|
function useOnyx(key, options) {
|
|
47
|
-
const connectionRef = (0, react_1.useRef)(null);
|
|
48
39
|
const selector = options === null || options === void 0 ? void 0 : options.selector;
|
|
49
|
-
//
|
|
50
|
-
// with a deepEqual fallback on the output to keep the returned reference stable.
|
|
51
|
-
const memoizedSelector = (0, react_1.useMemo)(() => {
|
|
52
|
-
if (!selector) {
|
|
53
|
-
return null;
|
|
54
|
-
}
|
|
55
|
-
return (0, createMemoizedSelector_1.default)(selector);
|
|
56
|
-
}, [selector]);
|
|
57
|
-
// Stores the previous cached value as it's necessary to compare with the new value in `getSnapshot()`.
|
|
58
|
-
// We initialize it to `null` to simulate that we don't have any value from cache yet.
|
|
59
|
-
const previousValueRef = (0, react_1.useRef)(null);
|
|
60
|
-
// Stores the newest cached value in order to compare with the previous one and optimize `getSnapshot()` execution.
|
|
61
|
-
const newValueRef = (0, react_1.useRef)(null);
|
|
62
|
-
// Stores the previously result returned by the hook, containing the data from cache and the fetch status.
|
|
63
|
-
// We initialize it to `undefined` and `loading` fetch status to simulate the initial result when the hook is loading from the cache.
|
|
64
|
-
const resultRef = (0, react_1.useRef)([
|
|
65
|
-
undefined,
|
|
66
|
-
{
|
|
67
|
-
status: 'loading',
|
|
68
|
-
},
|
|
69
|
-
]);
|
|
70
|
-
// Tracks which key has completed its first Onyx connection callback. When this doesn't match the
|
|
71
|
-
// current key, getSnapshot() treats the hook as being in its "first connection" state for that key.
|
|
72
|
-
// This is key-aware by design: when the key changes, connectedKeyRef still holds the old key (or null
|
|
73
|
-
// after cleanup), so the hook automatically enters first-connection mode for the new key without any
|
|
74
|
-
// explicit reset logic — eliminating the race condition where cleanup could clobber a boolean flag.
|
|
40
|
+
// First-render marker for the loading gate below.
|
|
75
41
|
const connectedKeyRef = (0, react_1.useRef)(null);
|
|
76
|
-
// Tracks whether the hook has completed its initial mount subscription.
|
|
77
|
-
// Unlike connectedKeyRef (which gets nulled by cleanup), this persists across re-subscriptions.
|
|
78
|
-
const hasMountedRef = (0, react_1.useRef)(false);
|
|
79
|
-
// Indicates if the hook is connecting to an Onyx key.
|
|
80
|
-
const isConnectingRef = (0, react_1.useRef)(false);
|
|
81
|
-
// Stores the `onStoreChange()` function, which can be used to trigger a `getSnapshot()` update when desired.
|
|
82
|
-
const onStoreChangeFnRef = (0, react_1.useRef)(null);
|
|
83
|
-
// Indicates if we should get the newest cached value from Onyx during `getSnapshot()` execution.
|
|
84
|
-
const shouldGetCachedValueRef = (0, react_1.useRef)(true);
|
|
85
|
-
// Cache the options key to avoid regenerating it every getSnapshot call
|
|
86
|
-
const cacheKey = (0, react_1.useMemo)(() => OnyxSnapshotCache_1.default.registerConsumer(key, {
|
|
87
|
-
selector: options === null || options === void 0 ? void 0 : options.selector,
|
|
88
|
-
}), [key, options === null || options === void 0 ? void 0 : options.selector]);
|
|
89
|
-
(0, react_1.useEffect)(() => () => OnyxSnapshotCache_1.default.deregisterConsumer(key, cacheKey), [key, cacheKey]);
|
|
90
|
-
// Tracks the last memoizedSelector reference that getSnapshot() has computed with.
|
|
91
|
-
// When the selector changes, this mismatch forces getSnapshot() to re-evaluate
|
|
92
|
-
// even if all other conditions (isFirstConnection, shouldGetCachedValue, key) are false.
|
|
93
|
-
const lastComputedSelectorRef = (0, react_1.useRef)(memoizedSelector);
|
|
94
|
-
const getSnapshot = (0, react_1.useCallback)(() => {
|
|
95
|
-
var _a, _b, _c;
|
|
96
|
-
// Check if we have any cache for this Onyx key
|
|
97
|
-
// Don't use cache during active data updates (when shouldGetCachedValueRef is true)
|
|
98
|
-
const isFirstConnection = connectedKeyRef.current !== key;
|
|
99
|
-
if (!shouldGetCachedValueRef.current) {
|
|
100
|
-
const cachedResult = OnyxSnapshotCache_1.default.getCachedResult(key, cacheKey);
|
|
101
|
-
if (cachedResult !== undefined) {
|
|
102
|
-
// The slot is shared by all subscribers of the same (key, selector) pair, so it can hold a content-equal
|
|
103
|
-
// result computed by another subscriber. Keep our own result then, otherwise we would needlessly change
|
|
104
|
-
// this hook's result identity and re-render its consumer.
|
|
105
|
-
if (cachedResult !== resultRef.current && (0, memoizedShallowEqual_1.default)(cachedResult[0], resultRef.current[0]) && cachedResult[1].status === resultRef.current[1].status) {
|
|
106
|
-
return resultRef.current;
|
|
107
|
-
}
|
|
108
|
-
resultRef.current = cachedResult;
|
|
109
|
-
return cachedResult;
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
// We get the value from cache while the first connection to Onyx is being made or if the key has changed,
|
|
113
|
-
// so we can return any cached value right away. For the case where the key has changed, If we don't return the cached value right away, then the UI will show the incorrect (previous) value for a brief period which looks like a UI glitch to the user. After the connection is made, we only
|
|
114
|
-
// update `newValueRef` when `Onyx.connect()` callback is fired.
|
|
115
|
-
const hasSelectorChanged = lastComputedSelectorRef.current !== memoizedSelector;
|
|
116
|
-
if (isFirstConnection || shouldGetCachedValueRef.current || hasSelectorChanged) {
|
|
117
|
-
// Gets the value from cache and maps it with selector. It changes `null` to `undefined` for `useOnyx` compatibility.
|
|
118
|
-
const value = OnyxUtils_1.default.tryGetCachedValue(key);
|
|
119
|
-
const selectedValue = memoizedSelector ? memoizedSelector(value) : value;
|
|
120
|
-
lastComputedSelectorRef.current = memoizedSelector;
|
|
121
|
-
newValueRef.current = (selectedValue !== null && selectedValue !== void 0 ? selectedValue : undefined);
|
|
122
|
-
// We set this flag to `false` again since we don't want to get the newest cached value every time `getSnapshot()` is executed,
|
|
123
|
-
// and only when `Onyx.connect()` callback is fired.
|
|
124
|
-
shouldGetCachedValueRef.current = false;
|
|
125
|
-
}
|
|
126
|
-
const hasCacheForKey = OnyxCache_1.default.hasCacheForKey(key);
|
|
127
|
-
// Since the fetch status can be different given the use cases below, we define the variable right away.
|
|
128
|
-
let newFetchStatus;
|
|
129
|
-
// If we have pending merge operations for the key during the first connection, we set the new value to `undefined`
|
|
130
|
-
// and fetch status to `loading` to simulate that it is still being loaded until we have the most updated data.
|
|
131
|
-
if (isFirstConnection && OnyxUtils_1.default.hasPendingMergeForKey(key)) {
|
|
132
|
-
newValueRef.current = undefined;
|
|
133
|
-
newFetchStatus = 'loading';
|
|
134
|
-
}
|
|
135
|
-
// shallowEqual checks === first (O(1) for frozen snapshots and stable selector references),
|
|
136
|
-
// then falls back to comparing top-level properties for individual keys that may have
|
|
137
|
-
// new references with equivalent content. The comparison is memoized by object identity
|
|
138
|
-
// (see `memoizedShallowEqual`) so N hooks comparing the same two cache objects pay for
|
|
139
|
-
// one walk in total instead of one walk each.
|
|
140
|
-
// Normalize null to undefined to ensure consistent comparison (both represent "no value").
|
|
141
|
-
const areValuesEqual = (0, memoizedShallowEqual_1.default)((_a = previousValueRef.current) !== null && _a !== void 0 ? _a : undefined, (_b = newValueRef.current) !== null && _b !== void 0 ? _b : undefined);
|
|
142
|
-
// We update the cached value and the result in the following conditions:
|
|
143
|
-
// We will update the cached value and the result in any of the following situations:
|
|
144
|
-
// - The previously cached value is different from the new value.
|
|
145
|
-
// - The previously cached value is `null` (not set from cache yet) and we have cache for this key
|
|
146
|
-
// OR we have a pending `Onyx.clear()` task (if `Onyx.clear()` is running cache might not be available anymore
|
|
147
|
-
// OR the subscriber is triggered (the value is gotten from the storage)
|
|
148
|
-
// so we update the cached value/result right away in order to prevent infinite loading state issues).
|
|
149
|
-
const shouldUpdateResult = !areValuesEqual || (previousValueRef.current === null && (hasCacheForKey || OnyxCache_1.default.hasPendingTask(OnyxCache_1.TASK.CLEAR) || !isFirstConnection));
|
|
150
|
-
if (shouldUpdateResult) {
|
|
151
|
-
previousValueRef.current = newValueRef.current;
|
|
152
|
-
// If the new value is `null` we default it to `undefined` to ensure the consumer gets a consistent result from the hook.
|
|
153
|
-
newFetchStatus = newFetchStatus !== null && newFetchStatus !== void 0 ? newFetchStatus : 'loaded';
|
|
154
|
-
resultRef.current = [
|
|
155
|
-
(_c = previousValueRef.current) !== null && _c !== void 0 ? _c : undefined,
|
|
156
|
-
{
|
|
157
|
-
status: newFetchStatus,
|
|
158
|
-
},
|
|
159
|
-
];
|
|
160
|
-
}
|
|
161
|
-
if (newFetchStatus !== 'loading') {
|
|
162
|
-
OnyxSnapshotCache_1.default.setCachedResult(key, cacheKey, resultRef.current);
|
|
163
|
-
}
|
|
164
|
-
return resultRef.current;
|
|
165
|
-
}, [key, memoizedSelector, cacheKey]);
|
|
166
42
|
const subscribe = (0, react_1.useCallback)((onStoreChange) => {
|
|
167
|
-
|
|
168
|
-
//
|
|
169
|
-
//
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
hasMountedRef.current = true;
|
|
177
|
-
isConnectingRef.current = true;
|
|
178
|
-
onStoreChangeFnRef.current = onStoreChange;
|
|
179
|
-
connectionRef.current = OnyxConnectionManager_1.default.connect({
|
|
180
|
-
key,
|
|
181
|
-
callback: () => {
|
|
182
|
-
isConnectingRef.current = false;
|
|
183
|
-
onStoreChangeFnRef.current = onStoreChange;
|
|
184
|
-
// Signals that the first connection was made for this key, so some logics
|
|
185
|
-
// in `getSnapshot()` won't be executed anymore.
|
|
186
|
-
connectedKeyRef.current = key;
|
|
187
|
-
// Signals that we want to get the newest cached value again in `getSnapshot()`.
|
|
188
|
-
shouldGetCachedValueRef.current = true;
|
|
189
|
-
// Invalidate snapshot cache for this key when data changes
|
|
190
|
-
OnyxSnapshotCache_1.default.invalidateForKey(key);
|
|
191
|
-
// Finally, we signal that the store changed, making `getSnapshot()` be called again.
|
|
43
|
+
const unsubscribe = OnyxSubscriptionManager_1.default.subscribe(key, onStoreChange);
|
|
44
|
+
// `Onyx.init` hydrates the cache without notifying anyone, so a subscriber that mounted
|
|
45
|
+
// before it finished would never hear about the stored value. Re-read once init lands.
|
|
46
|
+
let isActive = true;
|
|
47
|
+
if (!isOnyxInitialised()) {
|
|
48
|
+
OnyxUtils_1.default.getDeferredInitTask().promise.then(() => {
|
|
49
|
+
if (!isActive) {
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
192
52
|
onStoreChange();
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
});
|
|
53
|
+
});
|
|
54
|
+
}
|
|
196
55
|
return () => {
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
}
|
|
200
|
-
OnyxConnectionManager_1.default.disconnect(connectionRef.current);
|
|
201
|
-
connectedKeyRef.current = null;
|
|
202
|
-
isConnectingRef.current = false;
|
|
203
|
-
onStoreChangeFnRef.current = null;
|
|
56
|
+
isActive = false;
|
|
57
|
+
unsubscribe();
|
|
204
58
|
};
|
|
205
|
-
}, [key
|
|
206
|
-
const
|
|
207
|
-
return
|
|
59
|
+
}, [key]);
|
|
60
|
+
const getSnapshot = (0, react_1.useCallback)(() => OnyxSubscriptionManager_1.default.getState(key), [key]);
|
|
61
|
+
const select = (0, react_1.useCallback)((data) => { var _a; return (_a = (selector ? selector(data) : data)) !== null && _a !== void 0 ? _a : undefined; }, [selector]);
|
|
62
|
+
// Deep-equal only with a selector (its output may be freshly allocated); raw values are ref-stable.
|
|
63
|
+
const isEqual = selector ? fast_equals_1.deepEqual : undefined;
|
|
64
|
+
const value = (0, with_selector_1.useSyncExternalStoreWithSelector)(subscribe, getSnapshot, undefined, select, isEqual);
|
|
65
|
+
// Reactive availability, so the first value landing re-renders even when the selector output is unchanged.
|
|
66
|
+
const availability = (0, react_1.useSyncExternalStore)(subscribe, () => getAvailability(key));
|
|
67
|
+
const isCached = availability === AVAILABILITY.PRESENT;
|
|
68
|
+
// Loading while a first value is still on its way: init hasn't hydrated yet, or nothing is cached
|
|
69
|
+
// and a merge is in flight.
|
|
70
|
+
// eslint-disable-next-line react-hooks/refs
|
|
71
|
+
const isLoading = !isOnyxInitialised() || (connectedKeyRef.current !== key && !isCached && OnyxUtils_1.default.hasPendingMergeForKey(key));
|
|
72
|
+
const loadingStatus = isLoading ? 'loading' : 'loaded';
|
|
73
|
+
// Only advance the marker once a value exists, so an unrelated re-render can't end loading early.
|
|
74
|
+
(0, react_1.useEffect)(() => {
|
|
75
|
+
if (!isCached) {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
connectedKeyRef.current = key;
|
|
79
|
+
}, [isCached, key]);
|
|
80
|
+
// Blank the value while loading: the pending merge isn't in cache yet.
|
|
81
|
+
const result = isLoading ? undefined : value;
|
|
82
|
+
return (0, react_1.useMemo)(() => [result, { status: loadingStatus }], [result, loadingStatus]);
|
|
208
83
|
}
|
|
209
84
|
exports.default = useOnyx;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-native-onyx",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.118",
|
|
4
4
|
"author": "Expensify, Inc.",
|
|
5
5
|
"homepage": "https://expensify.com",
|
|
6
6
|
"description": "State management for React Native",
|
|
@@ -49,7 +49,8 @@
|
|
|
49
49
|
"lodash.clone": "^4.5.0",
|
|
50
50
|
"lodash.pick": "^4.4.0",
|
|
51
51
|
"lodash.transform": "^4.6.0",
|
|
52
|
-
"underscore": "^1.13.6"
|
|
52
|
+
"underscore": "^1.13.6",
|
|
53
|
+
"use-sync-external-store": "^1.6.0"
|
|
53
54
|
},
|
|
54
55
|
"devDependencies": {
|
|
55
56
|
"@actions/core": "^1.10.1",
|
|
@@ -70,6 +71,7 @@
|
|
|
70
71
|
"@types/react": "^18.2.14",
|
|
71
72
|
"@types/react-native": "^0.70.0",
|
|
72
73
|
"@types/underscore": "^1.11.15",
|
|
74
|
+
"@types/use-sync-external-store": "^1.5.0",
|
|
73
75
|
"@typescript-eslint/eslint-plugin": "^8.51.0",
|
|
74
76
|
"@typescript-eslint/parser": "^8.51.0",
|
|
75
77
|
"@vercel/ncc": "0.38.1",
|
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
import type { ConnectOptions } from './Onyx';
|
|
2
|
-
import type { OnyxKey } from './types';
|
|
3
|
-
/**
|
|
4
|
-
* Represents the connection object returned by `Onyx.connect()`.
|
|
5
|
-
*/
|
|
6
|
-
type Connection = {
|
|
7
|
-
/**
|
|
8
|
-
* The ID used to identify this particular connection.
|
|
9
|
-
*/
|
|
10
|
-
id: string;
|
|
11
|
-
/**
|
|
12
|
-
* The ID of the subscriber's callback that is associated to this connection.
|
|
13
|
-
*/
|
|
14
|
-
callbackID: string;
|
|
15
|
-
};
|
|
16
|
-
/**
|
|
17
|
-
* Manages Onyx connections of `Onyx.connect()` and `useOnyx()` subscribers.
|
|
18
|
-
*/
|
|
19
|
-
declare class OnyxConnectionManager {
|
|
20
|
-
/**
|
|
21
|
-
* A map where the key is the connection ID generated inside `connect()` and the value is the metadata of that connection.
|
|
22
|
-
*/
|
|
23
|
-
private connectionsMap;
|
|
24
|
-
/**
|
|
25
|
-
* Stores the last generated callback ID which will be incremented when making a new connection.
|
|
26
|
-
*/
|
|
27
|
-
private lastCallbackID;
|
|
28
|
-
/**
|
|
29
|
-
* Stores the last generated session ID for the connection manager. The current session ID
|
|
30
|
-
* is appended to the connection IDs and it's used to create new different connections for the same key
|
|
31
|
-
* when `refreshSessionID()` is called.
|
|
32
|
-
*
|
|
33
|
-
* When calling `Onyx.clear()` after a logout operation some connections might remain active as they
|
|
34
|
-
* aren't tied to the React's lifecycle e.g. `Onyx.connect()` usage, causing infinite loading state issues to new `useOnyx()` subscribers
|
|
35
|
-
* that are connecting to the same key as we didn't populate the cache again because we are still reusing such connections.
|
|
36
|
-
*
|
|
37
|
-
* To elimitate this problem, the session ID must be refreshed during the `Onyx.clear()` call (by using `refreshSessionID()`)
|
|
38
|
-
* in order to create fresh connections when new subscribers connect to the same keys again, allowing them
|
|
39
|
-
* to use the cache system correctly and avoid the mentioned issues in `useOnyx()`.
|
|
40
|
-
*/
|
|
41
|
-
private sessionID;
|
|
42
|
-
constructor();
|
|
43
|
-
/**
|
|
44
|
-
* Generates a connection ID based on the `connectOptions` object passed to the function.
|
|
45
|
-
*
|
|
46
|
-
* The properties used to generate the ID are handpicked for performance reasons and
|
|
47
|
-
* according to their purpose and effect they produce in the Onyx connection.
|
|
48
|
-
*/
|
|
49
|
-
private generateConnectionID;
|
|
50
|
-
/**
|
|
51
|
-
* Fires all the subscribers callbacks associated with that connection ID.
|
|
52
|
-
*/
|
|
53
|
-
private fireCallbacks;
|
|
54
|
-
/**
|
|
55
|
-
* Connects to an Onyx key given the options passed and listens to its changes.
|
|
56
|
-
*
|
|
57
|
-
* @param connectOptions The options object that will define the behavior of the connection.
|
|
58
|
-
* @returns The connection object to use when calling `disconnect()`.
|
|
59
|
-
*/
|
|
60
|
-
connect<TKey extends OnyxKey>(connectOptions: ConnectOptions<TKey>): Connection;
|
|
61
|
-
/**
|
|
62
|
-
* Disconnects and removes the listener from the Onyx key.
|
|
63
|
-
*
|
|
64
|
-
* @param connection Connection object returned by calling `connect()`.
|
|
65
|
-
*/
|
|
66
|
-
disconnect(connection: Connection): void;
|
|
67
|
-
/**
|
|
68
|
-
* Disconnect all subscribers from Onyx.
|
|
69
|
-
*/
|
|
70
|
-
disconnectAll(): void;
|
|
71
|
-
/**
|
|
72
|
-
* Refreshes the connection manager's session ID.
|
|
73
|
-
*/
|
|
74
|
-
refreshSessionID(): void;
|
|
75
|
-
}
|
|
76
|
-
declare const connectionManager: OnyxConnectionManager;
|
|
77
|
-
export default connectionManager;
|
|
78
|
-
export type { Connection };
|