react-native-onyx 3.0.56 → 3.0.58

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.
@@ -34,25 +34,39 @@ var __importStar = (this && this.__importStar) || (function () {
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  const IDB = __importStar(require("idb-keyval"));
37
- const Logger_1 = require("../../../Logger");
37
+ const Logger = __importStar(require("../../../Logger"));
38
38
  // This is a copy of the createStore function from idb-keyval, we need a custom implementation
39
39
  // because we need to create the database manually in order to ensure that the store exists before we use it.
40
40
  // If the store does not exist, idb-keyval will throw an error
41
41
  // source: https://github.com/jakearchibald/idb-keyval/blob/9d19315b4a83897df1e0193dccdc29f78466a0f3/src/index.ts#L12
42
42
  function createStore(dbName, storeName) {
43
43
  let dbp;
44
+ const attachHandlers = (db) => {
45
+ // Browsers may close idle IDB connections at any time, especially Safari.
46
+ // We clear the cached promise so the next operation opens a fresh connection.
47
+ // https://developer.mozilla.org/en-US/docs/Web/API/IDBDatabase/close_event
48
+ // eslint-disable-next-line no-param-reassign
49
+ db.onclose = () => {
50
+ Logger.logInfo('IDB connection closed by browser', { dbName, storeName });
51
+ dbp = undefined;
52
+ };
53
+ // When another tab triggers a DB version upgrade, we must close the connection
54
+ // to unblock the upgrade; otherwise the other tab's open request hangs indefinitely.
55
+ // https://developer.mozilla.org/en-US/docs/Web/API/IDBDatabase/versionchange_event
56
+ // eslint-disable-next-line no-param-reassign
57
+ db.onversionchange = () => {
58
+ Logger.logInfo('IDB connection closing due to version change', { dbName, storeName });
59
+ db.close();
60
+ dbp = undefined;
61
+ };
62
+ };
44
63
  const getDB = () => {
45
64
  if (dbp)
46
65
  return dbp;
47
66
  const request = indexedDB.open(dbName);
48
67
  request.onupgradeneeded = () => request.result.createObjectStore(storeName);
49
68
  dbp = IDB.promisifyRequest(request);
50
- dbp.then((db) => {
51
- // It seems like Safari sometimes likes to just close the connection.
52
- // It's supposed to fire this event when that happens. Let's hope it does!
53
- // eslint-disable-next-line no-param-reassign
54
- db.onclose = () => (dbp = undefined);
55
- },
69
+ dbp.then(attachHandlers,
56
70
  // eslint-disable-next-line @typescript-eslint/no-empty-function
57
71
  () => { });
58
72
  return dbp;
@@ -63,7 +77,7 @@ function createStore(dbName, storeName) {
63
77
  if (db.objectStoreNames.contains(storeName)) {
64
78
  return db;
65
79
  }
66
- (0, Logger_1.logInfo)(`Store ${storeName} does not exist in database ${dbName}.`);
80
+ Logger.logInfo(`Store ${storeName} does not exist in database ${dbName}.`);
67
81
  const nextVersion = db.version + 1;
68
82
  db.close();
69
83
  const request = indexedDB.open(dbName, nextVersion);
@@ -72,14 +86,34 @@ function createStore(dbName, storeName) {
72
86
  if (updatedDatabase.objectStoreNames.contains(storeName)) {
73
87
  return;
74
88
  }
75
- (0, Logger_1.logInfo)(`Creating store ${storeName} in database ${dbName}.`);
89
+ Logger.logInfo(`Creating store ${storeName} in database ${dbName}.`);
76
90
  updatedDatabase.createObjectStore(storeName);
77
91
  };
78
92
  dbp = IDB.promisifyRequest(request);
93
+ // eslint-disable-next-line @typescript-eslint/no-empty-function
94
+ dbp.then(attachHandlers, () => { });
79
95
  return dbp;
80
96
  };
81
- return (txMode, callback) => getDB()
82
- .then(verifyStoreExists)
83
- .then((db) => callback(db.transaction(storeName, txMode).objectStore(storeName)));
97
+ function executeTransaction(txMode, callback) {
98
+ return getDB()
99
+ .then(verifyStoreExists)
100
+ .then((db) => callback(db.transaction(storeName, txMode).objectStore(storeName)));
101
+ }
102
+ // If the connection was closed between getDB() resolving and db.transaction() executing,
103
+ // the transaction throws InvalidStateError. We catch it and retry once with a fresh connection.
104
+ return (txMode, callback) => executeTransaction(txMode, callback).catch((error) => {
105
+ if (error instanceof DOMException && error.name === 'InvalidStateError') {
106
+ Logger.logAlert('IDB InvalidStateError, retrying with fresh connection', {
107
+ dbName,
108
+ storeName,
109
+ txMode,
110
+ errorMessage: error.message,
111
+ });
112
+ dbp = undefined;
113
+ // Retry only once — this call is not wrapped, so if it also fails the error propagates normally.
114
+ return executeTransaction(txMode, callback);
115
+ }
116
+ throw error;
117
+ });
84
118
  }
85
119
  exports.default = createStore;
package/dist/utils.d.ts CHANGED
@@ -37,7 +37,7 @@ type FastMergeResult<TValue> = {
37
37
  declare function fastMerge<TValue>(target: TValue, source: TValue, options?: FastMergeOptions, metadata?: FastMergeMetadata, basePath?: string[]): FastMergeResult<TValue>;
38
38
  /** Checks whether the given object is an object and not null/undefined. */
39
39
  declare function isEmptyObject<T>(obj: T | EmptyValue): obj is EmptyValue;
40
- /** Deep removes the nested null values from the given value. */
40
+ /** Deep removes the nested null values from the given value. Returns the original reference if no nulls were found. */
41
41
  declare function removeNestedNullValues<TValue extends OnyxInput<OnyxKey> | null>(value: TValue): TValue;
42
42
  /** Formats the action name by uppercasing and adding the key if provided. */
43
43
  declare function formatActionName(method: string, key?: OnyxKey): string;
package/dist/utils.js CHANGED
@@ -39,6 +39,9 @@ function fastMerge(target, source, options, metadata, basePath = []) {
39
39
  function mergeObject(target, source, options, metadata, basePath) {
40
40
  const destination = {};
41
41
  const targetObject = isMergeableObject(target) ? target : undefined;
42
+ // Track whether the merge actually changed anything compared to target.
43
+ // If nothing changed, we return the original target reference for reference stability.
44
+ let hasChanged = !targetObject;
42
45
  // First we want to copy over all keys from the target into the destination object,
43
46
  // in case "target" is a mergable object.
44
47
  // If "shouldRemoveNestedNulls" is true, we want to remove null values from the merged object
@@ -51,6 +54,7 @@ function mergeObject(target, source, options, metadata, basePath) {
51
54
  // If either the source or target value is null, we want to omit the key from the merged object.
52
55
  const shouldOmitNullishProperty = options.shouldRemoveNestedNulls && (targetProperty === null || sourceProperty === null);
53
56
  if (targetProperty === undefined || shouldOmitNullishProperty) {
57
+ hasChanged = true;
54
58
  continue;
55
59
  }
56
60
  destination[key] = targetProperty;
@@ -68,6 +72,9 @@ function mergeObject(target, source, options, metadata, basePath) {
68
72
  }
69
73
  // If the source value is not a mergable object, we need to set the key directly.
70
74
  if (!isMergeableObject(sourceProperty)) {
75
+ if (destination[key] !== sourceProperty) {
76
+ hasChanged = true;
77
+ }
71
78
  destination[key] = sourceProperty;
72
79
  continue;
73
80
  }
@@ -76,6 +83,7 @@ function mergeObject(target, source, options, metadata, basePath) {
76
83
  // To achieve this, we first mark these nested objects with an internal flag.
77
84
  // When calling fastMerge again with "mark" removal mode, the marked objects will be removed.
78
85
  if (options.objectRemovalMode === 'mark' && targetProperty === null) {
86
+ hasChanged = true;
79
87
  targetProperty = { [ONYX_INTERNALS__REPLACE_OBJECT_MARK]: true };
80
88
  metadata.replaceNullPatches.push([[...basePath, key], Object.assign({}, sourceProperty)]);
81
89
  }
@@ -83,6 +91,7 @@ function mergeObject(target, source, options, metadata, basePath) {
83
91
  // has the internal flag set, we replace the entire destination object with the source one and remove
84
92
  // the flag.
85
93
  if (options.objectRemovalMode === 'replace' && sourceProperty[ONYX_INTERNALS__REPLACE_OBJECT_MARK]) {
94
+ hasChanged = true;
86
95
  // We do a spread here in order to have a new object reference and allow us to delete the internal flag
87
96
  // of the merged object only.
88
97
  const sourcePropertyWithoutMark = Object.assign({}, sourceProperty);
@@ -90,9 +99,13 @@ function mergeObject(target, source, options, metadata, basePath) {
90
99
  destination[key] = sourcePropertyWithoutMark;
91
100
  continue;
92
101
  }
93
- destination[key] = fastMerge(targetProperty, sourceProperty, options, metadata, [...basePath, key]).result;
102
+ const merged = fastMerge(targetProperty, sourceProperty, options, metadata, [...basePath, key]).result;
103
+ if (merged !== targetProperty) {
104
+ hasChanged = true;
105
+ }
106
+ destination[key] = merged;
94
107
  }
95
- return destination;
108
+ return hasChanged ? destination : targetObject;
96
109
  }
97
110
  /** Checks whether the given object is an object and not null/undefined. */
98
111
  function isEmptyObject(obj) {
@@ -106,30 +119,32 @@ function isMergeableObject(value) {
106
119
  const isNonNullObject = value != null ? typeof value === 'object' : false;
107
120
  return isNonNullObject && !(value instanceof RegExp) && !(value instanceof Date) && !Array.isArray(value);
108
121
  }
109
- /** Deep removes the nested null values from the given value. */
122
+ /** Deep removes the nested null values from the given value. Returns the original reference if no nulls were found. */
110
123
  function removeNestedNullValues(value) {
111
- if (value === null || value === undefined || typeof value !== 'object') {
124
+ if (value === null || value === undefined || typeof value !== 'object' || Array.isArray(value)) {
112
125
  return value;
113
126
  }
114
- if (Array.isArray(value)) {
115
- return [...value];
116
- }
127
+ let hasChanged = false;
117
128
  const result = {};
118
129
  // eslint-disable-next-line no-restricted-syntax, guard-for-in
119
130
  for (const key in value) {
120
131
  const propertyValue = value[key];
121
132
  if (propertyValue === null || propertyValue === undefined) {
133
+ hasChanged = true;
122
134
  continue;
123
135
  }
124
136
  if (typeof propertyValue === 'object' && !Array.isArray(propertyValue)) {
125
- const valueWithoutNestedNulls = removeNestedNullValues(propertyValue);
126
- result[key] = valueWithoutNestedNulls;
137
+ const cleaned = removeNestedNullValues(propertyValue);
138
+ if (cleaned !== propertyValue) {
139
+ hasChanged = true;
140
+ }
141
+ result[key] = cleaned;
127
142
  }
128
143
  else {
129
144
  result[key] = propertyValue;
130
145
  }
131
146
  }
132
- return result;
147
+ return hasChanged ? result : value;
133
148
  }
134
149
  /** Formats the action name by uppercasing and adding the key if provided. */
135
150
  function formatActionName(method, key) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-onyx",
3
- "version": "3.0.56",
3
+ "version": "3.0.58",
4
4
  "author": "Expensify, Inc.",
5
5
  "homepage": "https://expensify.com",
6
6
  "description": "State management for React Native",