Skip to content

Commit 56aeabc

Browse files
committed
feat(async-storage): implement web storage with idb
1 parent 2486865 commit 56aeabc

File tree

10 files changed

+227
-24
lines changed

10 files changed

+227
-24
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
"build": "turbo run build",
77
"build:android": "./scripts/build-native-lib.sh android",
88
"build:apple": "./scripts/build-native-lib.sh apple",
9+
"build:js": "yarn workspace @react-native-async-storage/async-storage build",
910
"format": "concurrently 'yarn:format:*'",
1011
"format:c": "clang-format -i $(git ls-files '*.cpp' '*.h' '*.m' '*.mm') --style file:.config/.clang-format",
1112
"format:js": "prettier --write $(git ls-files '*.js' '*.json' '*.ts' '*.tsx' '*.yml' 'README.md')",

packages/async-storage/package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,9 @@
3737
"!**/__mocks__",
3838
"!**/.*"
3939
],
40+
"dependencies": {
41+
"idb": "8.0.3"
42+
},
4043
"devDependencies": {
4144
"@react-native-community/cli": "18.0.0",
4245
"@types/react": "^19.0.0",

packages/async-storage/src/AsyncStorageError.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,12 @@ enum AsyncStorageErrorType {
44
*/
55
NativeModuleError = "NativeModuleError",
66

7+
/**
8+
* Related to web storage (indexedDB)
9+
* https://developer.mozilla.org/en-US/docs/Web/API/IDBRequest/error
10+
*/
11+
WebStorageError = "WebStorageError",
12+
713
/**
814
* Error thrown from Sqlite itself
915
* https://www.sqlite.org/rescode.html

packages/async-storage/src/createAsyncStorage.native.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ class AsyncStorageImpl implements AsyncStorage {
2121
getItem = async (key: string): Promise<string | null> => {
2222
try {
2323
const result = await this.db.getValues(this.dbName, [key]);
24-
const value = result[0] ?? null;
24+
const value = result?.[0] ?? null;
2525
return value?.value ?? null;
2626
} catch (e) {
2727
throw AsyncStorageError.nativeError(e);

packages/async-storage/src/createAsyncStorage.ts

Lines changed: 41 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,76 +1,96 @@
11
import type { AsyncStorage } from "./AsyncStorage";
22
import { AsyncStorageError } from "./AsyncStorageError";
3+
import IndexedDBStorage from "./web-module/IndexedDBStorage";
34

45
class AsyncStorageWebImpl implements AsyncStorage {
5-
constructor(private readonly dbName: string) {}
6+
private db: IndexedDBStorage;
7+
8+
constructor(dbName: string) {
9+
this.db = new IndexedDBStorage(dbName);
10+
}
611

712
getItem = async (key: string): Promise<string | null> => {
813
try {
9-
// todo:
10-
return null;
14+
const result = await this.db.getValues([key]);
15+
const value = result?.[0] ?? null;
16+
return value?.value ?? null;
1117
} catch (e) {
12-
throw AsyncStorageError.nativeError(e);
18+
throw this.createError(e);
1319
}
1420
};
1521

1622
setItem = async (key: string, value: string): Promise<void> => {
1723
try {
18-
// todo
24+
await this.db.setValues([{ key, value }]);
1925
} catch (e) {
20-
throw AsyncStorageError.nativeError(e);
26+
throw this.createError(e);
2127
}
2228
};
2329

2430
removeItem = async (key: string): Promise<void> => {
2531
try {
26-
// todo
32+
await this.db.removeValues([key]);
2733
} catch (e) {
28-
throw AsyncStorageError.nativeError(e);
34+
throw this.createError(e);
2935
}
3036
};
3137

3238
getMany = async (keys: string[]): Promise<Record<string, string | null>> => {
3339
try {
34-
// todo
35-
return {};
40+
return await this.db.getValues(keys).then((entries) =>
41+
entries.reduce<Record<string, string | null>>((values, current) => {
42+
values[current.key] = current.value;
43+
return values;
44+
}, {})
45+
);
3646
} catch (e) {
37-
throw AsyncStorageError.nativeError(e);
47+
throw this.createError(e);
3848
}
3949
};
4050

4151
setMany = async (entries: Record<string, string>): Promise<void> => {
4252
try {
43-
// todo
53+
await this.db.setValues(
54+
Object.entries(entries).map(([key, value]) => ({ key, value }))
55+
);
4456
} catch (e) {
45-
throw AsyncStorageError.nativeError(e);
57+
throw this.createError(e);
4658
}
4759
};
4860

4961
removeMany = async (keys: string[]): Promise<void> => {
5062
try {
51-
// todo
63+
await this.db.removeValues(keys);
5264
} catch (e) {
53-
throw AsyncStorageError.nativeError(e);
65+
throw this.createError(e);
5466
}
5567
};
5668

5769
getAllKeys = async (): Promise<string[]> => {
5870
try {
59-
// todo
60-
return [];
71+
return await this.db.getKeys();
6172
} catch (e) {
62-
throw AsyncStorageError.nativeError(e);
73+
throw this.createError(e);
6374
}
6475
};
6576

6677
clear = async (): Promise<void> => {
6778
try {
68-
// return await this.db.clearStorage(this.dbName);
69-
// todo
79+
return await this.db.clearStorage();
7080
} catch (e) {
71-
throw AsyncStorageError.nativeError(e);
81+
throw this.createError(e);
7282
}
7383
};
84+
85+
private createError(e: any): AsyncStorageError {
86+
if (e instanceof AsyncStorageError) {
87+
return e;
88+
}
89+
return AsyncStorageError.jsError(
90+
e?.message ?? `Web storage error: ${e}`,
91+
AsyncStorageError.Type.WebStorageError
92+
);
93+
}
7494
}
7595

7696
export function createAsyncStorage(databaseName: string): AsyncStorage {
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import type { DBSchema, IDBPDatabase } from "idb";
2+
import { openDB } from "idb";
3+
import { AsyncStorageError } from "../AsyncStorageError";
4+
5+
const WebStorageTableName = "entries" as const;
6+
7+
export interface AsyncStorageWeb extends DBSchema {
8+
[WebStorageTableName]: {
9+
key: string;
10+
value: string | null;
11+
};
12+
}
13+
14+
/**
15+
* Registry to keep one IndexedDB connection per database name.
16+
*/
17+
class IndexedDBConnectionRegistry {
18+
private registry: Map<string, Promise<IDBPDatabase<AsyncStorageWeb>>> =
19+
new Map();
20+
21+
public TableName = WebStorageTableName;
22+
23+
public getOrCreate(dbName: string): Promise<IDBPDatabase<AsyncStorageWeb>> {
24+
if (this.registry.has(dbName)) {
25+
return this.registry.get(dbName)!;
26+
}
27+
const db = openDB<AsyncStorageWeb>(dbName, 1, {
28+
upgrade: (db) => {
29+
if (!db.objectStoreNames.contains(WebStorageTableName)) {
30+
db.createObjectStore(WebStorageTableName);
31+
}
32+
},
33+
blocked: (
34+
currentVersion: number,
35+
blockedVersion: number | null,
36+
_: IDBVersionChangeEvent
37+
) => {
38+
throw AsyncStorageError.jsError(
39+
`New version (${blockedVersion}) is blocked by current one (${currentVersion})`,
40+
AsyncStorageError.Type.WebStorageError
41+
);
42+
},
43+
blocking: (
44+
currentVersion: number,
45+
blockedVersion: number | null,
46+
_: IDBVersionChangeEvent
47+
) => {
48+
throw AsyncStorageError.jsError(
49+
`Current db version (${currentVersion}) is blocking upgrade to next version (${blockedVersion})`,
50+
AsyncStorageError.Type.WebStorageError
51+
);
52+
},
53+
});
54+
55+
this.registry.set(dbName, db);
56+
57+
// in case of error while opening, clear the storage to retry
58+
db.catch((err) => {
59+
this.registry.delete(dbName);
60+
throw err;
61+
});
62+
63+
return db;
64+
}
65+
}
66+
67+
export default new IndexedDBConnectionRegistry();
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import { AsyncStorageError } from "../AsyncStorageError";
2+
import registry from "./IndexedDBConnectionRegistry";
3+
4+
class IndexedDBStorage {
5+
constructor(private dbName: string) {}
6+
7+
getValues = async (
8+
keys: string[]
9+
): Promise<{ key: string; value: string | null }[]> => {
10+
try {
11+
const db = await this.db();
12+
const tx = db.transaction(registry.TableName, "readonly");
13+
const store = tx.objectStore(registry.TableName);
14+
15+
const result = await Promise.all(
16+
keys.map(async (key) => {
17+
const entry = await store.get(key);
18+
return { key, value: entry || null };
19+
})
20+
);
21+
22+
await tx.done;
23+
return result;
24+
} catch (e: any) {
25+
throw this.createError(e);
26+
}
27+
};
28+
29+
setValues = async (
30+
values: { key: string; value: string | null }[]
31+
): Promise<{ key: string; value: string | null }[]> => {
32+
try {
33+
const db = await this.db();
34+
const tx = db.transaction(registry.TableName, "readwrite");
35+
const store = tx.objectStore(registry.TableName);
36+
37+
const result = await Promise.all(
38+
values.map(async (entry) => {
39+
await store.put(entry.value, entry.key);
40+
return { key: entry.key, value: entry.value };
41+
})
42+
);
43+
44+
await tx.done;
45+
return result;
46+
} catch (e: any) {
47+
throw this.createError(e);
48+
}
49+
};
50+
51+
removeValues = async (keys: string[]): Promise<void> => {
52+
try {
53+
const db = await this.db();
54+
const tx = db.transaction(registry.TableName, "readwrite");
55+
const store = tx.objectStore(registry.TableName);
56+
57+
await Promise.all(
58+
keys.map(async (key) => {
59+
await store.delete(key);
60+
})
61+
);
62+
} catch (e: any) {
63+
throw this.createError(e);
64+
}
65+
};
66+
67+
clearStorage = async (): Promise<void> => {
68+
try {
69+
const db = await this.db();
70+
await db.clear(registry.TableName);
71+
} catch (e: any) {
72+
throw this.createError(e);
73+
}
74+
};
75+
76+
getKeys = async (): Promise<string[]> => {
77+
try {
78+
const db = await this.db();
79+
return await db.getAllKeys(registry.TableName);
80+
} catch (e: any) {
81+
throw this.createError(e);
82+
}
83+
};
84+
85+
private db = () => registry.getOrCreate(this.dbName);
86+
87+
private createError(e: any): AsyncStorageError {
88+
if (e instanceof AsyncStorageError) {
89+
return e;
90+
}
91+
return AsyncStorageError.jsError(
92+
e?.message ?? `IndexedDB error: ${e}`,
93+
AsyncStorageError.Type.WebStorageError
94+
);
95+
}
96+
}
97+
98+
export default IndexedDBStorage;

packages/async-storage/tsconfig.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
"esModuleInterop": true,
88
"forceConsistentCasingInFileNames": true,
99
"jsx": "react-jsx",
10-
"lib": ["ESNext"],
10+
"lib": ["ESNext", "dom"],
1111
"module": "ESNext",
1212
"moduleResolution": "bundler",
1313
"noEmit": false,

shared-storage/src/commonMain/kotlin/org/asyncstorage/shared_storage/database/StorageDatabase.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import kotlinx.coroutines.flow.Flow
55

66
private const val DATABASE_VERSION = 1
77

8-
@Entity(tableName = "storage")
8+
@Entity(tableName = "entries")
99
internal data class StorageEntry(@PrimaryKey val key: String, val value: String?)
1010

1111
@Dao

yarn.lock

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3850,6 +3850,7 @@ __metadata:
38503850
"@types/react": "npm:^19.0.0"
38513851
del-cli: "npm:^5.1.0"
38523852
eslint: "npm:9.34.0"
3853+
idb: "npm:8.0.3"
38533854
prettier: "npm:3.6.2"
38543855
react: "npm:19.0.0"
38553856
react-native: "npm:0.79.6"
@@ -11739,6 +11740,13 @@ __metadata:
1173911740
languageName: node
1174011741
linkType: hard
1174111742

11743+
"idb@npm:8.0.3":
11744+
version: 8.0.3
11745+
resolution: "idb@npm:8.0.3"
11746+
checksum: 10c0/421cd9a3281b7564528857031cc33fd9e95753f8191e483054cb25d1ceea7303a0d1462f4f69f5b41606f0f066156999e067478abf2460dfcf9cab80dae2a2b2
11747+
languageName: node
11748+
linkType: hard
11749+
1174211750
"ieee754@npm:^1.1.13":
1174311751
version: 1.2.1
1174411752
resolution: "ieee754@npm:1.2.1"

0 commit comments

Comments
 (0)