react-native-onyx 3.0.91 → 3.0.93

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.
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Identity-pair-memoized shallowEqual: same (a, b) references → cached verdict, no walk.
3
+ */
4
+ declare function memoizedShallowEqual(a: unknown, b: unknown): boolean;
5
+ export default memoizedShallowEqual;
@@ -0,0 +1,34 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const fast_equals_1 = require("fast-equals");
4
+ /**
5
+ * Memoizes shallowEqual verdicts by the identity of the compared objects. Onyx values are
6
+ * treated as immutable (merge/set replace objects, never mutate), so a (prev, next) reference
7
+ * pair always yields the same verdict. In the hot case — N no-selector hooks on the same big
8
+ * key — every hook compares the exact same two cache-owned objects, so the first hook pays for
9
+ * the O(keys) walk and the rest resolve in O(1). WeakMap keys make stale entries impossible to
10
+ * read (lookup requires holding both exact objects) and let GC reclaim them.
11
+ */
12
+ const shallowEqualVerdicts = new WeakMap();
13
+ /**
14
+ * Identity-pair-memoized shallowEqual: same (a, b) references → cached verdict, no walk.
15
+ */
16
+ function memoizedShallowEqual(a, b) {
17
+ // Only object pairs are memoizable (WeakMap keys) — anything else is O(1) to compare anyway.
18
+ if (typeof a !== 'object' || a === null || typeof b !== 'object' || b === null) {
19
+ return (0, fast_equals_1.shallowEqual)(a, b);
20
+ }
21
+ let verdictsForA = shallowEqualVerdicts.get(a);
22
+ if (!verdictsForA) {
23
+ verdictsForA = new WeakMap();
24
+ shallowEqualVerdicts.set(a, verdictsForA);
25
+ }
26
+ const cachedVerdict = verdictsForA.get(b);
27
+ if (cachedVerdict !== undefined) {
28
+ return cachedVerdict;
29
+ }
30
+ const verdict = (0, fast_equals_1.shallowEqual)(a, b);
31
+ verdictsForA.set(b, verdict);
32
+ return verdict;
33
+ }
34
+ exports.default = memoizedShallowEqual;
@@ -8,6 +8,34 @@ const react_native_device_info_1 = require("react-native-device-info");
8
8
  const utils_1 = __importDefault(require("../../utils"));
9
9
  const classifySQLiteError_1 = __importDefault(require("./classifySQLiteError"));
10
10
  const DB_NAME = 'OnyxDB';
11
+ const SQLITE_MAX_VARIABLE_NUMBER = 32766;
12
+ const COMPILE_OPTIONS = {
13
+ MAX_VARIABLE_NUMBER: 'MAX_VARIABLE_NUMBER',
14
+ };
15
+ /** SQLite's maximum number of bound parameters per statement, read once from PRAGMA compile_options in init(). */
16
+ let sqliteMaxVariableNumber = SQLITE_MAX_VARIABLE_NUMBER;
17
+ /**
18
+ * Returns the value of a compile option from the rows returned by `PRAGMA compile_options`.
19
+ * For flag-only options (e.g. `ENABLE_FTS3`), returns an empty string when the option is present.
20
+ */
21
+ function getCompileOptionValue(compileOptionsResult, optionName) {
22
+ var _a, _b, _c, _d;
23
+ const optionPrefix = `${optionName}=`;
24
+ const rowCount = (_b = (_a = compileOptionsResult.rows) === null || _a === void 0 ? void 0 : _a.length) !== null && _b !== void 0 ? _b : 0;
25
+ for (let index = 0; index < rowCount; index++) {
26
+ const compileOption = (_d = (_c = compileOptionsResult.rows) === null || _c === void 0 ? void 0 : _c.item(index)) === null || _d === void 0 ? void 0 : _d.compile_options;
27
+ if (!compileOption) {
28
+ continue;
29
+ }
30
+ if (compileOption === optionName) {
31
+ return '';
32
+ }
33
+ if (compileOption.startsWith(optionPrefix)) {
34
+ return compileOption.slice(optionPrefix.length);
35
+ }
36
+ }
37
+ return undefined;
38
+ }
11
39
  /**
12
40
  * Prevents the stringifying of the object markers.
13
41
  */
@@ -47,6 +75,11 @@ const provider = {
47
75
  provider.store.execute('PRAGMA CACHE_SIZE=-20000;');
48
76
  provider.store.execute('PRAGMA synchronous=NORMAL;');
49
77
  provider.store.execute('PRAGMA journal_mode=WAL;');
78
+ const compileOptionsResult = provider.store.execute('PRAGMA compile_options;');
79
+ // Get the value of MAX_VARIABLE_NUMBER from the compile options and
80
+ // stores it in a global variable, that is going to be used during runtime.
81
+ const maxVariableNumber = Number(getCompileOptionValue(compileOptionsResult, COMPILE_OPTIONS.MAX_VARIABLE_NUMBER));
82
+ sqliteMaxVariableNumber = maxVariableNumber > 0 ? maxVariableNumber : SQLITE_MAX_VARIABLE_NUMBER;
50
83
  },
51
84
  getItem(key) {
52
85
  if (!provider.store) {
@@ -67,12 +100,22 @@ const provider = {
67
100
  if (!provider.store) {
68
101
  throw new Error('Store is not initialized!');
69
102
  }
70
- const placeholders = keys.map(() => '?').join(',');
71
- const command = `SELECT record_key, valueJSON FROM keyvaluepairs WHERE record_key IN (${placeholders});`;
72
- return provider.store.executeAsync(command, keys).then(({ rows }) => {
103
+ if (keys.length === 0) {
104
+ return Promise.resolve([]);
105
+ }
106
+ const keyChunks = utils_1.default.chunkArray(keys, sqliteMaxVariableNumber);
107
+ return Promise.all(keyChunks.map((keyChunk) => {
108
+ if (!provider.store) {
109
+ throw new Error('Store is not initialized!');
110
+ }
111
+ const placeholders = keyChunk.map(() => '?').join(',');
112
+ const command = `SELECT record_key, valueJSON FROM keyvaluepairs WHERE record_key IN (${placeholders});`;
113
+ return provider.store.executeAsync(command, keyChunk);
114
+ })).then((results) => {
115
+ const result = results.flatMap(({ rows }) => { var _a;
73
116
  // eslint-disable-next-line no-underscore-dangle
74
- const result = rows === null || rows === void 0 ? void 0 : rows._array.map((row) => [row.record_key, JSON.parse(row.valueJSON)]);
75
- return (result !== null && result !== void 0 ? result : []);
117
+ return (_a = rows === null || rows === void 0 ? void 0 : rows._array.map((row) => [row.record_key, JSON.parse(row.valueJSON)])) !== null && _a !== void 0 ? _a : []; });
118
+ return result;
76
119
  });
77
120
  },
78
121
  setItem(key, value) {
@@ -86,7 +129,7 @@ const provider = {
86
129
  throw new Error('Store is not initialized!');
87
130
  }
88
131
  const query = 'REPLACE INTO keyvaluepairs (record_key, valueJSON) VALUES (?, ?);';
89
- const params = pairs.map((pair) => [pair[0], JSON.stringify(pair[1] === undefined ? null : pair[1])]);
132
+ const params = pairs.map(([key, value]) => [key, JSON.stringify(value === undefined ? null : value)]);
90
133
  if (utils_1.default.isEmptyObject(params)) {
91
134
  return Promise.resolve();
92
135
  }
@@ -173,9 +216,23 @@ const provider = {
173
216
  if (!provider.store) {
174
217
  throw new Error('Store is not initialized!');
175
218
  }
176
- const placeholders = keys.map(() => '?').join(',');
177
- const query = `DELETE FROM keyvaluepairs WHERE record_key IN (${placeholders});`;
178
- return provider.store.executeAsync(query, keys).then(() => undefined);
219
+ if (keys.length === 0) {
220
+ return Promise.resolve();
221
+ }
222
+ const keyChunks = utils_1.default.chunkArray(keys, sqliteMaxVariableNumber);
223
+ const buildDeleteQuery = (keyChunk) => {
224
+ const placeholders = keyChunk.map(() => '?').join(',');
225
+ return `DELETE FROM keyvaluepairs WHERE record_key IN (${placeholders});`;
226
+ };
227
+ if (keyChunks.length === 1) {
228
+ const keyChunk = keyChunks[0];
229
+ return provider.store.executeAsync(buildDeleteQuery(keyChunk), keyChunk).then(() => undefined);
230
+ }
231
+ const commands = keyChunks.map((keyChunk) => ({
232
+ query: buildDeleteQuery(keyChunk),
233
+ params: keyChunk,
234
+ }));
235
+ return provider.store.executeBatchAsync(commands).then(() => undefined);
179
236
  },
180
237
  clear() {
181
238
  if (!provider.store) {
package/dist/useOnyx.js CHANGED
@@ -42,6 +42,7 @@ const OnyxCache_1 = __importStar(require("./OnyxCache"));
42
42
  const OnyxConnectionManager_1 = __importDefault(require("./OnyxConnectionManager"));
43
43
  const OnyxUtils_1 = __importDefault(require("./OnyxUtils"));
44
44
  const OnyxSnapshotCache_1 = __importDefault(require("./OnyxSnapshotCache"));
45
+ const memoizedShallowEqual_1 = __importDefault(require("./memoizedShallowEqual"));
45
46
  const useLiveRef_1 = __importDefault(require("./useLiveRef"));
46
47
  function useOnyx(key, options, dependencies = []) {
47
48
  const connectionRef = (0, react_1.useRef)(null);
@@ -170,9 +171,11 @@ function useOnyx(key, options, dependencies = []) {
170
171
  }
171
172
  // shallowEqual checks === first (O(1) for frozen snapshots and stable selector references),
172
173
  // then falls back to comparing top-level properties for individual keys that may have
173
- // new references with equivalent content.
174
+ // new references with equivalent content. The comparison is memoized by object identity
175
+ // (see `memoizedShallowEqual`) so N hooks comparing the same two cache objects pay for
176
+ // one walk in total instead of one walk each.
174
177
  // Normalize null to undefined to ensure consistent comparison (both represent "no value").
175
- const areValuesEqual = (0, fast_equals_1.shallowEqual)((_a = previousValueRef.current) !== null && _a !== void 0 ? _a : undefined, (_b = newValueRef.current) !== null && _b !== void 0 ? _b : undefined);
178
+ const areValuesEqual = (0, memoizedShallowEqual_1.default)((_a = previousValueRef.current) !== null && _a !== void 0 ? _a : undefined, (_b = newValueRef.current) !== null && _b !== void 0 ? _b : undefined);
176
179
  // We update the cached value and the result in the following conditions:
177
180
  // We will update the cached value and the result in any of the following situations:
178
181
  // - The previously cached value is different from the new value.
package/dist/utils.d.ts CHANGED
@@ -64,6 +64,10 @@ declare function pick<TValue>(obj: Record<string, TValue>, condition: string | s
64
64
  * @returns The object containing only the remaining entries after omission.
65
65
  */
66
66
  declare function omit<TValue>(obj: Record<string, TValue>, condition: string | string[] | ((entry: [string, TValue]) => boolean)): Record<string, TValue>;
67
+ /**
68
+ * Splits an array into chunks no larger than maxChunkSize.
69
+ */
70
+ declare function chunkArray<T>(items: readonly T[], maxChunkSize: number): T[][];
67
71
  declare const _default: {
68
72
  fastMerge: typeof fastMerge;
69
73
  isEmptyObject: typeof isEmptyObject;
@@ -72,6 +76,7 @@ declare const _default: {
72
76
  checkCompatibilityWithExistingValue: typeof checkCompatibilityWithExistingValue;
73
77
  pick: typeof pick;
74
78
  omit: typeof omit;
79
+ chunkArray: typeof chunkArray;
75
80
  ONYX_INTERNALS__REPLACE_OBJECT_MARK: string;
76
81
  };
77
82
  export default _default;
package/dist/utils.js CHANGED
@@ -238,6 +238,22 @@ function pick(obj, condition) {
238
238
  function omit(obj, condition) {
239
239
  return filterObject(obj, condition, false);
240
240
  }
241
+ /**
242
+ * Splits an array into chunks no larger than maxChunkSize.
243
+ */
244
+ function chunkArray(items, maxChunkSize) {
245
+ if (items.length === 0) {
246
+ return [];
247
+ }
248
+ if (items.length <= maxChunkSize) {
249
+ return [items];
250
+ }
251
+ const chunks = [];
252
+ for (let i = 0; i < items.length; i += maxChunkSize) {
253
+ chunks.push(items.slice(i, i + maxChunkSize));
254
+ }
255
+ return chunks;
256
+ }
241
257
  exports.default = {
242
258
  fastMerge,
243
259
  isEmptyObject,
@@ -246,5 +262,6 @@ exports.default = {
246
262
  checkCompatibilityWithExistingValue,
247
263
  pick,
248
264
  omit,
265
+ chunkArray,
249
266
  ONYX_INTERNALS__REPLACE_OBJECT_MARK,
250
267
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-onyx",
3
- "version": "3.0.91",
3
+ "version": "3.0.93",
4
4
  "author": "Expensify, Inc.",
5
5
  "homepage": "https://expensify.com",
6
6
  "description": "State management for React Native",