@optimystic/db-p2p-storage-web 0.13.4
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 +104 -0
- package/dist/src/db.d.ts +57 -0
- package/dist/src/db.d.ts.map +1 -0
- package/dist/src/db.js +35 -0
- package/dist/src/db.js.map +1 -0
- package/dist/src/identity.d.ts +15 -0
- package/dist/src/identity.d.ts.map +1 -0
- package/dist/src/identity.js +23 -0
- package/dist/src/identity.js.map +1 -0
- package/dist/src/index.d.ts +5 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +5 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/indexeddb-kv-store.d.ts +20 -0
- package/dist/src/indexeddb-kv-store.d.ts.map +1 -0
- package/dist/src/indexeddb-kv-store.js +45 -0
- package/dist/src/indexeddb-kv-store.js.map +1 -0
- package/dist/src/indexeddb-storage.d.ts +31 -0
- package/dist/src/indexeddb-storage.d.ts.map +1 -0
- package/dist/src/indexeddb-storage.js +120 -0
- package/dist/src/indexeddb-storage.js.map +1 -0
- package/dist/src/logger.d.ts +3 -0
- package/dist/src/logger.d.ts.map +1 -0
- package/dist/src/logger.js +6 -0
- package/dist/src/logger.js.map +1 -0
- package/package.json +66 -0
- package/src/db.ts +86 -0
- package/src/identity.ts +29 -0
- package/src/index.ts +4 -0
- package/src/indexeddb-kv-store.ts +49 -0
- package/src/indexeddb-storage.ts +134 -0
- package/src/logger.ts +7 -0
package/README.md
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# @optimystic/db-p2p-storage-web
|
|
2
|
+
|
|
3
|
+
IndexedDB-backed storage backend for Optimystic browser peers. Provides:
|
|
4
|
+
|
|
5
|
+
- **`IndexedDBRawStorage`** — implements `IRawStorage` so a browser node persists
|
|
6
|
+
block metadata, revisions, pending transactions, committed transactions, and
|
|
7
|
+
materialized blocks across page reloads.
|
|
8
|
+
- **`IndexedDBKVStore`** — implements `IKVStore` for the persistent transaction
|
|
9
|
+
state used to recover crashed two-phase commits.
|
|
10
|
+
- **`loadOrCreateBrowserPeerKey`** — generates an Ed25519 libp2p private key on
|
|
11
|
+
first run and persists it in the same IndexedDB database, giving the browser
|
|
12
|
+
peer a stable, reload-surviving identity.
|
|
13
|
+
|
|
14
|
+
This package is browser-only — it imports no Node built-ins. Counterparts:
|
|
15
|
+
|
|
16
|
+
- `@optimystic/db-p2p-storage-rn` — React Native (MMKV)
|
|
17
|
+
- `@optimystic/db-p2p-storage-fs` — Node filesystem
|
|
18
|
+
|
|
19
|
+
## Install
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
yarn add @optimystic/db-p2p-storage-web @optimystic/db-p2p @optimystic/db-core
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
`@optimystic/db-p2p` is consumed via its `react-native` / `./rn` entry point so
|
|
26
|
+
the Node-only filesystem code does not get bundled into the browser:
|
|
27
|
+
|
|
28
|
+
```ts
|
|
29
|
+
import { createLibp2pNode } from '@optimystic/db-p2p/rn';
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Usage
|
|
33
|
+
|
|
34
|
+
```ts
|
|
35
|
+
import { createLibp2pNode } from '@optimystic/db-p2p/rn';
|
|
36
|
+
import {
|
|
37
|
+
openOptimysticWebDb,
|
|
38
|
+
IndexedDBRawStorage,
|
|
39
|
+
IndexedDBKVStore,
|
|
40
|
+
loadOrCreateBrowserPeerKey,
|
|
41
|
+
} from '@optimystic/db-p2p-storage-web';
|
|
42
|
+
|
|
43
|
+
const db = await openOptimysticWebDb(); // single shared handle
|
|
44
|
+
const rawStorage = new IndexedDBRawStorage(db); // → IRawStorage
|
|
45
|
+
const kvStore = new IndexedDBKVStore(db); // → IKVStore (txn recovery)
|
|
46
|
+
const privateKey = await loadOrCreateBrowserPeerKey(db);
|
|
47
|
+
|
|
48
|
+
const libp2p = await createLibp2pNode({
|
|
49
|
+
bootstrapNodes: [/* … */],
|
|
50
|
+
networkName: 'my-network',
|
|
51
|
+
privateKey,
|
|
52
|
+
});
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
The same `IDBPDatabase` handle is shared by all three consumers — IndexedDB
|
|
56
|
+
permits concurrent transactions across disjoint object stores, so the storage
|
|
57
|
+
layer parallelises naturally.
|
|
58
|
+
|
|
59
|
+
## Persistence semantics
|
|
60
|
+
|
|
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` |
|
|
72
|
+
|
|
73
|
+
`listRevisions` and `listPendingTransactions` use real IndexedDB range cursors
|
|
74
|
+
— never `getAllKeys()` + JS filter — so list latency stays bounded as the store
|
|
75
|
+
grows. `promotePendingTransaction` runs as a single `readwrite` transaction
|
|
76
|
+
spanning `pending` and `transactions`, so the move is atomic across crashes.
|
|
77
|
+
|
|
78
|
+
`getApproximateBytesUsed()` returns `(await navigator.storage.estimate()).usage`,
|
|
79
|
+
which is **per-origin**, not per-database. That is adequate for `StorageMonitor`
|
|
80
|
+
— which uses the figure as an advisory ring-selection input — but is not a
|
|
81
|
+
precise per-database accounting.
|
|
82
|
+
|
|
83
|
+
## Identity
|
|
84
|
+
|
|
85
|
+
`loadOrCreateBrowserPeerKey(db, keyName?)` writes the libp2p private key as
|
|
86
|
+
raw protobuf bytes (`Uint8Array`) under `keyName` in the `kv` store
|
|
87
|
+
(default `peer-private-key`). IndexedDB stores typed arrays natively, so no
|
|
88
|
+
base64 round-trip is needed. To rotate the identity, delete that one key:
|
|
89
|
+
|
|
90
|
+
```ts
|
|
91
|
+
await db.delete('kv', 'peer-private-key');
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
To wipe the entire backing store (debugging / dev reset):
|
|
95
|
+
|
|
96
|
+
```ts
|
|
97
|
+
indexedDB.deleteDatabase('optimystic');
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## Tests
|
|
101
|
+
|
|
102
|
+
`yarn test` runs the spec suite under [`fake-indexeddb`](https://www.npmjs.com/package/fake-indexeddb)
|
|
103
|
+
in Node so CI doesn't need a real browser. Production code only uses the
|
|
104
|
+
IndexedDB W3C API, so fake-indexeddb is a faithful behavioural surrogate.
|
package/dist/src/db.d.ts
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
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';
|
|
4
|
+
/**
|
|
5
|
+
* IndexedDB schema for the Optimystic browser storage backend.
|
|
6
|
+
*
|
|
7
|
+
* - `metadata`: per-block `BlockMetadata` keyed by `blockId`.
|
|
8
|
+
* - `revisions`: revision lookup keyed by `[blockId, rev]` mapping to an `ActionId`.
|
|
9
|
+
* Range scans use `IDBKeyRange.bound([blockId, startRev], [blockId, endRev])`.
|
|
10
|
+
* - `pending`: pending transforms keyed by `[blockId, actionId]`. Listing pending
|
|
11
|
+
* 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.
|
|
15
|
+
* - `kv`: a generic string keyspace shared by `IndexedDBKVStore` and the
|
|
16
|
+
* identity helper. Identity keys are stored as raw `Uint8Array` blobs (under
|
|
17
|
+
* a separate logical store from string-only `IKVStore` data, but the same
|
|
18
|
+
* IndexedDB object store — IndexedDB stores typed arrays natively).
|
|
19
|
+
*/
|
|
20
|
+
export interface OptimysticWebDB extends DBSchema {
|
|
21
|
+
metadata: {
|
|
22
|
+
key: BlockId;
|
|
23
|
+
value: BlockMetadata;
|
|
24
|
+
};
|
|
25
|
+
revisions: {
|
|
26
|
+
key: [BlockId, number];
|
|
27
|
+
value: ActionId;
|
|
28
|
+
};
|
|
29
|
+
pending: {
|
|
30
|
+
key: [BlockId, ActionId];
|
|
31
|
+
value: Transform;
|
|
32
|
+
};
|
|
33
|
+
transactions: {
|
|
34
|
+
key: [BlockId, ActionId];
|
|
35
|
+
value: Transform;
|
|
36
|
+
};
|
|
37
|
+
materialized: {
|
|
38
|
+
key: [BlockId, ActionId];
|
|
39
|
+
value: IBlock;
|
|
40
|
+
};
|
|
41
|
+
kv: {
|
|
42
|
+
key: string;
|
|
43
|
+
value: string | Uint8Array;
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
export declare const DEFAULT_DB_NAME = "optimystic";
|
|
47
|
+
export declare const DEFAULT_DB_VERSION = 1;
|
|
48
|
+
export type OptimysticWebDBHandle = IDBPDatabase<OptimysticWebDB>;
|
|
49
|
+
/**
|
|
50
|
+
* Opens (and creates if necessary) the Optimystic IndexedDB database used by
|
|
51
|
+
* `IndexedDBRawStorage`, `IndexedDBKVStore`, and `loadOrCreateBrowserPeerKey`.
|
|
52
|
+
*
|
|
53
|
+
* The same handle can be safely shared across all three — IndexedDB connections
|
|
54
|
+
* support concurrent transactions across disjoint object stores.
|
|
55
|
+
*/
|
|
56
|
+
export declare function openOptimysticWebDb(name?: string, version?: number): Promise<OptimysticWebDBHandle>;
|
|
57
|
+
//# sourceMappingURL=db.d.ts.map
|
|
@@ -0,0 +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"}
|
package/dist/src/db.js
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { openDB } from 'idb';
|
|
2
|
+
export const DEFAULT_DB_NAME = 'optimystic';
|
|
3
|
+
export const DEFAULT_DB_VERSION = 1;
|
|
4
|
+
/**
|
|
5
|
+
* Opens (and creates if necessary) the Optimystic IndexedDB database used by
|
|
6
|
+
* `IndexedDBRawStorage`, `IndexedDBKVStore`, and `loadOrCreateBrowserPeerKey`.
|
|
7
|
+
*
|
|
8
|
+
* The same handle can be safely shared across all three — IndexedDB connections
|
|
9
|
+
* support concurrent transactions across disjoint object stores.
|
|
10
|
+
*/
|
|
11
|
+
export async function openOptimysticWebDb(name = DEFAULT_DB_NAME, version = DEFAULT_DB_VERSION) {
|
|
12
|
+
return openDB(name, version, {
|
|
13
|
+
upgrade(db) {
|
|
14
|
+
if (!db.objectStoreNames.contains('metadata')) {
|
|
15
|
+
db.createObjectStore('metadata');
|
|
16
|
+
}
|
|
17
|
+
if (!db.objectStoreNames.contains('revisions')) {
|
|
18
|
+
db.createObjectStore('revisions');
|
|
19
|
+
}
|
|
20
|
+
if (!db.objectStoreNames.contains('pending')) {
|
|
21
|
+
db.createObjectStore('pending');
|
|
22
|
+
}
|
|
23
|
+
if (!db.objectStoreNames.contains('transactions')) {
|
|
24
|
+
db.createObjectStore('transactions');
|
|
25
|
+
}
|
|
26
|
+
if (!db.objectStoreNames.contains('materialized')) {
|
|
27
|
+
db.createObjectStore('materialized');
|
|
28
|
+
}
|
|
29
|
+
if (!db.objectStoreNames.contains('kv')) {
|
|
30
|
+
db.createObjectStore('kv');
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
//# sourceMappingURL=db.js.map
|
|
@@ -0,0 +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"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { PrivateKey } from '@libp2p/interface';
|
|
2
|
+
import type { OptimysticWebDBHandle } from './db.js';
|
|
3
|
+
export declare const DEFAULT_PEER_KEY_NAME = "peer-private-key";
|
|
4
|
+
/**
|
|
5
|
+
* Loads the persisted browser peer's libp2p private key, or generates a fresh
|
|
6
|
+
* Ed25519 key on first call and persists it.
|
|
7
|
+
*
|
|
8
|
+
* Stores the key as raw protobuf bytes (a `Uint8Array`) under `keyName` in the
|
|
9
|
+
* `kv` object store. IndexedDB stores typed arrays natively — no base64 round-trip.
|
|
10
|
+
*
|
|
11
|
+
* The returned `PrivateKey` can be passed directly to `createLibp2pNode({ privateKey })`,
|
|
12
|
+
* giving a browser peer a stable, reload-surviving identity.
|
|
13
|
+
*/
|
|
14
|
+
export declare function loadOrCreateBrowserPeerKey(db: OptimysticWebDBHandle, keyName?: string): Promise<PrivateKey>;
|
|
15
|
+
//# sourceMappingURL=identity.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"identity.d.ts","sourceRoot":"","sources":["../../src/identity.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AACpD,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,SAAS,CAAC;AAErD,eAAO,MAAM,qBAAqB,qBAAqB,CAAC;AAExD;;;;;;;;;GASG;AACH,wBAAsB,0BAA0B,CAC/C,EAAE,EAAE,qBAAqB,EACzB,OAAO,GAAE,MAA8B,GACrC,OAAO,CAAC,UAAU,CAAC,CASrB"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { generateKeyPair, privateKeyFromProtobuf, privateKeyToProtobuf } from '@libp2p/crypto/keys';
|
|
2
|
+
export const DEFAULT_PEER_KEY_NAME = 'peer-private-key';
|
|
3
|
+
/**
|
|
4
|
+
* Loads the persisted browser peer's libp2p private key, or generates a fresh
|
|
5
|
+
* Ed25519 key on first call and persists it.
|
|
6
|
+
*
|
|
7
|
+
* Stores the key as raw protobuf bytes (a `Uint8Array`) under `keyName` in the
|
|
8
|
+
* `kv` object store. IndexedDB stores typed arrays natively — no base64 round-trip.
|
|
9
|
+
*
|
|
10
|
+
* The returned `PrivateKey` can be passed directly to `createLibp2pNode({ privateKey })`,
|
|
11
|
+
* giving a browser peer a stable, reload-surviving identity.
|
|
12
|
+
*/
|
|
13
|
+
export async function loadOrCreateBrowserPeerKey(db, keyName = DEFAULT_PEER_KEY_NAME) {
|
|
14
|
+
const stored = await db.get('kv', keyName);
|
|
15
|
+
if (stored instanceof Uint8Array) {
|
|
16
|
+
return privateKeyFromProtobuf(stored);
|
|
17
|
+
}
|
|
18
|
+
const key = await generateKeyPair('Ed25519');
|
|
19
|
+
const bytes = privateKeyToProtobuf(key);
|
|
20
|
+
await db.put('kv', bytes, keyName);
|
|
21
|
+
return key;
|
|
22
|
+
}
|
|
23
|
+
//# sourceMappingURL=identity.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"identity.js","sourceRoot":"","sources":["../../src/identity.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,sBAAsB,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAC;AAIpG,MAAM,CAAC,MAAM,qBAAqB,GAAG,kBAAkB,CAAC;AAExD;;;;;;;;;GASG;AACH,MAAM,CAAC,KAAK,UAAU,0BAA0B,CAC/C,EAAyB,EACzB,UAAkB,qBAAqB;IAEvC,MAAM,MAAM,GAAG,MAAM,EAAE,CAAC,GAAG,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;IAC3C,IAAI,MAAM,YAAY,UAAU,EAAE,CAAC;QAClC,OAAO,sBAAsB,CAAC,MAAM,CAAC,CAAC;IACvC,CAAC;IACD,MAAM,GAAG,GAAG,MAAM,eAAe,CAAC,SAAS,CAAC,CAAC;IAC7C,MAAM,KAAK,GAAG,oBAAoB,CAAC,GAAG,CAAC,CAAC;IACxC,MAAM,EAAE,CAAC,GAAG,CAAC,IAAI,EAAE,KAAK,EAAE,OAAO,CAAC,CAAC;IACnC,OAAO,GAAG,CAAC;AACZ,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,SAAS,CAAC;AACxB,cAAc,wBAAwB,CAAC;AACvC,cAAc,yBAAyB,CAAC;AACxC,cAAc,eAAe,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,SAAS,CAAC;AACxB,cAAc,wBAAwB,CAAC;AACvC,cAAc,yBAAyB,CAAC;AACxC,cAAc,eAAe,CAAC"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { IKVStore } from '@optimystic/db-p2p';
|
|
2
|
+
import type { OptimysticWebDBHandle } from './db.js';
|
|
3
|
+
/**
|
|
4
|
+
* IndexedDB-backed `IKVStore` adapter for browser peers.
|
|
5
|
+
*
|
|
6
|
+
* Stored keys are namespaced with `prefix` so the `kv` object store can be
|
|
7
|
+
* shared with the identity helper without collisions. `list(prefix)` uses a
|
|
8
|
+
* range-bounded key cursor — never `getAllKeys()` — so a large `kv` store
|
|
9
|
+
* does not pay an O(n) JS-side filter cost.
|
|
10
|
+
*/
|
|
11
|
+
export declare class IndexedDBKVStore implements IKVStore {
|
|
12
|
+
private readonly db;
|
|
13
|
+
private readonly prefix;
|
|
14
|
+
constructor(db: OptimysticWebDBHandle, prefix?: string);
|
|
15
|
+
get(key: string): Promise<string | undefined>;
|
|
16
|
+
set(key: string, value: string): Promise<void>;
|
|
17
|
+
delete(key: string): Promise<void>;
|
|
18
|
+
list(prefix: string): Promise<string[]>;
|
|
19
|
+
}
|
|
20
|
+
//# sourceMappingURL=indexeddb-kv-store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"indexeddb-kv-store.d.ts","sourceRoot":"","sources":["../../src/indexeddb-kv-store.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AACnD,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,SAAS,CAAC;AAErD;;;;;;;GAOG;AACH,qBAAa,gBAAiB,YAAW,QAAQ;IAE/C,OAAO,CAAC,QAAQ,CAAC,EAAE;IACnB,OAAO,CAAC,QAAQ,CAAC,MAAM;gBADN,EAAE,EAAE,qBAAqB,EACzB,MAAM,GAAE,MAA0B;IAG9C,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC;IAK7C,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAI9C,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAIlC,IAAI,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;CAkB7C"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* IndexedDB-backed `IKVStore` adapter for browser peers.
|
|
3
|
+
*
|
|
4
|
+
* Stored keys are namespaced with `prefix` so the `kv` object store can be
|
|
5
|
+
* shared with the identity helper without collisions. `list(prefix)` uses a
|
|
6
|
+
* range-bounded key cursor — never `getAllKeys()` — so a large `kv` store
|
|
7
|
+
* does not pay an O(n) JS-side filter cost.
|
|
8
|
+
*/
|
|
9
|
+
export class IndexedDBKVStore {
|
|
10
|
+
db;
|
|
11
|
+
prefix;
|
|
12
|
+
constructor(db, prefix = 'optimystic:txn:') {
|
|
13
|
+
this.db = db;
|
|
14
|
+
this.prefix = prefix;
|
|
15
|
+
}
|
|
16
|
+
async get(key) {
|
|
17
|
+
const value = await this.db.get('kv', this.prefix + key);
|
|
18
|
+
return typeof value === 'string' ? value : undefined;
|
|
19
|
+
}
|
|
20
|
+
async set(key, value) {
|
|
21
|
+
await this.db.put('kv', value, this.prefix + key);
|
|
22
|
+
}
|
|
23
|
+
async delete(key) {
|
|
24
|
+
await this.db.delete('kv', this.prefix + key);
|
|
25
|
+
}
|
|
26
|
+
async list(prefix) {
|
|
27
|
+
const fullPrefix = this.prefix + prefix;
|
|
28
|
+
// '' is the highest BMP code unit; appending it produces a string
|
|
29
|
+
// that is >= every string starting with `fullPrefix`. IndexedDB compares
|
|
30
|
+
// strings by UTF-16 code units, so this bound is exact.
|
|
31
|
+
const range = IDBKeyRange.bound(fullPrefix, fullPrefix + '');
|
|
32
|
+
const tx = this.db.transaction('kv', 'readonly');
|
|
33
|
+
const store = tx.objectStore('kv');
|
|
34
|
+
const keys = [];
|
|
35
|
+
let cursor = await store.openKeyCursor(range);
|
|
36
|
+
while (cursor) {
|
|
37
|
+
const fullKey = cursor.key;
|
|
38
|
+
keys.push(fullKey.slice(this.prefix.length));
|
|
39
|
+
cursor = await cursor.continue();
|
|
40
|
+
}
|
|
41
|
+
await tx.done;
|
|
42
|
+
return keys;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
//# sourceMappingURL=indexeddb-kv-store.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"indexeddb-kv-store.js","sourceRoot":"","sources":["../../src/indexeddb-kv-store.ts"],"names":[],"mappings":"AAGA;;;;;;;GAOG;AACH,MAAM,OAAO,gBAAgB;IAEV;IACA;IAFlB,YACkB,EAAyB,EACzB,SAAiB,iBAAiB;QADlC,OAAE,GAAF,EAAE,CAAuB;QACzB,WAAM,GAAN,MAAM,CAA4B;IACjD,CAAC;IAEJ,KAAK,CAAC,GAAG,CAAC,GAAW;QACpB,MAAM,KAAK,GAAG,MAAM,IAAI,CAAC,EAAE,CAAC,GAAG,CAAC,IAAI,EAAE,IAAI,CAAC,MAAM,GAAG,GAAG,CAAC,CAAC;QACzD,OAAO,OAAO,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,SAAS,CAAC;IACtD,CAAC;IAED,KAAK,CAAC,GAAG,CAAC,GAAW,EAAE,KAAa;QACnC,MAAM,IAAI,CAAC,EAAE,CAAC,GAAG,CAAC,IAAI,EAAE,KAAK,EAAE,IAAI,CAAC,MAAM,GAAG,GAAG,CAAC,CAAC;IACnD,CAAC;IAED,KAAK,CAAC,MAAM,CAAC,GAAW;QACvB,MAAM,IAAI,CAAC,EAAE,CAAC,MAAM,CAAC,IAAI,EAAE,IAAI,CAAC,MAAM,GAAG,GAAG,CAAC,CAAC;IAC/C,CAAC;IAED,KAAK,CAAC,IAAI,CAAC,MAAc;QACxB,MAAM,UAAU,GAAG,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;QACxC,mEAAmE;QACnE,yEAAyE;QACzE,wDAAwD;QACxD,MAAM,KAAK,GAAG,WAAW,CAAC,KAAK,CAAC,UAAU,EAAE,UAAU,GAAG,GAAG,CAAC,CAAC;QAC9D,MAAM,EAAE,GAAG,IAAI,CAAC,EAAE,CAAC,WAAW,CAAC,IAAI,EAAE,UAAU,CAAC,CAAC;QACjD,MAAM,KAAK,GAAG,EAAE,CAAC,WAAW,CAAC,IAAI,CAAC,CAAC;QACnC,MAAM,IAAI,GAAa,EAAE,CAAC;QAC1B,IAAI,MAAM,GAAG,MAAM,KAAK,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC;QAC9C,OAAO,MAAM,EAAE,CAAC;YACf,MAAM,OAAO,GAAG,MAAM,CAAC,GAAa,CAAC;YACrC,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC;YAC7C,MAAM,GAAG,MAAM,MAAM,CAAC,QAAQ,EAAE,CAAC;QAClC,CAAC;QACD,MAAM,EAAE,CAAC,IAAI,CAAC;QACd,OAAO,IAAI,CAAC;IACb,CAAC;CACD"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { ActionId, ActionRev, BlockId, IBlock, Transform } from '@optimystic/db-core';
|
|
2
|
+
import type { BlockMetadata, IRawStorage } from '@optimystic/db-p2p';
|
|
3
|
+
import type { OptimysticWebDBHandle } from './db.js';
|
|
4
|
+
/**
|
|
5
|
+
* IndexedDB-backed `IRawStorage` implementation for browser peers.
|
|
6
|
+
*
|
|
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.
|
|
11
|
+
*/
|
|
12
|
+
export declare class IndexedDBRawStorage implements IRawStorage {
|
|
13
|
+
private readonly db;
|
|
14
|
+
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>;
|
|
30
|
+
}
|
|
31
|
+
//# sourceMappingURL=indexeddb-storage.d.ts.map
|
|
@@ -0,0 +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"}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { createLogger } from './logger.js';
|
|
2
|
+
const log = createLogger('storage:indexeddb');
|
|
3
|
+
/**
|
|
4
|
+
* IndexedDB-backed `IRawStorage` implementation for browser peers.
|
|
5
|
+
*
|
|
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
|
+
*/
|
|
11
|
+
export class IndexedDBRawStorage {
|
|
12
|
+
db;
|
|
13
|
+
constructor(db) {
|
|
14
|
+
this.db = db;
|
|
15
|
+
}
|
|
16
|
+
async getMetadata(blockId) {
|
|
17
|
+
return this.db.get('metadata', blockId);
|
|
18
|
+
}
|
|
19
|
+
async saveMetadata(blockId, metadata) {
|
|
20
|
+
await this.db.put('metadata', metadata, blockId);
|
|
21
|
+
}
|
|
22
|
+
async getRevision(blockId, rev) {
|
|
23
|
+
return this.db.get('revisions', [blockId, rev]);
|
|
24
|
+
}
|
|
25
|
+
async saveRevision(blockId, rev, actionId) {
|
|
26
|
+
await this.db.put('revisions', actionId, [blockId, rev]);
|
|
27
|
+
}
|
|
28
|
+
async *listRevisions(blockId, startRev, endRev) {
|
|
29
|
+
const ascending = startRev <= endRev;
|
|
30
|
+
const lo = ascending ? startRev : endRev;
|
|
31
|
+
const hi = ascending ? endRev : startRev;
|
|
32
|
+
const range = IDBKeyRange.bound([blockId, lo], [blockId, hi]);
|
|
33
|
+
const tx = this.db.transaction('revisions', 'readonly');
|
|
34
|
+
const store = tx.objectStore('revisions');
|
|
35
|
+
// Snapshot first so we don't hold the transaction open across yields —
|
|
36
|
+
// IndexedDB auto-commits idle transactions, which would invalidate the
|
|
37
|
+
// cursor between awaits at the consumer.
|
|
38
|
+
const results = [];
|
|
39
|
+
let cursor = await store.openCursor(range, ascending ? 'next' : 'prev');
|
|
40
|
+
while (cursor) {
|
|
41
|
+
results.push({ rev: cursor.key[1], actionId: cursor.value });
|
|
42
|
+
cursor = await cursor.continue();
|
|
43
|
+
}
|
|
44
|
+
await tx.done;
|
|
45
|
+
for (const result of results) {
|
|
46
|
+
yield result;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
async getPendingTransaction(blockId, actionId) {
|
|
50
|
+
return this.db.get('pending', [blockId, actionId]);
|
|
51
|
+
}
|
|
52
|
+
async savePendingTransaction(blockId, actionId, transform) {
|
|
53
|
+
await this.db.put('pending', transform, [blockId, actionId]);
|
|
54
|
+
}
|
|
55
|
+
async deletePendingTransaction(blockId, actionId) {
|
|
56
|
+
await this.db.delete('pending', [blockId, actionId]);
|
|
57
|
+
}
|
|
58
|
+
async *listPendingTransactions(blockId) {
|
|
59
|
+
// IndexedDB key ordering: array keys compare element-by-element, and a
|
|
60
|
+
// shorter prefix-equal array is less than a longer one; arrays sort
|
|
61
|
+
// above all primitive types. So `[blockId]` < `[blockId, anyActionId]`
|
|
62
|
+
// < `[blockId, []]`, which captures exactly every key for this block.
|
|
63
|
+
const range = IDBKeyRange.bound([blockId], [blockId, []]);
|
|
64
|
+
const tx = this.db.transaction('pending', 'readonly');
|
|
65
|
+
const store = tx.objectStore('pending');
|
|
66
|
+
const results = [];
|
|
67
|
+
let cursor = await store.openKeyCursor(range);
|
|
68
|
+
while (cursor) {
|
|
69
|
+
results.push(cursor.key[1]);
|
|
70
|
+
cursor = await cursor.continue();
|
|
71
|
+
}
|
|
72
|
+
await tx.done;
|
|
73
|
+
for (const actionId of results) {
|
|
74
|
+
yield actionId;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
async getTransaction(blockId, actionId) {
|
|
78
|
+
return this.db.get('transactions', [blockId, actionId]);
|
|
79
|
+
}
|
|
80
|
+
async saveTransaction(blockId, actionId, transform) {
|
|
81
|
+
await this.db.put('transactions', transform, [blockId, actionId]);
|
|
82
|
+
}
|
|
83
|
+
async getMaterializedBlock(blockId, actionId) {
|
|
84
|
+
return this.db.get('materialized', [blockId, actionId]);
|
|
85
|
+
}
|
|
86
|
+
async saveMaterializedBlock(blockId, actionId, block) {
|
|
87
|
+
const key = [blockId, actionId];
|
|
88
|
+
if (block) {
|
|
89
|
+
await this.db.put('materialized', block, key);
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
await this.db.delete('materialized', key);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
async getApproximateBytesUsed() {
|
|
96
|
+
try {
|
|
97
|
+
const estimate = await navigator.storage?.estimate?.();
|
|
98
|
+
return estimate?.usage ?? 0;
|
|
99
|
+
}
|
|
100
|
+
catch (err) {
|
|
101
|
+
log('navigator.storage.estimate() failed: %o', err);
|
|
102
|
+
return 0;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
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;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
//# sourceMappingURL=indexeddb-storage.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"indexeddb-storage.js","sourceRoot":"","sources":["../../src/indexeddb-storage.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"logger.d.ts","sourceRoot":"","sources":["../../src/logger.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAC;AAI1B,wBAAgB,YAAY,CAAC,YAAY,EAAE,MAAM,GAAG,KAAK,CAAC,QAAQ,CAEjE"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"logger.js","sourceRoot":"","sources":["../../src/logger.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAC;AAE1B,MAAM,cAAc,GAAG,+BAA+B,CAAC;AAEvD,MAAM,UAAU,YAAY,CAAC,YAAoB;IAChD,OAAO,KAAK,CAAC,GAAG,cAAc,IAAI,YAAY,EAAE,CAAC,CAAC;AACnD,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@optimystic/db-p2p-storage-web",
|
|
3
|
+
"version": "0.13.4",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Browser IndexedDB storage backend for @optimystic/db-p2p",
|
|
6
|
+
"main": "dist/src/index.js",
|
|
7
|
+
"types": "./dist/src/index.d.ts",
|
|
8
|
+
"files": [
|
|
9
|
+
"src",
|
|
10
|
+
"dist",
|
|
11
|
+
"!dist/test",
|
|
12
|
+
"!**/*.tsbuildinfo"
|
|
13
|
+
],
|
|
14
|
+
"exports": {
|
|
15
|
+
".": {
|
|
16
|
+
"types": "./dist/src/index.d.ts",
|
|
17
|
+
"import": "./dist/src/index.js"
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"repository": {
|
|
21
|
+
"type": "git",
|
|
22
|
+
"url": "https://github.com/gotchoices/optimystic.git",
|
|
23
|
+
"directory": "packages/db-p2p-storage-web"
|
|
24
|
+
},
|
|
25
|
+
"author": "Got Choices Foundation",
|
|
26
|
+
"license": "MIT",
|
|
27
|
+
"keywords": [
|
|
28
|
+
"optimystic",
|
|
29
|
+
"database",
|
|
30
|
+
"p2p",
|
|
31
|
+
"storage",
|
|
32
|
+
"browser",
|
|
33
|
+
"indexeddb"
|
|
34
|
+
],
|
|
35
|
+
"scripts": {
|
|
36
|
+
"clean": "rimraf dist",
|
|
37
|
+
"build": "tsc",
|
|
38
|
+
"test": "node --import ./register.mjs node_modules/mocha/bin/mocha.js \"test/**/*.spec.ts\" --colors --reporter min",
|
|
39
|
+
"test:verbose": "node --import ./register.mjs node_modules/mocha/bin/mocha.js \"test/**/*.spec.ts\" --colors --reporter spec"
|
|
40
|
+
},
|
|
41
|
+
"devDependencies": {
|
|
42
|
+
"@libp2p/crypto": "^5.1.13",
|
|
43
|
+
"@libp2p/interface": "^3.1.0",
|
|
44
|
+
"@libp2p/peer-id": "^6.0.4",
|
|
45
|
+
"@types/chai": "^5.2.3",
|
|
46
|
+
"@types/debug": "^4.1.12",
|
|
47
|
+
"@types/mocha": "^10.0.10",
|
|
48
|
+
"@types/node": "^25.1.0",
|
|
49
|
+
"chai": "^6.2.2",
|
|
50
|
+
"fake-indexeddb": "^6.2.4",
|
|
51
|
+
"mocha": "^11.7.5",
|
|
52
|
+
"rimraf": "^6.1.2",
|
|
53
|
+
"ts-node": "^10.9.2",
|
|
54
|
+
"typescript": "^5.9.3"
|
|
55
|
+
},
|
|
56
|
+
"dependencies": {
|
|
57
|
+
"@optimystic/db-core": "^0.13.4",
|
|
58
|
+
"@optimystic/db-p2p": "^0.13.4",
|
|
59
|
+
"debug": "^4.4.3",
|
|
60
|
+
"idb": "^8.0.3"
|
|
61
|
+
},
|
|
62
|
+
"peerDependencies": {
|
|
63
|
+
"@libp2p/crypto": "^5.1.13",
|
|
64
|
+
"@libp2p/interface": "^3.1.0"
|
|
65
|
+
}
|
|
66
|
+
}
|
package/src/db.ts
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
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';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* IndexedDB schema for the Optimystic browser storage backend.
|
|
7
|
+
*
|
|
8
|
+
* - `metadata`: per-block `BlockMetadata` keyed by `blockId`.
|
|
9
|
+
* - `revisions`: revision lookup keyed by `[blockId, rev]` mapping to an `ActionId`.
|
|
10
|
+
* Range scans use `IDBKeyRange.bound([blockId, startRev], [blockId, endRev])`.
|
|
11
|
+
* - `pending`: pending transforms keyed by `[blockId, actionId]`. Listing pending
|
|
12
|
+
* 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.
|
|
16
|
+
* - `kv`: a generic string keyspace shared by `IndexedDBKVStore` and the
|
|
17
|
+
* identity helper. Identity keys are stored as raw `Uint8Array` blobs (under
|
|
18
|
+
* a separate logical store from string-only `IKVStore` data, but the same
|
|
19
|
+
* IndexedDB object store — IndexedDB stores typed arrays natively).
|
|
20
|
+
*/
|
|
21
|
+
export interface OptimysticWebDB extends DBSchema {
|
|
22
|
+
metadata: {
|
|
23
|
+
key: BlockId;
|
|
24
|
+
value: BlockMetadata;
|
|
25
|
+
};
|
|
26
|
+
revisions: {
|
|
27
|
+
key: [BlockId, number];
|
|
28
|
+
value: ActionId;
|
|
29
|
+
};
|
|
30
|
+
pending: {
|
|
31
|
+
key: [BlockId, ActionId];
|
|
32
|
+
value: Transform;
|
|
33
|
+
};
|
|
34
|
+
transactions: {
|
|
35
|
+
key: [BlockId, ActionId];
|
|
36
|
+
value: Transform;
|
|
37
|
+
};
|
|
38
|
+
materialized: {
|
|
39
|
+
key: [BlockId, ActionId];
|
|
40
|
+
value: IBlock;
|
|
41
|
+
};
|
|
42
|
+
kv: {
|
|
43
|
+
key: string;
|
|
44
|
+
value: string | Uint8Array;
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export const DEFAULT_DB_NAME = 'optimystic';
|
|
49
|
+
export const DEFAULT_DB_VERSION = 1;
|
|
50
|
+
|
|
51
|
+
export type OptimysticWebDBHandle = IDBPDatabase<OptimysticWebDB>;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Opens (and creates if necessary) the Optimystic IndexedDB database used by
|
|
55
|
+
* `IndexedDBRawStorage`, `IndexedDBKVStore`, and `loadOrCreateBrowserPeerKey`.
|
|
56
|
+
*
|
|
57
|
+
* The same handle can be safely shared across all three — IndexedDB connections
|
|
58
|
+
* support concurrent transactions across disjoint object stores.
|
|
59
|
+
*/
|
|
60
|
+
export async function openOptimysticWebDb(
|
|
61
|
+
name: string = DEFAULT_DB_NAME,
|
|
62
|
+
version: number = DEFAULT_DB_VERSION,
|
|
63
|
+
): Promise<OptimysticWebDBHandle> {
|
|
64
|
+
return openDB<OptimysticWebDB>(name, version, {
|
|
65
|
+
upgrade(db) {
|
|
66
|
+
if (!db.objectStoreNames.contains('metadata')) {
|
|
67
|
+
db.createObjectStore('metadata');
|
|
68
|
+
}
|
|
69
|
+
if (!db.objectStoreNames.contains('revisions')) {
|
|
70
|
+
db.createObjectStore('revisions');
|
|
71
|
+
}
|
|
72
|
+
if (!db.objectStoreNames.contains('pending')) {
|
|
73
|
+
db.createObjectStore('pending');
|
|
74
|
+
}
|
|
75
|
+
if (!db.objectStoreNames.contains('transactions')) {
|
|
76
|
+
db.createObjectStore('transactions');
|
|
77
|
+
}
|
|
78
|
+
if (!db.objectStoreNames.contains('materialized')) {
|
|
79
|
+
db.createObjectStore('materialized');
|
|
80
|
+
}
|
|
81
|
+
if (!db.objectStoreNames.contains('kv')) {
|
|
82
|
+
db.createObjectStore('kv');
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
});
|
|
86
|
+
}
|
package/src/identity.ts
ADDED
|
@@ -0,0 +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
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +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
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import type { ActionId, ActionRev, BlockId, IBlock, Transform } from '@optimystic/db-core';
|
|
2
|
+
import type { BlockMetadata, IRawStorage } 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-backed `IRawStorage` implementation for browser peers.
|
|
10
|
+
*
|
|
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.
|
|
15
|
+
*/
|
|
16
|
+
export class IndexedDBRawStorage implements IRawStorage {
|
|
17
|
+
constructor(private readonly db: OptimysticWebDBHandle) {}
|
|
18
|
+
|
|
19
|
+
async getMetadata(blockId: BlockId): Promise<BlockMetadata | undefined> {
|
|
20
|
+
return this.db.get('metadata', blockId);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async saveMetadata(blockId: BlockId, metadata: BlockMetadata): Promise<void> {
|
|
24
|
+
await this.db.put('metadata', metadata, blockId);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async getRevision(blockId: BlockId, rev: number): Promise<ActionId | undefined> {
|
|
28
|
+
return this.db.get('revisions', [blockId, rev]);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async saveRevision(blockId: BlockId, rev: number, actionId: ActionId): Promise<void> {
|
|
32
|
+
await this.db.put('revisions', actionId, [blockId, rev]);
|
|
33
|
+
}
|
|
34
|
+
|
|
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;
|
|
39
|
+
const range = IDBKeyRange.bound([blockId, lo], [blockId, hi]);
|
|
40
|
+
const tx = this.db.transaction('revisions', 'readonly');
|
|
41
|
+
const store = tx.objectStore('revisions');
|
|
42
|
+
// Snapshot first so we don't hold the transaction open across yields —
|
|
43
|
+
// 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');
|
|
47
|
+
while (cursor) {
|
|
48
|
+
results.push({ rev: (cursor.key as [BlockId, number])[1], actionId: cursor.value });
|
|
49
|
+
cursor = await cursor.continue();
|
|
50
|
+
}
|
|
51
|
+
await tx.done;
|
|
52
|
+
for (const result of results) {
|
|
53
|
+
yield result;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async getPendingTransaction(blockId: BlockId, actionId: ActionId): Promise<Transform | undefined> {
|
|
58
|
+
return this.db.get('pending', [blockId, actionId]);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async savePendingTransaction(blockId: BlockId, actionId: ActionId, transform: Transform): Promise<void> {
|
|
62
|
+
await this.db.put('pending', transform, [blockId, actionId]);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async deletePendingTransaction(blockId: BlockId, actionId: ActionId): Promise<void> {
|
|
66
|
+
await this.db.delete('pending', [blockId, actionId]);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async *listPendingTransactions(blockId: BlockId): AsyncIterable<ActionId> {
|
|
70
|
+
// IndexedDB key ordering: array keys compare element-by-element, and a
|
|
71
|
+
// shorter prefix-equal array is less than a longer one; arrays sort
|
|
72
|
+
// above all primitive types. So `[blockId]` < `[blockId, anyActionId]`
|
|
73
|
+
// < `[blockId, []]`, which captures exactly every key for this block.
|
|
74
|
+
const range = IDBKeyRange.bound([blockId] as IDBValidKey, [blockId, []] as IDBValidKey);
|
|
75
|
+
const tx = this.db.transaction('pending', 'readonly');
|
|
76
|
+
const store = tx.objectStore('pending');
|
|
77
|
+
const results: ActionId[] = [];
|
|
78
|
+
let cursor = await store.openKeyCursor(range);
|
|
79
|
+
while (cursor) {
|
|
80
|
+
results.push((cursor.key as [BlockId, ActionId])[1]);
|
|
81
|
+
cursor = await cursor.continue();
|
|
82
|
+
}
|
|
83
|
+
await tx.done;
|
|
84
|
+
for (const actionId of results) {
|
|
85
|
+
yield actionId;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async getTransaction(blockId: BlockId, actionId: ActionId): Promise<Transform | undefined> {
|
|
90
|
+
return this.db.get('transactions', [blockId, actionId]);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async saveTransaction(blockId: BlockId, actionId: ActionId, transform: Transform): Promise<void> {
|
|
94
|
+
await this.db.put('transactions', transform, [blockId, actionId]);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async getMaterializedBlock(blockId: BlockId, actionId: ActionId): Promise<IBlock | undefined> {
|
|
98
|
+
return this.db.get('materialized', [blockId, actionId]);
|
|
99
|
+
}
|
|
100
|
+
|
|
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
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
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
|
+
}
|
|
119
|
+
|
|
120
|
+
async promotePendingTransaction(blockId: BlockId, actionId: ActionId): Promise<void> {
|
|
121
|
+
const key: [BlockId, ActionId] = [blockId, actionId];
|
|
122
|
+
const tx = this.db.transaction(['pending', 'transactions'], 'readwrite');
|
|
123
|
+
const pendingStore = tx.objectStore('pending');
|
|
124
|
+
const transactionsStore = tx.objectStore('transactions');
|
|
125
|
+
const transform = await pendingStore.get(key);
|
|
126
|
+
if (!transform) {
|
|
127
|
+
await tx.done.catch(() => undefined);
|
|
128
|
+
throw new Error(`Pending action ${actionId} not found for block ${blockId}`);
|
|
129
|
+
}
|
|
130
|
+
await transactionsStore.put(transform, key);
|
|
131
|
+
await pendingStore.delete(key);
|
|
132
|
+
await tx.done;
|
|
133
|
+
}
|
|
134
|
+
}
|