@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.
Files changed (52) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +217 -0
  3. package/android/build.gradle +59 -0
  4. package/android/proguard-rules.pro +8 -0
  5. package/android/src/main/AndroidManifest.xml +1 -0
  6. package/android/src/main/java/com/okint/rnstorage/OkintRnStorageModule.kt +354 -0
  7. package/android/src/main/java/com/okint/rnstorage/OkintRnStoragePackage.kt +14 -0
  8. package/android/src/main/jni/CMakeLists.txt +24 -0
  9. package/android/src/main/jni/OkintJNI.cpp +15 -0
  10. package/cpp/OkintJSI.cpp +170 -0
  11. package/cpp/OkintJSI.h +18 -0
  12. package/ios/OkintRnStorage.m +457 -0
  13. package/ios/OkintRnStorageJSI.mm +24 -0
  14. package/lib/backends/memory.d.ts +16 -0
  15. package/lib/backends/memory.js +31 -0
  16. package/lib/backends/native-backend.d.ts +24 -0
  17. package/lib/backends/native-backend.js +73 -0
  18. package/lib/errors.d.ts +9 -0
  19. package/lib/errors.js +19 -0
  20. package/lib/facade.d.ts +29 -0
  21. package/lib/facade.js +93 -0
  22. package/lib/index.d.ts +48 -0
  23. package/lib/index.js +172 -0
  24. package/lib/native/bridge.d.ts +12 -0
  25. package/lib/native/bridge.js +23 -0
  26. package/lib/native/jsi.d.ts +7 -0
  27. package/lib/native/jsi.js +33 -0
  28. package/lib/sync/jsi-store.d.ts +28 -0
  29. package/lib/sync/jsi-store.js +81 -0
  30. package/lib/sync/persistence.d.ts +19 -0
  31. package/lib/sync/persistence.js +49 -0
  32. package/lib/sync/sync-store.d.ts +49 -0
  33. package/lib/sync/sync-store.js +159 -0
  34. package/lib/types.d.ts +140 -0
  35. package/lib/types.js +10 -0
  36. package/lib/validate.d.ts +20 -0
  37. package/lib/validate.js +91 -0
  38. package/okint-rn-storage.podspec +27 -0
  39. package/package.json +74 -0
  40. package/react-native.config.js +15 -0
  41. package/src/backends/memory.ts +35 -0
  42. package/src/backends/native-backend.ts +69 -0
  43. package/src/errors.ts +26 -0
  44. package/src/facade.ts +118 -0
  45. package/src/index.ts +194 -0
  46. package/src/native/bridge.ts +28 -0
  47. package/src/native/jsi.ts +37 -0
  48. package/src/sync/jsi-store.ts +98 -0
  49. package/src/sync/persistence.ts +47 -0
  50. package/src/sync/sync-store.ts +186 -0
  51. package/src/types.ts +174 -0
  52. 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
+ }