@okint-digital/okint-rn-storage 0.6.0
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/LICENSE +21 -0
- package/README.md +217 -0
- package/android/build.gradle +59 -0
- package/android/proguard-rules.pro +8 -0
- package/android/src/main/AndroidManifest.xml +1 -0
- package/android/src/main/java/com/okint/rnstorage/OkintRnStorageModule.kt +354 -0
- package/android/src/main/java/com/okint/rnstorage/OkintRnStoragePackage.kt +14 -0
- package/android/src/main/jni/CMakeLists.txt +24 -0
- package/android/src/main/jni/OkintJNI.cpp +15 -0
- package/cpp/OkintJSI.cpp +170 -0
- package/cpp/OkintJSI.h +18 -0
- package/ios/OkintRnStorage.m +457 -0
- package/ios/OkintRnStorageJSI.mm +24 -0
- package/lib/backends/memory.d.ts +16 -0
- package/lib/backends/memory.js +31 -0
- package/lib/backends/native-backend.d.ts +24 -0
- package/lib/backends/native-backend.js +73 -0
- package/lib/errors.d.ts +9 -0
- package/lib/errors.js +19 -0
- package/lib/facade.d.ts +29 -0
- package/lib/facade.js +93 -0
- package/lib/index.d.ts +48 -0
- package/lib/index.js +172 -0
- package/lib/native/bridge.d.ts +12 -0
- package/lib/native/bridge.js +23 -0
- package/lib/native/jsi.d.ts +7 -0
- package/lib/native/jsi.js +33 -0
- package/lib/sync/jsi-store.d.ts +28 -0
- package/lib/sync/jsi-store.js +81 -0
- package/lib/sync/persistence.d.ts +19 -0
- package/lib/sync/persistence.js +49 -0
- package/lib/sync/sync-store.d.ts +49 -0
- package/lib/sync/sync-store.js +159 -0
- package/lib/types.d.ts +140 -0
- package/lib/types.js +10 -0
- package/lib/validate.d.ts +20 -0
- package/lib/validate.js +91 -0
- package/okint-rn-storage.podspec +27 -0
- package/package.json +74 -0
- package/react-native.config.js +15 -0
- package/src/backends/memory.ts +35 -0
- package/src/backends/native-backend.ts +69 -0
- package/src/errors.ts +26 -0
- package/src/facade.ts +118 -0
- package/src/index.ts +194 -0
- package/src/native/bridge.ts +28 -0
- package/src/native/jsi.ts +37 -0
- package/src/sync/jsi-store.ts +98 -0
- package/src/sync/persistence.ts +47 -0
- package/src/sync/sync-store.ts +186 -0
- package/src/types.ts +174 -0
- package/src/validate.ts +102 -0
package/package.json
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@okint-digital/okint-rn-storage",
|
|
3
|
+
"version": "0.6.0",
|
|
4
|
+
"description": "Vanilla, pluggable React Native storage — one API over swappable backends: hardware Keystore/Keychain (secure), AES-encrypted blobs (encrypted), SQLite, SharedPreferences/UserDefaults (async), a synchronous fast store, or in-memory.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"react-native",
|
|
7
|
+
"storage",
|
|
8
|
+
"keystore",
|
|
9
|
+
"keychain",
|
|
10
|
+
"secure-storage",
|
|
11
|
+
"encrypted-storage",
|
|
12
|
+
"async-storage",
|
|
13
|
+
"mmkv-alternative",
|
|
14
|
+
"okint"
|
|
15
|
+
],
|
|
16
|
+
"license": "MIT",
|
|
17
|
+
"author": "Okint Digital",
|
|
18
|
+
"homepage": "https://github.com/okint-digital/okint-rn-storage#readme",
|
|
19
|
+
"repository": {
|
|
20
|
+
"type": "git",
|
|
21
|
+
"url": "git+https://github.com/okint-digital/okint-rn-storage.git"
|
|
22
|
+
},
|
|
23
|
+
"bugs": {
|
|
24
|
+
"url": "https://github.com/okint-digital/okint-rn-storage/issues"
|
|
25
|
+
},
|
|
26
|
+
"main": "lib/index.js",
|
|
27
|
+
"types": "lib/index.d.ts",
|
|
28
|
+
"react-native": "src/index.ts",
|
|
29
|
+
"source": "src/index.ts",
|
|
30
|
+
"files": [
|
|
31
|
+
"src",
|
|
32
|
+
"lib",
|
|
33
|
+
"android",
|
|
34
|
+
"ios",
|
|
35
|
+
"cpp",
|
|
36
|
+
"okint-rn-storage.podspec",
|
|
37
|
+
"react-native.config.js",
|
|
38
|
+
"README.md",
|
|
39
|
+
"LICENSE",
|
|
40
|
+
"!**/__tests__",
|
|
41
|
+
"!**/*.spec.ts"
|
|
42
|
+
],
|
|
43
|
+
"scripts": {
|
|
44
|
+
"build": "tsc -p tsconfig.build.json",
|
|
45
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
46
|
+
"test": "jest",
|
|
47
|
+
"lint": "eslint src",
|
|
48
|
+
"prepack": "npm run build"
|
|
49
|
+
},
|
|
50
|
+
"peerDependencies": {
|
|
51
|
+
"react": "*",
|
|
52
|
+
"react-native": "*"
|
|
53
|
+
},
|
|
54
|
+
"devDependencies": {
|
|
55
|
+
"@eslint/js": "^9.18.0",
|
|
56
|
+
"@types/jest": "^29.5.14",
|
|
57
|
+
"@types/node": "^22.10.0",
|
|
58
|
+
"@types/react": "^19.0.0",
|
|
59
|
+
"eslint": "^9.18.0",
|
|
60
|
+
"globals": "^15.14.0",
|
|
61
|
+
"jest": "^29.7.0",
|
|
62
|
+
"react": "^19.1.0",
|
|
63
|
+
"react-native": "^0.81.0",
|
|
64
|
+
"ts-jest": "^29.2.5",
|
|
65
|
+
"typescript": "^5.7.3",
|
|
66
|
+
"typescript-eslint": "^8.20.0"
|
|
67
|
+
},
|
|
68
|
+
"engines": {
|
|
69
|
+
"node": ">=18"
|
|
70
|
+
},
|
|
71
|
+
"publishConfig": {
|
|
72
|
+
"access": "public"
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// Autolinking descriptor for consuming apps.
|
|
2
|
+
module.exports = {
|
|
3
|
+
dependency: {
|
|
4
|
+
platforms: {
|
|
5
|
+
android: {
|
|
6
|
+
sourceDir: 'android',
|
|
7
|
+
packageImportPath: 'import com.okint.rnstorage.OkintRnStoragePackage;',
|
|
8
|
+
packageInstance: 'new OkintRnStoragePackage()',
|
|
9
|
+
},
|
|
10
|
+
ios: {
|
|
11
|
+
podspecPath: __dirname + '/okint-rn-storage.podspec',
|
|
12
|
+
},
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { BackendKind, StorageBackend } from '../types';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Pure-JS, in-process backend. Zero native dependencies. Data lives only for the
|
|
5
|
+
* lifetime of the instance — ideal for tests, ephemeral caches, and as a safe
|
|
6
|
+
* default before the native module is available.
|
|
7
|
+
*/
|
|
8
|
+
export class MemoryBackend implements StorageBackend {
|
|
9
|
+
readonly kind: BackendKind;
|
|
10
|
+
private readonly store = new Map<string, string>();
|
|
11
|
+
|
|
12
|
+
constructor(kind: BackendKind = 'memory') {
|
|
13
|
+
this.kind = kind;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async getString(key: string): Promise<string | null> {
|
|
17
|
+
return this.store.has(key) ? (this.store.get(key) as string) : null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async setString(key: string, value: string): Promise<void> {
|
|
21
|
+
this.store.set(key, value);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async remove(key: string): Promise<void> {
|
|
25
|
+
this.store.delete(key);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async clear(): Promise<void> {
|
|
29
|
+
this.store.clear();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async keys(): Promise<string[]> {
|
|
33
|
+
return [...this.store.keys()];
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { NativeOkintStorage, NativeStoreKind, StorageBackend } from '../types';
|
|
2
|
+
import { OkintStorageError } from '../errors';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Backend that delegates to the native module. One class drives all four
|
|
6
|
+
* native-backed stores, selected by `kind`, which is forwarded to native as the
|
|
7
|
+
* `store` discriminator:
|
|
8
|
+
* - `secure` → hardware Keystore / Keychain
|
|
9
|
+
* - `async` → SharedPreferences / UserDefaults (plaintext)
|
|
10
|
+
* - `encrypted` → AES-encrypted blobs, key in Keystore/Keychain (large values)
|
|
11
|
+
* - `sqlite` → SQLite-backed key/value
|
|
12
|
+
*
|
|
13
|
+
* The native module is injected (not imported here) so this file stays free of
|
|
14
|
+
* `react-native` and is unit-testable under plain Node with a fake bridge.
|
|
15
|
+
*/
|
|
16
|
+
export class NativeBackend implements StorageBackend {
|
|
17
|
+
constructor(
|
|
18
|
+
private readonly native: NativeOkintStorage,
|
|
19
|
+
private readonly service: string,
|
|
20
|
+
readonly kind: NativeStoreKind,
|
|
21
|
+
) {}
|
|
22
|
+
|
|
23
|
+
async getString(key: string): Promise<string | null> {
|
|
24
|
+
try {
|
|
25
|
+
const v = await this.native.getItem(this.service, key, this.kind);
|
|
26
|
+
return v ?? null;
|
|
27
|
+
} catch (e) {
|
|
28
|
+
throw wrap(e, `get "${key}"`);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async setString(key: string, value: string): Promise<void> {
|
|
33
|
+
try {
|
|
34
|
+
await this.native.setItem(this.service, key, value, this.kind);
|
|
35
|
+
} catch (e) {
|
|
36
|
+
throw wrap(e, `set "${key}"`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async remove(key: string): Promise<void> {
|
|
41
|
+
try {
|
|
42
|
+
await this.native.removeItem(this.service, key, this.kind);
|
|
43
|
+
} catch (e) {
|
|
44
|
+
throw wrap(e, `remove "${key}"`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async clear(): Promise<void> {
|
|
49
|
+
try {
|
|
50
|
+
await this.native.clear(this.service, this.kind);
|
|
51
|
+
} catch (e) {
|
|
52
|
+
throw wrap(e, 'clear');
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async keys(): Promise<string[]> {
|
|
57
|
+
try {
|
|
58
|
+
const ks = await this.native.getAllKeys(this.service, this.kind);
|
|
59
|
+
return ks ?? [];
|
|
60
|
+
} catch (e) {
|
|
61
|
+
throw wrap(e, 'keys');
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function wrap(cause: unknown, op: string): OkintStorageError {
|
|
67
|
+
const msg = cause instanceof Error ? cause.message : String(cause);
|
|
68
|
+
return new OkintStorageError('NATIVE_ERROR', `Native storage failed during ${op}: ${msg}`, cause);
|
|
69
|
+
}
|
package/src/errors.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export type OkintStorageErrorCode =
|
|
2
|
+
| 'NATIVE_MODULE_MISSING'
|
|
3
|
+
| 'BACKEND_NOT_IMPLEMENTED'
|
|
4
|
+
| 'UNKNOWN_BACKEND'
|
|
5
|
+
| 'PARSE_ERROR'
|
|
6
|
+
| 'INVALID_VALUE'
|
|
7
|
+
| 'INVALID_NAMESPACE'
|
|
8
|
+
| 'INVALID_KEY'
|
|
9
|
+
| 'NATIVE_ERROR';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Single error type for the whole package. Carries a stable `code` so callers
|
|
13
|
+
* can branch without string-matching messages.
|
|
14
|
+
*/
|
|
15
|
+
export class OkintStorageError extends Error {
|
|
16
|
+
readonly code: OkintStorageErrorCode;
|
|
17
|
+
|
|
18
|
+
constructor(code: OkintStorageErrorCode, message: string, cause?: unknown) {
|
|
19
|
+
// `cause` is carried by the native Error.cause (ES2022) — we don't redeclare it.
|
|
20
|
+
super(message, cause !== undefined ? { cause } : undefined);
|
|
21
|
+
this.name = 'OkintStorageError';
|
|
22
|
+
this.code = code;
|
|
23
|
+
// Restore prototype chain (TS targeting ES5/ES2015 class-extends-builtin caveat).
|
|
24
|
+
Object.setPrototypeOf(this, OkintStorageError.prototype);
|
|
25
|
+
}
|
|
26
|
+
}
|
package/src/facade.ts
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import type { BackendKind, OkintStorage, StorageBackend } from './types';
|
|
2
|
+
import {
|
|
3
|
+
assertKey,
|
|
4
|
+
assertStringValue,
|
|
5
|
+
fromJson,
|
|
6
|
+
numberToString,
|
|
7
|
+
stringToBoolean,
|
|
8
|
+
stringToNumber,
|
|
9
|
+
toJson,
|
|
10
|
+
} from './validate';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Wraps a low-level StorageBackend with ergonomic, typed accessors. All values
|
|
14
|
+
* are persisted as strings; the facade handles JSON / number / boolean
|
|
15
|
+
* (de)serialization, key validation, and typed errors.
|
|
16
|
+
*
|
|
17
|
+
* Every method is async and REJECTS (never throws synchronously) on validation
|
|
18
|
+
* errors, so callers can rely on a single error channel (.catch / try-await).
|
|
19
|
+
*/
|
|
20
|
+
export class StorageFacade implements OkintStorage {
|
|
21
|
+
constructor(private readonly impl: StorageBackend) {}
|
|
22
|
+
|
|
23
|
+
get backend(): BackendKind {
|
|
24
|
+
return this.impl.kind;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async getString(key: string): Promise<string | null> {
|
|
28
|
+
assertKey(key);
|
|
29
|
+
return this.impl.getString(key);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async setString(key: string, value: string): Promise<void> {
|
|
33
|
+
assertKey(key);
|
|
34
|
+
assertStringValue(value);
|
|
35
|
+
return this.impl.setString(key, value);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async getItem<T>(key: string): Promise<T | null> {
|
|
39
|
+
assertKey(key);
|
|
40
|
+
const raw = await this.impl.getString(key);
|
|
41
|
+
return raw == null ? null : fromJson<T>(key, raw);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async setItem<T>(key: string, value: T): Promise<void> {
|
|
45
|
+
assertKey(key);
|
|
46
|
+
return this.impl.setString(key, toJson(key, value));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async getNumber(key: string): Promise<number | null> {
|
|
50
|
+
assertKey(key);
|
|
51
|
+
const raw = await this.impl.getString(key);
|
|
52
|
+
return raw == null ? null : stringToNumber(raw);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async setNumber(key: string, value: number): Promise<void> {
|
|
56
|
+
assertKey(key);
|
|
57
|
+
return this.impl.setString(key, numberToString(key, value));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async getBoolean(key: string): Promise<boolean | null> {
|
|
61
|
+
assertKey(key);
|
|
62
|
+
const raw = await this.impl.getString(key);
|
|
63
|
+
return raw == null ? null : stringToBoolean(raw);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async setBoolean(key: string, value: boolean): Promise<void> {
|
|
67
|
+
assertKey(key);
|
|
68
|
+
return this.impl.setString(key, value ? 'true' : 'false');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async has(key: string): Promise<boolean> {
|
|
72
|
+
assertKey(key);
|
|
73
|
+
return (await this.impl.getString(key)) !== null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async remove(key: string): Promise<void> {
|
|
77
|
+
assertKey(key);
|
|
78
|
+
return this.impl.remove(key);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
clear(): Promise<void> {
|
|
82
|
+
return this.impl.clear();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
keys(): Promise<string[]> {
|
|
86
|
+
return this.impl.keys();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async multiGet(keys: string[]): Promise<Record<string, string | null>> {
|
|
90
|
+
const out: Record<string, string | null> = {};
|
|
91
|
+
await Promise.all(
|
|
92
|
+
keys.map(async (k) => {
|
|
93
|
+
assertKey(k);
|
|
94
|
+
out[k] = await this.impl.getString(k);
|
|
95
|
+
}),
|
|
96
|
+
);
|
|
97
|
+
return out;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async multiSet(entries: Record<string, string>): Promise<void> {
|
|
101
|
+
await Promise.all(
|
|
102
|
+
Object.entries(entries).map(([k, v]) => {
|
|
103
|
+
assertKey(k);
|
|
104
|
+
assertStringValue(v);
|
|
105
|
+
return this.impl.setString(k, v);
|
|
106
|
+
}),
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async multiRemove(keys: string[]): Promise<void> {
|
|
111
|
+
await Promise.all(
|
|
112
|
+
keys.map((k) => {
|
|
113
|
+
assertKey(k);
|
|
114
|
+
return this.impl.remove(k);
|
|
115
|
+
}),
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
OkintStorage,
|
|
3
|
+
OkintStorageOptions,
|
|
4
|
+
StorageBackend,
|
|
5
|
+
BackendKind,
|
|
6
|
+
OkintSyncStorage,
|
|
7
|
+
OkintSyncStorageOptions,
|
|
8
|
+
SyncPersistence,
|
|
9
|
+
} from './types';
|
|
10
|
+
import { StorageFacade } from './facade';
|
|
11
|
+
import { MemoryBackend } from './backends/memory';
|
|
12
|
+
import { NativeBackend } from './backends/native-backend';
|
|
13
|
+
import { OkintSyncStore } from './sync/sync-store';
|
|
14
|
+
import { JSISyncStore } from './sync/jsi-store';
|
|
15
|
+
import { MemorySyncPersistence, BackendSyncPersistence } from './sync/persistence';
|
|
16
|
+
import { getNativeModule } from './native/bridge';
|
|
17
|
+
import { getJSIStore } from './native/jsi';
|
|
18
|
+
import { OkintStorageError } from './errors';
|
|
19
|
+
import { normalizeNamespace } from './validate';
|
|
20
|
+
|
|
21
|
+
const DEFAULT_NAMESPACE = 'okint';
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Create a storage instance bound to a backend + namespace.
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* const secure = createStorage({ backend: 'secure', namespace: 'auth' });
|
|
28
|
+
* await secure.setString('refreshToken', token); // hardware-encrypted
|
|
29
|
+
*
|
|
30
|
+
* const cache = createStorage({ backend: 'memory' });
|
|
31
|
+
* await cache.setItem('campaigns', list);
|
|
32
|
+
*/
|
|
33
|
+
export function createStorage(options: OkintStorageOptions): OkintStorage {
|
|
34
|
+
const namespace = normalizeNamespace(options.namespace, DEFAULT_NAMESPACE);
|
|
35
|
+
return new StorageFacade(resolveBackend(options.backend, namespace));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function resolveBackend(kind: BackendKind, namespace: string): StorageBackend {
|
|
39
|
+
switch (kind) {
|
|
40
|
+
case 'memory':
|
|
41
|
+
return new MemoryBackend('memory');
|
|
42
|
+
case 'secure':
|
|
43
|
+
case 'async':
|
|
44
|
+
case 'encrypted':
|
|
45
|
+
case 'sqlite':
|
|
46
|
+
return new NativeBackend(getNativeModule(), namespace, kind);
|
|
47
|
+
default:
|
|
48
|
+
throw new OkintStorageError('UNKNOWN_BACKEND', `Unknown storage backend "${String(kind)}".`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Create a SYNCHRONOUS storage instance. Resolves once the snapshot is loaded;
|
|
54
|
+
* thereafter all get/set are synchronous. This is the MMKV-style store — use it
|
|
55
|
+
* for state persistence/rehydration, feature flags, and hot-path UI state.
|
|
56
|
+
*
|
|
57
|
+
* @example
|
|
58
|
+
* const fast = await createSyncStorage({ backend: 'fast', namespace: 'app' });
|
|
59
|
+
* fast.setBoolean('onboarded', true); // sync write (persists in bg)
|
|
60
|
+
* const onboarded = fast.getBoolean('onboarded'); // sync read
|
|
61
|
+
* await fast.flush(); // ensure durability (e.g. on background)
|
|
62
|
+
*/
|
|
63
|
+
// Sync stores are interned per (backend, namespace): two callers asking for the
|
|
64
|
+
// same fast store get the SAME instance, so their in-memory snapshots can't
|
|
65
|
+
// diverge and silently overwrite each other. The cached promise is evicted on
|
|
66
|
+
// load failure so a later call can retry.
|
|
67
|
+
const syncRegistry = new Map<string, Promise<OkintSyncStorage>>();
|
|
68
|
+
|
|
69
|
+
export function createSyncStorage(options: OkintSyncStorageOptions): Promise<OkintSyncStorage> {
|
|
70
|
+
// Promise-returning factory → surface validation as a rejection, not a sync throw.
|
|
71
|
+
let namespace: string;
|
|
72
|
+
try {
|
|
73
|
+
namespace = normalizeNamespace(options.namespace, DEFAULT_NAMESPACE);
|
|
74
|
+
} catch (e) {
|
|
75
|
+
return Promise.reject(e);
|
|
76
|
+
}
|
|
77
|
+
const registryKey = `${options.backend}:${namespace}`;
|
|
78
|
+
|
|
79
|
+
const existing = syncRegistry.get(registryKey);
|
|
80
|
+
if (existing) return existing;
|
|
81
|
+
|
|
82
|
+
const built = buildSyncStore(options.backend, namespace);
|
|
83
|
+
syncRegistry.set(registryKey, built);
|
|
84
|
+
built.catch(() => syncRegistry.delete(registryKey));
|
|
85
|
+
return built;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Synchronous, zero-load sync stores: hydrated in one blocking native call at
|
|
89
|
+
// construction, then pure in-JS reads (maximum read performance). Interned per
|
|
90
|
+
// (backend, namespace) like the async variant.
|
|
91
|
+
const syncRegistrySync = new Map<string, OkintSyncStorage>();
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Create a synchronous storage instance WITHOUT an async load step. Hydrates the
|
|
95
|
+
* snapshot in a single blocking native bulk-read, then all get/set are
|
|
96
|
+
* synchronous in-JS-memory ops (writes persist in the background). Use this when
|
|
97
|
+
* you need state available immediately at startup (e.g. before first render).
|
|
98
|
+
*
|
|
99
|
+
* @example
|
|
100
|
+
* const fast = createSyncStorageSync({ backend: 'fast', namespace: 'app' });
|
|
101
|
+
* const onboarded = fast.getBoolean('onboarded'); // sync, zero-load
|
|
102
|
+
*/
|
|
103
|
+
export function createSyncStorageSync(options: OkintSyncStorageOptions): OkintSyncStorage {
|
|
104
|
+
const namespace = normalizeNamespace(options.namespace, DEFAULT_NAMESPACE);
|
|
105
|
+
const registryKey = `${options.backend}:${namespace}`;
|
|
106
|
+
const existing = syncRegistrySync.get(registryKey);
|
|
107
|
+
if (existing) return existing;
|
|
108
|
+
|
|
109
|
+
let store: OkintSyncStore;
|
|
110
|
+
switch (options.backend) {
|
|
111
|
+
case 'memory':
|
|
112
|
+
store = new OkintSyncStore('memory', new MemorySyncPersistence());
|
|
113
|
+
store.loadSync({});
|
|
114
|
+
break;
|
|
115
|
+
case 'fast': {
|
|
116
|
+
const native = getNativeModule();
|
|
117
|
+
const entries = native.getEntriesSync(namespace, 'async');
|
|
118
|
+
store = new OkintSyncStore('fast', new BackendSyncPersistence(new NativeBackend(native, namespace, 'async')));
|
|
119
|
+
store.loadSync(entries);
|
|
120
|
+
break;
|
|
121
|
+
}
|
|
122
|
+
default:
|
|
123
|
+
throw new OkintStorageError('UNKNOWN_BACKEND', `Unknown sync backend "${String(options.backend)}".`);
|
|
124
|
+
}
|
|
125
|
+
syncRegistrySync.set(registryKey, store);
|
|
126
|
+
return store;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// JSI stores are interned per namespace (one HostObject per logical store).
|
|
130
|
+
const jsiRegistry = new Map<string, OkintSyncStorage>();
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Create a synchronous store backed by the C++/JSI engine — get/set go straight
|
|
134
|
+
* into C++ with no bridge serialization and no snapshot (maximum performance,
|
|
135
|
+
* zero JS memory overhead). Installs the native engine on first use; throws if
|
|
136
|
+
* the JSI runtime is unreachable (e.g. remote JS debugging) — fall back to
|
|
137
|
+
* `createSyncStorageSync` there.
|
|
138
|
+
*
|
|
139
|
+
* @example
|
|
140
|
+
* const kv = createJSIStorage({ namespace: 'app' });
|
|
141
|
+
* kv.setString('theme', 'dark'); // sync, in C++
|
|
142
|
+
* const theme = kv.getString('theme'); // sync, in C++
|
|
143
|
+
*/
|
|
144
|
+
export function createJSIStorage(options: { namespace?: string } = {}): OkintSyncStorage {
|
|
145
|
+
const namespace = normalizeNamespace(options.namespace, DEFAULT_NAMESPACE);
|
|
146
|
+
const existing = jsiRegistry.get(namespace);
|
|
147
|
+
if (existing) return existing;
|
|
148
|
+
const store = new JSISyncStore(getJSIStore(getNativeModule(), namespace));
|
|
149
|
+
jsiRegistry.set(namespace, store);
|
|
150
|
+
return store;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async function buildSyncStore(
|
|
154
|
+
backend: OkintSyncStorageOptions['backend'],
|
|
155
|
+
namespace: string,
|
|
156
|
+
): Promise<OkintSyncStorage> {
|
|
157
|
+
let persistence: SyncPersistence;
|
|
158
|
+
switch (backend) {
|
|
159
|
+
case 'memory':
|
|
160
|
+
persistence = new MemorySyncPersistence();
|
|
161
|
+
break;
|
|
162
|
+
case 'fast':
|
|
163
|
+
persistence = new BackendSyncPersistence(
|
|
164
|
+
new NativeBackend(getNativeModule(), namespace, 'async'),
|
|
165
|
+
);
|
|
166
|
+
break;
|
|
167
|
+
default:
|
|
168
|
+
throw new OkintStorageError('UNKNOWN_BACKEND', `Unknown sync backend "${String(backend)}".`);
|
|
169
|
+
}
|
|
170
|
+
const store = new OkintSyncStore(backend, persistence);
|
|
171
|
+
await store.load();
|
|
172
|
+
return store;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export { StorageFacade } from './facade';
|
|
176
|
+
export { MemoryBackend } from './backends/memory';
|
|
177
|
+
export { NativeBackend } from './backends/native-backend';
|
|
178
|
+
export { OkintSyncStore } from './sync/sync-store';
|
|
179
|
+
export { JSISyncStore } from './sync/jsi-store';
|
|
180
|
+
export { MemorySyncPersistence, BackendSyncPersistence } from './sync/persistence';
|
|
181
|
+
export { OkintStorageError } from './errors';
|
|
182
|
+
export type { OkintStorageErrorCode } from './errors';
|
|
183
|
+
export type {
|
|
184
|
+
OkintStorage,
|
|
185
|
+
OkintStorageOptions,
|
|
186
|
+
StorageBackend,
|
|
187
|
+
BackendKind,
|
|
188
|
+
NativeOkintStorage,
|
|
189
|
+
JSIStore,
|
|
190
|
+
OkintSyncStorage,
|
|
191
|
+
OkintSyncStorageOptions,
|
|
192
|
+
SyncBackendKind,
|
|
193
|
+
SyncPersistence,
|
|
194
|
+
} from './types';
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { NativeModules } from 'react-native';
|
|
2
|
+
import type { NativeOkintStorage } from '../types';
|
|
3
|
+
import { OkintStorageError } from '../errors';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Resolves the native module. This is the ONLY file that imports `react-native`,
|
|
7
|
+
* so the rest of the package (facade, backends, types) stays runtime-agnostic
|
|
8
|
+
* and unit-testable under Node.
|
|
9
|
+
*
|
|
10
|
+
* Uses the classic NativeModules registry (works on both the legacy and the New
|
|
11
|
+
* Architecture via the interop layer). If the module isn't present — e.g. the
|
|
12
|
+
* app wasn't rebuilt after install — we throw a clear, actionable error rather
|
|
13
|
+
* than failing deep in a call.
|
|
14
|
+
*/
|
|
15
|
+
export function getNativeModule(): NativeOkintStorage {
|
|
16
|
+
const mod = (NativeModules as Record<string, unknown>)['OkintRnStorage'] as
|
|
17
|
+
| NativeOkintStorage
|
|
18
|
+
| undefined;
|
|
19
|
+
|
|
20
|
+
if (!mod) {
|
|
21
|
+
throw new OkintStorageError(
|
|
22
|
+
'NATIVE_MODULE_MISSING',
|
|
23
|
+
"okint-rn-storage native module not found. Rebuild the app after install " +
|
|
24
|
+
"(pod install on iOS / gradle sync on Android), or use backend: 'memory'.",
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
return mod;
|
|
28
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { JSIStore, NativeOkintStorage } from '../types';
|
|
2
|
+
import { OkintStorageError } from '../errors';
|
|
3
|
+
|
|
4
|
+
const GLOBAL_FACTORY = '__okintCreateJSI';
|
|
5
|
+
|
|
6
|
+
function factory(): ((namespace: string) => JSIStore) | undefined {
|
|
7
|
+
const g = globalThis as Record<string, unknown>;
|
|
8
|
+
const f = g[GLOBAL_FACTORY];
|
|
9
|
+
return typeof f === 'function' ? (f as (namespace: string) => JSIStore) : undefined;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
let installed = false;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Resolve a JSI HostObject store for `namespace`, installing the native C++
|
|
16
|
+
* engine on first use. Throws a clear error if the JSI runtime is unreachable
|
|
17
|
+
* (e.g. remote JS debugging) — callers should fall back to `createSyncStorageSync`.
|
|
18
|
+
*/
|
|
19
|
+
export function getJSIStore(native: NativeOkintStorage, namespace: string): JSIStore {
|
|
20
|
+
if (!installed && factory() === undefined) {
|
|
21
|
+
try {
|
|
22
|
+
native.installJSI();
|
|
23
|
+
} catch {
|
|
24
|
+
// fall through to the missing-engine error below
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
const create = factory();
|
|
28
|
+
if (create === undefined) {
|
|
29
|
+
throw new OkintStorageError(
|
|
30
|
+
'NATIVE_MODULE_MISSING',
|
|
31
|
+
'okint JSI engine is unavailable (remote JS debugging or unsupported runtime). ' +
|
|
32
|
+
'Use createSyncStorageSync for synchronous access instead.',
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
installed = true;
|
|
36
|
+
return create(namespace);
|
|
37
|
+
}
|