@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 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
- | Store | Key | Value |
65
- |----------------|----------------------|------------------------|
66
- | `metadata` | `blockId` | `BlockMetadata` |
67
- | `revisions` | `[blockId, rev]` | `ActionId` |
68
- | `pending` | `[blockId, actionId]`| `Transform` |
69
- | `transactions` | `[blockId, actionId]`| `Transform` |
70
- | `materialized` | `[blockId, actionId]`| `IBlock` |
71
- | `kv` | `key` | `string` or `Uint8Array` |
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, IBlock, Transform } from '@optimystic/db-core';
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
- * - `metadata`: per-block `BlockMetadata` keyed by `blockId`.
8
- * - `revisions`: revision lookup keyed by `[blockId, rev]` mapping to an `ActionId`.
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 transforms keyed by `[blockId, actionId]`. Listing 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 transforms keyed by `[blockId, actionId]`.
13
- * - `materialized`: materialized blocks keyed by `[blockId, actionId]`. Saving
14
- * `undefined` deletes the row.
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: BlockMetadata;
29
+ value: Uint8Array;
24
30
  };
25
31
  revisions: {
26
32
  key: [BlockId, number];
27
- value: ActionId;
33
+ value: Uint8Array;
28
34
  };
29
35
  pending: {
30
36
  key: [BlockId, ActionId];
31
- value: Transform;
37
+ value: Uint8Array;
32
38
  };
33
39
  transactions: {
34
40
  key: [BlockId, ActionId];
35
- value: Transform;
41
+ value: Uint8Array;
36
42
  };
37
43
  materialized: {
38
44
  key: [BlockId, ActionId];
39
- value: IBlock;
45
+ value: Uint8Array;
40
46
  };
41
47
  kv: {
42
48
  key: string;
@@ -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,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAChF,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAExD;;;;;;;;;;;;;;;GAeG;AACH,MAAM,WAAW,eAAgB,SAAQ,QAAQ;IAChD,QAAQ,EAAE;QACT,GAAG,EAAE,OAAO,CAAC;QACb,KAAK,EAAE,aAAa,CAAC;KACrB,CAAC;IACF,SAAS,EAAE;QACV,GAAG,EAAE,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;QACvB,KAAK,EAAE,QAAQ,CAAC;KAChB,CAAC;IACF,OAAO,EAAE;QACR,GAAG,EAAE,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC;QACzB,KAAK,EAAE,SAAS,CAAC;KACjB,CAAC;IACF,YAAY,EAAE;QACb,GAAG,EAAE,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC;QACzB,KAAK,EAAE,SAAS,CAAC;KACjB,CAAC;IACF,YAAY,EAAE;QACb,GAAG,EAAE,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC;QACzB,KAAK,EAAE,MAAM,CAAC;KACd,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"}
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"}
@@ -1 +1 @@
1
- {"version":3,"file":"db.js","sourceRoot":"","sources":["../../src/db.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAoC,MAAM,KAAK,CAAC;AA+C/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
+ {"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, ActionRev, BlockId, IBlock, Transform } from '@optimystic/db-core';
2
- import type { BlockMetadata, IRawStorage } from '@optimystic/db-p2p';
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-backed `IRawStorage` implementation for browser peers.
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
- * Uses real range cursors for `listRevisions` and key cursors for
8
- * `listPendingTransactions`, and runs `promotePendingTransaction` as a single
9
- * `readwrite` transaction over the `pending` and `transactions` stores so the
10
- * move is atomic.
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 IndexedDBRawStorage implements IRawStorage {
16
+ export declare class IndexedDBStoreDriver implements RawStoreDriver {
13
17
  private readonly db;
14
18
  constructor(db: OptimysticWebDBHandle);
15
- getMetadata(blockId: BlockId): Promise<BlockMetadata | undefined>;
16
- saveMetadata(blockId: BlockId, metadata: BlockMetadata): Promise<void>;
17
- getRevision(blockId: BlockId, rev: number): Promise<ActionId | undefined>;
18
- saveRevision(blockId: BlockId, rev: number, actionId: ActionId): Promise<void>;
19
- listRevisions(blockId: BlockId, startRev: number, endRev: number): AsyncIterable<ActionRev>;
20
- getPendingTransaction(blockId: BlockId, actionId: ActionId): Promise<Transform | undefined>;
21
- savePendingTransaction(blockId: BlockId, actionId: ActionId, transform: Transform): Promise<void>;
22
- deletePendingTransaction(blockId: BlockId, actionId: ActionId): Promise<void>;
23
- listPendingTransactions(blockId: BlockId): AsyncIterable<ActionId>;
24
- getTransaction(blockId: BlockId, actionId: ActionId): Promise<Transform | undefined>;
25
- saveTransaction(blockId: BlockId, actionId: ActionId, transform: Transform): Promise<void>;
26
- getMaterializedBlock(blockId: BlockId, actionId: ActionId): Promise<IBlock | undefined>;
27
- saveMaterializedBlock(blockId: BlockId, actionId: ActionId, block?: IBlock): Promise<void>;
28
- getApproximateBytesUsed(): Promise<number>;
29
- promotePendingTransaction(blockId: BlockId, actionId: ActionId): Promise<void>;
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,SAAS,EAAE,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAC3F,OAAO,KAAK,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AACrE,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,SAAS,CAAC;AAKrD;;;;;;;GAOG;AACH,qBAAa,mBAAoB,YAAW,WAAW;IAC1C,OAAO,CAAC,QAAQ,CAAC,EAAE;gBAAF,EAAE,EAAE,qBAAqB;IAEhD,WAAW,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,aAAa,GAAG,SAAS,CAAC;IAIjE,YAAY,CAAC,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC;IAItE,WAAW,CAAC,OAAO,EAAE,OAAO,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,GAAG,SAAS,CAAC;IAIzE,YAAY,CAAC,OAAO,EAAE,OAAO,EAAE,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC;IAI7E,aAAa,CAAC,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,aAAa,CAAC,SAAS,CAAC;IAsB5F,qBAAqB,CAAC,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,QAAQ,GAAG,OAAO,CAAC,SAAS,GAAG,SAAS,CAAC;IAI3F,sBAAsB,CAAC,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,QAAQ,EAAE,SAAS,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC;IAIjG,wBAAwB,CAAC,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC;IAI5E,uBAAuB,CAAC,OAAO,EAAE,OAAO,GAAG,aAAa,CAAC,QAAQ,CAAC;IAoBnE,cAAc,CAAC,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,QAAQ,GAAG,OAAO,CAAC,SAAS,GAAG,SAAS,CAAC;IAIpF,eAAe,CAAC,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,QAAQ,EAAE,SAAS,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC;IAI1F,oBAAoB,CAAC,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,QAAQ,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC;IAIvF,qBAAqB,CAAC,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,QAAQ,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAS1F,uBAAuB,IAAI,OAAO,CAAC,MAAM,CAAC;IAU1C,yBAAyB,CAAC,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC;CAcpF"}
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-backed `IRawStorage` implementation for browser peers.
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
- * Uses real range cursors for `listRevisions` and key cursors for
7
- * `listPendingTransactions`, and runs `promotePendingTransaction` as a single
8
- * `readwrite` transaction over the `pending` and `transactions` stores so the
9
- * move is atomic.
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 IndexedDBRawStorage {
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 saveMetadata(blockId, metadata) {
20
- await this.db.put('metadata', metadata, blockId);
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 saveRevision(blockId, rev, actionId) {
26
- await this.db.put('revisions', actionId, [blockId, rev]);
32
+ async putRevision(blockId, rev, value) {
33
+ await this.db.put('revisions', value, [blockId, rev]);
27
34
  }
28
- async *listRevisions(blockId, startRev, endRev) {
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 at the consumer.
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, ascending ? 'next' : 'prev');
44
+ let cursor = await store.openCursor(range, reverse ? 'prev' : 'next');
40
45
  while (cursor) {
41
- results.push({ rev: cursor.key[1], actionId: cursor.value });
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
- async getPendingTransaction(blockId, actionId) {
54
+ // --- pending ---
55
+ async getPending(blockId, actionId) {
50
56
  return this.db.get('pending', [blockId, actionId]);
51
57
  }
52
- async savePendingTransaction(blockId, actionId, transform) {
53
- await this.db.put('pending', transform, [blockId, actionId]);
58
+ async putPending(blockId, actionId, value) {
59
+ await this.db.put('pending', value, [blockId, actionId]);
54
60
  }
55
- async deletePendingTransaction(blockId, actionId) {
61
+ async deletePending(blockId, actionId) {
56
62
  await this.db.delete('pending', [blockId, actionId]);
57
63
  }
58
- async *listPendingTransactions(blockId) {
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 saveTransaction(blockId, actionId, transform) {
81
- await this.db.put('transactions', transform, [blockId, actionId]);
88
+ async putTransaction(blockId, actionId, value) {
89
+ await this.db.put('transactions', value, [blockId, actionId]);
82
90
  }
83
- async getMaterializedBlock(blockId, actionId) {
91
+ // --- materialized ---
92
+ async getMaterialized(blockId, actionId) {
84
93
  return this.db.get('materialized', [blockId, actionId]);
85
94
  }
86
- async saveMaterializedBlock(blockId, actionId, block) {
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
- if (block) {
89
- await this.db.put('materialized', block, key);
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
- else {
92
- await this.db.delete('materialized', key);
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 getApproximateBytesUsed() {
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
- async promotePendingTransaction(blockId, actionId) {
106
- const key = [blockId, actionId];
107
- const tx = this.db.transaction(['pending', 'transactions'], 'readwrite');
108
- const pendingStore = tx.objectStore('pending');
109
- const transactionsStore = tx.objectStore('transactions');
110
- const transform = await pendingStore.get(key);
111
- if (!transform) {
112
- await tx.done.catch(() => undefined);
113
- throw new Error(`Pending action ${actionId} not found for block ${blockId}`);
114
- }
115
- await transactionsStore.put(transform, key);
116
- await pendingStore.delete(key);
117
- await tx.done;
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":"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"}
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.14.1",
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.14.1",
58
- "@optimystic/db-p2p": "^0.14.1",
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, IBlock, Transform } from '@optimystic/db-core';
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
- * - `metadata`: per-block `BlockMetadata` keyed by `blockId`.
9
- * - `revisions`: revision lookup keyed by `[blockId, rev]` mapping to an `ActionId`.
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 transforms keyed by `[blockId, actionId]`. Listing 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 transforms keyed by `[blockId, actionId]`.
14
- * - `materialized`: materialized blocks keyed by `[blockId, actionId]`. Saving
15
- * `undefined` deletes the row.
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: BlockMetadata;
30
+ value: Uint8Array;
25
31
  };
26
32
  revisions: {
27
33
  key: [BlockId, number];
28
- value: ActionId;
34
+ value: Uint8Array;
29
35
  };
30
36
  pending: {
31
37
  key: [BlockId, ActionId];
32
- value: Transform;
38
+ value: Uint8Array;
33
39
  };
34
40
  transactions: {
35
41
  key: [BlockId, ActionId];
36
- value: Transform;
42
+ value: Uint8Array;
37
43
  };
38
44
  materialized: {
39
45
  key: [BlockId, ActionId];
40
- value: IBlock;
46
+ value: Uint8Array;
41
47
  };
42
48
  kv: {
43
49
  key: string;
@@ -1,51 +1,57 @@
1
- import type { ActionId, ActionRev, BlockId, IBlock, Transform } from '@optimystic/db-core';
2
- import type { BlockMetadata, IRawStorage } from '@optimystic/db-p2p';
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-backed `IRawStorage` implementation for browser peers.
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
- * Uses real range cursors for `listRevisions` and key cursors for
12
- * `listPendingTransactions`, and runs `promotePendingTransaction` as a single
13
- * `readwrite` transaction over the `pending` and `transactions` stores so the
14
- * move is atomic.
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 IndexedDBRawStorage implements IRawStorage {
20
+ export class IndexedDBStoreDriver implements RawStoreDriver {
17
21
  constructor(private readonly db: OptimysticWebDBHandle) {}
18
22
 
19
- async getMetadata(blockId: BlockId): Promise<BlockMetadata | undefined> {
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 saveMetadata(blockId: BlockId, metadata: BlockMetadata): Promise<void> {
24
- await this.db.put('metadata', metadata, blockId);
29
+ async putMetadata(blockId: BlockId, value: Uint8Array): Promise<void> {
30
+ await this.db.put('metadata', value, blockId);
25
31
  }
26
32
 
27
- async getRevision(blockId: BlockId, rev: number): Promise<ActionId | undefined> {
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 saveRevision(blockId: BlockId, rev: number, actionId: ActionId): Promise<void> {
32
- await this.db.put('revisions', actionId, [blockId, rev]);
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 *listRevisions(blockId: BlockId, startRev: number, endRev: number): AsyncIterable<ActionRev> {
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 at the consumer.
45
- const results: ActionRev[] = [];
46
- let cursor = await store.openCursor(range, ascending ? 'next' : 'prev');
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({ rev: (cursor.key as [BlockId, number])[1], actionId: cursor.value });
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
- async getPendingTransaction(blockId: BlockId, actionId: ActionId): Promise<Transform | undefined> {
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 savePendingTransaction(blockId: BlockId, actionId: ActionId, transform: Transform): Promise<void> {
62
- await this.db.put('pending', transform, [blockId, actionId]);
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 deletePendingTransaction(blockId: BlockId, actionId: ActionId): Promise<void> {
73
+ async deletePending(blockId: BlockId, actionId: ActionId): Promise<void> {
66
74
  await this.db.delete('pending', [blockId, actionId]);
67
75
  }
68
76
 
69
- async *listPendingTransactions(blockId: BlockId): AsyncIterable<ActionId> {
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
- async getTransaction(blockId: BlockId, actionId: ActionId): Promise<Transform | undefined> {
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 saveTransaction(blockId: BlockId, actionId: ActionId, transform: Transform): Promise<void> {
94
- await this.db.put('transactions', transform, [blockId, actionId]);
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
- async getMaterializedBlock(blockId: BlockId, actionId: ActionId): Promise<IBlock | undefined> {
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 saveMaterializedBlock(blockId: BlockId, actionId: ActionId, block?: IBlock): Promise<void> {
102
- const key: [BlockId, ActionId] = [blockId, actionId];
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
- async getApproximateBytesUsed(): Promise<number> {
111
- try {
112
- const estimate = await navigator.storage?.estimate?.();
113
- return estimate?.usage ?? 0;
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
- async promotePendingTransaction(blockId: BlockId, actionId: ActionId): Promise<void> {
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 transform = await pendingStore.get(key);
126
- if (!transform) {
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(transform, key);
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
  }