@optimystic/db-p2p-storage-rn 0.12.0 → 0.13.4

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 (45) hide show
  1. package/dist/src/identity.d.ts +18 -0
  2. package/dist/src/identity.d.ts.map +1 -0
  3. package/dist/src/identity.js +27 -0
  4. package/dist/src/identity.js.map +1 -0
  5. package/dist/src/index.d.ts +4 -2
  6. package/dist/src/index.d.ts.map +1 -1
  7. package/dist/src/index.js +4 -2
  8. package/dist/src/index.js.map +1 -1
  9. package/dist/src/keys.d.ts +55 -0
  10. package/dist/src/keys.d.ts.map +1 -0
  11. package/dist/src/keys.js +126 -0
  12. package/dist/src/keys.js.map +1 -0
  13. package/dist/src/leveldb-kv-store.d.ts +21 -0
  14. package/dist/src/leveldb-kv-store.d.ts.map +1 -0
  15. package/dist/src/leveldb-kv-store.js +39 -0
  16. package/dist/src/leveldb-kv-store.js.map +1 -0
  17. package/dist/src/leveldb-like.d.ts +68 -0
  18. package/dist/src/leveldb-like.d.ts.map +1 -0
  19. package/dist/src/leveldb-like.js +34 -0
  20. package/dist/src/leveldb-like.js.map +1 -0
  21. package/dist/src/{mmkv-storage.d.ts → leveldb-storage.d.ts} +19 -32
  22. package/dist/src/leveldb-storage.d.ts.map +1 -0
  23. package/dist/src/leveldb-storage.js +138 -0
  24. package/dist/src/leveldb-storage.js.map +1 -0
  25. package/dist/src/rn-opener.d.ts +73 -0
  26. package/dist/src/rn-opener.d.ts.map +1 -0
  27. package/dist/src/rn-opener.js +200 -0
  28. package/dist/src/rn-opener.js.map +1 -0
  29. package/package.json +20 -8
  30. package/src/identity.ts +33 -0
  31. package/src/index.ts +14 -3
  32. package/src/keys.ts +139 -0
  33. package/src/leveldb-kv-store.ts +43 -0
  34. package/src/leveldb-like.ts +84 -0
  35. package/src/leveldb-storage.ts +155 -0
  36. package/src/rn-opener.ts +274 -0
  37. package/dist/src/mmkv-kv-store.d.ts +0 -13
  38. package/dist/src/mmkv-kv-store.d.ts.map +0 -1
  39. package/dist/src/mmkv-kv-store.js +0 -25
  40. package/dist/src/mmkv-kv-store.js.map +0 -1
  41. package/dist/src/mmkv-storage.d.ts.map +0 -1
  42. package/dist/src/mmkv-storage.js +0 -138
  43. package/dist/src/mmkv-storage.js.map +0 -1
  44. package/src/mmkv-kv-store.ts +0 -26
  45. package/src/mmkv-storage.ts +0 -181
package/src/keys.ts ADDED
@@ -0,0 +1,139 @@
1
+ /**
2
+ * Key encoding for the LevelDB-backed storage. All keys live in a single
3
+ * database and are sorted lexicographically by byte order. A leading tag
4
+ * byte partitions the keyspace per logical store; a 4-byte big-endian
5
+ * length prefix on the `blockId` ensures prefix scans cannot be confused
6
+ * by the variable-length `actionId` suffix of the previous store.
7
+ *
8
+ * Layout:
9
+ * `tag (1)` || `len(blockId) (4 BE)` || `blockId UTF-8` || `suffix`
10
+ *
11
+ * Per-store suffix encoding:
12
+ * - metadata: (empty)
13
+ * - revisions: rev (8-byte big-endian unsigned via DataView.setBigUint64)
14
+ * - pending: actionId UTF-8 (terminal)
15
+ * - transactions: actionId UTF-8 (terminal)
16
+ * - materialized: actionId UTF-8 (terminal)
17
+ *
18
+ * `kv` and `identity` keys are flat — no `blockId` envelope — under their
19
+ * own tag bytes (`TAG_KV`, `TAG_IDENTITY`) and use UTF-8 of the full key.
20
+ *
21
+ * The tag bytes are deliberately spaced (`0x01`, `0x02`, …, `0x10`, `0x20`)
22
+ * so a future logical store can slot in between without colliding with
23
+ * existing prefix scans.
24
+ */
25
+
26
+ export const TAG_METADATA = 0x01;
27
+ export const TAG_REVISIONS = 0x02;
28
+ export const TAG_PENDING = 0x03;
29
+ export const TAG_TRANSACTIONS = 0x04;
30
+ export const TAG_MATERIALIZED = 0x05;
31
+ export const TAG_KV = 0x10;
32
+ export const TAG_IDENTITY = 0x20;
33
+
34
+ const textEncoder = new TextEncoder();
35
+ const textDecoder = new TextDecoder();
36
+
37
+ function encodeBlockEnvelope(tag: number, blockId: string): Uint8Array {
38
+ const blockIdBytes = textEncoder.encode(blockId);
39
+ const out = new Uint8Array(1 + 4 + blockIdBytes.length);
40
+ out[0] = tag;
41
+ new DataView(out.buffer, out.byteOffset).setUint32(1, blockIdBytes.length, false);
42
+ out.set(blockIdBytes, 5);
43
+ return out;
44
+ }
45
+
46
+ function concat(...parts: Uint8Array[]): Uint8Array {
47
+ let total = 0;
48
+ for (const p of parts) total += p.length;
49
+ const out = new Uint8Array(total);
50
+ let off = 0;
51
+ for (const p of parts) {
52
+ out.set(p, off);
53
+ off += p.length;
54
+ }
55
+ return out;
56
+ }
57
+
58
+ export function metadataKey(blockId: string): Uint8Array {
59
+ return encodeBlockEnvelope(TAG_METADATA, blockId);
60
+ }
61
+
62
+ export function revisionKey(blockId: string, rev: number): Uint8Array {
63
+ const envelope = encodeBlockEnvelope(TAG_REVISIONS, blockId);
64
+ const out = new Uint8Array(envelope.length + 8);
65
+ out.set(envelope, 0);
66
+ new DataView(out.buffer, out.byteOffset).setBigUint64(envelope.length, BigInt(rev), false);
67
+ return out;
68
+ }
69
+
70
+ /** Decode the trailing 8-byte big-endian rev from a `revisionKey`-encoded key. */
71
+ export function revisionFromKey(key: Uint8Array): number {
72
+ const view = new DataView(key.buffer, key.byteOffset, key.byteLength);
73
+ return Number(view.getBigUint64(key.byteLength - 8, false));
74
+ }
75
+
76
+ export function pendingKey(blockId: string, actionId: string): Uint8Array {
77
+ return concat(encodeBlockEnvelope(TAG_PENDING, blockId), textEncoder.encode(actionId));
78
+ }
79
+
80
+ export function transactionKey(blockId: string, actionId: string): Uint8Array {
81
+ return concat(encodeBlockEnvelope(TAG_TRANSACTIONS, blockId), textEncoder.encode(actionId));
82
+ }
83
+
84
+ export function materializedKey(blockId: string, actionId: string): Uint8Array {
85
+ return concat(encodeBlockEnvelope(TAG_MATERIALIZED, blockId), textEncoder.encode(actionId));
86
+ }
87
+
88
+ /** Returns the inclusive lower / exclusive upper range covering every key for `(tag, blockId, *)`. */
89
+ export function blockEnvelopeRange(tag: number, blockId: string): { gte: Uint8Array; lt: Uint8Array } {
90
+ const gte = encodeBlockEnvelope(tag, blockId);
91
+ const lt = new Uint8Array(gte.length);
92
+ lt.set(gte);
93
+ // Increment the last byte to get the exclusive upper bound. The envelope
94
+ // ends in the last byte of the blockId (UTF-8); since the longest possible
95
+ // UTF-8 lead byte is 0xF4 and continuation bytes are <= 0xBF, no envelope
96
+ // byte is ever 0xFF — incrementing the last byte is always well-defined.
97
+ const lastIndex = lt.length - 1;
98
+ const lastByte = lt[lastIndex];
99
+ if (lastByte === undefined) throw new Error('empty blockId envelope');
100
+ lt[lastIndex] = lastByte + 1;
101
+ return { gte, lt };
102
+ }
103
+
104
+ /** Decode the `actionId` suffix from a `pendingKey` / `transactionKey` / `materializedKey`. */
105
+ export function actionIdFromKey(key: Uint8Array, blockId: string): string {
106
+ const blockIdLen = textEncoder.encode(blockId).length;
107
+ const suffixOffset = 1 + 4 + blockIdLen;
108
+ return textDecoder.decode(key.subarray(suffixOffset));
109
+ }
110
+
111
+ export function kvKey(key: string): Uint8Array {
112
+ return concat(Uint8Array.of(TAG_KV), textEncoder.encode(key));
113
+ }
114
+
115
+ /** Returns the inclusive lower / exclusive upper range covering every kv key starting with `prefix`. */
116
+ export function kvPrefixRange(prefix: string): { gte: Uint8Array; lt: Uint8Array } {
117
+ const prefixBytes = textEncoder.encode(prefix);
118
+ const gte = new Uint8Array(1 + prefixBytes.length);
119
+ gte[0] = TAG_KV;
120
+ gte.set(prefixBytes, 1);
121
+ // Upper bound: any string whose UTF-8 bytes start with `prefixBytes` sorts
122
+ // strictly below `[TAG_KV, ...prefixBytes, 0xFF]`. UTF-8 never produces a
123
+ // 0xFF byte (the maximum valid lead byte is 0xF4 and continuation bytes
124
+ // top out at 0xBF), so appending 0xFF yields an exact exclusive upper bound.
125
+ const lt = new Uint8Array(1 + prefixBytes.length + 1);
126
+ lt[0] = TAG_KV;
127
+ lt.set(prefixBytes, 1);
128
+ lt[lt.length - 1] = 0xff;
129
+ return { gte, lt };
130
+ }
131
+
132
+ /** Strip the leading `TAG_KV` byte from a key, returning the UTF-8 string portion. */
133
+ export function kvKeyToString(raw: Uint8Array): string {
134
+ return textDecoder.decode(raw.subarray(1));
135
+ }
136
+
137
+ export function identityKey(keyName: string): Uint8Array {
138
+ return concat(Uint8Array.of(TAG_IDENTITY), textEncoder.encode(keyName));
139
+ }
@@ -0,0 +1,43 @@
1
+ import type { IKVStore } from '@optimystic/db-p2p';
2
+ import { drain, type LevelDBLike } from './leveldb-like.js';
3
+ import { kvKey, kvKeyToString, kvPrefixRange } from './keys.js';
4
+
5
+ const textEncoder = new TextEncoder();
6
+ const textDecoder = new TextDecoder();
7
+
8
+ /**
9
+ * LevelDB-backed `IKVStore` adapter for React Native peers.
10
+ *
11
+ * Shares one `LevelDBLike` database with `LevelDBRawStorage` and the identity
12
+ * helper. KV keys are tagged with `TAG_KV` (and identity with `TAG_IDENTITY`),
13
+ * so the three subsystems can't collide regardless of the user-chosen
14
+ * `prefix`. `list(prefix)` is a range-bounded scan — never a full-database
15
+ * iteration plus JS-side filter — so listing latency stays bounded.
16
+ */
17
+ export class LevelDBKVStore implements IKVStore {
18
+ private readonly prefix: string;
19
+
20
+ constructor(private readonly db: LevelDBLike, prefix: string = 'optimystic:txn:') {
21
+ this.prefix = prefix;
22
+ }
23
+
24
+ async get(key: string): Promise<string | undefined> {
25
+ const bytes = await this.db.get(kvKey(this.prefix + key));
26
+ if (!bytes) return undefined;
27
+ return textDecoder.decode(bytes);
28
+ }
29
+
30
+ async set(key: string, value: string): Promise<void> {
31
+ await this.db.put(kvKey(this.prefix + key), textEncoder.encode(value));
32
+ }
33
+
34
+ async delete(key: string): Promise<void> {
35
+ await this.db.delete(kvKey(this.prefix + key));
36
+ }
37
+
38
+ async list(prefix: string): Promise<string[]> {
39
+ const range = kvPrefixRange(this.prefix + prefix);
40
+ const entries = await drain(this.db.iterator({ gte: range.gte, lt: range.lt, keys: true }));
41
+ return entries.map(([rawKey]) => kvKeyToString(rawKey).slice(this.prefix.length));
42
+ }
43
+ }
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Package-private LevelDB driver surface.
3
+ *
4
+ * The storage classes (`LevelDBRawStorage`, `LevelDBKVStore`) and the identity
5
+ * helper (`loadOrCreateRNPeerKey`) only depend on the interfaces declared here
6
+ * — never on `rn-leveldb` directly. That lets the suite run under Node mocha
7
+ * against `classic-level`, matching the pattern `db-p2p-storage-ns` uses with
8
+ * `node:sqlite` and `db-p2p-storage-web` uses with `fake-indexeddb`.
9
+ *
10
+ * Only `openOptimysticRNDb`, the user-facing constructors, and the identity
11
+ * helper are exported from `index.ts`; the interfaces in this file are
12
+ * internal — consumers never see them.
13
+ */
14
+
15
+ /** Range options for a `LevelDBLike.iterator` scan. */
16
+ export interface LevelDBIteratorOptions {
17
+ /** Inclusive lower bound. */
18
+ gte?: Uint8Array;
19
+ /** Exclusive lower bound. */
20
+ gt?: Uint8Array;
21
+ /** Inclusive upper bound. */
22
+ lte?: Uint8Array;
23
+ /** Exclusive upper bound. */
24
+ lt?: Uint8Array;
25
+ /** Reverse iteration order. */
26
+ reverse?: boolean;
27
+ /** Maximum number of entries to yield. */
28
+ limit?: number;
29
+ /** Skip values (return zero-length Uint8Array for value). Used by `getApproximateBytesUsed`. */
30
+ keys?: boolean;
31
+ }
32
+
33
+ /** Cursor over a `LevelDBLike` range scan. Caller must `close()` exactly once. */
34
+ export interface LevelDBIteratorLike {
35
+ /** Return the next entry, or `undefined` when the range is exhausted. */
36
+ next(): Promise<[Uint8Array, Uint8Array] | undefined>;
37
+ /** Release any native resources held by the iterator. */
38
+ close(): Promise<void>;
39
+ }
40
+
41
+ /** Atomic batch of `put` / `delete` operations against a single database. */
42
+ export interface LevelDBWriteBatchLike {
43
+ put(key: Uint8Array, value: Uint8Array): this;
44
+ delete(key: Uint8Array): this;
45
+ /** Commit the batch atomically. */
46
+ write(): Promise<void>;
47
+ }
48
+
49
+ /**
50
+ * Minimal LevelDB driver surface used by this package.
51
+ *
52
+ * Wraps either `rn-leveldb` (in production) or `classic-level` (in tests).
53
+ * The interface is `Promise`-returning on every method so the rn-leveldb
54
+ * adapter — which forwards to a synchronous native module — can stay
55
+ * uniform with `classic-level`'s native async API.
56
+ */
57
+ export interface LevelDBLike {
58
+ get(key: Uint8Array): Promise<Uint8Array | undefined>;
59
+ put(key: Uint8Array, value: Uint8Array): Promise<void>;
60
+ delete(key: Uint8Array): Promise<void>;
61
+ batch(): LevelDBWriteBatchLike;
62
+ iterator(options?: LevelDBIteratorOptions): LevelDBIteratorLike;
63
+ /** Release the underlying database handle. */
64
+ close(): Promise<void>;
65
+ }
66
+
67
+ /**
68
+ * Drain an iterator into an array. Used by storage classes so a native
69
+ * iterator never stays open across consumer awaits — same rationale as the
70
+ * IndexedDB and SQLite backends.
71
+ */
72
+ export async function drain(iter: LevelDBIteratorLike): Promise<Array<[Uint8Array, Uint8Array]>> {
73
+ const out: Array<[Uint8Array, Uint8Array]> = [];
74
+ try {
75
+ while (true) {
76
+ const entry = await iter.next();
77
+ if (!entry) break;
78
+ out.push(entry);
79
+ }
80
+ } finally {
81
+ await iter.close();
82
+ }
83
+ return out;
84
+ }
@@ -0,0 +1,155 @@
1
+ import type { ActionId, ActionRev, BlockId, IBlock, Transform } from '@optimystic/db-core';
2
+ import type { BlockMetadata, IRawStorage } from '@optimystic/db-p2p';
3
+ import { drain, type LevelDBLike } from './leveldb-like.js';
4
+ import {
5
+ TAG_PENDING,
6
+ actionIdFromKey,
7
+ blockEnvelopeRange,
8
+ materializedKey,
9
+ metadataKey,
10
+ pendingKey,
11
+ revisionFromKey,
12
+ revisionKey,
13
+ transactionKey,
14
+ } from './keys.js';
15
+ import { createLogger } from './logger.js';
16
+
17
+ const log = createLogger('storage:leveldb');
18
+
19
+ const textEncoder = new TextEncoder();
20
+ const textDecoder = new TextDecoder();
21
+
22
+ /**
23
+ * LevelDB-backed `IRawStorage` implementation for React Native peers.
24
+ *
25
+ * All data lives in a single LevelDB database; per-store partitioning is by a
26
+ * leading tag byte (see `./keys.ts`). `listRevisions` and
27
+ * `listPendingTransactions` use range iterators with explicit bounds, drained
28
+ * into an array before yielding (same rationale as the IndexedDB / SQLite
29
+ * backends — a native iterator must not stay open across consumer awaits).
30
+ *
31
+ * `promotePendingTransaction` runs as a single `WriteBatch`, making the
32
+ * pending → committed move atomic against crashes.
33
+ */
34
+ export class LevelDBRawStorage implements IRawStorage {
35
+ constructor(private readonly db: LevelDBLike) {}
36
+
37
+ async getMetadata(blockId: BlockId): Promise<BlockMetadata | undefined> {
38
+ const bytes = await this.db.get(metadataKey(blockId));
39
+ if (!bytes) return undefined;
40
+ return JSON.parse(textDecoder.decode(bytes)) as BlockMetadata;
41
+ }
42
+
43
+ async saveMetadata(blockId: BlockId, metadata: BlockMetadata): Promise<void> {
44
+ await this.db.put(metadataKey(blockId), textEncoder.encode(JSON.stringify(metadata)));
45
+ }
46
+
47
+ async getRevision(blockId: BlockId, rev: number): Promise<ActionId | undefined> {
48
+ const bytes = await this.db.get(revisionKey(blockId, rev));
49
+ if (!bytes) return undefined;
50
+ return textDecoder.decode(bytes) as ActionId;
51
+ }
52
+
53
+ async saveRevision(blockId: BlockId, rev: number, actionId: ActionId): Promise<void> {
54
+ await this.db.put(revisionKey(blockId, rev), textEncoder.encode(actionId));
55
+ }
56
+
57
+ async *listRevisions(blockId: BlockId, startRev: number, endRev: number): AsyncIterable<ActionRev> {
58
+ const ascending = startRev <= endRev;
59
+ const lo = ascending ? startRev : endRev;
60
+ const hi = ascending ? endRev : startRev;
61
+ // `revisionKey(blockId, hi)` is exactly the inclusive upper bound; LevelDB
62
+ // uses exclusive `lt`, so request `lte` via `lt = key(hi)+0x01` would
63
+ // require an extra byte. Easier: use `lt = revisionKey(blockId, hi+1)`.
64
+ const gte = revisionKey(blockId, lo);
65
+ const lt = revisionKey(blockId, hi + 1);
66
+ const entries = await drain(this.db.iterator({ gte, lt, reverse: !ascending }));
67
+ for (const [key, value] of entries) {
68
+ yield {
69
+ rev: revisionFromKey(key),
70
+ actionId: textDecoder.decode(value) as ActionId,
71
+ };
72
+ }
73
+ }
74
+
75
+ async getPendingTransaction(blockId: BlockId, actionId: ActionId): Promise<Transform | undefined> {
76
+ const bytes = await this.db.get(pendingKey(blockId, actionId));
77
+ if (!bytes) return undefined;
78
+ return JSON.parse(textDecoder.decode(bytes)) as Transform;
79
+ }
80
+
81
+ async savePendingTransaction(blockId: BlockId, actionId: ActionId, transform: Transform): Promise<void> {
82
+ await this.db.put(pendingKey(blockId, actionId), textEncoder.encode(JSON.stringify(transform)));
83
+ }
84
+
85
+ async deletePendingTransaction(blockId: BlockId, actionId: ActionId): Promise<void> {
86
+ await this.db.delete(pendingKey(blockId, actionId));
87
+ }
88
+
89
+ async *listPendingTransactions(blockId: BlockId): AsyncIterable<ActionId> {
90
+ const range = blockEnvelopeRange(TAG_PENDING, blockId);
91
+ const entries = await drain(this.db.iterator({ gte: range.gte, lt: range.lt, keys: true }));
92
+ for (const [key] of entries) {
93
+ yield actionIdFromKey(key, blockId) as ActionId;
94
+ }
95
+ }
96
+
97
+ async getTransaction(blockId: BlockId, actionId: ActionId): Promise<Transform | undefined> {
98
+ const bytes = await this.db.get(transactionKey(blockId, actionId));
99
+ if (!bytes) return undefined;
100
+ return JSON.parse(textDecoder.decode(bytes)) as Transform;
101
+ }
102
+
103
+ async saveTransaction(blockId: BlockId, actionId: ActionId, transform: Transform): Promise<void> {
104
+ await this.db.put(transactionKey(blockId, actionId), textEncoder.encode(JSON.stringify(transform)));
105
+ }
106
+
107
+ async getMaterializedBlock(blockId: BlockId, actionId: ActionId): Promise<IBlock | undefined> {
108
+ const bytes = await this.db.get(materializedKey(blockId, actionId));
109
+ if (!bytes) return undefined;
110
+ return JSON.parse(textDecoder.decode(bytes)) as IBlock;
111
+ }
112
+
113
+ async saveMaterializedBlock(blockId: BlockId, actionId: ActionId, block?: IBlock): Promise<void> {
114
+ const key = materializedKey(blockId, actionId);
115
+ if (block) {
116
+ await this.db.put(key, textEncoder.encode(JSON.stringify(block)));
117
+ } else {
118
+ await this.db.delete(key);
119
+ }
120
+ }
121
+
122
+ async getApproximateBytesUsed(): Promise<number> {
123
+ try {
124
+ let total = 0;
125
+ const iter = this.db.iterator();
126
+ try {
127
+ while (true) {
128
+ const entry = await iter.next();
129
+ if (!entry) break;
130
+ total += entry[0].byteLength + entry[1].byteLength;
131
+ }
132
+ } finally {
133
+ await iter.close();
134
+ }
135
+ return total;
136
+ } catch (err) {
137
+ log('getApproximateBytesUsed iterator failed: %o', err);
138
+ return 0;
139
+ }
140
+ }
141
+
142
+ async promotePendingTransaction(blockId: BlockId, actionId: ActionId): Promise<void> {
143
+ const pKey = pendingKey(blockId, actionId);
144
+ const value = await this.db.get(pKey);
145
+ if (!value) {
146
+ throw new Error(`Pending action ${actionId} not found for block ${blockId}`);
147
+ }
148
+ const tKey = transactionKey(blockId, actionId);
149
+ await this.db
150
+ .batch()
151
+ .put(tKey, value)
152
+ .delete(pKey)
153
+ .write();
154
+ }
155
+ }
@@ -0,0 +1,274 @@
1
+ import type {
2
+ LevelDBIteratorLike,
3
+ LevelDBIteratorOptions,
4
+ LevelDBLike,
5
+ LevelDBWriteBatchLike,
6
+ } from './leveldb-like.js';
7
+
8
+ /**
9
+ * Minimal subset of `rn-leveldb`'s `LevelDB` we depend on. Re-declared
10
+ * locally so this module's *types* don't pull in the plugin's declarations
11
+ * (the plugin is a peer dependency that may not be installed at typecheck
12
+ * time on non-RN consumers).
13
+ *
14
+ * Matches the shape used by `@quereus/plugin-react-native-leveldb` so apps
15
+ * embedding both Optimystic and Quereus can share one native module.
16
+ */
17
+ export interface RNLevelDBNative {
18
+ put(key: ArrayBuffer | string, value: ArrayBuffer | string): void;
19
+ getBuf(key: ArrayBuffer | string): ArrayBuffer | null;
20
+ delete(key: ArrayBuffer | string): void;
21
+ close(): void;
22
+ newIterator(): RNLevelDBIteratorNative;
23
+ write(batch: RNLevelDBWriteBatchNative): void;
24
+ }
25
+
26
+ export interface RNLevelDBWriteBatchNative {
27
+ put(key: ArrayBuffer | string, value: ArrayBuffer | string): void;
28
+ delete(key: ArrayBuffer | string): void;
29
+ close(): void;
30
+ }
31
+
32
+ export interface RNLevelDBIteratorNative {
33
+ valid(): boolean;
34
+ seek(target: ArrayBuffer | string): RNLevelDBIteratorNative;
35
+ seekToFirst(): RNLevelDBIteratorNative;
36
+ seekLast(): RNLevelDBIteratorNative;
37
+ next(): void;
38
+ prev(): void;
39
+ keyBuf(): ArrayBuffer;
40
+ valueBuf(): ArrayBuffer;
41
+ close(): void;
42
+ }
43
+
44
+ /** Constructor type for `rn-leveldb`'s `LevelDBWriteBatch`. */
45
+ export type RNLevelDBWriteBatchCtor = new () => RNLevelDBWriteBatchNative;
46
+
47
+ /**
48
+ * Open function shape — typically `(name, createIfMissing, errorIfExists) =>
49
+ * new LevelDB(name, createIfMissing, errorIfExists)`. The caller controls
50
+ * how `rn-leveldb` is imported so this package never directly imports it,
51
+ * which keeps the unit tests runnable under Node.
52
+ */
53
+ export type RNLevelDBOpenFn = (name: string, createIfMissing: boolean, errorIfExists: boolean) => RNLevelDBNative;
54
+
55
+ export const DEFAULT_DB_NAME = 'optimystic';
56
+
57
+ export interface OpenOptimysticRNDbOptions {
58
+ /** `rn-leveldb`'s `LevelDB` constructor wrapped as an open function. */
59
+ openFn: RNLevelDBOpenFn;
60
+ /** `rn-leveldb`'s `LevelDBWriteBatch` constructor. */
61
+ WriteBatch: RNLevelDBWriteBatchCtor;
62
+ /** Database name (LevelDB directory). Default `optimystic`. */
63
+ name?: string;
64
+ /** Whether to create the database if it doesn't exist. Default `true`. */
65
+ createIfMissing?: boolean;
66
+ /** Whether to error if the database already exists. Default `false`. */
67
+ errorIfExists?: boolean;
68
+ }
69
+
70
+ /**
71
+ * Opens (creating if needed) the Optimystic LevelDB database used by
72
+ * `LevelDBRawStorage`, `LevelDBKVStore`, and `loadOrCreateRNPeerKey`.
73
+ *
74
+ * The caller passes the `rn-leveldb` constructors in; this keeps the
75
+ * native module out of the package's static import graph. Apps embedding
76
+ * both Optimystic and Quereus can pass the same constructors to both —
77
+ * one native module, one Podfile entry.
78
+ */
79
+ export function openOptimysticRNDb(options: OpenOptimysticRNDbOptions): LevelDBLike {
80
+ const native = options.openFn(
81
+ options.name ?? DEFAULT_DB_NAME,
82
+ options.createIfMissing ?? true,
83
+ options.errorIfExists ?? false,
84
+ );
85
+ return wrapRNLevelDB(native, options.WriteBatch);
86
+ }
87
+
88
+ /**
89
+ * Wraps an already-open `rn-leveldb` `LevelDB` instance to satisfy the
90
+ * `LevelDBLike` interface. Exported for callers that already hold a handle
91
+ * (rare — usually `openOptimysticRNDb` is the right entry point).
92
+ */
93
+ export function wrapRNLevelDB(native: RNLevelDBNative, WriteBatch: RNLevelDBWriteBatchCtor): LevelDBLike {
94
+ return new RNLevelDBAdapter(native, WriteBatch);
95
+ }
96
+
97
+ class RNLevelDBAdapter implements LevelDBLike {
98
+ constructor(
99
+ private readonly native: RNLevelDBNative,
100
+ private readonly WriteBatch: RNLevelDBWriteBatchCtor,
101
+ ) {}
102
+
103
+ async get(key: Uint8Array): Promise<Uint8Array | undefined> {
104
+ const result = this.native.getBuf(toArrayBuffer(key));
105
+ return result === null ? undefined : new Uint8Array(result);
106
+ }
107
+
108
+ async put(key: Uint8Array, value: Uint8Array): Promise<void> {
109
+ this.native.put(toArrayBuffer(key), toArrayBuffer(value));
110
+ }
111
+
112
+ async delete(key: Uint8Array): Promise<void> {
113
+ this.native.delete(toArrayBuffer(key));
114
+ }
115
+
116
+ batch(): LevelDBWriteBatchLike {
117
+ return new RNLevelDBWriteBatchAdapter(this.native, new this.WriteBatch());
118
+ }
119
+
120
+ iterator(options: LevelDBIteratorOptions = {}): LevelDBIteratorLike {
121
+ return new RNLevelDBIteratorAdapter(this.native.newIterator(), options);
122
+ }
123
+
124
+ async close(): Promise<void> {
125
+ this.native.close();
126
+ }
127
+ }
128
+
129
+ class RNLevelDBWriteBatchAdapter implements LevelDBWriteBatchLike {
130
+ constructor(
131
+ private readonly native: RNLevelDBNative,
132
+ private readonly batch: RNLevelDBWriteBatchNative,
133
+ ) {}
134
+
135
+ put(key: Uint8Array, value: Uint8Array): this {
136
+ this.batch.put(toArrayBuffer(key), toArrayBuffer(value));
137
+ return this;
138
+ }
139
+
140
+ delete(key: Uint8Array): this {
141
+ this.batch.delete(toArrayBuffer(key));
142
+ return this;
143
+ }
144
+
145
+ async write(): Promise<void> {
146
+ try {
147
+ this.native.write(this.batch);
148
+ } finally {
149
+ this.batch.close();
150
+ }
151
+ }
152
+ }
153
+
154
+ class RNLevelDBIteratorAdapter implements LevelDBIteratorLike {
155
+ private positioned = false;
156
+ private yielded = 0;
157
+ private done = false;
158
+
159
+ constructor(
160
+ private readonly iter: RNLevelDBIteratorNative,
161
+ private readonly opts: LevelDBIteratorOptions,
162
+ ) {}
163
+
164
+ async next(): Promise<[Uint8Array, Uint8Array] | undefined> {
165
+ if (this.done) return undefined;
166
+ if (this.opts.limit !== undefined && this.yielded >= this.opts.limit) {
167
+ this.done = true;
168
+ return undefined;
169
+ }
170
+
171
+ if (!this.positioned) {
172
+ this.positionInitial();
173
+ this.positioned = true;
174
+ } else if (this.opts.reverse) {
175
+ this.iter.prev();
176
+ } else {
177
+ this.iter.next();
178
+ }
179
+
180
+ if (!this.iter.valid()) {
181
+ this.done = true;
182
+ return undefined;
183
+ }
184
+
185
+ const key = new Uint8Array(this.iter.keyBuf());
186
+
187
+ // Range bounds: bail out as soon as we cross.
188
+ if (this.opts.reverse) {
189
+ if (this.opts.gte && compareBytes(key, this.opts.gte) < 0) {
190
+ this.done = true;
191
+ return undefined;
192
+ }
193
+ if (this.opts.gt && compareBytes(key, this.opts.gt) <= 0) {
194
+ this.done = true;
195
+ return undefined;
196
+ }
197
+ } else {
198
+ if (this.opts.lt && compareBytes(key, this.opts.lt) >= 0) {
199
+ this.done = true;
200
+ return undefined;
201
+ }
202
+ if (this.opts.lte && compareBytes(key, this.opts.lte) > 0) {
203
+ this.done = true;
204
+ return undefined;
205
+ }
206
+ }
207
+
208
+ // `keys: true` means caller wants only keys; skip the `valueBuf` native call.
209
+ const value = this.opts.keys ? new Uint8Array(0) : new Uint8Array(this.iter.valueBuf());
210
+ this.yielded++;
211
+ return [key, value];
212
+ }
213
+
214
+ async close(): Promise<void> {
215
+ this.iter.close();
216
+ }
217
+
218
+ private positionInitial(): void {
219
+ if (this.opts.reverse) {
220
+ if (this.opts.lte !== undefined) {
221
+ this.iter.seek(toArrayBuffer(this.opts.lte));
222
+ if (!this.iter.valid()) {
223
+ this.iter.seekLast();
224
+ } else {
225
+ const key = new Uint8Array(this.iter.keyBuf());
226
+ if (compareBytes(key, this.opts.lte) > 0) this.iter.prev();
227
+ }
228
+ } else if (this.opts.lt !== undefined) {
229
+ this.iter.seek(toArrayBuffer(this.opts.lt));
230
+ if (this.iter.valid()) {
231
+ this.iter.prev();
232
+ } else {
233
+ this.iter.seekLast();
234
+ }
235
+ } else {
236
+ this.iter.seekLast();
237
+ }
238
+ } else {
239
+ if (this.opts.gte !== undefined) {
240
+ this.iter.seek(toArrayBuffer(this.opts.gte));
241
+ } else if (this.opts.gt !== undefined) {
242
+ this.iter.seek(toArrayBuffer(this.opts.gt));
243
+ if (this.iter.valid()) {
244
+ const key = new Uint8Array(this.iter.keyBuf());
245
+ if (compareBytes(key, this.opts.gt) === 0) this.iter.next();
246
+ }
247
+ } else {
248
+ this.iter.seekToFirst();
249
+ }
250
+ }
251
+ }
252
+ }
253
+
254
+ function toArrayBuffer(bytes: Uint8Array): ArrayBuffer {
255
+ const buffer = bytes.buffer;
256
+ if (buffer instanceof ArrayBuffer
257
+ && bytes.byteOffset === 0
258
+ && bytes.byteLength === buffer.byteLength) {
259
+ return buffer;
260
+ }
261
+ const copy = new ArrayBuffer(bytes.byteLength);
262
+ new Uint8Array(copy).set(bytes);
263
+ return copy;
264
+ }
265
+
266
+ function compareBytes(a: Uint8Array, b: Uint8Array): number {
267
+ const minLength = Math.min(a.length, b.length);
268
+ for (let i = 0; i < minLength; i++) {
269
+ const av = a[i]!;
270
+ const bv = b[i]!;
271
+ if (av !== bv) return av - bv;
272
+ }
273
+ return a.length - b.length;
274
+ }