@optimystic/db-p2p-storage-rn 0.13.0 → 0.13.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/dist/src/identity.d.ts +18 -0
- package/dist/src/identity.d.ts.map +1 -0
- package/dist/src/identity.js +27 -0
- package/dist/src/identity.js.map +1 -0
- package/dist/src/index.d.ts +4 -2
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +4 -2
- package/dist/src/index.js.map +1 -1
- package/dist/src/keys.d.ts +55 -0
- package/dist/src/keys.d.ts.map +1 -0
- package/dist/src/keys.js +126 -0
- package/dist/src/keys.js.map +1 -0
- package/dist/src/leveldb-kv-store.d.ts +21 -0
- package/dist/src/leveldb-kv-store.d.ts.map +1 -0
- package/dist/src/leveldb-kv-store.js +39 -0
- package/dist/src/leveldb-kv-store.js.map +1 -0
- package/dist/src/leveldb-like.d.ts +68 -0
- package/dist/src/leveldb-like.d.ts.map +1 -0
- package/dist/src/leveldb-like.js +34 -0
- package/dist/src/leveldb-like.js.map +1 -0
- package/dist/src/{mmkv-storage.d.ts → leveldb-storage.d.ts} +19 -32
- package/dist/src/leveldb-storage.d.ts.map +1 -0
- package/dist/src/leveldb-storage.js +138 -0
- package/dist/src/leveldb-storage.js.map +1 -0
- package/dist/src/rn-opener.d.ts +73 -0
- package/dist/src/rn-opener.d.ts.map +1 -0
- package/dist/src/rn-opener.js +200 -0
- package/dist/src/rn-opener.js.map +1 -0
- package/package.json +13 -7
- package/src/identity.ts +33 -0
- package/src/index.ts +14 -3
- package/src/keys.ts +139 -0
- package/src/leveldb-kv-store.ts +43 -0
- package/src/leveldb-like.ts +84 -0
- package/src/leveldb-storage.ts +155 -0
- package/src/rn-opener.ts +274 -0
- package/dist/src/mmkv-kv-store.d.ts +0 -13
- package/dist/src/mmkv-kv-store.d.ts.map +0 -1
- package/dist/src/mmkv-kv-store.js +0 -25
- package/dist/src/mmkv-kv-store.js.map +0 -1
- package/dist/src/mmkv-storage.d.ts.map +0 -1
- package/dist/src/mmkv-storage.js +0 -138
- package/dist/src/mmkv-storage.js.map +0 -1
- package/src/mmkv-kv-store.ts +0 -26
- 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
|
+
}
|
package/src/rn-opener.ts
ADDED
|
@@ -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
|
+
}
|