react-native-onyx 3.0.119 → 3.0.120
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/dist/OnyxUtils.d.ts +4 -4
- package/dist/OnyxUtils.js +7 -4
- package/dist/storage/__mocks__/index.d.ts +1 -0
- package/dist/storage/__mocks__/index.js +1 -0
- package/dist/storage/errors.d.ts +4 -4
- package/dist/storage/errors.js +4 -4
- package/dist/storage/index.d.ts +1 -0
- package/dist/storage/index.js +10 -1
- package/dist/storage/providers/IDBKeyValProvider/classifyError.js +39 -2
- package/dist/storage/providers/IDBKeyValProvider/createStore.js +9 -3
- package/dist/storage/providers/IDBKeyValProvider/errorMessages.d.ts +11 -0
- package/dist/storage/providers/IDBKeyValProvider/errorMessages.js +14 -0
- package/dist/storage/providers/IDBKeyValProvider/index.js +3 -2
- package/dist/storage/providers/IDBKeyValProvider/isIndexedDBAvailable.d.ts +0 -3
- package/dist/storage/providers/IDBKeyValProvider/isIndexedDBAvailable.js +0 -4
- package/package.json +1 -1
package/dist/OnyxUtils.d.ts
CHANGED
|
@@ -122,16 +122,16 @@ declare function reportStorageQuota(error?: Error): Promise<void>;
|
|
|
122
122
|
* The connection layer (createStore) owns connection/transport recovery; this operation layer owns
|
|
123
123
|
* capacity recovery (eviction) so that a given failure is retried by exactly one layer:
|
|
124
124
|
* - INVALID_DATA: logs an alert and throws (the same data will always fail).
|
|
125
|
-
* - TRANSIENT / FATAL: the connection layer already retried (transient) or
|
|
126
|
-
*
|
|
125
|
+
* - TRANSIENT / FATAL: the connection layer already retried (transient) or spent a heal attempt
|
|
126
|
+
* (fatal). Retrying here would only re-amplify, so we skip the write quietly.
|
|
127
127
|
* - CAPACITY: evicts the least recently accessed evictable key and retries, under a session-level
|
|
128
128
|
* circuit breaker (see lib/StorageCircuitBreaker.ts) that halts the loop once eviction stops making
|
|
129
129
|
* progress or failures storm — the per-operation budget alone cannot stop a session-wide storm.
|
|
130
130
|
* - DISK_PRESSURE: the device disk itself is full (or the database files are unreadable), so neither
|
|
131
131
|
* retries nor in-DB eviction can free space — the write is dropped (cache stays authoritative) with
|
|
132
132
|
* a single throttled alert + quota snapshot per burst.
|
|
133
|
-
* - UNAVAILABLE: the storage engine does not exist in this environment
|
|
134
|
-
* already degraded to the in-memory provider. No retry.
|
|
133
|
+
* - UNAVAILABLE: the storage engine does not exist in this environment or its heal budget ran out, so
|
|
134
|
+
* the storage layer has already degraded to the in-memory provider. No retry.
|
|
135
135
|
* - UNKNOWN: the provider couldn't classify it — log the full error shape (name + message +
|
|
136
136
|
* provider) once so it's visible, then bounded retry without eviction.
|
|
137
137
|
*/
|
package/dist/OnyxUtils.js
CHANGED
|
@@ -469,16 +469,16 @@ function reportStorageQuota(error) {
|
|
|
469
469
|
* The connection layer (createStore) owns connection/transport recovery; this operation layer owns
|
|
470
470
|
* capacity recovery (eviction) so that a given failure is retried by exactly one layer:
|
|
471
471
|
* - INVALID_DATA: logs an alert and throws (the same data will always fail).
|
|
472
|
-
* - TRANSIENT / FATAL: the connection layer already retried (transient) or
|
|
473
|
-
*
|
|
472
|
+
* - TRANSIENT / FATAL: the connection layer already retried (transient) or spent a heal attempt
|
|
473
|
+
* (fatal). Retrying here would only re-amplify, so we skip the write quietly.
|
|
474
474
|
* - CAPACITY: evicts the least recently accessed evictable key and retries, under a session-level
|
|
475
475
|
* circuit breaker (see lib/StorageCircuitBreaker.ts) that halts the loop once eviction stops making
|
|
476
476
|
* progress or failures storm — the per-operation budget alone cannot stop a session-wide storm.
|
|
477
477
|
* - DISK_PRESSURE: the device disk itself is full (or the database files are unreadable), so neither
|
|
478
478
|
* retries nor in-DB eviction can free space — the write is dropped (cache stays authoritative) with
|
|
479
479
|
* a single throttled alert + quota snapshot per burst.
|
|
480
|
-
* - UNAVAILABLE: the storage engine does not exist in this environment
|
|
481
|
-
* already degraded to the in-memory provider. No retry.
|
|
480
|
+
* - UNAVAILABLE: the storage engine does not exist in this environment or its heal budget ran out, so
|
|
481
|
+
* the storage layer has already degraded to the in-memory provider. No retry.
|
|
482
482
|
* - UNKNOWN: the provider couldn't classify it — log the full error shape (name + message +
|
|
483
483
|
* provider) once so it's visible, then bounded retry without eviction.
|
|
484
484
|
*/
|
|
@@ -752,6 +752,9 @@ function initializeWithDefaultKeyStates() {
|
|
|
752
752
|
})
|
|
753
753
|
.catch((error) => {
|
|
754
754
|
Logger.logAlert(`Failed to load data from storage during init. The app will boot with default key states only. Error: ${error}`);
|
|
755
|
+
if (storage_1.default.classifyError(error) === errors_1.StorageErrorClass.FATAL) {
|
|
756
|
+
storage_1.default.degradeToMemoryOnly(error);
|
|
757
|
+
}
|
|
755
758
|
// Populate the key index so getAllKeys() returns correct results for default keys.
|
|
756
759
|
// Without this, subscribers that check getAllKeys() would see an empty set even
|
|
757
760
|
// though we have default values in cache.
|
|
@@ -12,6 +12,7 @@ declare const StorageMock: {
|
|
|
12
12
|
getStorageProvider: jest.Mock<import("../providers/types").default<{
|
|
13
13
|
[x: string]: unknown;
|
|
14
14
|
}>, [], any>;
|
|
15
|
+
degradeToMemoryOnly: jest.Mock<any, any, any>;
|
|
15
16
|
getItem: jest.Mock<Promise<unknown>, [key: any], any>;
|
|
16
17
|
multiGet: jest.Mock<Promise<import("../providers/types").StorageKeyValuePair[]>, [keys: import("../providers/types").StorageKeyList], any>;
|
|
17
18
|
setItem: jest.Mock<Promise<void>, [key: any, value: unknown], any>;
|
|
@@ -53,6 +53,7 @@ const StorageMock = {
|
|
|
53
53
|
init,
|
|
54
54
|
classifyError: jest.fn(classifyError),
|
|
55
55
|
getStorageProvider: jest.fn(() => MemoryOnlyProvider_1.default),
|
|
56
|
+
degradeToMemoryOnly: jest.fn(),
|
|
56
57
|
getItem: jest.fn(MemoryOnlyProvider_1.default.getItem),
|
|
57
58
|
multiGet: jest.fn(MemoryOnlyProvider_1.default.multiGet),
|
|
58
59
|
setItem: jest.fn(MemoryOnlyProvider_1.default.setItem),
|
package/dist/storage/errors.d.ts
CHANGED
|
@@ -22,12 +22,12 @@ declare const StorageErrorClass: {
|
|
|
22
22
|
readonly DISK_PRESSURE: "diskPressure";
|
|
23
23
|
/** Non-serializable payload. Never retriable — the same data will always fail. */
|
|
24
24
|
readonly INVALID_DATA: "invalidData";
|
|
25
|
-
/** Backing-store corruption. Owner: connection layer — budgeted heal, then
|
|
25
|
+
/** Backing-store corruption. Owner: connection layer — budgeted heal, then rethrow as UNAVAILABLE. */
|
|
26
26
|
readonly FATAL: "fatal";
|
|
27
27
|
/** The storage engine itself does not exist in this environment (for example `indexedDB` is an
|
|
28
|
-
* undeclared global in Chrome for iOS private tabs and in Lockdown Mode)
|
|
29
|
-
* engine cannot
|
|
30
|
-
* to the in-memory provider. */
|
|
28
|
+
* undeclared global in Chrome for iOS private tabs and in Lockdown Mode), or a FATAL heal budget
|
|
29
|
+
* ran out. Never retriable — the engine cannot recover mid-session. Owner: the storage layer —
|
|
30
|
+
* degrade to the in-memory provider. */
|
|
31
31
|
readonly UNAVAILABLE: "unavailable";
|
|
32
32
|
/** Unmatched by the active provider. Owner: operation layer — bounded retry, and log the shape so
|
|
33
33
|
* recurring cases can be promoted into one of the classes above. */
|
package/dist/storage/errors.js
CHANGED
|
@@ -26,12 +26,12 @@ const StorageErrorClass = {
|
|
|
26
26
|
DISK_PRESSURE: 'diskPressure',
|
|
27
27
|
/** Non-serializable payload. Never retriable — the same data will always fail. */
|
|
28
28
|
INVALID_DATA: 'invalidData',
|
|
29
|
-
/** Backing-store corruption. Owner: connection layer — budgeted heal, then
|
|
29
|
+
/** Backing-store corruption. Owner: connection layer — budgeted heal, then rethrow as UNAVAILABLE. */
|
|
30
30
|
FATAL: 'fatal',
|
|
31
31
|
/** The storage engine itself does not exist in this environment (for example `indexedDB` is an
|
|
32
|
-
* undeclared global in Chrome for iOS private tabs and in Lockdown Mode)
|
|
33
|
-
* engine cannot
|
|
34
|
-
* to the in-memory provider. */
|
|
32
|
+
* undeclared global in Chrome for iOS private tabs and in Lockdown Mode), or a FATAL heal budget
|
|
33
|
+
* ran out. Never retriable — the engine cannot recover mid-session. Owner: the storage layer —
|
|
34
|
+
* degrade to the in-memory provider. */
|
|
35
35
|
UNAVAILABLE: 'unavailable',
|
|
36
36
|
/** Unmatched by the active provider. Owner: operation layer — bounded retry, and log the shape so
|
|
37
37
|
* recurring cases can be promoted into one of the classes above. */
|
package/dist/storage/index.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type StorageProvider from './providers/types';
|
|
2
2
|
type Storage = {
|
|
3
3
|
getStorageProvider: () => StorageProvider<unknown>;
|
|
4
|
+
degradeToMemoryOnly: (error: unknown) => void;
|
|
4
5
|
} & Omit<StorageProvider<unknown>, 'name' | 'store'>;
|
|
5
6
|
declare const storage: Storage;
|
|
6
7
|
export default storage;
|
package/dist/storage/index.js
CHANGED
|
@@ -56,6 +56,9 @@ const initPromise = new Promise((resolve) => {
|
|
|
56
56
|
* Degrade performance by removing the storage provider and only using cache
|
|
57
57
|
*/
|
|
58
58
|
function degradePerformance(error) {
|
|
59
|
+
if (provider === MemoryOnlyProvider_1.default) {
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
59
62
|
let causeMessage = '';
|
|
60
63
|
if (error.cause instanceof Error) {
|
|
61
64
|
causeMessage = error.cause.message;
|
|
@@ -95,6 +98,7 @@ const storage = {
|
|
|
95
98
|
getStorageProvider() {
|
|
96
99
|
return provider;
|
|
97
100
|
},
|
|
101
|
+
degradeToMemoryOnly: (error) => degradePerformance(error instanceof Error ? error : new Error(String(error))),
|
|
98
102
|
/**
|
|
99
103
|
* Classifies a write error using the platform provider's own classifier. Synchronous and pure —
|
|
100
104
|
* never wrapped in tryOrDegradePerformance.
|
|
@@ -212,7 +216,12 @@ const storage = {
|
|
|
212
216
|
if (!InstanceSync_1.default.shouldBeUsed)
|
|
213
217
|
return;
|
|
214
218
|
shouldKeepInstancesSync = true;
|
|
215
|
-
InstanceSync_1.default.init(
|
|
219
|
+
InstanceSync_1.default.init((pairs) => {
|
|
220
|
+
if (provider === MemoryOnlyProvider_1.default) {
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
onStorageKeysChanged(pairs);
|
|
224
|
+
}, this);
|
|
216
225
|
},
|
|
217
226
|
};
|
|
218
227
|
exports.default = storage;
|
|
@@ -1,7 +1,40 @@
|
|
|
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
|
+
})();
|
|
2
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
36
|
const errors_1 = require("../../errors");
|
|
4
|
-
const
|
|
37
|
+
const errorMessages_1 = __importStar(require("./errorMessages"));
|
|
5
38
|
/**
|
|
6
39
|
* Classifies an IndexedDB write failure into the shared storage taxonomy (lib/storage/errors.ts).
|
|
7
40
|
* Matching is done on the lowercased error name and message. This is the IndexedDB engine's own
|
|
@@ -10,7 +43,11 @@ const isIndexedDBAvailable_1 = require("./isIndexedDBAvailable");
|
|
|
10
43
|
function classifyIDBError(error) {
|
|
11
44
|
const { name, message } = (0, errors_1.getErrorParts)(error);
|
|
12
45
|
// The engine is absent, not broken e.g. private tabs and Lockdown Mode on WebKit.
|
|
13
|
-
if (message.includes("can't find variable: indexeddb") || message.includes('indexeddb is not defined') || message.includes(
|
|
46
|
+
if (message.includes("can't find variable: indexeddb") || message.includes('indexeddb is not defined') || message.includes(errorMessages_1.default.UNAVAILABLE.toLowerCase())) {
|
|
47
|
+
return errors_1.StorageErrorClass.UNAVAILABLE;
|
|
48
|
+
}
|
|
49
|
+
// Reopen budget spent on FATAL errors, so the store is treated as gone for the rest of the session.
|
|
50
|
+
if (name === errorMessages_1.IDBErrorName.HEAL_EXHAUSTED.toLowerCase()) {
|
|
14
51
|
return errors_1.StorageErrorClass.UNAVAILABLE;
|
|
15
52
|
}
|
|
16
53
|
// Non-serializable data passed to IDBObjectStore.put — retrying is futile.
|
|
@@ -40,7 +40,8 @@ const IDB = __importStar(require("idb-keyval"));
|
|
|
40
40
|
const Logger = __importStar(require("../../../Logger"));
|
|
41
41
|
const errors_1 = require("../../errors");
|
|
42
42
|
const classifyError_1 = __importDefault(require("./classifyError"));
|
|
43
|
-
const
|
|
43
|
+
const errorMessages_1 = __importStar(require("./errorMessages"));
|
|
44
|
+
const isIndexedDBAvailable_1 = __importDefault(require("./isIndexedDBAvailable"));
|
|
44
45
|
const HEAL_ATTEMPTS_MAX = 3;
|
|
45
46
|
// This is a copy of the createStore function from idb-keyval, we need a custom implementation
|
|
46
47
|
// because we need to create the database manually in order to ensure that the store exists before we use it.
|
|
@@ -90,7 +91,7 @@ function createStore(dbName, storeName) {
|
|
|
90
91
|
if (dbp)
|
|
91
92
|
return dbp;
|
|
92
93
|
if (!(0, isIndexedDBAvailable_1.default)()) {
|
|
93
|
-
return Promise.reject(new Error(
|
|
94
|
+
return Promise.reject(new Error(errorMessages_1.default.UNAVAILABLE));
|
|
94
95
|
}
|
|
95
96
|
const request = indexedDB.open(dbName);
|
|
96
97
|
request.onupgradeneeded = () => request.result.createObjectStore(storeName);
|
|
@@ -218,12 +219,17 @@ function createStore(dbName, storeName) {
|
|
|
218
219
|
});
|
|
219
220
|
}
|
|
220
221
|
if (errorClass === errors_1.StorageErrorClass.FATAL) {
|
|
222
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
221
223
|
Logger.logAlert('IDB heal: backing store error — heal budget exhausted, giving up', {
|
|
222
224
|
dbName,
|
|
223
225
|
storeName,
|
|
226
|
+
errorMessage,
|
|
224
227
|
});
|
|
228
|
+
const healExhaustedError = new Error(`${errorMessages_1.default.HEAL_EXHAUSTED}: ${errorMessage}`, { cause: error });
|
|
229
|
+
healExhaustedError.name = errorMessages_1.IDBErrorName.HEAL_EXHAUSTED;
|
|
230
|
+
throw healExhaustedError;
|
|
225
231
|
}
|
|
226
|
-
|
|
232
|
+
if (errorClass === errors_1.StorageErrorClass.UNKNOWN) {
|
|
227
233
|
// UNKNOWN — unexpected at this layer; record it so it's visible. CAPACITY is the
|
|
228
234
|
// expected propagation path (the operation layer owns its logging, and suppresses it
|
|
229
235
|
// entirely once the circuit breaker is open), so we do NOT log it here — doing so was a
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** Messages of errors thrown by the IndexedDB provider itself. `classifyIDBError` maps `UNAVAILABLE` to `StorageErrorClass.UNAVAILABLE`. */
|
|
2
|
+
declare const IDBErrorMessage: {
|
|
3
|
+
readonly UNAVAILABLE: "IndexedDB is not available in this environment";
|
|
4
|
+
readonly HEAL_EXHAUSTED: "IndexedDB heal budget exhausted";
|
|
5
|
+
};
|
|
6
|
+
/** Names of errors thrown by the IndexedDB provider itself. `classifyIDBError` maps `HEAL_EXHAUSTED` to `StorageErrorClass.UNAVAILABLE`. */
|
|
7
|
+
declare const IDBErrorName: {
|
|
8
|
+
readonly HEAL_EXHAUSTED: "IDBHealExhaustedError";
|
|
9
|
+
};
|
|
10
|
+
export default IDBErrorMessage;
|
|
11
|
+
export { IDBErrorName };
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.IDBErrorName = void 0;
|
|
4
|
+
/** Messages of errors thrown by the IndexedDB provider itself. `classifyIDBError` maps `UNAVAILABLE` to `StorageErrorClass.UNAVAILABLE`. */
|
|
5
|
+
const IDBErrorMessage = {
|
|
6
|
+
UNAVAILABLE: 'IndexedDB is not available in this environment',
|
|
7
|
+
HEAL_EXHAUSTED: 'IndexedDB heal budget exhausted',
|
|
8
|
+
};
|
|
9
|
+
/** Names of errors thrown by the IndexedDB provider itself. `classifyIDBError` maps `HEAL_EXHAUSTED` to `StorageErrorClass.UNAVAILABLE`. */
|
|
10
|
+
const IDBErrorName = {
|
|
11
|
+
HEAL_EXHAUSTED: 'IDBHealExhaustedError',
|
|
12
|
+
};
|
|
13
|
+
exports.IDBErrorName = IDBErrorName;
|
|
14
|
+
exports.default = IDBErrorMessage;
|
|
@@ -40,7 +40,8 @@ const IDB = __importStar(require("idb-keyval"));
|
|
|
40
40
|
const utils_1 = __importDefault(require("../../../utils"));
|
|
41
41
|
const createStore_1 = __importDefault(require("./createStore"));
|
|
42
42
|
const classifyError_1 = __importDefault(require("./classifyError"));
|
|
43
|
-
const
|
|
43
|
+
const errorMessages_1 = __importDefault(require("./errorMessages"));
|
|
44
|
+
const isIndexedDBAvailable_1 = __importDefault(require("./isIndexedDBAvailable"));
|
|
44
45
|
const DB_NAME = 'OnyxDB';
|
|
45
46
|
const STORE_NAME = 'keyvaluepairs';
|
|
46
47
|
/**
|
|
@@ -71,7 +72,7 @@ const provider = {
|
|
|
71
72
|
*/
|
|
72
73
|
init() {
|
|
73
74
|
if (!(0, isIndexedDBAvailable_1.default)()) {
|
|
74
|
-
throw new Error(`IDBKeyVal store could not be created: ${
|
|
75
|
+
throw new Error(`IDBKeyVal store could not be created: ${errorMessages_1.default.UNAVAILABLE}`);
|
|
75
76
|
}
|
|
76
77
|
const newIdbKeyValStore = (0, createStore_1.default)(DB_NAME, STORE_NAME);
|
|
77
78
|
if (newIdbKeyValStore == null) {
|
|
@@ -1,9 +1,6 @@
|
|
|
1
|
-
/** Matched by `classifyIDBError` so a guarded failure classifies as UNAVAILABLE like an unguarded one. */
|
|
2
|
-
declare const INDEXED_DB_UNAVAILABLE_MESSAGE = "indexedDB is not available in this environment";
|
|
3
1
|
/**
|
|
4
2
|
* `typeof` instead of `indexedDB === undefined`: on WebKit the global is undeclared in private tabs and
|
|
5
3
|
* Lockdown Mode, so reading it throws a `ReferenceError` instead of evaluating to `undefined`.
|
|
6
4
|
*/
|
|
7
5
|
declare function isIndexedDBAvailable(): boolean;
|
|
8
6
|
export default isIndexedDBAvailable;
|
|
9
|
-
export { INDEXED_DB_UNAVAILABLE_MESSAGE };
|
|
@@ -1,9 +1,5 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.INDEXED_DB_UNAVAILABLE_MESSAGE = void 0;
|
|
4
|
-
/** Matched by `classifyIDBError` so a guarded failure classifies as UNAVAILABLE like an unguarded one. */
|
|
5
|
-
const INDEXED_DB_UNAVAILABLE_MESSAGE = 'indexedDB is not available in this environment';
|
|
6
|
-
exports.INDEXED_DB_UNAVAILABLE_MESSAGE = INDEXED_DB_UNAVAILABLE_MESSAGE;
|
|
7
3
|
/**
|
|
8
4
|
* `typeof` instead of `indexedDB === undefined`: on WebKit the global is undeclared in private tabs and
|
|
9
5
|
* Lockdown Mode, so reading it throws a `ReferenceError` instead of evaluating to `undefined`.
|