@optimystic/db-p2p-storage-rn 1.11.0 → 1.12.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/package.json +3 -3
- package/src/index.ts +14 -14
- package/src/leveldb-storage.ts +227 -227
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@optimystic/db-p2p-storage-rn",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.12.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "React Native LevelDB storage backend for @optimystic/db-p2p",
|
|
6
6
|
"main": "dist/src/index.js",
|
|
@@ -54,8 +54,8 @@
|
|
|
54
54
|
"typescript": "^5.9.3"
|
|
55
55
|
},
|
|
56
56
|
"dependencies": {
|
|
57
|
-
"@optimystic/db-core": "^1.
|
|
58
|
-
"@optimystic/db-p2p": "^1.
|
|
57
|
+
"@optimystic/db-core": "^1.12.0",
|
|
58
|
+
"@optimystic/db-p2p": "^1.12.0",
|
|
59
59
|
"debug": "^4.4.3"
|
|
60
60
|
},
|
|
61
61
|
"peerDependencies": {
|
package/src/index.ts
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
export { LevelDBRawStorage, LevelDBStoreDriver } from './leveldb-storage.js';
|
|
2
|
-
export { LevelDBKVStore } from './leveldb-kv-store.js';
|
|
3
|
-
export { loadOrCreateRNPeerKey, DEFAULT_PEER_KEY_NAME } from './identity.js';
|
|
4
|
-
export {
|
|
5
|
-
openOptimysticRNDb,
|
|
6
|
-
wrapRNLevelDB,
|
|
7
|
-
DEFAULT_DB_NAME,
|
|
8
|
-
type OpenOptimysticRNDbOptions,
|
|
9
|
-
type RNLevelDBNative,
|
|
10
|
-
type RNLevelDBWriteBatchNative,
|
|
11
|
-
type RNLevelDBIteratorNative,
|
|
12
|
-
type RNLevelDBWriteBatchCtor,
|
|
13
|
-
type RNLevelDBOpenFn,
|
|
14
|
-
} from './rn-opener.js';
|
|
1
|
+
export { LevelDBRawStorage, LevelDBStoreDriver } from './leveldb-storage.js';
|
|
2
|
+
export { LevelDBKVStore } from './leveldb-kv-store.js';
|
|
3
|
+
export { loadOrCreateRNPeerKey, DEFAULT_PEER_KEY_NAME } from './identity.js';
|
|
4
|
+
export {
|
|
5
|
+
openOptimysticRNDb,
|
|
6
|
+
wrapRNLevelDB,
|
|
7
|
+
DEFAULT_DB_NAME,
|
|
8
|
+
type OpenOptimysticRNDbOptions,
|
|
9
|
+
type RNLevelDBNative,
|
|
10
|
+
type RNLevelDBWriteBatchNative,
|
|
11
|
+
type RNLevelDBIteratorNative,
|
|
12
|
+
type RNLevelDBWriteBatchCtor,
|
|
13
|
+
type RNLevelDBOpenFn,
|
|
14
|
+
} from './rn-opener.js';
|
package/src/leveldb-storage.ts
CHANGED
|
@@ -1,227 +1,227 @@
|
|
|
1
|
-
import type { ActionId, BlockId } from '@optimystic/db-core';
|
|
2
|
-
import { KvRawStorage, identityForHandle, type RawStoreDriver, type StoreIdentity } from '@optimystic/db-p2p';
|
|
3
|
-
import { drain, type LevelDBLike } from './leveldb-like.js';
|
|
4
|
-
import {
|
|
5
|
-
TAG_PENDING,
|
|
6
|
-
actionIdFromKey,
|
|
7
|
-
blockEnvelopeRange,
|
|
8
|
-
blockIdFromMetadataKey,
|
|
9
|
-
materializedKey,
|
|
10
|
-
metadataKey,
|
|
11
|
-
metadataRange,
|
|
12
|
-
pendingKey,
|
|
13
|
-
proofKey,
|
|
14
|
-
revisionFromKey,
|
|
15
|
-
revisionKey,
|
|
16
|
-
transactionKey,
|
|
17
|
-
} from './keys.js';
|
|
18
|
-
import { createLogger } from './logger.js';
|
|
19
|
-
|
|
20
|
-
const log = createLogger('storage:leveldb');
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* LevelDB {@link RawStoreDriver}: the six logical block-storage stores mapped
|
|
24
|
-
* into a single ordered byte keyspace, partitioned by a leading tag byte (see
|
|
25
|
-
* `./keys.ts`). LevelDB is already one ordered byte-keyspace, so this driver is
|
|
26
|
-
* mostly key encoding — the tag-prefixed byte scheme in `keys.ts` stays in this
|
|
27
|
-
* package; the kernel deals in logical keys.
|
|
28
|
-
*
|
|
29
|
-
* `KvRawStorage` now owns all JSON/UTF-8 serialization, so this driver only ever
|
|
30
|
-
* reads/writes raw `Uint8Array` values — no `TextEncoder`/`TextDecoder` for
|
|
31
|
-
* values, and no `JSON.parse`/`stringify`. A value round-trips as the exact bytes
|
|
32
|
-
* the kernel wrote. Everything LevelDB-specific stays here: the tag-range scans
|
|
33
|
-
* (drained snapshot-first before yielding, so a native iterator never straddles a
|
|
34
|
-
* consumer's `await`), and the single `WriteBatch` that makes `promote` atomic.
|
|
35
|
-
*/
|
|
36
|
-
export class LevelDBStoreDriver implements RawStoreDriver {
|
|
37
|
-
// NOTE: identity is the HANDLE object, so two handles opened over the same LevelDB PATH read
|
|
38
|
-
// as two identities. Resolving that would mean naming the underlying on-disk path, which
|
|
39
|
-
// `LevelDBLike` does not expose. The package opener (`rn-opener.ts`) hands out one handle per
|
|
40
|
-
// path in practice, so this is the reachable case, not the complete one.
|
|
41
|
-
private readonly identity: StoreIdentity;
|
|
42
|
-
|
|
43
|
-
constructor(private readonly db: LevelDBLike) {
|
|
44
|
-
this.identity = identityForHandle('leveldb-handle', db);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
storeIdentity(): StoreIdentity {
|
|
48
|
-
return this.identity;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
// --- metadata ---
|
|
52
|
-
|
|
53
|
-
async getMetadata(blockId: BlockId): Promise<Uint8Array | undefined> {
|
|
54
|
-
return this.db.get(metadataKey(blockId));
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
async putMetadata(blockId: BlockId, value: Uint8Array): Promise<void> {
|
|
58
|
-
await this.db.put(metadataKey(blockId), value);
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
// --- revisions ---
|
|
62
|
-
|
|
63
|
-
async getRevision(blockId: BlockId, rev: number): Promise<Uint8Array | undefined> {
|
|
64
|
-
return this.db.get(revisionKey(blockId, rev));
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
async putRevision(blockId: BlockId, rev: number, value: Uint8Array): Promise<void> {
|
|
68
|
-
await this.db.put(revisionKey(blockId, rev), value);
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
async *rangeRevisions(blockId: BlockId, lo: number, hi: number, reverse: boolean): AsyncIterable<[number, Uint8Array]> {
|
|
72
|
-
// Revs are encoded 8-byte big-endian (`revisionKey`), so byte order == numeric
|
|
73
|
-
// order and `revisionKey(blockId, hi)` is exactly the inclusive upper bound;
|
|
74
|
-
// LevelDB's upper bound is exclusive (`lt`), so use `lt = revisionKey(hi + 1)`.
|
|
75
|
-
const gte = revisionKey(blockId, lo);
|
|
76
|
-
const lt = revisionKey(blockId, hi + 1);
|
|
77
|
-
// Drain before yielding — a native LevelDB iterator must not stay open across
|
|
78
|
-
// the consumer's awaits (the kernel's drain-before-yield contract).
|
|
79
|
-
const entries = await drain(this.db.iterator({ gte, lt, reverse }));
|
|
80
|
-
for (const [key, value] of entries) {
|
|
81
|
-
yield [revisionFromKey(key), value];
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
// --- pending ---
|
|
86
|
-
|
|
87
|
-
async getPending(blockId: BlockId, actionId: ActionId): Promise<Uint8Array | undefined> {
|
|
88
|
-
return this.db.get(pendingKey(blockId, actionId));
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
async putPending(blockId: BlockId, actionId: ActionId, value: Uint8Array): Promise<void> {
|
|
92
|
-
await this.db.put(pendingKey(blockId, actionId), value);
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
async deletePending(blockId: BlockId, actionId: ActionId): Promise<void> {
|
|
96
|
-
await this.db.delete(pendingKey(blockId, actionId));
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
async *listPendingActionIds(blockId: BlockId): AsyncIterable<ActionId> {
|
|
100
|
-
const range = blockEnvelopeRange(TAG_PENDING, blockId);
|
|
101
|
-
// Drain before yielding, same rationale as rangeRevisions.
|
|
102
|
-
const entries = await drain(this.db.iterator({ gte: range.gte, lt: range.lt, keys: true }));
|
|
103
|
-
for (const [key] of entries) {
|
|
104
|
-
yield actionIdFromKey(key, blockId) as ActionId;
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
// --- transactions ---
|
|
109
|
-
|
|
110
|
-
async getTransaction(blockId: BlockId, actionId: ActionId): Promise<Uint8Array | undefined> {
|
|
111
|
-
return this.db.get(transactionKey(blockId, actionId));
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
async putTransaction(blockId: BlockId, actionId: ActionId, value: Uint8Array): Promise<void> {
|
|
115
|
-
await this.db.put(transactionKey(blockId, actionId), value);
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
// --- proofs (own tag byte; no delete — see RawStoreDriver.getProof) ---
|
|
119
|
-
|
|
120
|
-
async getProof(blockId: BlockId, rev: number): Promise<Uint8Array | undefined> {
|
|
121
|
-
return this.db.get(proofKey(blockId, rev));
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
async putProof(blockId: BlockId, rev: number, value: Uint8Array): Promise<void> {
|
|
125
|
-
await this.db.put(proofKey(blockId, rev), value);
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
// --- materialized ---
|
|
129
|
-
|
|
130
|
-
async getMaterialized(blockId: BlockId, actionId: ActionId): Promise<Uint8Array | undefined> {
|
|
131
|
-
return this.db.get(materializedKey(blockId, actionId));
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
async putMaterialized(blockId: BlockId, actionId: ActionId, value: Uint8Array): Promise<void> {
|
|
135
|
-
await this.db.put(materializedKey(blockId, actionId), value);
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
// The kernel owns the put-or-delete branch of `saveMaterializedBlock`, so the
|
|
139
|
-
// driver exposes delete as a separate op.
|
|
140
|
-
async deleteMaterialized(blockId: BlockId, actionId: ActionId): Promise<void> {
|
|
141
|
-
await this.db.delete(materializedKey(blockId, actionId));
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
// --- promote (the only cross-key atomic op) ---
|
|
145
|
-
|
|
146
|
-
async promote(blockId: BlockId, actionId: ActionId): Promise<void> {
|
|
147
|
-
const pKey = pendingKey(blockId, actionId);
|
|
148
|
-
const value = await this.db.get(pKey);
|
|
149
|
-
if (!value) {
|
|
150
|
-
throw new Error(`Pending action ${actionId} not found for block ${blockId}`);
|
|
151
|
-
}
|
|
152
|
-
// The single `WriteBatch` IS the atomic move: a crash leaves either the
|
|
153
|
-
// pending or the committed entry, never both/neither.
|
|
154
|
-
const tKey = transactionKey(blockId, actionId);
|
|
155
|
-
await this.db
|
|
156
|
-
.batch()
|
|
157
|
-
.put(tKey, value)
|
|
158
|
-
.delete(pKey)
|
|
159
|
-
.write();
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
// --- optional passthroughs ---
|
|
163
|
-
|
|
164
|
-
async *listBlockIds(): AsyncIterable<BlockId> {
|
|
165
|
-
// Scan the whole TAG_METADATA keyspace; each metadata key is one distinct
|
|
166
|
-
// block id (empty suffix). keys:true skips values — we only need the ids.
|
|
167
|
-
// Drained before yielding (same discipline as the range scans): a native
|
|
168
|
-
// iterator must not stay open across consumer awaits.
|
|
169
|
-
const range = metadataRange();
|
|
170
|
-
const entries = await drain(this.db.iterator({ gte: range.gte, lt: range.lt, keys: true }));
|
|
171
|
-
for (const [key] of entries) {
|
|
172
|
-
yield blockIdFromMetadataKey(key) as BlockId;
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
// NOTE: full-scan byte sum — O(total bytes) per call. Fine as an advisory
|
|
177
|
-
// StorageMonitor input at current sizes; if a StorageMonitor ever polls this
|
|
178
|
-
// on a large db, replace with the kernel's incremental write-path counter
|
|
179
|
-
// (see the seam NOTE in kv-raw-storage.ts's saveMetadata).
|
|
180
|
-
async approximateBytesUsed(): Promise<number> {
|
|
181
|
-
try {
|
|
182
|
-
let total = 0;
|
|
183
|
-
const iter = this.db.iterator();
|
|
184
|
-
try {
|
|
185
|
-
while (true) {
|
|
186
|
-
const entry = await iter.next();
|
|
187
|
-
if (!entry) break;
|
|
188
|
-
total += entry[0].byteLength + entry[1].byteLength;
|
|
189
|
-
}
|
|
190
|
-
} finally {
|
|
191
|
-
await iter.close();
|
|
192
|
-
}
|
|
193
|
-
return total;
|
|
194
|
-
} catch (err) {
|
|
195
|
-
log('approximateBytesUsed iterator failed: %o', err);
|
|
196
|
-
return 0;
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
// NOTE: intentionally NO close(). The `LevelDBLike` handle is shared with
|
|
201
|
-
// `LevelDBKVStore` and `loadOrCreateRNPeerKey` (one db per RN peer — see the
|
|
202
|
-
// README usage), so the driver must not own its lifecycle; closing it here
|
|
203
|
-
// would break the other two. Same rationale as the IndexedDB driver. The
|
|
204
|
-
// kernel never wires the optional close() regardless.
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
/**
|
|
208
|
-
* LevelDB-backed {@link IRawStorage} for React Native peers, now a thin shell
|
|
209
|
-
* over the shared {@link KvRawStorage} kernel driven by a {@link LevelDBStoreDriver}.
|
|
210
|
-
* The public name/constructor (`new LevelDBRawStorage(db)`) is unchanged so
|
|
211
|
-
* existing imports keep resolving; the kernel supplies the `IRawStorage` surface
|
|
212
|
-
* and the driver supplies LevelDB behavior.
|
|
213
|
-
*
|
|
214
|
-
* `listBlockIds`/`getApproximateBytesUsed` are re-declared here as always-present
|
|
215
|
-
* (the LevelDB driver always implements them, so the kernel constructor always
|
|
216
|
-
* wires them) — the base declares them optional, but every RN consumer relies on
|
|
217
|
-
* them.
|
|
218
|
-
*/
|
|
219
|
-
export class LevelDBRawStorage extends KvRawStorage {
|
|
220
|
-
declare listBlockIds: () => AsyncIterable<BlockId>;
|
|
221
|
-
declare getApproximateBytesUsed: () => Promise<number>;
|
|
222
|
-
declare getStoreIdentity: () => StoreIdentity;
|
|
223
|
-
|
|
224
|
-
constructor(db: LevelDBLike) {
|
|
225
|
-
super(new LevelDBStoreDriver(db));
|
|
226
|
-
}
|
|
227
|
-
}
|
|
1
|
+
import type { ActionId, BlockId } from '@optimystic/db-core';
|
|
2
|
+
import { KvRawStorage, identityForHandle, type RawStoreDriver, type StoreIdentity } from '@optimystic/db-p2p';
|
|
3
|
+
import { drain, type LevelDBLike } from './leveldb-like.js';
|
|
4
|
+
import {
|
|
5
|
+
TAG_PENDING,
|
|
6
|
+
actionIdFromKey,
|
|
7
|
+
blockEnvelopeRange,
|
|
8
|
+
blockIdFromMetadataKey,
|
|
9
|
+
materializedKey,
|
|
10
|
+
metadataKey,
|
|
11
|
+
metadataRange,
|
|
12
|
+
pendingKey,
|
|
13
|
+
proofKey,
|
|
14
|
+
revisionFromKey,
|
|
15
|
+
revisionKey,
|
|
16
|
+
transactionKey,
|
|
17
|
+
} from './keys.js';
|
|
18
|
+
import { createLogger } from './logger.js';
|
|
19
|
+
|
|
20
|
+
const log = createLogger('storage:leveldb');
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* LevelDB {@link RawStoreDriver}: the six logical block-storage stores mapped
|
|
24
|
+
* into a single ordered byte keyspace, partitioned by a leading tag byte (see
|
|
25
|
+
* `./keys.ts`). LevelDB is already one ordered byte-keyspace, so this driver is
|
|
26
|
+
* mostly key encoding — the tag-prefixed byte scheme in `keys.ts` stays in this
|
|
27
|
+
* package; the kernel deals in logical keys.
|
|
28
|
+
*
|
|
29
|
+
* `KvRawStorage` now owns all JSON/UTF-8 serialization, so this driver only ever
|
|
30
|
+
* reads/writes raw `Uint8Array` values — no `TextEncoder`/`TextDecoder` for
|
|
31
|
+
* values, and no `JSON.parse`/`stringify`. A value round-trips as the exact bytes
|
|
32
|
+
* the kernel wrote. Everything LevelDB-specific stays here: the tag-range scans
|
|
33
|
+
* (drained snapshot-first before yielding, so a native iterator never straddles a
|
|
34
|
+
* consumer's `await`), and the single `WriteBatch` that makes `promote` atomic.
|
|
35
|
+
*/
|
|
36
|
+
export class LevelDBStoreDriver implements RawStoreDriver {
|
|
37
|
+
// NOTE: identity is the HANDLE object, so two handles opened over the same LevelDB PATH read
|
|
38
|
+
// as two identities. Resolving that would mean naming the underlying on-disk path, which
|
|
39
|
+
// `LevelDBLike` does not expose. The package opener (`rn-opener.ts`) hands out one handle per
|
|
40
|
+
// path in practice, so this is the reachable case, not the complete one.
|
|
41
|
+
private readonly identity: StoreIdentity;
|
|
42
|
+
|
|
43
|
+
constructor(private readonly db: LevelDBLike) {
|
|
44
|
+
this.identity = identityForHandle('leveldb-handle', db);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
storeIdentity(): StoreIdentity {
|
|
48
|
+
return this.identity;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// --- metadata ---
|
|
52
|
+
|
|
53
|
+
async getMetadata(blockId: BlockId): Promise<Uint8Array | undefined> {
|
|
54
|
+
return this.db.get(metadataKey(blockId));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async putMetadata(blockId: BlockId, value: Uint8Array): Promise<void> {
|
|
58
|
+
await this.db.put(metadataKey(blockId), value);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// --- revisions ---
|
|
62
|
+
|
|
63
|
+
async getRevision(blockId: BlockId, rev: number): Promise<Uint8Array | undefined> {
|
|
64
|
+
return this.db.get(revisionKey(blockId, rev));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async putRevision(blockId: BlockId, rev: number, value: Uint8Array): Promise<void> {
|
|
68
|
+
await this.db.put(revisionKey(blockId, rev), value);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async *rangeRevisions(blockId: BlockId, lo: number, hi: number, reverse: boolean): AsyncIterable<[number, Uint8Array]> {
|
|
72
|
+
// Revs are encoded 8-byte big-endian (`revisionKey`), so byte order == numeric
|
|
73
|
+
// order and `revisionKey(blockId, hi)` is exactly the inclusive upper bound;
|
|
74
|
+
// LevelDB's upper bound is exclusive (`lt`), so use `lt = revisionKey(hi + 1)`.
|
|
75
|
+
const gte = revisionKey(blockId, lo);
|
|
76
|
+
const lt = revisionKey(blockId, hi + 1);
|
|
77
|
+
// Drain before yielding — a native LevelDB iterator must not stay open across
|
|
78
|
+
// the consumer's awaits (the kernel's drain-before-yield contract).
|
|
79
|
+
const entries = await drain(this.db.iterator({ gte, lt, reverse }));
|
|
80
|
+
for (const [key, value] of entries) {
|
|
81
|
+
yield [revisionFromKey(key), value];
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// --- pending ---
|
|
86
|
+
|
|
87
|
+
async getPending(blockId: BlockId, actionId: ActionId): Promise<Uint8Array | undefined> {
|
|
88
|
+
return this.db.get(pendingKey(blockId, actionId));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async putPending(blockId: BlockId, actionId: ActionId, value: Uint8Array): Promise<void> {
|
|
92
|
+
await this.db.put(pendingKey(blockId, actionId), value);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async deletePending(blockId: BlockId, actionId: ActionId): Promise<void> {
|
|
96
|
+
await this.db.delete(pendingKey(blockId, actionId));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async *listPendingActionIds(blockId: BlockId): AsyncIterable<ActionId> {
|
|
100
|
+
const range = blockEnvelopeRange(TAG_PENDING, blockId);
|
|
101
|
+
// Drain before yielding, same rationale as rangeRevisions.
|
|
102
|
+
const entries = await drain(this.db.iterator({ gte: range.gte, lt: range.lt, keys: true }));
|
|
103
|
+
for (const [key] of entries) {
|
|
104
|
+
yield actionIdFromKey(key, blockId) as ActionId;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// --- transactions ---
|
|
109
|
+
|
|
110
|
+
async getTransaction(blockId: BlockId, actionId: ActionId): Promise<Uint8Array | undefined> {
|
|
111
|
+
return this.db.get(transactionKey(blockId, actionId));
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async putTransaction(blockId: BlockId, actionId: ActionId, value: Uint8Array): Promise<void> {
|
|
115
|
+
await this.db.put(transactionKey(blockId, actionId), value);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// --- proofs (own tag byte; no delete — see RawStoreDriver.getProof) ---
|
|
119
|
+
|
|
120
|
+
async getProof(blockId: BlockId, rev: number): Promise<Uint8Array | undefined> {
|
|
121
|
+
return this.db.get(proofKey(blockId, rev));
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async putProof(blockId: BlockId, rev: number, value: Uint8Array): Promise<void> {
|
|
125
|
+
await this.db.put(proofKey(blockId, rev), value);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// --- materialized ---
|
|
129
|
+
|
|
130
|
+
async getMaterialized(blockId: BlockId, actionId: ActionId): Promise<Uint8Array | undefined> {
|
|
131
|
+
return this.db.get(materializedKey(blockId, actionId));
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async putMaterialized(blockId: BlockId, actionId: ActionId, value: Uint8Array): Promise<void> {
|
|
135
|
+
await this.db.put(materializedKey(blockId, actionId), value);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// The kernel owns the put-or-delete branch of `saveMaterializedBlock`, so the
|
|
139
|
+
// driver exposes delete as a separate op.
|
|
140
|
+
async deleteMaterialized(blockId: BlockId, actionId: ActionId): Promise<void> {
|
|
141
|
+
await this.db.delete(materializedKey(blockId, actionId));
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// --- promote (the only cross-key atomic op) ---
|
|
145
|
+
|
|
146
|
+
async promote(blockId: BlockId, actionId: ActionId): Promise<void> {
|
|
147
|
+
const pKey = pendingKey(blockId, actionId);
|
|
148
|
+
const value = await this.db.get(pKey);
|
|
149
|
+
if (!value) {
|
|
150
|
+
throw new Error(`Pending action ${actionId} not found for block ${blockId}`);
|
|
151
|
+
}
|
|
152
|
+
// The single `WriteBatch` IS the atomic move: a crash leaves either the
|
|
153
|
+
// pending or the committed entry, never both/neither.
|
|
154
|
+
const tKey = transactionKey(blockId, actionId);
|
|
155
|
+
await this.db
|
|
156
|
+
.batch()
|
|
157
|
+
.put(tKey, value)
|
|
158
|
+
.delete(pKey)
|
|
159
|
+
.write();
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// --- optional passthroughs ---
|
|
163
|
+
|
|
164
|
+
async *listBlockIds(): AsyncIterable<BlockId> {
|
|
165
|
+
// Scan the whole TAG_METADATA keyspace; each metadata key is one distinct
|
|
166
|
+
// block id (empty suffix). keys:true skips values — we only need the ids.
|
|
167
|
+
// Drained before yielding (same discipline as the range scans): a native
|
|
168
|
+
// iterator must not stay open across consumer awaits.
|
|
169
|
+
const range = metadataRange();
|
|
170
|
+
const entries = await drain(this.db.iterator({ gte: range.gte, lt: range.lt, keys: true }));
|
|
171
|
+
for (const [key] of entries) {
|
|
172
|
+
yield blockIdFromMetadataKey(key) as BlockId;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// NOTE: full-scan byte sum — O(total bytes) per call. Fine as an advisory
|
|
177
|
+
// StorageMonitor input at current sizes; if a StorageMonitor ever polls this
|
|
178
|
+
// on a large db, replace with the kernel's incremental write-path counter
|
|
179
|
+
// (see the seam NOTE in kv-raw-storage.ts's saveMetadata).
|
|
180
|
+
async approximateBytesUsed(): Promise<number> {
|
|
181
|
+
try {
|
|
182
|
+
let total = 0;
|
|
183
|
+
const iter = this.db.iterator();
|
|
184
|
+
try {
|
|
185
|
+
while (true) {
|
|
186
|
+
const entry = await iter.next();
|
|
187
|
+
if (!entry) break;
|
|
188
|
+
total += entry[0].byteLength + entry[1].byteLength;
|
|
189
|
+
}
|
|
190
|
+
} finally {
|
|
191
|
+
await iter.close();
|
|
192
|
+
}
|
|
193
|
+
return total;
|
|
194
|
+
} catch (err) {
|
|
195
|
+
log('approximateBytesUsed iterator failed: %o', err);
|
|
196
|
+
return 0;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// NOTE: intentionally NO close(). The `LevelDBLike` handle is shared with
|
|
201
|
+
// `LevelDBKVStore` and `loadOrCreateRNPeerKey` (one db per RN peer — see the
|
|
202
|
+
// README usage), so the driver must not own its lifecycle; closing it here
|
|
203
|
+
// would break the other two. Same rationale as the IndexedDB driver. The
|
|
204
|
+
// kernel never wires the optional close() regardless.
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* LevelDB-backed {@link IRawStorage} for React Native peers, now a thin shell
|
|
209
|
+
* over the shared {@link KvRawStorage} kernel driven by a {@link LevelDBStoreDriver}.
|
|
210
|
+
* The public name/constructor (`new LevelDBRawStorage(db)`) is unchanged so
|
|
211
|
+
* existing imports keep resolving; the kernel supplies the `IRawStorage` surface
|
|
212
|
+
* and the driver supplies LevelDB behavior.
|
|
213
|
+
*
|
|
214
|
+
* `listBlockIds`/`getApproximateBytesUsed` are re-declared here as always-present
|
|
215
|
+
* (the LevelDB driver always implements them, so the kernel constructor always
|
|
216
|
+
* wires them) — the base declares them optional, but every RN consumer relies on
|
|
217
|
+
* them.
|
|
218
|
+
*/
|
|
219
|
+
export class LevelDBRawStorage extends KvRawStorage {
|
|
220
|
+
declare listBlockIds: () => AsyncIterable<BlockId>;
|
|
221
|
+
declare getApproximateBytesUsed: () => Promise<number>;
|
|
222
|
+
declare getStoreIdentity: () => StoreIdentity;
|
|
223
|
+
|
|
224
|
+
constructor(db: LevelDBLike) {
|
|
225
|
+
super(new LevelDBStoreDriver(db));
|
|
226
|
+
}
|
|
227
|
+
}
|