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
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
126
|
-
|
|
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) {
|