@optimystic/db-p2p-storage-web 0.14.1 → 0.16.3
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/README.md +14 -10
- package/dist/src/db.d.ts +19 -13
- package/dist/src/db.d.ts.map +1 -1
- package/dist/src/db.js.map +1 -1
- package/dist/src/indexeddb-storage.d.ts +46 -23
- package/dist/src/indexeddb-storage.d.ts.map +1 -1
- package/dist/src/indexeddb-storage.js +85 -44
- package/dist/src/indexeddb-storage.js.map +1 -1
- package/package.json +3 -3
- package/src/db.ts +19 -13
- package/src/indexeddb-storage.ts +104 -50
package/README.md
CHANGED
|
@@ -59,16 +59,20 @@ layer parallelises naturally.
|
|
|
59
59
|
## Persistence semantics
|
|
60
60
|
|
|
61
61
|
The package opens a single IndexedDB database (`optimystic` by default) with
|
|
62
|
-
six object stores
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
|
69
|
-
|
|
70
|
-
| `
|
|
71
|
-
| `
|
|
62
|
+
six object stores. `IndexedDBRawStorage` is a thin shell over the shared
|
|
63
|
+
`KvRawStorage` kernel driven by an `IndexedDBStoreDriver`: the kernel owns all
|
|
64
|
+
JSON/UTF-8 serialization, so the five block-storage stores now hold opaque
|
|
65
|
+
kernel-encoded `Uint8Array` bytes rather than live objects. The keys and the
|
|
66
|
+
logical types the kernel decodes them back into are unchanged:
|
|
67
|
+
|
|
68
|
+
| Store | Key | Stored value | Decoded (logical) type |
|
|
69
|
+
|----------------|----------------------|--------------|------------------------|
|
|
70
|
+
| `metadata` | `blockId` | `Uint8Array` | `BlockMetadata` |
|
|
71
|
+
| `revisions` | `[blockId, rev]` | `Uint8Array` | `ActionId` |
|
|
72
|
+
| `pending` | `[blockId, actionId]`| `Uint8Array` | `Transform` |
|
|
73
|
+
| `transactions` | `[blockId, actionId]`| `Uint8Array` | `Transform` |
|
|
74
|
+
| `materialized` | `[blockId, actionId]`| `Uint8Array` | `IBlock` |
|
|
75
|
+
| `kv` | `key` | `string` or `Uint8Array` | — (not kernel-backed) |
|
|
72
76
|
|
|
73
77
|
`listRevisions` and `listPendingTransactions` use real IndexedDB range cursors
|
|
74
78
|
— never `getAllKeys()` + JS filter — so list latency stays bounded as the store
|
package/dist/src/db.d.ts
CHANGED
|
@@ -1,17 +1,23 @@
|
|
|
1
1
|
import { type DBSchema, type IDBPDatabase } from 'idb';
|
|
2
|
-
import type { ActionId, BlockId
|
|
3
|
-
import type { BlockMetadata } from '@optimystic/db-p2p';
|
|
2
|
+
import type { ActionId, BlockId } from '@optimystic/db-core';
|
|
4
3
|
/**
|
|
5
4
|
* IndexedDB schema for the Optimystic browser storage backend.
|
|
6
5
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
6
|
+
* The five block-storage stores keep their original object stores and compound
|
|
7
|
+
* array keys, but their values are now opaque `Uint8Array` blobs: the shared
|
|
8
|
+
* `KvRawStorage` kernel owns JSON (de)serialization, so `IndexedDBStoreDriver`
|
|
9
|
+
* only ever hands IndexedDB kernel-encoded bytes. IndexedDB stores a
|
|
10
|
+
* `Uint8Array` natively via structured clone and returns a `Uint8Array`, so the
|
|
11
|
+
* bytes round-trip without a codec here.
|
|
12
|
+
*
|
|
13
|
+
* - `metadata`: per-block metadata bytes keyed by `blockId`.
|
|
14
|
+
* - `revisions`: revision lookup keyed by `[blockId, rev]` mapping to actionId bytes.
|
|
9
15
|
* Range scans use `IDBKeyRange.bound([blockId, startRev], [blockId, endRev])`.
|
|
10
|
-
* - `pending`: pending
|
|
16
|
+
* - `pending`: pending transform bytes keyed by `[blockId, actionId]`. Listing pending
|
|
11
17
|
* transactions for a block uses a key cursor over `[blockId, ...]`.
|
|
12
|
-
* - `transactions`: committed
|
|
13
|
-
* - `materialized`: materialized
|
|
14
|
-
* `
|
|
18
|
+
* - `transactions`: committed transform bytes keyed by `[blockId, actionId]`.
|
|
19
|
+
* - `materialized`: materialized block bytes keyed by `[blockId, actionId]`. Deleting
|
|
20
|
+
* the row is a driver `delete`, not a stored `undefined`.
|
|
15
21
|
* - `kv`: a generic string keyspace shared by `IndexedDBKVStore` and the
|
|
16
22
|
* identity helper. Identity keys are stored as raw `Uint8Array` blobs (under
|
|
17
23
|
* a separate logical store from string-only `IKVStore` data, but the same
|
|
@@ -20,23 +26,23 @@ import type { BlockMetadata } from '@optimystic/db-p2p';
|
|
|
20
26
|
export interface OptimysticWebDB extends DBSchema {
|
|
21
27
|
metadata: {
|
|
22
28
|
key: BlockId;
|
|
23
|
-
value:
|
|
29
|
+
value: Uint8Array;
|
|
24
30
|
};
|
|
25
31
|
revisions: {
|
|
26
32
|
key: [BlockId, number];
|
|
27
|
-
value:
|
|
33
|
+
value: Uint8Array;
|
|
28
34
|
};
|
|
29
35
|
pending: {
|
|
30
36
|
key: [BlockId, ActionId];
|
|
31
|
-
value:
|
|
37
|
+
value: Uint8Array;
|
|
32
38
|
};
|
|
33
39
|
transactions: {
|
|
34
40
|
key: [BlockId, ActionId];
|
|
35
|
-
value:
|
|
41
|
+
value: Uint8Array;
|
|
36
42
|
};
|
|
37
43
|
materialized: {
|
|
38
44
|
key: [BlockId, ActionId];
|
|
39
|
-
value:
|
|
45
|
+
value: Uint8Array;
|
|
40
46
|
};
|
|
41
47
|
kv: {
|
|
42
48
|
key: string;
|
package/dist/src/db.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"db.d.ts","sourceRoot":"","sources":["../../src/db.ts"],"names":[],"mappings":"AAAA,OAAO,EAAU,KAAK,QAAQ,EAAE,KAAK,YAAY,EAAE,MAAM,KAAK,CAAC;AAC/D,OAAO,KAAK,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,
|
|
1
|
+
{"version":3,"file":"db.d.ts","sourceRoot":"","sources":["../../src/db.ts"],"names":[],"mappings":"AAAA,OAAO,EAAU,KAAK,QAAQ,EAAE,KAAK,YAAY,EAAE,MAAM,KAAK,CAAC;AAC/D,OAAO,KAAK,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAC;AAE7D;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,MAAM,WAAW,eAAgB,SAAQ,QAAQ;IAChD,QAAQ,EAAE;QACT,GAAG,EAAE,OAAO,CAAC;QACb,KAAK,EAAE,UAAU,CAAC;KAClB,CAAC;IACF,SAAS,EAAE;QACV,GAAG,EAAE,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;QACvB,KAAK,EAAE,UAAU,CAAC;KAClB,CAAC;IACF,OAAO,EAAE;QACR,GAAG,EAAE,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC;QACzB,KAAK,EAAE,UAAU,CAAC;KAClB,CAAC;IACF,YAAY,EAAE;QACb,GAAG,EAAE,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC;QACzB,KAAK,EAAE,UAAU,CAAC;KAClB,CAAC;IACF,YAAY,EAAE;QACb,GAAG,EAAE,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC;QACzB,KAAK,EAAE,UAAU,CAAC;KAClB,CAAC;IACF,EAAE,EAAE;QACH,GAAG,EAAE,MAAM,CAAC;QACZ,KAAK,EAAE,MAAM,GAAG,UAAU,CAAC;KAC3B,CAAC;CACF;AAED,eAAO,MAAM,eAAe,eAAe,CAAC;AAC5C,eAAO,MAAM,kBAAkB,IAAI,CAAC;AAEpC,MAAM,MAAM,qBAAqB,GAAG,YAAY,CAAC,eAAe,CAAC,CAAC;AAElE;;;;;;GAMG;AACH,wBAAsB,mBAAmB,CACxC,IAAI,GAAE,MAAwB,EAC9B,OAAO,GAAE,MAA2B,GAClC,OAAO,CAAC,qBAAqB,CAAC,CAuBhC"}
|
package/dist/src/db.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"db.js","sourceRoot":"","sources":["../../src/db.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAoC,MAAM,KAAK,CAAC;
|
|
1
|
+
{"version":3,"file":"db.js","sourceRoot":"","sources":["../../src/db.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAoC,MAAM,KAAK,CAAC;AAqD/D,MAAM,CAAC,MAAM,eAAe,GAAG,YAAY,CAAC;AAC5C,MAAM,CAAC,MAAM,kBAAkB,GAAG,CAAC,CAAC;AAIpC;;;;;;GAMG;AACH,MAAM,CAAC,KAAK,UAAU,mBAAmB,CACxC,OAAe,eAAe,EAC9B,UAAkB,kBAAkB;IAEpC,OAAO,MAAM,CAAkB,IAAI,EAAE,OAAO,EAAE;QAC7C,OAAO,CAAC,EAAE;YACT,IAAI,CAAC,EAAE,CAAC,gBAAgB,CAAC,QAAQ,CAAC,UAAU,CAAC,EAAE,CAAC;gBAC/C,EAAE,CAAC,iBAAiB,CAAC,UAAU,CAAC,CAAC;YAClC,CAAC;YACD,IAAI,CAAC,EAAE,CAAC,gBAAgB,CAAC,QAAQ,CAAC,WAAW,CAAC,EAAE,CAAC;gBAChD,EAAE,CAAC,iBAAiB,CAAC,WAAW,CAAC,CAAC;YACnC,CAAC;YACD,IAAI,CAAC,EAAE,CAAC,gBAAgB,CAAC,QAAQ,CAAC,SAAS,CAAC,EAAE,CAAC;gBAC9C,EAAE,CAAC,iBAAiB,CAAC,SAAS,CAAC,CAAC;YACjC,CAAC;YACD,IAAI,CAAC,EAAE,CAAC,gBAAgB,CAAC,QAAQ,CAAC,cAAc,CAAC,EAAE,CAAC;gBACnD,EAAE,CAAC,iBAAiB,CAAC,cAAc,CAAC,CAAC;YACtC,CAAC;YACD,IAAI,CAAC,EAAE,CAAC,gBAAgB,CAAC,QAAQ,CAAC,cAAc,CAAC,EAAE,CAAC;gBACnD,EAAE,CAAC,iBAAiB,CAAC,cAAc,CAAC,CAAC;YACtC,CAAC;YACD,IAAI,CAAC,EAAE,CAAC,gBAAgB,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC;gBACzC,EAAE,CAAC,iBAAiB,CAAC,IAAI,CAAC,CAAC;YAC5B,CAAC;QACF,CAAC;KACD,CAAC,CAAC;AACJ,CAAC"}
|
|
@@ -1,31 +1,54 @@
|
|
|
1
|
-
import type { ActionId,
|
|
2
|
-
import
|
|
1
|
+
import type { ActionId, BlockId } from '@optimystic/db-core';
|
|
2
|
+
import { KvRawStorage, type RawStoreDriver } from '@optimystic/db-p2p';
|
|
3
3
|
import type { OptimysticWebDBHandle } from './db.js';
|
|
4
4
|
/**
|
|
5
|
-
* IndexedDB
|
|
5
|
+
* IndexedDB {@link RawStoreDriver}: the five logical block-storage stores mapped
|
|
6
|
+
* to the five IndexedDB object stores (`metadata`, `revisions`, `pending`,
|
|
7
|
+
* `transactions`, `materialized`) with their original compound array keys. This
|
|
8
|
+
* is a code refactor, not a storage-format change at the key level.
|
|
6
9
|
*
|
|
7
|
-
*
|
|
8
|
-
* `
|
|
9
|
-
*
|
|
10
|
-
*
|
|
10
|
+
* `KvRawStorage` now owns all JSON serialization, so this driver only ever
|
|
11
|
+
* reads/writes `Uint8Array` values — IndexedDB stores a typed array natively via
|
|
12
|
+
* structured clone and returns a `Uint8Array`, so no codec lives here. Everything
|
|
13
|
+
* IndexedDB-specific stays: the range/key cursors (drained snapshot-first before
|
|
14
|
+
* yielding), and the single `readwrite` transaction that makes `promote` atomic.
|
|
11
15
|
*/
|
|
12
|
-
export declare class
|
|
16
|
+
export declare class IndexedDBStoreDriver implements RawStoreDriver {
|
|
13
17
|
private readonly db;
|
|
14
18
|
constructor(db: OptimysticWebDBHandle);
|
|
15
|
-
getMetadata(blockId: BlockId): Promise<
|
|
16
|
-
|
|
17
|
-
getRevision(blockId: BlockId, rev: number): Promise<
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
getTransaction(blockId: BlockId, actionId: ActionId): Promise<
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
19
|
+
getMetadata(blockId: BlockId): Promise<Uint8Array | undefined>;
|
|
20
|
+
putMetadata(blockId: BlockId, value: Uint8Array): Promise<void>;
|
|
21
|
+
getRevision(blockId: BlockId, rev: number): Promise<Uint8Array | undefined>;
|
|
22
|
+
putRevision(blockId: BlockId, rev: number, value: Uint8Array): Promise<void>;
|
|
23
|
+
rangeRevisions(blockId: BlockId, lo: number, hi: number, reverse: boolean): AsyncIterable<[number, Uint8Array]>;
|
|
24
|
+
getPending(blockId: BlockId, actionId: ActionId): Promise<Uint8Array | undefined>;
|
|
25
|
+
putPending(blockId: BlockId, actionId: ActionId, value: Uint8Array): Promise<void>;
|
|
26
|
+
deletePending(blockId: BlockId, actionId: ActionId): Promise<void>;
|
|
27
|
+
listPendingActionIds(blockId: BlockId): AsyncIterable<ActionId>;
|
|
28
|
+
getTransaction(blockId: BlockId, actionId: ActionId): Promise<Uint8Array | undefined>;
|
|
29
|
+
putTransaction(blockId: BlockId, actionId: ActionId, value: Uint8Array): Promise<void>;
|
|
30
|
+
getMaterialized(blockId: BlockId, actionId: ActionId): Promise<Uint8Array | undefined>;
|
|
31
|
+
putMaterialized(blockId: BlockId, actionId: ActionId, value: Uint8Array): Promise<void>;
|
|
32
|
+
deleteMaterialized(blockId: BlockId, actionId: ActionId): Promise<void>;
|
|
33
|
+
promote(blockId: BlockId, actionId: ActionId): Promise<void>;
|
|
34
|
+
listBlockIds(): AsyncIterable<BlockId>;
|
|
35
|
+
approximateBytesUsed(): Promise<number>;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* IndexedDB-backed {@link IRawStorage} for browser peers, now a thin shell over
|
|
39
|
+
* the shared {@link KvRawStorage} kernel driven by an {@link IndexedDBStoreDriver}.
|
|
40
|
+
* The public name/constructor (`new IndexedDBRawStorage(handle)`) is unchanged so
|
|
41
|
+
* existing imports keep resolving; the kernel supplies the `IRawStorage` surface
|
|
42
|
+
* and the driver supplies IndexedDB behavior.
|
|
43
|
+
*
|
|
44
|
+
* `listBlockIds`/`getApproximateBytesUsed` are re-declared here as always-present
|
|
45
|
+
* (the IndexedDB driver always implements them, so the kernel constructor always
|
|
46
|
+
* wires them) — the base declares them optional, but every web consumer relies
|
|
47
|
+
* on them.
|
|
48
|
+
*/
|
|
49
|
+
export declare class IndexedDBRawStorage extends KvRawStorage {
|
|
50
|
+
listBlockIds: () => AsyncIterable<BlockId>;
|
|
51
|
+
getApproximateBytesUsed: () => Promise<number>;
|
|
52
|
+
constructor(db: OptimysticWebDBHandle);
|
|
30
53
|
}
|
|
31
54
|
//# sourceMappingURL=indexeddb-storage.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"indexeddb-storage.d.ts","sourceRoot":"","sources":["../../src/indexeddb-storage.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,
|
|
1
|
+
{"version":3,"file":"indexeddb-storage.d.ts","sourceRoot":"","sources":["../../src/indexeddb-storage.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAC;AAC7D,OAAO,EAAE,YAAY,EAAE,KAAK,cAAc,EAAE,MAAM,oBAAoB,CAAC;AACvE,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,SAAS,CAAC;AAKrD;;;;;;;;;;;GAWG;AACH,qBAAa,oBAAqB,YAAW,cAAc;IAC9C,OAAO,CAAC,QAAQ,CAAC,EAAE;gBAAF,EAAE,EAAE,qBAAqB;IAIhD,WAAW,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,UAAU,GAAG,SAAS,CAAC;IAI9D,WAAW,CAAC,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC;IAM/D,WAAW,CAAC,OAAO,EAAE,OAAO,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,GAAG,SAAS,CAAC;IAI3E,WAAW,CAAC,OAAO,EAAE,OAAO,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC;IAI3E,cAAc,CAAC,OAAO,EAAE,OAAO,EAAE,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,GAAG,aAAa,CAAC,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;IAsBhH,UAAU,CAAC,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,QAAQ,GAAG,OAAO,CAAC,UAAU,GAAG,SAAS,CAAC;IAIjF,UAAU,CAAC,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,QAAQ,EAAE,KAAK,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC;IAIlF,aAAa,CAAC,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC;IAIjE,oBAAoB,CAAC,OAAO,EAAE,OAAO,GAAG,aAAa,CAAC,QAAQ,CAAC;IAuBhE,cAAc,CAAC,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,QAAQ,GAAG,OAAO,CAAC,UAAU,GAAG,SAAS,CAAC;IAIrF,cAAc,CAAC,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,QAAQ,EAAE,KAAK,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC;IAMtF,eAAe,CAAC,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,QAAQ,GAAG,OAAO,CAAC,UAAU,GAAG,SAAS,CAAC;IAItF,eAAe,CAAC,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,QAAQ,EAAE,KAAK,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC;IAMvF,kBAAkB,CAAC,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC;IAMvE,OAAO,CAAC,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC;IAqB3D,YAAY,IAAI,aAAa,CAAC,OAAO,CAAC;IAWvC,oBAAoB,IAAI,OAAO,CAAC,MAAM,CAAC;CAS7C;AAED;;;;;;;;;;;GAWG;AACH,qBAAa,mBAAoB,SAAQ,YAAY;IAC5C,YAAY,EAAE,MAAM,aAAa,CAAC,OAAO,CAAC,CAAC;IAC3C,uBAAuB,EAAE,MAAM,OAAO,CAAC,MAAM,CAAC,CAAC;gBAE3C,EAAE,EAAE,qBAAqB;CAGrC"}
|
|
@@ -1,44 +1,49 @@
|
|
|
1
|
+
import { KvRawStorage } from '@optimystic/db-p2p';
|
|
1
2
|
import { createLogger } from './logger.js';
|
|
2
3
|
const log = createLogger('storage:indexeddb');
|
|
3
4
|
/**
|
|
4
|
-
* IndexedDB
|
|
5
|
+
* IndexedDB {@link RawStoreDriver}: the five logical block-storage stores mapped
|
|
6
|
+
* to the five IndexedDB object stores (`metadata`, `revisions`, `pending`,
|
|
7
|
+
* `transactions`, `materialized`) with their original compound array keys. This
|
|
8
|
+
* is a code refactor, not a storage-format change at the key level.
|
|
5
9
|
*
|
|
6
|
-
*
|
|
7
|
-
* `
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
+
* `KvRawStorage` now owns all JSON serialization, so this driver only ever
|
|
11
|
+
* reads/writes `Uint8Array` values — IndexedDB stores a typed array natively via
|
|
12
|
+
* structured clone and returns a `Uint8Array`, so no codec lives here. Everything
|
|
13
|
+
* IndexedDB-specific stays: the range/key cursors (drained snapshot-first before
|
|
14
|
+
* yielding), and the single `readwrite` transaction that makes `promote` atomic.
|
|
10
15
|
*/
|
|
11
|
-
export class
|
|
16
|
+
export class IndexedDBStoreDriver {
|
|
12
17
|
db;
|
|
13
18
|
constructor(db) {
|
|
14
19
|
this.db = db;
|
|
15
20
|
}
|
|
21
|
+
// --- metadata ---
|
|
16
22
|
async getMetadata(blockId) {
|
|
17
23
|
return this.db.get('metadata', blockId);
|
|
18
24
|
}
|
|
19
|
-
async
|
|
20
|
-
await this.db.put('metadata',
|
|
25
|
+
async putMetadata(blockId, value) {
|
|
26
|
+
await this.db.put('metadata', value, blockId);
|
|
21
27
|
}
|
|
28
|
+
// --- revisions ---
|
|
22
29
|
async getRevision(blockId, rev) {
|
|
23
30
|
return this.db.get('revisions', [blockId, rev]);
|
|
24
31
|
}
|
|
25
|
-
async
|
|
26
|
-
await this.db.put('revisions',
|
|
32
|
+
async putRevision(blockId, rev, value) {
|
|
33
|
+
await this.db.put('revisions', value, [blockId, rev]);
|
|
27
34
|
}
|
|
28
|
-
async *
|
|
29
|
-
const ascending = startRev <= endRev;
|
|
30
|
-
const lo = ascending ? startRev : endRev;
|
|
31
|
-
const hi = ascending ? endRev : startRev;
|
|
35
|
+
async *rangeRevisions(blockId, lo, hi, reverse) {
|
|
32
36
|
const range = IDBKeyRange.bound([blockId, lo], [blockId, hi]);
|
|
33
37
|
const tx = this.db.transaction('revisions', 'readonly');
|
|
34
38
|
const store = tx.objectStore('revisions');
|
|
35
39
|
// Snapshot first so we don't hold the transaction open across yields —
|
|
36
40
|
// IndexedDB auto-commits idle transactions, which would invalidate the
|
|
37
|
-
// cursor between awaits
|
|
41
|
+
// cursor between the consumer's awaits (the kernel's drain-before-yield
|
|
42
|
+
// contract). Do NOT switch to lazy yielding mid-transaction.
|
|
38
43
|
const results = [];
|
|
39
|
-
let cursor = await store.openCursor(range,
|
|
44
|
+
let cursor = await store.openCursor(range, reverse ? 'prev' : 'next');
|
|
40
45
|
while (cursor) {
|
|
41
|
-
results.push(
|
|
46
|
+
results.push([cursor.key[1], cursor.value]);
|
|
42
47
|
cursor = await cursor.continue();
|
|
43
48
|
}
|
|
44
49
|
await tx.done;
|
|
@@ -46,16 +51,17 @@ export class IndexedDBRawStorage {
|
|
|
46
51
|
yield result;
|
|
47
52
|
}
|
|
48
53
|
}
|
|
49
|
-
|
|
54
|
+
// --- pending ---
|
|
55
|
+
async getPending(blockId, actionId) {
|
|
50
56
|
return this.db.get('pending', [blockId, actionId]);
|
|
51
57
|
}
|
|
52
|
-
async
|
|
53
|
-
await this.db.put('pending',
|
|
58
|
+
async putPending(blockId, actionId, value) {
|
|
59
|
+
await this.db.put('pending', value, [blockId, actionId]);
|
|
54
60
|
}
|
|
55
|
-
async
|
|
61
|
+
async deletePending(blockId, actionId) {
|
|
56
62
|
await this.db.delete('pending', [blockId, actionId]);
|
|
57
63
|
}
|
|
58
|
-
async *
|
|
64
|
+
async *listPendingActionIds(blockId) {
|
|
59
65
|
// IndexedDB key ordering: array keys compare element-by-element, and a
|
|
60
66
|
// shorter prefix-equal array is less than a longer one; arrays sort
|
|
61
67
|
// above all primitive types. So `[blockId]` < `[blockId, anyActionId]`
|
|
@@ -63,6 +69,7 @@ export class IndexedDBRawStorage {
|
|
|
63
69
|
const range = IDBKeyRange.bound([blockId], [blockId, []]);
|
|
64
70
|
const tx = this.db.transaction('pending', 'readonly');
|
|
65
71
|
const store = tx.objectStore('pending');
|
|
72
|
+
// Snapshot-first, same rationale as rangeRevisions.
|
|
66
73
|
const results = [];
|
|
67
74
|
let cursor = await store.openKeyCursor(range);
|
|
68
75
|
while (cursor) {
|
|
@@ -74,25 +81,56 @@ export class IndexedDBRawStorage {
|
|
|
74
81
|
yield actionId;
|
|
75
82
|
}
|
|
76
83
|
}
|
|
84
|
+
// --- transactions ---
|
|
77
85
|
async getTransaction(blockId, actionId) {
|
|
78
86
|
return this.db.get('transactions', [blockId, actionId]);
|
|
79
87
|
}
|
|
80
|
-
async
|
|
81
|
-
await this.db.put('transactions',
|
|
88
|
+
async putTransaction(blockId, actionId, value) {
|
|
89
|
+
await this.db.put('transactions', value, [blockId, actionId]);
|
|
82
90
|
}
|
|
83
|
-
|
|
91
|
+
// --- materialized ---
|
|
92
|
+
async getMaterialized(blockId, actionId) {
|
|
84
93
|
return this.db.get('materialized', [blockId, actionId]);
|
|
85
94
|
}
|
|
86
|
-
async
|
|
95
|
+
async putMaterialized(blockId, actionId, value) {
|
|
96
|
+
await this.db.put('materialized', value, [blockId, actionId]);
|
|
97
|
+
}
|
|
98
|
+
// The kernel owns the put-or-delete branch of `saveMaterializedBlock`, so the
|
|
99
|
+
// driver exposes delete as a separate op.
|
|
100
|
+
async deleteMaterialized(blockId, actionId) {
|
|
101
|
+
await this.db.delete('materialized', [blockId, actionId]);
|
|
102
|
+
}
|
|
103
|
+
// --- promote (the only cross-key atomic op) ---
|
|
104
|
+
async promote(blockId, actionId) {
|
|
87
105
|
const key = [blockId, actionId];
|
|
88
|
-
|
|
89
|
-
|
|
106
|
+
// Single readwrite transaction over both stores IS the atomic move: a crash
|
|
107
|
+
// leaves either the pending or the committed entry, never both/neither.
|
|
108
|
+
const tx = this.db.transaction(['pending', 'transactions'], 'readwrite');
|
|
109
|
+
const pendingStore = tx.objectStore('pending');
|
|
110
|
+
const transactionsStore = tx.objectStore('transactions');
|
|
111
|
+
const value = await pendingStore.get(key);
|
|
112
|
+
if (!value) {
|
|
113
|
+
// Settle the transaction before throwing so the failed promote does not
|
|
114
|
+
// leak an open transaction — keep this ordering.
|
|
115
|
+
await tx.done.catch(() => undefined);
|
|
116
|
+
throw new Error(`Pending action ${actionId} not found for block ${blockId}`);
|
|
90
117
|
}
|
|
91
|
-
|
|
92
|
-
|
|
118
|
+
await transactionsStore.put(value, key);
|
|
119
|
+
await pendingStore.delete(key);
|
|
120
|
+
await tx.done;
|
|
121
|
+
}
|
|
122
|
+
// --- optional passthroughs ---
|
|
123
|
+
async *listBlockIds() {
|
|
124
|
+
// The `metadata` store is keyed by blockId directly, so its keys ARE the
|
|
125
|
+
// distinct block ids. getAllKeys reads them under an implicit readonly
|
|
126
|
+
// transaction and returns an already-materialized array — no cursor held
|
|
127
|
+
// across yields (same rationale as the cursor scans' snapshot-first pattern).
|
|
128
|
+
const keys = await this.db.getAllKeys('metadata');
|
|
129
|
+
for (const key of keys) {
|
|
130
|
+
yield key;
|
|
93
131
|
}
|
|
94
132
|
}
|
|
95
|
-
async
|
|
133
|
+
async approximateBytesUsed() {
|
|
96
134
|
try {
|
|
97
135
|
const estimate = await navigator.storage?.estimate?.();
|
|
98
136
|
return estimate?.usage ?? 0;
|
|
@@ -102,19 +140,22 @@ export class IndexedDBRawStorage {
|
|
|
102
140
|
return 0;
|
|
103
141
|
}
|
|
104
142
|
}
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* IndexedDB-backed {@link IRawStorage} for browser peers, now a thin shell over
|
|
146
|
+
* the shared {@link KvRawStorage} kernel driven by an {@link IndexedDBStoreDriver}.
|
|
147
|
+
* The public name/constructor (`new IndexedDBRawStorage(handle)`) is unchanged so
|
|
148
|
+
* existing imports keep resolving; the kernel supplies the `IRawStorage` surface
|
|
149
|
+
* and the driver supplies IndexedDB behavior.
|
|
150
|
+
*
|
|
151
|
+
* `listBlockIds`/`getApproximateBytesUsed` are re-declared here as always-present
|
|
152
|
+
* (the IndexedDB driver always implements them, so the kernel constructor always
|
|
153
|
+
* wires them) — the base declares them optional, but every web consumer relies
|
|
154
|
+
* on them.
|
|
155
|
+
*/
|
|
156
|
+
export class IndexedDBRawStorage extends KvRawStorage {
|
|
157
|
+
constructor(db) {
|
|
158
|
+
super(new IndexedDBStoreDriver(db));
|
|
118
159
|
}
|
|
119
160
|
}
|
|
120
161
|
//# sourceMappingURL=indexeddb-storage.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"indexeddb-storage.js","sourceRoot":"","sources":["../../src/indexeddb-storage.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"indexeddb-storage.js","sourceRoot":"","sources":["../../src/indexeddb-storage.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@optimystic/db-p2p-storage-web",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.3",
|
|
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": "^0.
|
|
58
|
-
"@optimystic/db-p2p": "^0.
|
|
57
|
+
"@optimystic/db-core": "^0.16.3",
|
|
58
|
+
"@optimystic/db-p2p": "^0.16.3",
|
|
59
59
|
"debug": "^4.4.3",
|
|
60
60
|
"idb": "^8.0.3"
|
|
61
61
|
},
|
package/src/db.ts
CHANGED
|
@@ -1,18 +1,24 @@
|
|
|
1
1
|
import { openDB, type DBSchema, type IDBPDatabase } from 'idb';
|
|
2
|
-
import type { ActionId, BlockId
|
|
3
|
-
import type { BlockMetadata } from '@optimystic/db-p2p';
|
|
2
|
+
import type { ActionId, BlockId } from '@optimystic/db-core';
|
|
4
3
|
|
|
5
4
|
/**
|
|
6
5
|
* IndexedDB schema for the Optimystic browser storage backend.
|
|
7
6
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
7
|
+
* The five block-storage stores keep their original object stores and compound
|
|
8
|
+
* array keys, but their values are now opaque `Uint8Array` blobs: the shared
|
|
9
|
+
* `KvRawStorage` kernel owns JSON (de)serialization, so `IndexedDBStoreDriver`
|
|
10
|
+
* only ever hands IndexedDB kernel-encoded bytes. IndexedDB stores a
|
|
11
|
+
* `Uint8Array` natively via structured clone and returns a `Uint8Array`, so the
|
|
12
|
+
* bytes round-trip without a codec here.
|
|
13
|
+
*
|
|
14
|
+
* - `metadata`: per-block metadata bytes keyed by `blockId`.
|
|
15
|
+
* - `revisions`: revision lookup keyed by `[blockId, rev]` mapping to actionId bytes.
|
|
10
16
|
* Range scans use `IDBKeyRange.bound([blockId, startRev], [blockId, endRev])`.
|
|
11
|
-
* - `pending`: pending
|
|
17
|
+
* - `pending`: pending transform bytes keyed by `[blockId, actionId]`. Listing pending
|
|
12
18
|
* transactions for a block uses a key cursor over `[blockId, ...]`.
|
|
13
|
-
* - `transactions`: committed
|
|
14
|
-
* - `materialized`: materialized
|
|
15
|
-
* `
|
|
19
|
+
* - `transactions`: committed transform bytes keyed by `[blockId, actionId]`.
|
|
20
|
+
* - `materialized`: materialized block bytes keyed by `[blockId, actionId]`. Deleting
|
|
21
|
+
* the row is a driver `delete`, not a stored `undefined`.
|
|
16
22
|
* - `kv`: a generic string keyspace shared by `IndexedDBKVStore` and the
|
|
17
23
|
* identity helper. Identity keys are stored as raw `Uint8Array` blobs (under
|
|
18
24
|
* a separate logical store from string-only `IKVStore` data, but the same
|
|
@@ -21,23 +27,23 @@ import type { BlockMetadata } from '@optimystic/db-p2p';
|
|
|
21
27
|
export interface OptimysticWebDB extends DBSchema {
|
|
22
28
|
metadata: {
|
|
23
29
|
key: BlockId;
|
|
24
|
-
value:
|
|
30
|
+
value: Uint8Array;
|
|
25
31
|
};
|
|
26
32
|
revisions: {
|
|
27
33
|
key: [BlockId, number];
|
|
28
|
-
value:
|
|
34
|
+
value: Uint8Array;
|
|
29
35
|
};
|
|
30
36
|
pending: {
|
|
31
37
|
key: [BlockId, ActionId];
|
|
32
|
-
value:
|
|
38
|
+
value: Uint8Array;
|
|
33
39
|
};
|
|
34
40
|
transactions: {
|
|
35
41
|
key: [BlockId, ActionId];
|
|
36
|
-
value:
|
|
42
|
+
value: Uint8Array;
|
|
37
43
|
};
|
|
38
44
|
materialized: {
|
|
39
45
|
key: [BlockId, ActionId];
|
|
40
|
-
value:
|
|
46
|
+
value: Uint8Array;
|
|
41
47
|
};
|
|
42
48
|
kv: {
|
|
43
49
|
key: string;
|
package/src/indexeddb-storage.ts
CHANGED
|
@@ -1,51 +1,57 @@
|
|
|
1
|
-
import type { ActionId,
|
|
2
|
-
import
|
|
1
|
+
import type { ActionId, BlockId } from '@optimystic/db-core';
|
|
2
|
+
import { KvRawStorage, type RawStoreDriver } from '@optimystic/db-p2p';
|
|
3
3
|
import type { OptimysticWebDBHandle } from './db.js';
|
|
4
4
|
import { createLogger } from './logger.js';
|
|
5
5
|
|
|
6
6
|
const log = createLogger('storage:indexeddb');
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
|
-
* IndexedDB
|
|
9
|
+
* IndexedDB {@link RawStoreDriver}: the five logical block-storage stores mapped
|
|
10
|
+
* to the five IndexedDB object stores (`metadata`, `revisions`, `pending`,
|
|
11
|
+
* `transactions`, `materialized`) with their original compound array keys. This
|
|
12
|
+
* is a code refactor, not a storage-format change at the key level.
|
|
10
13
|
*
|
|
11
|
-
*
|
|
12
|
-
* `
|
|
13
|
-
*
|
|
14
|
-
*
|
|
14
|
+
* `KvRawStorage` now owns all JSON serialization, so this driver only ever
|
|
15
|
+
* reads/writes `Uint8Array` values — IndexedDB stores a typed array natively via
|
|
16
|
+
* structured clone and returns a `Uint8Array`, so no codec lives here. Everything
|
|
17
|
+
* IndexedDB-specific stays: the range/key cursors (drained snapshot-first before
|
|
18
|
+
* yielding), and the single `readwrite` transaction that makes `promote` atomic.
|
|
15
19
|
*/
|
|
16
|
-
export class
|
|
20
|
+
export class IndexedDBStoreDriver implements RawStoreDriver {
|
|
17
21
|
constructor(private readonly db: OptimysticWebDBHandle) {}
|
|
18
22
|
|
|
19
|
-
|
|
23
|
+
// --- metadata ---
|
|
24
|
+
|
|
25
|
+
async getMetadata(blockId: BlockId): Promise<Uint8Array | undefined> {
|
|
20
26
|
return this.db.get('metadata', blockId);
|
|
21
27
|
}
|
|
22
28
|
|
|
23
|
-
async
|
|
24
|
-
await this.db.put('metadata',
|
|
29
|
+
async putMetadata(blockId: BlockId, value: Uint8Array): Promise<void> {
|
|
30
|
+
await this.db.put('metadata', value, blockId);
|
|
25
31
|
}
|
|
26
32
|
|
|
27
|
-
|
|
33
|
+
// --- revisions ---
|
|
34
|
+
|
|
35
|
+
async getRevision(blockId: BlockId, rev: number): Promise<Uint8Array | undefined> {
|
|
28
36
|
return this.db.get('revisions', [blockId, rev]);
|
|
29
37
|
}
|
|
30
38
|
|
|
31
|
-
async
|
|
32
|
-
await this.db.put('revisions',
|
|
39
|
+
async putRevision(blockId: BlockId, rev: number, value: Uint8Array): Promise<void> {
|
|
40
|
+
await this.db.put('revisions', value, [blockId, rev]);
|
|
33
41
|
}
|
|
34
42
|
|
|
35
|
-
async *
|
|
36
|
-
const ascending = startRev <= endRev;
|
|
37
|
-
const lo = ascending ? startRev : endRev;
|
|
38
|
-
const hi = ascending ? endRev : startRev;
|
|
43
|
+
async *rangeRevisions(blockId: BlockId, lo: number, hi: number, reverse: boolean): AsyncIterable<[number, Uint8Array]> {
|
|
39
44
|
const range = IDBKeyRange.bound([blockId, lo], [blockId, hi]);
|
|
40
45
|
const tx = this.db.transaction('revisions', 'readonly');
|
|
41
46
|
const store = tx.objectStore('revisions');
|
|
42
47
|
// Snapshot first so we don't hold the transaction open across yields —
|
|
43
48
|
// IndexedDB auto-commits idle transactions, which would invalidate the
|
|
44
|
-
// cursor between awaits
|
|
45
|
-
|
|
46
|
-
|
|
49
|
+
// cursor between the consumer's awaits (the kernel's drain-before-yield
|
|
50
|
+
// contract). Do NOT switch to lazy yielding mid-transaction.
|
|
51
|
+
const results: [number, Uint8Array][] = [];
|
|
52
|
+
let cursor = await store.openCursor(range, reverse ? 'prev' : 'next');
|
|
47
53
|
while (cursor) {
|
|
48
|
-
results.push(
|
|
54
|
+
results.push([(cursor.key as [BlockId, number])[1], cursor.value]);
|
|
49
55
|
cursor = await cursor.continue();
|
|
50
56
|
}
|
|
51
57
|
await tx.done;
|
|
@@ -54,19 +60,21 @@ export class IndexedDBRawStorage implements IRawStorage {
|
|
|
54
60
|
}
|
|
55
61
|
}
|
|
56
62
|
|
|
57
|
-
|
|
63
|
+
// --- pending ---
|
|
64
|
+
|
|
65
|
+
async getPending(blockId: BlockId, actionId: ActionId): Promise<Uint8Array | undefined> {
|
|
58
66
|
return this.db.get('pending', [blockId, actionId]);
|
|
59
67
|
}
|
|
60
68
|
|
|
61
|
-
async
|
|
62
|
-
await this.db.put('pending',
|
|
69
|
+
async putPending(blockId: BlockId, actionId: ActionId, value: Uint8Array): Promise<void> {
|
|
70
|
+
await this.db.put('pending', value, [blockId, actionId]);
|
|
63
71
|
}
|
|
64
72
|
|
|
65
|
-
async
|
|
73
|
+
async deletePending(blockId: BlockId, actionId: ActionId): Promise<void> {
|
|
66
74
|
await this.db.delete('pending', [blockId, actionId]);
|
|
67
75
|
}
|
|
68
76
|
|
|
69
|
-
async *
|
|
77
|
+
async *listPendingActionIds(blockId: BlockId): AsyncIterable<ActionId> {
|
|
70
78
|
// IndexedDB key ordering: array keys compare element-by-element, and a
|
|
71
79
|
// shorter prefix-equal array is less than a longer one; arrays sort
|
|
72
80
|
// above all primitive types. So `[blockId]` < `[blockId, anyActionId]`
|
|
@@ -74,6 +82,7 @@ export class IndexedDBRawStorage implements IRawStorage {
|
|
|
74
82
|
const range = IDBKeyRange.bound([blockId] as IDBValidKey, [blockId, []] as IDBValidKey);
|
|
75
83
|
const tx = this.db.transaction('pending', 'readonly');
|
|
76
84
|
const store = tx.objectStore('pending');
|
|
85
|
+
// Snapshot-first, same rationale as rangeRevisions.
|
|
77
86
|
const results: ActionId[] = [];
|
|
78
87
|
let cursor = await store.openKeyCursor(range);
|
|
79
88
|
while (cursor) {
|
|
@@ -86,49 +95,94 @@ export class IndexedDBRawStorage implements IRawStorage {
|
|
|
86
95
|
}
|
|
87
96
|
}
|
|
88
97
|
|
|
89
|
-
|
|
98
|
+
// --- transactions ---
|
|
99
|
+
|
|
100
|
+
async getTransaction(blockId: BlockId, actionId: ActionId): Promise<Uint8Array | undefined> {
|
|
90
101
|
return this.db.get('transactions', [blockId, actionId]);
|
|
91
102
|
}
|
|
92
103
|
|
|
93
|
-
async
|
|
94
|
-
await this.db.put('transactions',
|
|
104
|
+
async putTransaction(blockId: BlockId, actionId: ActionId, value: Uint8Array): Promise<void> {
|
|
105
|
+
await this.db.put('transactions', value, [blockId, actionId]);
|
|
95
106
|
}
|
|
96
107
|
|
|
97
|
-
|
|
108
|
+
// --- materialized ---
|
|
109
|
+
|
|
110
|
+
async getMaterialized(blockId: BlockId, actionId: ActionId): Promise<Uint8Array | undefined> {
|
|
98
111
|
return this.db.get('materialized', [blockId, actionId]);
|
|
99
112
|
}
|
|
100
113
|
|
|
101
|
-
async
|
|
102
|
-
|
|
103
|
-
if (block) {
|
|
104
|
-
await this.db.put('materialized', block, key);
|
|
105
|
-
} else {
|
|
106
|
-
await this.db.delete('materialized', key);
|
|
107
|
-
}
|
|
114
|
+
async putMaterialized(blockId: BlockId, actionId: ActionId, value: Uint8Array): Promise<void> {
|
|
115
|
+
await this.db.put('materialized', value, [blockId, actionId]);
|
|
108
116
|
}
|
|
109
117
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
} catch (err) {
|
|
115
|
-
log('navigator.storage.estimate() failed: %o', err);
|
|
116
|
-
return 0;
|
|
117
|
-
}
|
|
118
|
+
// The kernel owns the put-or-delete branch of `saveMaterializedBlock`, so the
|
|
119
|
+
// driver exposes delete as a separate op.
|
|
120
|
+
async deleteMaterialized(blockId: BlockId, actionId: ActionId): Promise<void> {
|
|
121
|
+
await this.db.delete('materialized', [blockId, actionId]);
|
|
118
122
|
}
|
|
119
123
|
|
|
120
|
-
|
|
124
|
+
// --- promote (the only cross-key atomic op) ---
|
|
125
|
+
|
|
126
|
+
async promote(blockId: BlockId, actionId: ActionId): Promise<void> {
|
|
121
127
|
const key: [BlockId, ActionId] = [blockId, actionId];
|
|
128
|
+
// Single readwrite transaction over both stores IS the atomic move: a crash
|
|
129
|
+
// leaves either the pending or the committed entry, never both/neither.
|
|
122
130
|
const tx = this.db.transaction(['pending', 'transactions'], 'readwrite');
|
|
123
131
|
const pendingStore = tx.objectStore('pending');
|
|
124
132
|
const transactionsStore = tx.objectStore('transactions');
|
|
125
|
-
const
|
|
126
|
-
if (!
|
|
133
|
+
const value = await pendingStore.get(key);
|
|
134
|
+
if (!value) {
|
|
135
|
+
// Settle the transaction before throwing so the failed promote does not
|
|
136
|
+
// leak an open transaction — keep this ordering.
|
|
127
137
|
await tx.done.catch(() => undefined);
|
|
128
138
|
throw new Error(`Pending action ${actionId} not found for block ${blockId}`);
|
|
129
139
|
}
|
|
130
|
-
await transactionsStore.put(
|
|
140
|
+
await transactionsStore.put(value, key);
|
|
131
141
|
await pendingStore.delete(key);
|
|
132
142
|
await tx.done;
|
|
133
143
|
}
|
|
144
|
+
|
|
145
|
+
// --- optional passthroughs ---
|
|
146
|
+
|
|
147
|
+
async *listBlockIds(): AsyncIterable<BlockId> {
|
|
148
|
+
// The `metadata` store is keyed by blockId directly, so its keys ARE the
|
|
149
|
+
// distinct block ids. getAllKeys reads them under an implicit readonly
|
|
150
|
+
// transaction and returns an already-materialized array — no cursor held
|
|
151
|
+
// across yields (same rationale as the cursor scans' snapshot-first pattern).
|
|
152
|
+
const keys = await this.db.getAllKeys('metadata');
|
|
153
|
+
for (const key of keys) {
|
|
154
|
+
yield key as BlockId;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
async approximateBytesUsed(): Promise<number> {
|
|
159
|
+
try {
|
|
160
|
+
const estimate = await navigator.storage?.estimate?.();
|
|
161
|
+
return estimate?.usage ?? 0;
|
|
162
|
+
} catch (err) {
|
|
163
|
+
log('navigator.storage.estimate() failed: %o', err);
|
|
164
|
+
return 0;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* IndexedDB-backed {@link IRawStorage} for browser peers, now a thin shell over
|
|
171
|
+
* the shared {@link KvRawStorage} kernel driven by an {@link IndexedDBStoreDriver}.
|
|
172
|
+
* The public name/constructor (`new IndexedDBRawStorage(handle)`) is unchanged so
|
|
173
|
+
* existing imports keep resolving; the kernel supplies the `IRawStorage` surface
|
|
174
|
+
* and the driver supplies IndexedDB behavior.
|
|
175
|
+
*
|
|
176
|
+
* `listBlockIds`/`getApproximateBytesUsed` are re-declared here as always-present
|
|
177
|
+
* (the IndexedDB driver always implements them, so the kernel constructor always
|
|
178
|
+
* wires them) — the base declares them optional, but every web consumer relies
|
|
179
|
+
* on them.
|
|
180
|
+
*/
|
|
181
|
+
export class IndexedDBRawStorage extends KvRawStorage {
|
|
182
|
+
declare listBlockIds: () => AsyncIterable<BlockId>;
|
|
183
|
+
declare getApproximateBytesUsed: () => Promise<number>;
|
|
184
|
+
|
|
185
|
+
constructor(db: OptimysticWebDBHandle) {
|
|
186
|
+
super(new IndexedDBStoreDriver(db));
|
|
187
|
+
}
|
|
134
188
|
}
|