react-native-onyx 3.0.83 → 3.0.85
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.
|
@@ -35,12 +35,47 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
const IDB = __importStar(require("idb-keyval"));
|
|
37
37
|
const Logger = __importStar(require("../../../Logger"));
|
|
38
|
+
const HEAL_ATTEMPTS_MAX = 3;
|
|
39
|
+
/**
|
|
40
|
+
* Detects the Chromium-specific IDB backing store corruption error.
|
|
41
|
+
* Fires when LevelDB files backing IndexedDB are corrupted and Chrome's
|
|
42
|
+
* internal recovery (RepairDB -> delete -> recreate) also fails.
|
|
43
|
+
*/
|
|
44
|
+
function isBackingStoreError(error) {
|
|
45
|
+
return (error instanceof Error || error instanceof DOMException) && error.message.includes('Internal error opening backing store');
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Detects Safari/WebKit IDB connection termination errors.
|
|
49
|
+
* Fires when Safari kills the IDB server process for backgrounded tabs.
|
|
50
|
+
* WebKit bugs: https://bugs.webkit.org/show_bug.cgi?id=197050, https://bugs.webkit.org/show_bug.cgi?id=201483
|
|
51
|
+
*/
|
|
52
|
+
function isConnectionLostError(error) {
|
|
53
|
+
if (!(error instanceof Error || error instanceof DOMException))
|
|
54
|
+
return false;
|
|
55
|
+
const msg = error.message.toLowerCase();
|
|
56
|
+
return msg.includes('connection to indexed database server lost') || msg.includes('connection is closing');
|
|
57
|
+
}
|
|
58
|
+
function isInvalidStateError(error) {
|
|
59
|
+
return (error instanceof Error || error instanceof DOMException) && error.name === 'InvalidStateError';
|
|
60
|
+
}
|
|
61
|
+
/** Errors that trigger a budgeted heal-and-retry in store(). */
|
|
62
|
+
function isBudgetedHealError(error) {
|
|
63
|
+
return isBackingStoreError(error) || isConnectionLostError(error);
|
|
64
|
+
}
|
|
65
|
+
function getBudgetedHealErrorLabel(error) {
|
|
66
|
+
if (isBackingStoreError(error))
|
|
67
|
+
return 'backing store';
|
|
68
|
+
if (isConnectionLostError(error))
|
|
69
|
+
return 'connection lost';
|
|
70
|
+
return 'unknown';
|
|
71
|
+
}
|
|
38
72
|
// This is a copy of the createStore function from idb-keyval, we need a custom implementation
|
|
39
73
|
// because we need to create the database manually in order to ensure that the store exists before we use it.
|
|
40
74
|
// If the store does not exist, idb-keyval will throw an error
|
|
41
75
|
// source: https://github.com/jakearchibald/idb-keyval/blob/9d19315b4a83897df1e0193dccdc29f78466a0f3/src/index.ts#L12
|
|
42
76
|
function createStore(dbName, storeName) {
|
|
43
77
|
let dbp;
|
|
78
|
+
let healAttemptsRemaining = HEAL_ATTEMPTS_MAX;
|
|
44
79
|
const attachHandlers = (db) => {
|
|
45
80
|
// Browsers may close idle IDB connections at any time, especially Safari.
|
|
46
81
|
// We clear the cached promise so the next operation opens a fresh connection.
|
|
@@ -60,16 +95,27 @@ function createStore(dbName, storeName) {
|
|
|
60
95
|
dbp = undefined;
|
|
61
96
|
};
|
|
62
97
|
};
|
|
98
|
+
// Cache the open promise and attach handlers + rejection cleanup.
|
|
99
|
+
// On rejection, clears dbp so the next operation retries with a fresh indexedDB.open()
|
|
100
|
+
// instead of returning the same rejected promise.
|
|
101
|
+
// Guard: only clear if dbp hasn't been replaced by a concurrent heal/retry.
|
|
102
|
+
function cacheOpenPromise(openPromise) {
|
|
103
|
+
dbp = openPromise;
|
|
104
|
+
const currentPromise = openPromise;
|
|
105
|
+
openPromise.then(attachHandlers, () => {
|
|
106
|
+
if (dbp !== currentPromise) {
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
dbp = undefined;
|
|
110
|
+
});
|
|
111
|
+
return openPromise;
|
|
112
|
+
}
|
|
63
113
|
const getDB = () => {
|
|
64
114
|
if (dbp)
|
|
65
115
|
return dbp;
|
|
66
116
|
const request = indexedDB.open(dbName);
|
|
67
117
|
request.onupgradeneeded = () => request.result.createObjectStore(storeName);
|
|
68
|
-
|
|
69
|
-
dbp.then(attachHandlers,
|
|
70
|
-
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
|
71
|
-
() => { });
|
|
72
|
-
return dbp;
|
|
118
|
+
return cacheOpenPromise(IDB.promisifyRequest(request));
|
|
73
119
|
};
|
|
74
120
|
// Ensures the store exists in the DB. If missing, bumps the version to trigger
|
|
75
121
|
// onupgradeneeded, recreates the store, and returns a promise to the new DB.
|
|
@@ -89,29 +135,65 @@ function createStore(dbName, storeName) {
|
|
|
89
135
|
Logger.logInfo(`Creating store ${storeName} in database ${dbName}.`);
|
|
90
136
|
updatedDatabase.createObjectStore(storeName);
|
|
91
137
|
};
|
|
92
|
-
|
|
93
|
-
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
|
94
|
-
dbp.then(attachHandlers, () => { });
|
|
95
|
-
return dbp;
|
|
138
|
+
return cacheOpenPromise(IDB.promisifyRequest(request));
|
|
96
139
|
};
|
|
97
140
|
function executeTransaction(txMode, callback) {
|
|
98
141
|
return getDB()
|
|
99
142
|
.then(verifyStoreExists)
|
|
100
143
|
.then((db) => callback(db.transaction(storeName, txMode).objectStore(storeName)));
|
|
101
144
|
}
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
145
|
+
function resetHealBudget(result) {
|
|
146
|
+
healAttemptsRemaining = HEAL_ATTEMPTS_MAX;
|
|
147
|
+
return result;
|
|
148
|
+
}
|
|
149
|
+
// Handles three recoverable error classes:
|
|
150
|
+
// 1. InvalidStateError — connection closed between getDB() resolving and db.transaction().
|
|
151
|
+
// Retry once with a fresh connection. No budget limit (transient, always worth one reopen).
|
|
152
|
+
// 2. Backing store corruption (Chromium UnknownError) — drop cached connection and reopen.
|
|
153
|
+
// 3. Connection lost (Safari UnknownError) — IDB server terminated for backgrounded tabs.
|
|
154
|
+
// Both 2 and 3 share a heal budget (3 attempts, reset on success).
|
|
155
|
+
// Mirrors Dexie's PR1398_maxLoop pattern: https://github.com/dexie/Dexie.js/blob/master/src/functions/temp-transaction.ts
|
|
156
|
+
// Note: concurrent store() calls share the budget. Under overlapping failures each caller
|
|
157
|
+
// decrements independently, so the budget may drain faster than one-per-incident. This is
|
|
158
|
+
// acceptable — same as Dexie's approach — and the budget resets on any success.
|
|
159
|
+
return (txMode, callback) => executeTransaction(txMode, callback)
|
|
160
|
+
.then(resetHealBudget)
|
|
161
|
+
.catch((error) => {
|
|
162
|
+
if (isInvalidStateError(error)) {
|
|
163
|
+
Logger.logInfo('IDB InvalidStateError — dropping cached connection and retrying', {
|
|
107
164
|
dbName,
|
|
108
165
|
storeName,
|
|
109
166
|
txMode,
|
|
110
|
-
errorMessage: error.message,
|
|
167
|
+
errorMessage: error instanceof Error ? error.message : String(error),
|
|
111
168
|
});
|
|
112
169
|
dbp = undefined;
|
|
113
|
-
|
|
114
|
-
|
|
170
|
+
return executeTransaction(txMode, callback).then(resetHealBudget);
|
|
171
|
+
}
|
|
172
|
+
if (isBudgetedHealError(error) && healAttemptsRemaining > 0) {
|
|
173
|
+
healAttemptsRemaining--;
|
|
174
|
+
const label = getBudgetedHealErrorLabel(error);
|
|
175
|
+
Logger.logInfo(`IDB heal: ${label} error detected — dropping cached connection and reopening (${healAttemptsRemaining} attempts left)`, {
|
|
176
|
+
dbName,
|
|
177
|
+
storeName,
|
|
178
|
+
});
|
|
179
|
+
dbp = undefined;
|
|
180
|
+
return executeTransaction(txMode, callback).then((result) => {
|
|
181
|
+
Logger.logInfo(`IDB heal: successfully recovered after ${label} error`, { dbName, storeName });
|
|
182
|
+
return resetHealBudget(result);
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
if (isBudgetedHealError(error)) {
|
|
186
|
+
Logger.logAlert(`IDB heal: ${getBudgetedHealErrorLabel(error)} error — heal budget exhausted, giving up`, {
|
|
187
|
+
dbName,
|
|
188
|
+
storeName,
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
else {
|
|
192
|
+
Logger.logAlert('IDB error is not recoverable, giving up', {
|
|
193
|
+
dbName,
|
|
194
|
+
storeName,
|
|
195
|
+
errorMessage: error instanceof Error ? error.message : String(error),
|
|
196
|
+
});
|
|
115
197
|
}
|
|
116
198
|
throw error;
|
|
117
199
|
});
|
|
@@ -41,6 +41,17 @@ const utils_1 = __importDefault(require("../../../utils"));
|
|
|
41
41
|
const createStore_1 = __importDefault(require("./createStore"));
|
|
42
42
|
const DB_NAME = 'OnyxDB';
|
|
43
43
|
const STORE_NAME = 'keyvaluepairs';
|
|
44
|
+
/**
|
|
45
|
+
* Awaits an IndexedDB write transaction. idb-keyval's promisifyRequest rejects with
|
|
46
|
+
* `transaction.error`, which is `null` for an abort not caused by its own request
|
|
47
|
+
* (connection close / versionchange / a sibling transaction aborting). Normalize that
|
|
48
|
+
* `null` into a tagged AbortError.
|
|
49
|
+
*/
|
|
50
|
+
function promisifyWriteTransaction(transaction) {
|
|
51
|
+
return IDB.promisifyRequest(transaction).catch((error) => {
|
|
52
|
+
throw error !== null && error !== void 0 ? error : new DOMException('IDB write transaction aborted without an error', 'AbortError');
|
|
53
|
+
});
|
|
54
|
+
}
|
|
44
55
|
const provider = {
|
|
45
56
|
// We don't want to initialize the store while the JS bundle loads as idb-keyval will try to use global.indexedDB
|
|
46
57
|
// which might not be available in certain environments that load the bundle (e.g. electron main process).
|
|
@@ -66,7 +77,13 @@ const provider = {
|
|
|
66
77
|
if (value === null) {
|
|
67
78
|
return provider.removeItem(key);
|
|
68
79
|
}
|
|
69
|
-
|
|
80
|
+
// Drive the write through the manual store transaction so promisifyWriteTransaction can
|
|
81
|
+
// normalize a null abort error — idb-keyval's IDB.set() awaits the raw transaction and
|
|
82
|
+
// would propagate the unclassifiable "Error: null".
|
|
83
|
+
return provider.store('readwrite', (store) => {
|
|
84
|
+
store.put(value, key);
|
|
85
|
+
return promisifyWriteTransaction(store.transaction);
|
|
86
|
+
});
|
|
70
87
|
},
|
|
71
88
|
multiGet(keysParam) {
|
|
72
89
|
if (!provider.store) {
|
|
@@ -95,7 +112,7 @@ const provider = {
|
|
|
95
112
|
store.put(newValue, key);
|
|
96
113
|
}
|
|
97
114
|
}
|
|
98
|
-
return
|
|
115
|
+
return promisifyWriteTransaction(store.transaction);
|
|
99
116
|
});
|
|
100
117
|
});
|
|
101
118
|
},
|
|
@@ -116,7 +133,7 @@ const provider = {
|
|
|
116
133
|
store.put(value, key);
|
|
117
134
|
}
|
|
118
135
|
}
|
|
119
|
-
return
|
|
136
|
+
return promisifyWriteTransaction(store.transaction);
|
|
120
137
|
});
|
|
121
138
|
},
|
|
122
139
|
clear() {
|
|
@@ -149,13 +166,21 @@ const provider = {
|
|
|
149
166
|
if (!provider.store) {
|
|
150
167
|
throw new Error('Store not initialized!');
|
|
151
168
|
}
|
|
152
|
-
return
|
|
169
|
+
return provider.store('readwrite', (store) => {
|
|
170
|
+
store.delete(key);
|
|
171
|
+
return promisifyWriteTransaction(store.transaction);
|
|
172
|
+
});
|
|
153
173
|
},
|
|
154
174
|
removeItems(keysParam) {
|
|
155
175
|
if (!provider.store) {
|
|
156
176
|
throw new Error('Store not initialized!');
|
|
157
177
|
}
|
|
158
|
-
return
|
|
178
|
+
return provider.store('readwrite', (store) => {
|
|
179
|
+
for (const key of keysParam) {
|
|
180
|
+
store.delete(key);
|
|
181
|
+
}
|
|
182
|
+
return promisifyWriteTransaction(store.transaction);
|
|
183
|
+
});
|
|
159
184
|
},
|
|
160
185
|
getDatabaseSize() {
|
|
161
186
|
if (!provider.store) {
|