@optimystic/db-p2p-storage-web 1.5.0 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@optimystic/db-p2p-storage-web",
3
- "version": "1.5.0",
3
+ "version": "1.6.0",
4
4
  "type": "module",
5
5
  "description": "Browser IndexedDB 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.5.0",
58
- "@optimystic/db-p2p": "^1.5.0",
57
+ "@optimystic/db-core": "^1.6.0",
58
+ "@optimystic/db-p2p": "^1.6.0",
59
59
  "debug": "^4.4.3",
60
60
  "idb": "^8.0.3"
61
61
  },
package/src/identity.ts CHANGED
@@ -1,29 +1,29 @@
1
- import { generateKeyPair, privateKeyFromProtobuf, privateKeyToProtobuf } from '@libp2p/crypto/keys';
2
- import type { PrivateKey } from '@libp2p/interface';
3
- import type { OptimysticWebDBHandle } from './db.js';
4
-
5
- export const DEFAULT_PEER_KEY_NAME = 'peer-private-key';
6
-
7
- /**
8
- * Loads the persisted browser peer's libp2p private key, or generates a fresh
9
- * Ed25519 key on first call and persists it.
10
- *
11
- * Stores the key as raw protobuf bytes (a `Uint8Array`) under `keyName` in the
12
- * `kv` object store. IndexedDB stores typed arrays natively — no base64 round-trip.
13
- *
14
- * The returned `PrivateKey` can be passed directly to `createLibp2pNode({ privateKey })`,
15
- * giving a browser peer a stable, reload-surviving identity.
16
- */
17
- export async function loadOrCreateBrowserPeerKey(
18
- db: OptimysticWebDBHandle,
19
- keyName: string = DEFAULT_PEER_KEY_NAME,
20
- ): Promise<PrivateKey> {
21
- const stored = await db.get('kv', keyName);
22
- if (stored instanceof Uint8Array) {
23
- return privateKeyFromProtobuf(stored);
24
- }
25
- const key = await generateKeyPair('Ed25519');
26
- const bytes = privateKeyToProtobuf(key);
27
- await db.put('kv', bytes, keyName);
28
- return key;
29
- }
1
+ import { generateKeyPair, privateKeyFromProtobuf, privateKeyToProtobuf } from '@libp2p/crypto/keys';
2
+ import type { PrivateKey } from '@libp2p/interface';
3
+ import type { OptimysticWebDBHandle } from './db.js';
4
+
5
+ export const DEFAULT_PEER_KEY_NAME = 'peer-private-key';
6
+
7
+ /**
8
+ * Loads the persisted browser peer's libp2p private key, or generates a fresh
9
+ * Ed25519 key on first call and persists it.
10
+ *
11
+ * Stores the key as raw protobuf bytes (a `Uint8Array`) under `keyName` in the
12
+ * `kv` object store. IndexedDB stores typed arrays natively — no base64 round-trip.
13
+ *
14
+ * The returned `PrivateKey` can be passed directly to `createLibp2pNode({ privateKey })`,
15
+ * giving a browser peer a stable, reload-surviving identity.
16
+ */
17
+ export async function loadOrCreateBrowserPeerKey(
18
+ db: OptimysticWebDBHandle,
19
+ keyName: string = DEFAULT_PEER_KEY_NAME,
20
+ ): Promise<PrivateKey> {
21
+ const stored = await db.get('kv', keyName);
22
+ if (stored instanceof Uint8Array) {
23
+ return privateKeyFromProtobuf(stored);
24
+ }
25
+ const key = await generateKeyPair('Ed25519');
26
+ const bytes = privateKeyToProtobuf(key);
27
+ await db.put('kv', bytes, keyName);
28
+ return key;
29
+ }
package/src/index.ts CHANGED
@@ -1,4 +1,4 @@
1
- export * from './db.js';
2
- export * from './indexeddb-storage.js';
3
- export * from './indexeddb-kv-store.js';
4
- export * from './identity.js';
1
+ export * from './db.js';
2
+ export * from './indexeddb-storage.js';
3
+ export * from './indexeddb-kv-store.js';
4
+ export * from './identity.js';
@@ -1,49 +1,49 @@
1
- import type { IKVStore } from '@optimystic/db-p2p';
2
- import type { OptimysticWebDBHandle } from './db.js';
3
-
4
- /**
5
- * IndexedDB-backed `IKVStore` adapter for browser peers.
6
- *
7
- * Stored keys are namespaced with `prefix` so the `kv` object store can be
8
- * shared with the identity helper without collisions. `list(prefix)` uses a
9
- * range-bounded key cursor — never `getAllKeys()` — so a large `kv` store
10
- * does not pay an O(n) JS-side filter cost.
11
- */
12
- export class IndexedDBKVStore implements IKVStore {
13
- constructor(
14
- private readonly db: OptimysticWebDBHandle,
15
- private readonly prefix: string = 'optimystic:txn:',
16
- ) {}
17
-
18
- async get(key: string): Promise<string | undefined> {
19
- const value = await this.db.get('kv', this.prefix + key);
20
- return typeof value === 'string' ? value : undefined;
21
- }
22
-
23
- async set(key: string, value: string): Promise<void> {
24
- await this.db.put('kv', value, this.prefix + key);
25
- }
26
-
27
- async delete(key: string): Promise<void> {
28
- await this.db.delete('kv', this.prefix + key);
29
- }
30
-
31
- async list(prefix: string): Promise<string[]> {
32
- const fullPrefix = this.prefix + prefix;
33
- // '￿' is the highest BMP code unit; appending it produces a string
34
- // that is >= every string starting with `fullPrefix`. IndexedDB compares
35
- // strings by UTF-16 code units, so this bound is exact.
36
- const range = IDBKeyRange.bound(fullPrefix, fullPrefix + '￿');
37
- const tx = this.db.transaction('kv', 'readonly');
38
- const store = tx.objectStore('kv');
39
- const keys: string[] = [];
40
- let cursor = await store.openKeyCursor(range);
41
- while (cursor) {
42
- const fullKey = cursor.key as string;
43
- keys.push(fullKey.slice(this.prefix.length));
44
- cursor = await cursor.continue();
45
- }
46
- await tx.done;
47
- return keys;
48
- }
49
- }
1
+ import type { IKVStore } from '@optimystic/db-p2p';
2
+ import type { OptimysticWebDBHandle } from './db.js';
3
+
4
+ /**
5
+ * IndexedDB-backed `IKVStore` adapter for browser peers.
6
+ *
7
+ * Stored keys are namespaced with `prefix` so the `kv` object store can be
8
+ * shared with the identity helper without collisions. `list(prefix)` uses a
9
+ * range-bounded key cursor — never `getAllKeys()` — so a large `kv` store
10
+ * does not pay an O(n) JS-side filter cost.
11
+ */
12
+ export class IndexedDBKVStore implements IKVStore {
13
+ constructor(
14
+ private readonly db: OptimysticWebDBHandle,
15
+ private readonly prefix: string = 'optimystic:txn:',
16
+ ) {}
17
+
18
+ async get(key: string): Promise<string | undefined> {
19
+ const value = await this.db.get('kv', this.prefix + key);
20
+ return typeof value === 'string' ? value : undefined;
21
+ }
22
+
23
+ async set(key: string, value: string): Promise<void> {
24
+ await this.db.put('kv', value, this.prefix + key);
25
+ }
26
+
27
+ async delete(key: string): Promise<void> {
28
+ await this.db.delete('kv', this.prefix + key);
29
+ }
30
+
31
+ async list(prefix: string): Promise<string[]> {
32
+ const fullPrefix = this.prefix + prefix;
33
+ // '￿' is the highest BMP code unit; appending it produces a string
34
+ // that is >= every string starting with `fullPrefix`. IndexedDB compares
35
+ // strings by UTF-16 code units, so this bound is exact.
36
+ const range = IDBKeyRange.bound(fullPrefix, fullPrefix + '￿');
37
+ const tx = this.db.transaction('kv', 'readonly');
38
+ const store = tx.objectStore('kv');
39
+ const keys: string[] = [];
40
+ let cursor = await store.openKeyCursor(range);
41
+ while (cursor) {
42
+ const fullKey = cursor.key as string;
43
+ keys.push(fullKey.slice(this.prefix.length));
44
+ cursor = await cursor.continue();
45
+ }
46
+ await tx.done;
47
+ return keys;
48
+ }
49
+ }
@@ -1,210 +1,210 @@
1
- import type { ActionId, BlockId } from '@optimystic/db-core';
2
- import { KvRawStorage, identityForHandle, type RawStoreDriver, type StoreIdentity } from '@optimystic/db-p2p';
3
- import type { OptimysticWebDBHandle } from './db.js';
4
- import { createLogger } from './logger.js';
5
-
6
- const log = createLogger('storage:indexeddb');
7
-
8
- /**
9
- * IndexedDB {@link RawStoreDriver}: the six logical block-storage stores mapped
10
- * to the six IndexedDB object stores (`metadata`, `revisions`, `pending`,
11
- * `transactions`, `proofs`, `materialized`) with their compound array keys.
12
- *
13
- * `KvRawStorage` now owns all JSON serialization, so this driver only ever
14
- * reads/writes `Uint8Array` values — IndexedDB stores a typed array natively via
15
- * structured clone and returns a `Uint8Array`, so no codec lives here. Everything
16
- * IndexedDB-specific stays: the range/key cursors (drained snapshot-first before
17
- * yielding), and the single `readwrite` transaction that makes `promote` atomic.
18
- */
19
- export class IndexedDBStoreDriver implements RawStoreDriver {
20
- // NOTE: identity is the HANDLE object, so two handles opened over the same IndexedDB database
21
- // NAME read as two identities. Resolving that would mean naming the underlying database,
22
- // which `OptimysticWebDBHandle` does not expose. The package opener (`db.ts`) hands out one
23
- // handle per database name in practice, so this is the reachable case, not the complete one.
24
- private readonly identity: StoreIdentity;
25
-
26
- constructor(private readonly db: OptimysticWebDBHandle) {
27
- this.identity = identityForHandle('idb-handle', db);
28
- }
29
-
30
- storeIdentity(): StoreIdentity {
31
- return this.identity;
32
- }
33
-
34
- // --- metadata ---
35
-
36
- async getMetadata(blockId: BlockId): Promise<Uint8Array | undefined> {
37
- return this.db.get('metadata', blockId);
38
- }
39
-
40
- async putMetadata(blockId: BlockId, value: Uint8Array): Promise<void> {
41
- await this.db.put('metadata', value, blockId);
42
- }
43
-
44
- // --- revisions ---
45
-
46
- async getRevision(blockId: BlockId, rev: number): Promise<Uint8Array | undefined> {
47
- return this.db.get('revisions', [blockId, rev]);
48
- }
49
-
50
- async putRevision(blockId: BlockId, rev: number, value: Uint8Array): Promise<void> {
51
- await this.db.put('revisions', value, [blockId, rev]);
52
- }
53
-
54
- async *rangeRevisions(blockId: BlockId, lo: number, hi: number, reverse: boolean): AsyncIterable<[number, Uint8Array]> {
55
- const range = IDBKeyRange.bound([blockId, lo], [blockId, hi]);
56
- const tx = this.db.transaction('revisions', 'readonly');
57
- const store = tx.objectStore('revisions');
58
- // Snapshot first so we don't hold the transaction open across yields —
59
- // IndexedDB auto-commits idle transactions, which would invalidate the
60
- // cursor between the consumer's awaits (the kernel's drain-before-yield
61
- // contract). Do NOT switch to lazy yielding mid-transaction.
62
- const results: [number, Uint8Array][] = [];
63
- let cursor = await store.openCursor(range, reverse ? 'prev' : 'next');
64
- while (cursor) {
65
- results.push([(cursor.key as [BlockId, number])[1], cursor.value]);
66
- cursor = await cursor.continue();
67
- }
68
- await tx.done;
69
- for (const result of results) {
70
- yield result;
71
- }
72
- }
73
-
74
- // --- pending ---
75
-
76
- async getPending(blockId: BlockId, actionId: ActionId): Promise<Uint8Array | undefined> {
77
- return this.db.get('pending', [blockId, actionId]);
78
- }
79
-
80
- async putPending(blockId: BlockId, actionId: ActionId, value: Uint8Array): Promise<void> {
81
- await this.db.put('pending', value, [blockId, actionId]);
82
- }
83
-
84
- async deletePending(blockId: BlockId, actionId: ActionId): Promise<void> {
85
- await this.db.delete('pending', [blockId, actionId]);
86
- }
87
-
88
- async *listPendingActionIds(blockId: BlockId): AsyncIterable<ActionId> {
89
- // IndexedDB key ordering: array keys compare element-by-element, and a
90
- // shorter prefix-equal array is less than a longer one; arrays sort
91
- // above all primitive types. So `[blockId]` < `[blockId, anyActionId]`
92
- // < `[blockId, []]`, which captures exactly every key for this block.
93
- const range = IDBKeyRange.bound([blockId] as IDBValidKey, [blockId, []] as IDBValidKey);
94
- const tx = this.db.transaction('pending', 'readonly');
95
- const store = tx.objectStore('pending');
96
- // Snapshot-first, same rationale as rangeRevisions.
97
- const results: ActionId[] = [];
98
- let cursor = await store.openKeyCursor(range);
99
- while (cursor) {
100
- results.push((cursor.key as [BlockId, ActionId])[1]);
101
- cursor = await cursor.continue();
102
- }
103
- await tx.done;
104
- for (const actionId of results) {
105
- yield actionId;
106
- }
107
- }
108
-
109
- // --- transactions ---
110
-
111
- async getTransaction(blockId: BlockId, actionId: ActionId): Promise<Uint8Array | undefined> {
112
- return this.db.get('transactions', [blockId, actionId]);
113
- }
114
-
115
- async putTransaction(blockId: BlockId, actionId: ActionId, value: Uint8Array): Promise<void> {
116
- await this.db.put('transactions', value, [blockId, actionId]);
117
- }
118
-
119
- // --- proofs (keyed [blockId, rev] like revisions; no delete — see RawStoreDriver.getProof) ---
120
-
121
- async getProof(blockId: BlockId, rev: number): Promise<Uint8Array | undefined> {
122
- return this.db.get('proofs', [blockId, rev]);
123
- }
124
-
125
- async putProof(blockId: BlockId, rev: number, value: Uint8Array): Promise<void> {
126
- await this.db.put('proofs', value, [blockId, rev]);
127
- }
128
-
129
- // --- materialized ---
130
-
131
- async getMaterialized(blockId: BlockId, actionId: ActionId): Promise<Uint8Array | undefined> {
132
- return this.db.get('materialized', [blockId, actionId]);
133
- }
134
-
135
- async putMaterialized(blockId: BlockId, actionId: ActionId, value: Uint8Array): Promise<void> {
136
- await this.db.put('materialized', value, [blockId, actionId]);
137
- }
138
-
139
- // The kernel owns the put-or-delete branch of `saveMaterializedBlock`, so the
140
- // driver exposes delete as a separate op.
141
- async deleteMaterialized(blockId: BlockId, actionId: ActionId): Promise<void> {
142
- await this.db.delete('materialized', [blockId, actionId]);
143
- }
144
-
145
- // --- promote (the only cross-key atomic op) ---
146
-
147
- async promote(blockId: BlockId, actionId: ActionId): Promise<void> {
148
- const key: [BlockId, ActionId] = [blockId, actionId];
149
- // Single readwrite transaction over both stores IS the atomic move: a crash
150
- // leaves either the pending or the committed entry, never both/neither.
151
- const tx = this.db.transaction(['pending', 'transactions'], 'readwrite');
152
- const pendingStore = tx.objectStore('pending');
153
- const transactionsStore = tx.objectStore('transactions');
154
- const value = await pendingStore.get(key);
155
- if (!value) {
156
- // Settle the transaction before throwing so the failed promote does not
157
- // leak an open transaction — keep this ordering.
158
- await tx.done.catch(() => undefined);
159
- throw new Error(`Pending action ${actionId} not found for block ${blockId}`);
160
- }
161
- await transactionsStore.put(value, key);
162
- await pendingStore.delete(key);
163
- await tx.done;
164
- }
165
-
166
- // --- optional passthroughs ---
167
-
168
- async *listBlockIds(): AsyncIterable<BlockId> {
169
- // The `metadata` store is keyed by blockId directly, so its keys ARE the
170
- // distinct block ids. getAllKeys reads them under an implicit readonly
171
- // transaction and returns an already-materialized array — no cursor held
172
- // across yields (same rationale as the cursor scans' snapshot-first pattern).
173
- const keys = await this.db.getAllKeys('metadata');
174
- for (const key of keys) {
175
- yield key as BlockId;
176
- }
177
- }
178
-
179
- async approximateBytesUsed(): Promise<number> {
180
- try {
181
- const estimate = await navigator.storage?.estimate?.();
182
- return estimate?.usage ?? 0;
183
- } catch (err) {
184
- log('navigator.storage.estimate() failed: %o', err);
185
- return 0;
186
- }
187
- }
188
- }
189
-
190
- /**
191
- * IndexedDB-backed {@link IRawStorage} for browser peers, now a thin shell over
192
- * the shared {@link KvRawStorage} kernel driven by an {@link IndexedDBStoreDriver}.
193
- * The public name/constructor (`new IndexedDBRawStorage(handle)`) is unchanged so
194
- * existing imports keep resolving; the kernel supplies the `IRawStorage` surface
195
- * and the driver supplies IndexedDB behavior.
196
- *
197
- * `listBlockIds`/`getApproximateBytesUsed` are re-declared here as always-present
198
- * (the IndexedDB driver always implements them, so the kernel constructor always
199
- * wires them) — the base declares them optional, but every web consumer relies
200
- * on them.
201
- */
202
- export class IndexedDBRawStorage extends KvRawStorage {
203
- declare listBlockIds: () => AsyncIterable<BlockId>;
204
- declare getApproximateBytesUsed: () => Promise<number>;
205
- declare getStoreIdentity: () => StoreIdentity;
206
-
207
- constructor(db: OptimysticWebDBHandle) {
208
- super(new IndexedDBStoreDriver(db));
209
- }
210
- }
1
+ import type { ActionId, BlockId } from '@optimystic/db-core';
2
+ import { KvRawStorage, identityForHandle, type RawStoreDriver, type StoreIdentity } from '@optimystic/db-p2p';
3
+ import type { OptimysticWebDBHandle } from './db.js';
4
+ import { createLogger } from './logger.js';
5
+
6
+ const log = createLogger('storage:indexeddb');
7
+
8
+ /**
9
+ * IndexedDB {@link RawStoreDriver}: the six logical block-storage stores mapped
10
+ * to the six IndexedDB object stores (`metadata`, `revisions`, `pending`,
11
+ * `transactions`, `proofs`, `materialized`) with their compound array keys.
12
+ *
13
+ * `KvRawStorage` now owns all JSON serialization, so this driver only ever
14
+ * reads/writes `Uint8Array` values — IndexedDB stores a typed array natively via
15
+ * structured clone and returns a `Uint8Array`, so no codec lives here. Everything
16
+ * IndexedDB-specific stays: the range/key cursors (drained snapshot-first before
17
+ * yielding), and the single `readwrite` transaction that makes `promote` atomic.
18
+ */
19
+ export class IndexedDBStoreDriver implements RawStoreDriver {
20
+ // NOTE: identity is the HANDLE object, so two handles opened over the same IndexedDB database
21
+ // NAME read as two identities. Resolving that would mean naming the underlying database,
22
+ // which `OptimysticWebDBHandle` does not expose. The package opener (`db.ts`) hands out one
23
+ // handle per database name in practice, so this is the reachable case, not the complete one.
24
+ private readonly identity: StoreIdentity;
25
+
26
+ constructor(private readonly db: OptimysticWebDBHandle) {
27
+ this.identity = identityForHandle('idb-handle', db);
28
+ }
29
+
30
+ storeIdentity(): StoreIdentity {
31
+ return this.identity;
32
+ }
33
+
34
+ // --- metadata ---
35
+
36
+ async getMetadata(blockId: BlockId): Promise<Uint8Array | undefined> {
37
+ return this.db.get('metadata', blockId);
38
+ }
39
+
40
+ async putMetadata(blockId: BlockId, value: Uint8Array): Promise<void> {
41
+ await this.db.put('metadata', value, blockId);
42
+ }
43
+
44
+ // --- revisions ---
45
+
46
+ async getRevision(blockId: BlockId, rev: number): Promise<Uint8Array | undefined> {
47
+ return this.db.get('revisions', [blockId, rev]);
48
+ }
49
+
50
+ async putRevision(blockId: BlockId, rev: number, value: Uint8Array): Promise<void> {
51
+ await this.db.put('revisions', value, [blockId, rev]);
52
+ }
53
+
54
+ async *rangeRevisions(blockId: BlockId, lo: number, hi: number, reverse: boolean): AsyncIterable<[number, Uint8Array]> {
55
+ const range = IDBKeyRange.bound([blockId, lo], [blockId, hi]);
56
+ const tx = this.db.transaction('revisions', 'readonly');
57
+ const store = tx.objectStore('revisions');
58
+ // Snapshot first so we don't hold the transaction open across yields —
59
+ // IndexedDB auto-commits idle transactions, which would invalidate the
60
+ // cursor between the consumer's awaits (the kernel's drain-before-yield
61
+ // contract). Do NOT switch to lazy yielding mid-transaction.
62
+ const results: [number, Uint8Array][] = [];
63
+ let cursor = await store.openCursor(range, reverse ? 'prev' : 'next');
64
+ while (cursor) {
65
+ results.push([(cursor.key as [BlockId, number])[1], cursor.value]);
66
+ cursor = await cursor.continue();
67
+ }
68
+ await tx.done;
69
+ for (const result of results) {
70
+ yield result;
71
+ }
72
+ }
73
+
74
+ // --- pending ---
75
+
76
+ async getPending(blockId: BlockId, actionId: ActionId): Promise<Uint8Array | undefined> {
77
+ return this.db.get('pending', [blockId, actionId]);
78
+ }
79
+
80
+ async putPending(blockId: BlockId, actionId: ActionId, value: Uint8Array): Promise<void> {
81
+ await this.db.put('pending', value, [blockId, actionId]);
82
+ }
83
+
84
+ async deletePending(blockId: BlockId, actionId: ActionId): Promise<void> {
85
+ await this.db.delete('pending', [blockId, actionId]);
86
+ }
87
+
88
+ async *listPendingActionIds(blockId: BlockId): AsyncIterable<ActionId> {
89
+ // IndexedDB key ordering: array keys compare element-by-element, and a
90
+ // shorter prefix-equal array is less than a longer one; arrays sort
91
+ // above all primitive types. So `[blockId]` < `[blockId, anyActionId]`
92
+ // < `[blockId, []]`, which captures exactly every key for this block.
93
+ const range = IDBKeyRange.bound([blockId] as IDBValidKey, [blockId, []] as IDBValidKey);
94
+ const tx = this.db.transaction('pending', 'readonly');
95
+ const store = tx.objectStore('pending');
96
+ // Snapshot-first, same rationale as rangeRevisions.
97
+ const results: ActionId[] = [];
98
+ let cursor = await store.openKeyCursor(range);
99
+ while (cursor) {
100
+ results.push((cursor.key as [BlockId, ActionId])[1]);
101
+ cursor = await cursor.continue();
102
+ }
103
+ await tx.done;
104
+ for (const actionId of results) {
105
+ yield actionId;
106
+ }
107
+ }
108
+
109
+ // --- transactions ---
110
+
111
+ async getTransaction(blockId: BlockId, actionId: ActionId): Promise<Uint8Array | undefined> {
112
+ return this.db.get('transactions', [blockId, actionId]);
113
+ }
114
+
115
+ async putTransaction(blockId: BlockId, actionId: ActionId, value: Uint8Array): Promise<void> {
116
+ await this.db.put('transactions', value, [blockId, actionId]);
117
+ }
118
+
119
+ // --- proofs (keyed [blockId, rev] like revisions; no delete — see RawStoreDriver.getProof) ---
120
+
121
+ async getProof(blockId: BlockId, rev: number): Promise<Uint8Array | undefined> {
122
+ return this.db.get('proofs', [blockId, rev]);
123
+ }
124
+
125
+ async putProof(blockId: BlockId, rev: number, value: Uint8Array): Promise<void> {
126
+ await this.db.put('proofs', value, [blockId, rev]);
127
+ }
128
+
129
+ // --- materialized ---
130
+
131
+ async getMaterialized(blockId: BlockId, actionId: ActionId): Promise<Uint8Array | undefined> {
132
+ return this.db.get('materialized', [blockId, actionId]);
133
+ }
134
+
135
+ async putMaterialized(blockId: BlockId, actionId: ActionId, value: Uint8Array): Promise<void> {
136
+ await this.db.put('materialized', value, [blockId, actionId]);
137
+ }
138
+
139
+ // The kernel owns the put-or-delete branch of `saveMaterializedBlock`, so the
140
+ // driver exposes delete as a separate op.
141
+ async deleteMaterialized(blockId: BlockId, actionId: ActionId): Promise<void> {
142
+ await this.db.delete('materialized', [blockId, actionId]);
143
+ }
144
+
145
+ // --- promote (the only cross-key atomic op) ---
146
+
147
+ async promote(blockId: BlockId, actionId: ActionId): Promise<void> {
148
+ const key: [BlockId, ActionId] = [blockId, actionId];
149
+ // Single readwrite transaction over both stores IS the atomic move: a crash
150
+ // leaves either the pending or the committed entry, never both/neither.
151
+ const tx = this.db.transaction(['pending', 'transactions'], 'readwrite');
152
+ const pendingStore = tx.objectStore('pending');
153
+ const transactionsStore = tx.objectStore('transactions');
154
+ const value = await pendingStore.get(key);
155
+ if (!value) {
156
+ // Settle the transaction before throwing so the failed promote does not
157
+ // leak an open transaction — keep this ordering.
158
+ await tx.done.catch(() => undefined);
159
+ throw new Error(`Pending action ${actionId} not found for block ${blockId}`);
160
+ }
161
+ await transactionsStore.put(value, key);
162
+ await pendingStore.delete(key);
163
+ await tx.done;
164
+ }
165
+
166
+ // --- optional passthroughs ---
167
+
168
+ async *listBlockIds(): AsyncIterable<BlockId> {
169
+ // The `metadata` store is keyed by blockId directly, so its keys ARE the
170
+ // distinct block ids. getAllKeys reads them under an implicit readonly
171
+ // transaction and returns an already-materialized array — no cursor held
172
+ // across yields (same rationale as the cursor scans' snapshot-first pattern).
173
+ const keys = await this.db.getAllKeys('metadata');
174
+ for (const key of keys) {
175
+ yield key as BlockId;
176
+ }
177
+ }
178
+
179
+ async approximateBytesUsed(): Promise<number> {
180
+ try {
181
+ const estimate = await navigator.storage?.estimate?.();
182
+ return estimate?.usage ?? 0;
183
+ } catch (err) {
184
+ log('navigator.storage.estimate() failed: %o', err);
185
+ return 0;
186
+ }
187
+ }
188
+ }
189
+
190
+ /**
191
+ * IndexedDB-backed {@link IRawStorage} for browser peers, now a thin shell over
192
+ * the shared {@link KvRawStorage} kernel driven by an {@link IndexedDBStoreDriver}.
193
+ * The public name/constructor (`new IndexedDBRawStorage(handle)`) is unchanged so
194
+ * existing imports keep resolving; the kernel supplies the `IRawStorage` surface
195
+ * and the driver supplies IndexedDB behavior.
196
+ *
197
+ * `listBlockIds`/`getApproximateBytesUsed` are re-declared here as always-present
198
+ * (the IndexedDB driver always implements them, so the kernel constructor always
199
+ * wires them) — the base declares them optional, but every web consumer relies
200
+ * on them.
201
+ */
202
+ export class IndexedDBRawStorage extends KvRawStorage {
203
+ declare listBlockIds: () => AsyncIterable<BlockId>;
204
+ declare getApproximateBytesUsed: () => Promise<number>;
205
+ declare getStoreIdentity: () => StoreIdentity;
206
+
207
+ constructor(db: OptimysticWebDBHandle) {
208
+ super(new IndexedDBStoreDriver(db));
209
+ }
210
+ }
package/src/logger.ts CHANGED
@@ -1,11 +1,11 @@
1
- import debug from 'debug';
2
- import { registerDebugModule } from '@optimystic/db-core';
3
-
4
- const BASE_NAMESPACE = 'optimystic:db-p2p-storage-web';
5
-
6
- // So `enableOptimysticLogging` reaches this package's copy of `debug`, which may be no one else's.
7
- registerDebugModule('db-p2p-storage-web', debug);
8
-
9
- export function createLogger(subNamespace: string): debug.Debugger {
10
- return debug(`${BASE_NAMESPACE}:${subNamespace}`);
11
- }
1
+ import debug from 'debug';
2
+ import { registerDebugModule } from '@optimystic/db-core';
3
+
4
+ const BASE_NAMESPACE = 'optimystic:db-p2p-storage-web';
5
+
6
+ // So `enableOptimysticLogging` reaches this package's copy of `debug`, which may be no one else's.
7
+ registerDebugModule('db-p2p-storage-web', debug);
8
+
9
+ export function createLogger(subNamespace: string): debug.Debugger {
10
+ return debug(`${BASE_NAMESPACE}:${subNamespace}`);
11
+ }