react-native-nitro-storage 0.4.3 → 0.4.5
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/README.md +108 -8
- package/android/src/main/java/com/nitrostorage/AndroidStorageAdapter.kt +61 -10
- package/ios/IOSStorageAdapterCpp.mm +44 -14
- package/lib/commonjs/index.js +221 -5
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/index.web.js +444 -202
- package/lib/commonjs/index.web.js.map +1 -1
- package/lib/commonjs/indexeddb-backend.js +129 -7
- package/lib/commonjs/indexeddb-backend.js.map +1 -1
- package/lib/commonjs/storage-runtime.js +41 -0
- package/lib/commonjs/storage-runtime.js.map +1 -0
- package/lib/commonjs/web-storage-backend.js +90 -0
- package/lib/commonjs/web-storage-backend.js.map +1 -0
- package/lib/module/index.js +213 -5
- package/lib/module/index.js.map +1 -1
- package/lib/module/index.web.js +436 -202
- package/lib/module/index.web.js.map +1 -1
- package/lib/module/indexeddb-backend.js +129 -7
- package/lib/module/indexeddb-backend.js.map +1 -1
- package/lib/module/storage-runtime.js +36 -0
- package/lib/module/storage-runtime.js.map +1 -0
- package/lib/module/web-storage-backend.js +86 -0
- package/lib/module/web-storage-backend.js.map +1 -0
- package/lib/typescript/index.d.ts +11 -7
- package/lib/typescript/index.d.ts.map +1 -1
- package/lib/typescript/index.web.d.ts +12 -8
- package/lib/typescript/index.web.d.ts.map +1 -1
- package/lib/typescript/indexeddb-backend.d.ts +6 -2
- package/lib/typescript/indexeddb-backend.d.ts.map +1 -1
- package/lib/typescript/storage-runtime.d.ts +16 -0
- package/lib/typescript/storage-runtime.d.ts.map +1 -0
- package/lib/typescript/web-storage-backend.d.ts +30 -0
- package/lib/typescript/web-storage-backend.d.ts.map +1 -0
- package/nitro.json +8 -2
- package/nitrogen/generated/ios/NitroStorage+autolinking.rb +2 -0
- package/package.json +2 -2
- package/src/index.ts +268 -21
- package/src/index.web.ts +601 -246
- package/src/indexeddb-backend.ts +147 -6
- package/src/storage-runtime.ts +94 -0
- package/src/web-storage-backend.ts +129 -0
package/src/indexeddb-backend.ts
CHANGED
|
@@ -1,9 +1,17 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
WebSecureStorageBackend,
|
|
3
|
+
WebStorageChangeEvent,
|
|
4
|
+
} from "./web-storage-backend";
|
|
2
5
|
|
|
3
6
|
const DEFAULT_DB_NAME = "nitro-storage-secure";
|
|
4
7
|
const DEFAULT_STORE_NAME = "keyvalue";
|
|
5
8
|
const DB_VERSION = 1;
|
|
6
9
|
|
|
10
|
+
export type IndexedDBBackendOptions = {
|
|
11
|
+
channelName?: string;
|
|
12
|
+
onError?: (error: Error) => void;
|
|
13
|
+
};
|
|
14
|
+
|
|
7
15
|
/**
|
|
8
16
|
* Opens (or creates) an IndexedDB database and returns the underlying IDBDatabase.
|
|
9
17
|
* Rejects if IndexedDB is unavailable in the current environment.
|
|
@@ -59,9 +67,62 @@ function openDB(dbName: string, storeName: string): Promise<IDBDatabase> {
|
|
|
59
67
|
export async function createIndexedDBBackend(
|
|
60
68
|
dbName = DEFAULT_DB_NAME,
|
|
61
69
|
storeName = DEFAULT_STORE_NAME,
|
|
70
|
+
options: IndexedDBBackendOptions = {},
|
|
62
71
|
): Promise<WebSecureStorageBackend> {
|
|
63
72
|
const db = await openDB(dbName, storeName);
|
|
64
73
|
const cache = new Map<string, string>();
|
|
74
|
+
const pendingWrites = new Set<Promise<void>>();
|
|
75
|
+
const subscribers = new Set<(event: WebStorageChangeEvent) => void>();
|
|
76
|
+
const sourceId = `nitro-storage-${Math.random().toString(36).slice(2)}`;
|
|
77
|
+
const channelName =
|
|
78
|
+
options.channelName ?? `nitro-storage:${dbName}:${storeName}`;
|
|
79
|
+
const channel =
|
|
80
|
+
typeof BroadcastChannel !== "undefined"
|
|
81
|
+
? new BroadcastChannel(channelName)
|
|
82
|
+
: null;
|
|
83
|
+
|
|
84
|
+
function emitExternal(event: WebStorageChangeEvent): void {
|
|
85
|
+
subscribers.forEach((subscriber) => {
|
|
86
|
+
subscriber(event);
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function handleAsyncError(error: unknown): void {
|
|
91
|
+
const normalized =
|
|
92
|
+
error instanceof Error
|
|
93
|
+
? error
|
|
94
|
+
: new Error(String(error ?? "Unknown IndexedDB error"));
|
|
95
|
+
options.onError?.(normalized);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
channel?.addEventListener("message", (event: MessageEvent) => {
|
|
99
|
+
const data = event.data as
|
|
100
|
+
| (WebStorageChangeEvent & { sourceId?: string })
|
|
101
|
+
| undefined;
|
|
102
|
+
if (!data || data.sourceId === sourceId) {
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (data.key === null) {
|
|
107
|
+
cache.clear();
|
|
108
|
+
} else if (data.newValue === null) {
|
|
109
|
+
cache.delete(data.key);
|
|
110
|
+
} else {
|
|
111
|
+
cache.set(data.key, data.newValue);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
emitExternal({
|
|
115
|
+
key: data.key,
|
|
116
|
+
newValue: data.newValue,
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
function publish(event: WebStorageChangeEvent): void {
|
|
121
|
+
channel?.postMessage({
|
|
122
|
+
...event,
|
|
123
|
+
sourceId,
|
|
124
|
+
});
|
|
125
|
+
}
|
|
65
126
|
|
|
66
127
|
// Hydrate the in-memory cache from IndexedDB.
|
|
67
128
|
await new Promise<void>((resolve, reject) => {
|
|
@@ -85,14 +146,39 @@ export async function createIndexedDBBackend(
|
|
|
85
146
|
reject(tx.error ?? new Error("Failed to load IndexedDB entries."));
|
|
86
147
|
});
|
|
87
148
|
|
|
88
|
-
|
|
89
|
-
|
|
149
|
+
function trackWrite(tx: IDBTransaction): void {
|
|
150
|
+
const pending = new Promise<void>((resolve) => {
|
|
151
|
+
tx.oncomplete = () => resolve();
|
|
152
|
+
tx.onerror = () => {
|
|
153
|
+
handleAsyncError(
|
|
154
|
+
tx.error ?? new Error("Failed to persist IndexedDB transaction."),
|
|
155
|
+
);
|
|
156
|
+
resolve();
|
|
157
|
+
};
|
|
158
|
+
tx.onabort = () => {
|
|
159
|
+
handleAsyncError(
|
|
160
|
+
tx.error ?? new Error("IndexedDB transaction was aborted."),
|
|
161
|
+
);
|
|
162
|
+
resolve();
|
|
163
|
+
};
|
|
164
|
+
});
|
|
165
|
+
pendingWrites.add(pending);
|
|
166
|
+
void pending.finally(() => {
|
|
167
|
+
pendingWrites.delete(pending);
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** Fire-and-forget IndexedDB write. The in-memory cache remains authoritative,
|
|
172
|
+
* but async persistence failures are surfaced through `onError` and `flush()`. */
|
|
90
173
|
function persistSet(key: string, value: string): void {
|
|
91
174
|
try {
|
|
92
175
|
const tx = db.transaction(storeName, "readwrite");
|
|
93
176
|
tx.objectStore(storeName).put(value, key);
|
|
177
|
+
trackWrite(tx);
|
|
94
178
|
} catch {
|
|
95
|
-
|
|
179
|
+
handleAsyncError(
|
|
180
|
+
new Error(`Failed to queue IndexedDB write for "${key}".`),
|
|
181
|
+
);
|
|
96
182
|
}
|
|
97
183
|
}
|
|
98
184
|
|
|
@@ -100,8 +186,11 @@ export async function createIndexedDBBackend(
|
|
|
100
186
|
try {
|
|
101
187
|
const tx = db.transaction(storeName, "readwrite");
|
|
102
188
|
tx.objectStore(storeName).delete(key);
|
|
189
|
+
trackWrite(tx);
|
|
103
190
|
} catch {
|
|
104
|
-
|
|
191
|
+
handleAsyncError(
|
|
192
|
+
new Error(`Failed to queue IndexedDB delete for "${key}".`),
|
|
193
|
+
);
|
|
105
194
|
}
|
|
106
195
|
}
|
|
107
196
|
|
|
@@ -109,12 +198,14 @@ export async function createIndexedDBBackend(
|
|
|
109
198
|
try {
|
|
110
199
|
const tx = db.transaction(storeName, "readwrite");
|
|
111
200
|
tx.objectStore(storeName).clear();
|
|
201
|
+
trackWrite(tx);
|
|
112
202
|
} catch {
|
|
113
|
-
|
|
203
|
+
handleAsyncError(new Error("Failed to queue IndexedDB clear."));
|
|
114
204
|
}
|
|
115
205
|
}
|
|
116
206
|
|
|
117
207
|
const backend: WebSecureStorageBackend = {
|
|
208
|
+
name: `indexeddb:${dbName}/${storeName}`,
|
|
118
209
|
getItem(key: string): string | null {
|
|
119
210
|
return cache.get(key) ?? null;
|
|
120
211
|
},
|
|
@@ -122,21 +213,71 @@ export async function createIndexedDBBackend(
|
|
|
122
213
|
setItem(key: string, value: string): void {
|
|
123
214
|
cache.set(key, value);
|
|
124
215
|
persistSet(key, value);
|
|
216
|
+
publish({ key, newValue: value });
|
|
125
217
|
},
|
|
126
218
|
|
|
127
219
|
removeItem(key: string): void {
|
|
128
220
|
cache.delete(key);
|
|
129
221
|
persistDelete(key);
|
|
222
|
+
publish({ key, newValue: null });
|
|
130
223
|
},
|
|
131
224
|
|
|
132
225
|
clear(): void {
|
|
133
226
|
cache.clear();
|
|
134
227
|
persistClear();
|
|
228
|
+
publish({ key: null, newValue: null });
|
|
135
229
|
},
|
|
136
230
|
|
|
137
231
|
getAllKeys(): string[] {
|
|
138
232
|
return Array.from(cache.keys());
|
|
139
233
|
},
|
|
234
|
+
getMany(keys: string[]): (string | null)[] {
|
|
235
|
+
return keys.map((key) => cache.get(key) ?? null);
|
|
236
|
+
},
|
|
237
|
+
setMany(entries): void {
|
|
238
|
+
entries.forEach(([key, value]) => {
|
|
239
|
+
cache.set(key, value);
|
|
240
|
+
});
|
|
241
|
+
try {
|
|
242
|
+
const tx = db.transaction(storeName, "readwrite");
|
|
243
|
+
const store = tx.objectStore(storeName);
|
|
244
|
+
entries.forEach(([key, value]) => {
|
|
245
|
+
store.put(value, key);
|
|
246
|
+
publish({ key, newValue: value });
|
|
247
|
+
});
|
|
248
|
+
trackWrite(tx);
|
|
249
|
+
} catch {
|
|
250
|
+
handleAsyncError(new Error("Failed to queue IndexedDB batch write."));
|
|
251
|
+
}
|
|
252
|
+
},
|
|
253
|
+
removeMany(keys: string[]): void {
|
|
254
|
+
keys.forEach((key) => {
|
|
255
|
+
cache.delete(key);
|
|
256
|
+
});
|
|
257
|
+
try {
|
|
258
|
+
const tx = db.transaction(storeName, "readwrite");
|
|
259
|
+
const store = tx.objectStore(storeName);
|
|
260
|
+
keys.forEach((key) => {
|
|
261
|
+
store.delete(key);
|
|
262
|
+
publish({ key, newValue: null });
|
|
263
|
+
});
|
|
264
|
+
trackWrite(tx);
|
|
265
|
+
} catch {
|
|
266
|
+
handleAsyncError(new Error("Failed to queue IndexedDB batch delete."));
|
|
267
|
+
}
|
|
268
|
+
},
|
|
269
|
+
size(): number {
|
|
270
|
+
return cache.size;
|
|
271
|
+
},
|
|
272
|
+
subscribe(listener): () => void {
|
|
273
|
+
subscribers.add(listener);
|
|
274
|
+
return () => {
|
|
275
|
+
subscribers.delete(listener);
|
|
276
|
+
};
|
|
277
|
+
},
|
|
278
|
+
async flush(): Promise<void> {
|
|
279
|
+
await Promise.all(Array.from(pendingWrites));
|
|
280
|
+
},
|
|
140
281
|
};
|
|
141
282
|
|
|
142
283
|
return backend;
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
export type StorageErrorCode =
|
|
2
|
+
| "keychain_locked"
|
|
3
|
+
| "authentication_required"
|
|
4
|
+
| "key_invalidated"
|
|
5
|
+
| "storage_corruption"
|
|
6
|
+
| "biometric_unavailable"
|
|
7
|
+
| "unsupported";
|
|
8
|
+
|
|
9
|
+
export type StorageCapabilities = {
|
|
10
|
+
platform: "native" | "web";
|
|
11
|
+
backend: {
|
|
12
|
+
disk: string;
|
|
13
|
+
secure: string;
|
|
14
|
+
};
|
|
15
|
+
writeBuffering: {
|
|
16
|
+
disk: boolean;
|
|
17
|
+
secure: boolean;
|
|
18
|
+
};
|
|
19
|
+
errorClassification: boolean;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const STORAGE_ERROR_TAG_PATTERN = /\[nitro-error:([a-z_]+)\]/;
|
|
23
|
+
|
|
24
|
+
export function getStorageErrorCode(
|
|
25
|
+
err: unknown,
|
|
26
|
+
): StorageErrorCode | undefined {
|
|
27
|
+
if (!(err instanceof Error)) {
|
|
28
|
+
return undefined;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const message = err.message;
|
|
32
|
+
const taggedCode = message.match(STORAGE_ERROR_TAG_PATTERN)?.[1];
|
|
33
|
+
|
|
34
|
+
if (
|
|
35
|
+
taggedCode === "keychain_locked" ||
|
|
36
|
+
taggedCode === "authentication_required" ||
|
|
37
|
+
taggedCode === "key_invalidated" ||
|
|
38
|
+
taggedCode === "storage_corruption" ||
|
|
39
|
+
taggedCode === "biometric_unavailable" ||
|
|
40
|
+
taggedCode === "unsupported"
|
|
41
|
+
) {
|
|
42
|
+
return taggedCode;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (message.includes("errSecInteractionNotAllowed")) {
|
|
46
|
+
return "keychain_locked";
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (
|
|
50
|
+
message.includes("UserNotAuthenticatedException") ||
|
|
51
|
+
message.includes("KeyStoreException") ||
|
|
52
|
+
message.includes("android.security.keystore")
|
|
53
|
+
) {
|
|
54
|
+
return "authentication_required";
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (
|
|
58
|
+
message.includes("KeyPermanentlyInvalidatedException") ||
|
|
59
|
+
message.includes("InvalidKeyException")
|
|
60
|
+
) {
|
|
61
|
+
return "key_invalidated";
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (
|
|
65
|
+
message.includes("AEADBadTagException") ||
|
|
66
|
+
message.toLowerCase().includes("storage corruption") ||
|
|
67
|
+
message.toLowerCase().includes("corrupted storage")
|
|
68
|
+
) {
|
|
69
|
+
return "storage_corruption";
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (
|
|
73
|
+
message.toLowerCase().includes("biometric storage unavailable") ||
|
|
74
|
+
message.toLowerCase().includes("biometric storage is not available")
|
|
75
|
+
) {
|
|
76
|
+
return "biometric_unavailable";
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (message.toLowerCase().includes("unsupported")) {
|
|
80
|
+
return "unsupported";
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return undefined;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function isLockedStorageErrorCode(
|
|
87
|
+
code: StorageErrorCode | undefined,
|
|
88
|
+
): boolean {
|
|
89
|
+
return (
|
|
90
|
+
code === "keychain_locked" ||
|
|
91
|
+
code === "authentication_required" ||
|
|
92
|
+
code === "key_invalidated"
|
|
93
|
+
);
|
|
94
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { StorageScope } from "./Storage.types";
|
|
2
|
+
|
|
3
|
+
export type WebStorageChangeEvent = {
|
|
4
|
+
key: string | null;
|
|
5
|
+
newValue: string | null;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export type WebStorageScope = StorageScope.Disk | StorageScope.Secure;
|
|
9
|
+
|
|
10
|
+
export type WebStorageBackend = {
|
|
11
|
+
getItem: (key: string) => string | null;
|
|
12
|
+
setItem: (key: string, value: string) => void;
|
|
13
|
+
removeItem: (key: string) => void;
|
|
14
|
+
clear: () => void;
|
|
15
|
+
getAllKeys: () => string[];
|
|
16
|
+
getMany?: (keys: string[]) => (string | null)[];
|
|
17
|
+
setMany?: (
|
|
18
|
+
entries: readonly (readonly [key: string, value: string])[],
|
|
19
|
+
) => void;
|
|
20
|
+
removeMany?: (keys: string[]) => void;
|
|
21
|
+
size?: () => number;
|
|
22
|
+
subscribe?: (listener: (event: WebStorageChangeEvent) => void) => () => void;
|
|
23
|
+
flush?: () => Promise<void>;
|
|
24
|
+
name?: string;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export type WebDiskStorageBackend = WebStorageBackend;
|
|
28
|
+
export type WebSecureStorageBackend = WebStorageBackend;
|
|
29
|
+
|
|
30
|
+
type LocalStorageBackendOptions = {
|
|
31
|
+
includeKey?: (key: string) => boolean;
|
|
32
|
+
name?: string;
|
|
33
|
+
resolveStorage?: () => Storage | undefined;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
function getResolvedStorage(
|
|
37
|
+
resolveStorage: (() => Storage | undefined) | undefined,
|
|
38
|
+
): Storage | undefined {
|
|
39
|
+
if (resolveStorage) {
|
|
40
|
+
return resolveStorage();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (typeof globalThis.localStorage === "undefined") {
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return globalThis.localStorage;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function createLocalStorageWebBackend(
|
|
51
|
+
options: LocalStorageBackendOptions = {},
|
|
52
|
+
): WebStorageBackend {
|
|
53
|
+
const includeKey = options.includeKey;
|
|
54
|
+
const resolveStorage = options.resolveStorage;
|
|
55
|
+
|
|
56
|
+
const listKeys = (): string[] => {
|
|
57
|
+
const storage = getResolvedStorage(resolveStorage);
|
|
58
|
+
if (!storage) {
|
|
59
|
+
return [];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const keys: string[] = [];
|
|
63
|
+
for (let index = 0; index < storage.length; index += 1) {
|
|
64
|
+
const key = storage.key(index);
|
|
65
|
+
if (!key) {
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
if (includeKey && !includeKey(key)) {
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
keys.push(key);
|
|
72
|
+
}
|
|
73
|
+
return keys;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
name: options.name ?? "localStorage",
|
|
78
|
+
getItem(key: string): string | null {
|
|
79
|
+
return getResolvedStorage(resolveStorage)?.getItem(key) ?? null;
|
|
80
|
+
},
|
|
81
|
+
setItem(key: string, value: string): void {
|
|
82
|
+
getResolvedStorage(resolveStorage)?.setItem(key, value);
|
|
83
|
+
},
|
|
84
|
+
removeItem(key: string): void {
|
|
85
|
+
getResolvedStorage(resolveStorage)?.removeItem(key);
|
|
86
|
+
},
|
|
87
|
+
clear(): void {
|
|
88
|
+
const storage = getResolvedStorage(resolveStorage);
|
|
89
|
+
if (!storage) {
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
listKeys().forEach((key) => {
|
|
94
|
+
storage.removeItem(key);
|
|
95
|
+
});
|
|
96
|
+
},
|
|
97
|
+
getAllKeys(): string[] {
|
|
98
|
+
return listKeys();
|
|
99
|
+
},
|
|
100
|
+
getMany(keys: string[]): (string | null)[] {
|
|
101
|
+
const storage = getResolvedStorage(resolveStorage);
|
|
102
|
+
if (!storage) {
|
|
103
|
+
return keys.map(() => null);
|
|
104
|
+
}
|
|
105
|
+
return keys.map((key) => storage.getItem(key));
|
|
106
|
+
},
|
|
107
|
+
setMany(entries): void {
|
|
108
|
+
const storage = getResolvedStorage(resolveStorage);
|
|
109
|
+
if (!storage) {
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
entries.forEach(([key, value]) => {
|
|
113
|
+
storage.setItem(key, value);
|
|
114
|
+
});
|
|
115
|
+
},
|
|
116
|
+
removeMany(keys: string[]): void {
|
|
117
|
+
const storage = getResolvedStorage(resolveStorage);
|
|
118
|
+
if (!storage) {
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
keys.forEach((key) => {
|
|
122
|
+
storage.removeItem(key);
|
|
123
|
+
});
|
|
124
|
+
},
|
|
125
|
+
size(): number {
|
|
126
|
+
return listKeys().length;
|
|
127
|
+
},
|
|
128
|
+
};
|
|
129
|
+
}
|