@optimystic/db-p2p-storage-rn 1.11.0 → 1.12.1

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@optimystic/db-p2p-storage-rn",
3
- "version": "1.11.0",
3
+ "version": "1.12.1",
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.11.0",
58
- "@optimystic/db-p2p": "^1.11.0",
57
+ "@optimystic/db-core": "^1.12.1",
58
+ "@optimystic/db-p2p": "^1.12.1",
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';
@@ -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
+ }