dignity.js 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +78 -15
- package/dist/dignity.cjs.js +272 -6
- package/dist/dignity.cjs.js.map +4 -4
- package/dist/dignity.esm.js +272 -6
- package/dist/dignity.esm.js.map +3 -3
- package/dist/dignity.min.js +5 -5
- package/docs/assets/docs.js +47 -0
- package/docs/assets/highlight/github-dark.min.css +10 -0
- package/docs/assets/highlight/github.min.css +10 -0
- package/docs/assets/highlight/highlight.min.js +1244 -0
- package/docs/assets/styles.css +449 -38
- package/docs/index.html +601 -81
- package/docs/openapi-like.json +44 -6
- package/package.json +22 -4
- package/src/core/dignity-p2p.js +79 -0
- package/src/index.js +2 -0
- package/src/persistence/indexeddb-persistence.js +182 -0
- package/src/react/index.js +114 -0
- package/src/security/message-security-service.js +51 -6
package/README.md
CHANGED
|
@@ -11,12 +11,13 @@
|
|
|
11
11
|
</p>
|
|
12
12
|
|
|
13
13
|
<p align="center">
|
|
14
|
+
<a href="https://jose-compu.github.io/dignity.js/"><img src="https://img.shields.io/badge/docs-online-5B7FFF" alt="documentation"></a>
|
|
14
15
|
<a href="https://www.npmjs.com/package/dignity.js"><img src="https://img.shields.io/npm/v/dignity.js?color=cb3837&label=npm" alt="npm version"></a>
|
|
15
16
|
<a href="https://www.npmjs.com/package/dignity.js"><img src="https://img.shields.io/npm/dm/dignity.js?color=blue" alt="npm downloads"></a>
|
|
16
|
-
<img src="https://
|
|
17
|
-
<img src="https://img.shields.io/badge/
|
|
18
|
-
<img src="https://img.shields.io/badge/
|
|
19
|
-
<img src="https://img.shields.io/badge/
|
|
17
|
+
<a href="https://github.com/jose-compu/dignity.js/actions/workflows/ci.yml"><img src="https://github.com/jose-compu/dignity.js/actions/workflows/ci.yml/badge.svg" alt="CI"></a>
|
|
18
|
+
<img src="https://img.shields.io/badge/tests-150%2B%20passing-brightgreen" alt="tests passing">
|
|
19
|
+
<img src="https://img.shields.io/badge/coverage-99%25-brightgreen" alt="coverage">
|
|
20
|
+
<img src="https://img.shields.io/badge/license-Apache%202.0-black" alt="license">
|
|
20
21
|
</p>
|
|
21
22
|
|
|
22
23
|
REST-like P2P object API for decentralized JavaScript applications.
|
|
@@ -35,7 +36,10 @@ REST-like P2P object API for decentralized JavaScript applications.
|
|
|
35
36
|
- default `powSteps: 22` (calibrated on this machine to about 1000ms)
|
|
36
37
|
- automatic peer ban on invalid signature/PoW (`48h` default)
|
|
37
38
|
- Team/subapp scoped broadcast passwords (`broadcastScope` + `broadcastPasswords`)
|
|
38
|
-
-
|
|
39
|
+
- Optimistic concurrency helpers (`expectedVersion`, `updateWithRetry`, `conflict` events)
|
|
40
|
+
- Optional IndexedDB persistence for browser reload survival
|
|
41
|
+
- Optional React hooks via `dignity.js/react`
|
|
42
|
+
- Browser-first: published npm package includes IIFE, ESM, and CJS builds
|
|
39
43
|
|
|
40
44
|
## Install
|
|
41
45
|
|
|
@@ -146,15 +150,60 @@ bob.registerPeerPublicKey('alice', alice.getPublicKey());
|
|
|
146
150
|
await alice.sendDirectMessage('bob', 'dm', { text: 'private payload' });
|
|
147
151
|
```
|
|
148
152
|
|
|
149
|
-
##
|
|
153
|
+
## Optimistic Concurrency
|
|
150
154
|
|
|
151
|
-
|
|
155
|
+
Updates carry a monotonic `version`. Remote peers reject stale operations when `baseVersion` does not match.
|
|
152
156
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
157
|
+
```js
|
|
158
|
+
node.on('conflict', (event) => {
|
|
159
|
+
console.log('conflict', event.phase, event.expectedVersion, event.currentVersion);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
await node.update('games', 'g1', { score: 10 }, { expectedVersion: 3 });
|
|
163
|
+
|
|
164
|
+
await node.updateWithRetry('games', 'g1', (current) => ({
|
|
165
|
+
score: current.data.score + 1
|
|
166
|
+
}));
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
Use `expectedVersion` for fail-fast local writes. Use `updateWithRetry` for read-modify-write loops in fast multiplayer state.
|
|
170
|
+
|
|
171
|
+
## IndexedDB Persistence
|
|
172
|
+
|
|
173
|
+
Persist replicated collections across page reloads:
|
|
156
174
|
|
|
157
|
-
|
|
175
|
+
```js
|
|
176
|
+
const { DignityP2P, IndexedDBPersistence } = require('dignity.js');
|
|
177
|
+
|
|
178
|
+
const node = new DignityP2P({ nodeId, networkAdapter, security });
|
|
179
|
+
const persistence = new IndexedDBPersistence({
|
|
180
|
+
dbName: 'my-app',
|
|
181
|
+
collections: ['games', 'matches']
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
await node.start();
|
|
185
|
+
await persistence.attach(node);
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
## React Hooks
|
|
189
|
+
|
|
190
|
+
Optional React integration (`react >= 18` peer dependency):
|
|
191
|
+
|
|
192
|
+
```js
|
|
193
|
+
import { useDignity, useCollection, usePeers } from 'dignity.js/react';
|
|
194
|
+
|
|
195
|
+
function Room() {
|
|
196
|
+
const { node, status } = useDignity(config);
|
|
197
|
+
const games = useCollection(node, 'games');
|
|
198
|
+
const peers = usePeers(node, 'room:chess', { includeSelf: false });
|
|
199
|
+
|
|
200
|
+
return <pre>{JSON.stringify({ status, games, peers }, null, 2)}</pre>;
|
|
201
|
+
}
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
## Browser Usage
|
|
205
|
+
|
|
206
|
+
The published npm package includes pre-built bundles (IIFE, ESM, CJS) generated at publish time. The `dist/` folder is not checked into the repository.
|
|
158
207
|
|
|
159
208
|
```html
|
|
160
209
|
<script src="https://unpkg.com/dignity.js/dist/dignity.min.js"></script>
|
|
@@ -163,6 +212,19 @@ Example with CDN:
|
|
|
163
212
|
</script>
|
|
164
213
|
```
|
|
165
214
|
|
|
215
|
+
## Security Model
|
|
216
|
+
|
|
217
|
+
`dignity.js` provides two encryption modes:
|
|
218
|
+
|
|
219
|
+
- **Direct mode** (`targetId` set): true end-to-end encryption using X25519 key exchange between sender and recipient. Only the intended recipient can decrypt.
|
|
220
|
+
- **Broadcast mode** (no `targetId`): symmetric encryption using a shared password. All peers that know the password can decrypt all broadcast traffic in that scope. This is a **group shared-secret cipher**, not end-to-end encryption.
|
|
221
|
+
|
|
222
|
+
Broadcast encryption uses PBKDF2-SHA256 (default 100,000 iterations) with a random salt per message to derive the symmetric key. This protects against offline brute-force of weak passwords. The iteration count is configurable via `kdfIterations`.
|
|
223
|
+
|
|
224
|
+
Messages from peers running older versions that used the legacy single-hash KDF are still accepted and decrypted automatically (backward compatible).
|
|
225
|
+
|
|
226
|
+
**Important:** if the broadcast password leaks, all past captured traffic for that scope is retroactively decryptable. For sensitive data, use direct mode with per-peer public keys.
|
|
227
|
+
|
|
166
228
|
## Signaling Servers
|
|
167
229
|
|
|
168
230
|
Default signaling URLs include PeerJS-compatible public endpoints:
|
|
@@ -189,7 +251,8 @@ npm run test:pow-calibrate
|
|
|
189
251
|
|
|
190
252
|
## Docs and Examples
|
|
191
253
|
|
|
192
|
-
-
|
|
254
|
+
- **Documentation:** [jose-compu.github.io/dignity.js](https://jose-compu.github.io/dignity.js/)
|
|
255
|
+
- Docs site source: `docs/index.html` (serve locally with `npm run docs:serve`)
|
|
193
256
|
- API metadata: `docs/openapi-like.json`
|
|
194
257
|
- Minimal demos:
|
|
195
258
|
- `examples/decentralized-tictactoe.js`
|
|
@@ -198,11 +261,11 @@ npm run test:pow-calibrate
|
|
|
198
261
|
## Publish
|
|
199
262
|
|
|
200
263
|
```bash
|
|
201
|
-
npm test
|
|
202
|
-
npm run build
|
|
203
264
|
npm publish --access public
|
|
204
265
|
```
|
|
205
266
|
|
|
267
|
+
The `prepublishOnly` script runs tests and build automatically.
|
|
268
|
+
|
|
206
269
|
## License
|
|
207
270
|
|
|
208
|
-
Apache 2.0
|
|
271
|
+
Apache 2.0 — see [LICENSE](LICENSE).
|
package/dist/dignity.cjs.js
CHANGED
|
@@ -2434,7 +2434,8 @@ var require_message_security_service = __commonJS({
|
|
|
2434
2434
|
broadcastPasswords: {},
|
|
2435
2435
|
resolveBroadcastPassword: null,
|
|
2436
2436
|
powSteps: 22,
|
|
2437
|
-
trustedPeerKeys: {}
|
|
2437
|
+
trustedPeerKeys: {},
|
|
2438
|
+
kdfIterations: 1e5
|
|
2438
2439
|
};
|
|
2439
2440
|
function stableStringify(value) {
|
|
2440
2441
|
if (value === null || typeof value !== "object") {
|
|
@@ -2461,6 +2462,33 @@ var require_message_security_service = __commonJS({
|
|
|
2461
2462
|
function utf8ToBytes(value) {
|
|
2462
2463
|
return naclUtil.decodeUTF8(value);
|
|
2463
2464
|
}
|
|
2465
|
+
async function deriveBroadcastKey(password, salt, iterations) {
|
|
2466
|
+
const subtle = globalThis.crypto && globalThis.crypto.subtle;
|
|
2467
|
+
if (subtle) {
|
|
2468
|
+
const keyMaterial = await subtle.importKey(
|
|
2469
|
+
"raw",
|
|
2470
|
+
utf8ToBytes(password),
|
|
2471
|
+
"PBKDF2",
|
|
2472
|
+
false,
|
|
2473
|
+
["deriveBits"]
|
|
2474
|
+
);
|
|
2475
|
+
const bits = await subtle.deriveBits(
|
|
2476
|
+
{ name: "PBKDF2", salt, iterations, hash: "SHA-256" },
|
|
2477
|
+
keyMaterial,
|
|
2478
|
+
256
|
|
2479
|
+
);
|
|
2480
|
+
return new Uint8Array(bits);
|
|
2481
|
+
}
|
|
2482
|
+
try {
|
|
2483
|
+
const { pbkdf2Sync } = require("crypto");
|
|
2484
|
+
return new Uint8Array(pbkdf2Sync(password, Buffer.from(salt), iterations, 32, "sha256"));
|
|
2485
|
+
} catch (_ignored) {
|
|
2486
|
+
return hash32(concatBytes(utf8ToBytes(password), salt));
|
|
2487
|
+
}
|
|
2488
|
+
}
|
|
2489
|
+
function legacyBroadcastKey(password, salt) {
|
|
2490
|
+
return hash32(concatBytes(utf8ToBytes(password), salt));
|
|
2491
|
+
}
|
|
2464
2492
|
function normalizePeerPublicKey(publicKey) {
|
|
2465
2493
|
if (!publicKey || typeof publicKey !== "object") {
|
|
2466
2494
|
throw new Error("Public key must be an object with signingPublicKey and encryptionPublicKey");
|
|
@@ -2621,7 +2649,7 @@ var require_message_security_service = __commonJS({
|
|
|
2621
2649
|
if (envelope.security && envelope.security.signing && envelope.security.signing.enabled && this.options.signingEnabled) {
|
|
2622
2650
|
this.verifySignature(envelope);
|
|
2623
2651
|
}
|
|
2624
|
-
const payload = this.decryptPayload(envelope);
|
|
2652
|
+
const payload = await this.decryptPayload(envelope);
|
|
2625
2653
|
return {
|
|
2626
2654
|
ignored: false,
|
|
2627
2655
|
messageType: envelope.messageType,
|
|
@@ -2686,7 +2714,8 @@ var require_message_security_service = __commonJS({
|
|
|
2686
2714
|
const nonce = nacl.randomBytes(nacl.secretbox.nonceLength);
|
|
2687
2715
|
const salt = nacl.randomBytes(16);
|
|
2688
2716
|
const password = this.resolveBroadcastPassword(scope);
|
|
2689
|
-
const
|
|
2717
|
+
const iterations = this.options.kdfIterations || DEFAULT_SECURITY_OPTIONS2.kdfIterations;
|
|
2718
|
+
const key = await deriveBroadcastKey(password, salt, iterations);
|
|
2690
2719
|
const encrypted = nacl.secretbox(plainText, nonce, key);
|
|
2691
2720
|
return {
|
|
2692
2721
|
payload: naclUtil.encodeBase64(encrypted),
|
|
@@ -2695,11 +2724,13 @@ var require_message_security_service = __commonJS({
|
|
|
2695
2724
|
mode: "broadcast",
|
|
2696
2725
|
scope,
|
|
2697
2726
|
nonce: naclUtil.encodeBase64(nonce),
|
|
2698
|
-
salt: naclUtil.encodeBase64(salt)
|
|
2727
|
+
salt: naclUtil.encodeBase64(salt),
|
|
2728
|
+
kdf: "pbkdf2",
|
|
2729
|
+
kdfIterations: iterations
|
|
2699
2730
|
}
|
|
2700
2731
|
};
|
|
2701
2732
|
}
|
|
2702
|
-
decryptPayload(envelope) {
|
|
2733
|
+
async decryptPayload(envelope) {
|
|
2703
2734
|
const encryption = envelope.security ? envelope.security.encryption : null;
|
|
2704
2735
|
if (!encryption || !encryption.enabled || !this.options.encryptionEnabled) {
|
|
2705
2736
|
return envelope.payload;
|
|
@@ -2710,7 +2741,13 @@ var require_message_security_service = __commonJS({
|
|
|
2710
2741
|
const password = this.resolveBroadcastPassword(scope);
|
|
2711
2742
|
const salt = naclUtil.decodeBase64(encryption.salt);
|
|
2712
2743
|
const nonce = naclUtil.decodeBase64(encryption.nonce);
|
|
2713
|
-
|
|
2744
|
+
let key;
|
|
2745
|
+
if (encryption.kdf === "pbkdf2") {
|
|
2746
|
+
const iterations = encryption.kdfIterations || DEFAULT_SECURITY_OPTIONS2.kdfIterations;
|
|
2747
|
+
key = await deriveBroadcastKey(password, salt, iterations);
|
|
2748
|
+
} else {
|
|
2749
|
+
key = legacyBroadcastKey(password, salt);
|
|
2750
|
+
}
|
|
2714
2751
|
const decrypted = nacl.secretbox.open(encryptedBuffer, nonce, key);
|
|
2715
2752
|
if (!decrypted) {
|
|
2716
2753
|
throw new Error("Unable to decrypt broadcast payload");
|
|
@@ -2804,6 +2841,8 @@ var require_message_security_service = __commonJS({
|
|
|
2804
2841
|
module2.exports = {
|
|
2805
2842
|
MessageSecurityService: MessageSecurityService2,
|
|
2806
2843
|
stableStringify,
|
|
2844
|
+
deriveBroadcastKey,
|
|
2845
|
+
legacyBroadcastKey,
|
|
2807
2846
|
DEFAULT_SECURITY_OPTIONS: DEFAULT_SECURITY_OPTIONS2
|
|
2808
2847
|
};
|
|
2809
2848
|
}
|
|
@@ -2941,6 +2980,21 @@ var require_dignity_p2p = __commonJS({
|
|
|
2941
2980
|
if (existing.ownerId !== this.nodeId) {
|
|
2942
2981
|
throw new Error(`Only owner ${existing.ownerId} can update object ${id}`);
|
|
2943
2982
|
}
|
|
2983
|
+
if (typeof options.expectedVersion === "number" && existing.version !== options.expectedVersion) {
|
|
2984
|
+
this.emitConflict({
|
|
2985
|
+
kind: "update",
|
|
2986
|
+
collection: collectionName,
|
|
2987
|
+
id,
|
|
2988
|
+
expectedVersion: options.expectedVersion,
|
|
2989
|
+
currentVersion: existing.version,
|
|
2990
|
+
phase: "local"
|
|
2991
|
+
});
|
|
2992
|
+
const error = new Error(
|
|
2993
|
+
`Version conflict on ${collectionName}/${id}: expected ${options.expectedVersion}, current ${existing.version}`
|
|
2994
|
+
);
|
|
2995
|
+
error.code = "VERSION_CONFLICT";
|
|
2996
|
+
throw error;
|
|
2997
|
+
}
|
|
2944
2998
|
const operation = {
|
|
2945
2999
|
opId: this.idGenerator(),
|
|
2946
3000
|
kind: "update",
|
|
@@ -2961,6 +3015,27 @@ var require_dignity_p2p = __commonJS({
|
|
|
2961
3015
|
});
|
|
2962
3016
|
return this.read(collectionName, id);
|
|
2963
3017
|
}
|
|
3018
|
+
async updateWithRetry(collectionName, id, patchFn, options = {}) {
|
|
3019
|
+
const maxAttempts = typeof options.maxAttempts === "number" ? options.maxAttempts : 5;
|
|
3020
|
+
for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
|
|
3021
|
+
const current = this.read(collectionName, id);
|
|
3022
|
+
if (!current) {
|
|
3023
|
+
throw new Error(`Object ${id} does not exist in ${collectionName}`);
|
|
3024
|
+
}
|
|
3025
|
+
const patch = await patchFn(current);
|
|
3026
|
+
try {
|
|
3027
|
+
return await this.update(collectionName, id, patch, {
|
|
3028
|
+
...options,
|
|
3029
|
+
expectedVersion: current.version
|
|
3030
|
+
});
|
|
3031
|
+
} catch (error) {
|
|
3032
|
+
if (error.code !== "VERSION_CONFLICT" || attempt === maxAttempts - 1) {
|
|
3033
|
+
throw error;
|
|
3034
|
+
}
|
|
3035
|
+
}
|
|
3036
|
+
}
|
|
3037
|
+
throw new Error(`Unable to update ${collectionName}/${id} after ${maxAttempts} attempts`);
|
|
3038
|
+
}
|
|
2964
3039
|
async remove(collectionName, id, options = {}) {
|
|
2965
3040
|
const existing = this.getCollection(collectionName).get(id);
|
|
2966
3041
|
if (!existing || existing.deletedAt) {
|
|
@@ -3238,6 +3313,29 @@ var require_dignity_p2p = __commonJS({
|
|
|
3238
3313
|
isPeerBanned(peerId) {
|
|
3239
3314
|
return this.getBanInfo(peerId) !== null;
|
|
3240
3315
|
}
|
|
3316
|
+
emitConflict(details) {
|
|
3317
|
+
this.emit("conflict", details);
|
|
3318
|
+
}
|
|
3319
|
+
restoreRecord(collectionName, record) {
|
|
3320
|
+
if (!record || !record.id) {
|
|
3321
|
+
return false;
|
|
3322
|
+
}
|
|
3323
|
+
const collection = this.getCollection(collectionName);
|
|
3324
|
+
const current = collection.get(record.id);
|
|
3325
|
+
if (current && current.version >= record.version) {
|
|
3326
|
+
return false;
|
|
3327
|
+
}
|
|
3328
|
+
collection.set(record.id, {
|
|
3329
|
+
id: record.id,
|
|
3330
|
+
ownerId: record.ownerId,
|
|
3331
|
+
data: { ...record.data || {} },
|
|
3332
|
+
createdAt: record.createdAt,
|
|
3333
|
+
updatedAt: record.updatedAt,
|
|
3334
|
+
deletedAt: record.deletedAt || null,
|
|
3335
|
+
version: record.version
|
|
3336
|
+
});
|
|
3337
|
+
return true;
|
|
3338
|
+
}
|
|
3241
3339
|
applyOperation(operation) {
|
|
3242
3340
|
if (!operation || !operation.opId || this.appliedOperations.has(operation.opId)) {
|
|
3243
3341
|
return false;
|
|
@@ -3268,6 +3366,15 @@ var require_dignity_p2p = __commonJS({
|
|
|
3268
3366
|
return false;
|
|
3269
3367
|
}
|
|
3270
3368
|
if (typeof operation.baseVersion === "number" && operation.baseVersion !== current.version) {
|
|
3369
|
+
this.emitConflict({
|
|
3370
|
+
kind: operation.kind,
|
|
3371
|
+
collection: operation.collectionName,
|
|
3372
|
+
id: operation.id,
|
|
3373
|
+
expectedVersion: operation.baseVersion,
|
|
3374
|
+
currentVersion: current.version,
|
|
3375
|
+
phase: "remote",
|
|
3376
|
+
operation
|
|
3377
|
+
});
|
|
3271
3378
|
return false;
|
|
3272
3379
|
}
|
|
3273
3380
|
if (operation.kind === "update") {
|
|
@@ -10345,6 +10452,163 @@ var require_in_memory_network = __commonJS({
|
|
|
10345
10452
|
}
|
|
10346
10453
|
});
|
|
10347
10454
|
|
|
10455
|
+
// src/persistence/indexeddb-persistence.js
|
|
10456
|
+
var require_indexeddb_persistence = __commonJS({
|
|
10457
|
+
"src/persistence/indexeddb-persistence.js"(exports2, module2) {
|
|
10458
|
+
var IndexedDBPersistence2 = class {
|
|
10459
|
+
constructor({
|
|
10460
|
+
dbName = "dignity",
|
|
10461
|
+
storeName = "records",
|
|
10462
|
+
collections = null,
|
|
10463
|
+
indexedDB = typeof globalThis !== "undefined" ? globalThis.indexedDB : null
|
|
10464
|
+
} = {}) {
|
|
10465
|
+
this.dbName = dbName;
|
|
10466
|
+
this.storeName = storeName;
|
|
10467
|
+
this.collections = collections;
|
|
10468
|
+
this.indexedDB = indexedDB;
|
|
10469
|
+
this.node = null;
|
|
10470
|
+
this.changeHandler = null;
|
|
10471
|
+
}
|
|
10472
|
+
recordKey(collection, id) {
|
|
10473
|
+
return `${collection}:${id}`;
|
|
10474
|
+
}
|
|
10475
|
+
shouldPersist(collection) {
|
|
10476
|
+
if (!this.collections) {
|
|
10477
|
+
return true;
|
|
10478
|
+
}
|
|
10479
|
+
return this.collections.includes(collection);
|
|
10480
|
+
}
|
|
10481
|
+
openDb() {
|
|
10482
|
+
if (!this.indexedDB) {
|
|
10483
|
+
return Promise.reject(new Error("IndexedDB is not available"));
|
|
10484
|
+
}
|
|
10485
|
+
return new Promise((resolve, reject) => {
|
|
10486
|
+
const request = this.indexedDB.open(this.dbName, 1);
|
|
10487
|
+
request.onupgradeneeded = () => {
|
|
10488
|
+
const db = request.result;
|
|
10489
|
+
if (!db.objectStoreNames.contains(this.storeName)) {
|
|
10490
|
+
db.createObjectStore(this.storeName, { keyPath: "key" });
|
|
10491
|
+
}
|
|
10492
|
+
};
|
|
10493
|
+
request.onsuccess = () => resolve(request.result);
|
|
10494
|
+
request.onerror = () => reject(request.error || new Error("Unable to open IndexedDB"));
|
|
10495
|
+
});
|
|
10496
|
+
}
|
|
10497
|
+
runTransaction(mode, handler) {
|
|
10498
|
+
return this.openDb().then((db) => new Promise((resolve, reject) => {
|
|
10499
|
+
const transaction = db.transaction(this.storeName, mode);
|
|
10500
|
+
const store = transaction.objectStore(this.storeName);
|
|
10501
|
+
Promise.resolve(handler(store)).then(resolve).catch(reject);
|
|
10502
|
+
transaction.oncomplete = () => db.close();
|
|
10503
|
+
transaction.onerror = () => reject(transaction.error || new Error("IndexedDB transaction failed"));
|
|
10504
|
+
transaction.onabort = () => reject(transaction.error || new Error("IndexedDB transaction aborted"));
|
|
10505
|
+
}));
|
|
10506
|
+
}
|
|
10507
|
+
serializeRecord(collection, id) {
|
|
10508
|
+
const record = this.node.getCollection(collection).get(id);
|
|
10509
|
+
if (!record) {
|
|
10510
|
+
return null;
|
|
10511
|
+
}
|
|
10512
|
+
return {
|
|
10513
|
+
key: this.recordKey(collection, id),
|
|
10514
|
+
collection,
|
|
10515
|
+
id,
|
|
10516
|
+
ownerId: record.ownerId,
|
|
10517
|
+
data: { ...record.data },
|
|
10518
|
+
createdAt: record.createdAt,
|
|
10519
|
+
updatedAt: record.updatedAt,
|
|
10520
|
+
deletedAt: record.deletedAt,
|
|
10521
|
+
version: record.version
|
|
10522
|
+
};
|
|
10523
|
+
}
|
|
10524
|
+
async persistRecord(collection, id) {
|
|
10525
|
+
if (!this.node || !this.shouldPersist(collection)) {
|
|
10526
|
+
return;
|
|
10527
|
+
}
|
|
10528
|
+
const serialized = this.serializeRecord(collection, id);
|
|
10529
|
+
const key = this.recordKey(collection, id);
|
|
10530
|
+
if (!serialized) {
|
|
10531
|
+
await this.runTransaction("readwrite", (store) => new Promise((resolve, reject) => {
|
|
10532
|
+
const request = store.delete(key);
|
|
10533
|
+
request.onsuccess = () => resolve();
|
|
10534
|
+
request.onerror = () => reject(request.error);
|
|
10535
|
+
}));
|
|
10536
|
+
return;
|
|
10537
|
+
}
|
|
10538
|
+
await this.runTransaction("readwrite", (store) => new Promise((resolve, reject) => {
|
|
10539
|
+
const request = store.put(serialized);
|
|
10540
|
+
request.onsuccess = () => resolve();
|
|
10541
|
+
request.onerror = () => reject(request.error);
|
|
10542
|
+
}));
|
|
10543
|
+
}
|
|
10544
|
+
persistChange(event) {
|
|
10545
|
+
if (!event || !event.collection || !event.id) {
|
|
10546
|
+
return;
|
|
10547
|
+
}
|
|
10548
|
+
this.persistRecord(event.collection, event.id).catch((error) => {
|
|
10549
|
+
this.node.emit("warning", {
|
|
10550
|
+
type: "persistence-failed",
|
|
10551
|
+
collection: event.collection,
|
|
10552
|
+
id: event.id,
|
|
10553
|
+
error
|
|
10554
|
+
});
|
|
10555
|
+
});
|
|
10556
|
+
}
|
|
10557
|
+
async loadAllRecords() {
|
|
10558
|
+
return this.runTransaction("readonly", (store) => new Promise((resolve, reject) => {
|
|
10559
|
+
const request = store.getAll();
|
|
10560
|
+
request.onsuccess = () => resolve(request.result || []);
|
|
10561
|
+
request.onerror = () => reject(request.error);
|
|
10562
|
+
}));
|
|
10563
|
+
}
|
|
10564
|
+
async hydrate() {
|
|
10565
|
+
if (!this.node) {
|
|
10566
|
+
throw new Error("IndexedDBPersistence requires an attached node before hydrate");
|
|
10567
|
+
}
|
|
10568
|
+
const storedRecords = await this.loadAllRecords();
|
|
10569
|
+
for (const stored of storedRecords) {
|
|
10570
|
+
if (!this.shouldPersist(stored.collection)) {
|
|
10571
|
+
continue;
|
|
10572
|
+
}
|
|
10573
|
+
this.node.restoreRecord(stored.collection, {
|
|
10574
|
+
id: stored.id,
|
|
10575
|
+
ownerId: stored.ownerId,
|
|
10576
|
+
data: stored.data,
|
|
10577
|
+
createdAt: stored.createdAt,
|
|
10578
|
+
updatedAt: stored.updatedAt,
|
|
10579
|
+
deletedAt: stored.deletedAt,
|
|
10580
|
+
version: stored.version
|
|
10581
|
+
});
|
|
10582
|
+
}
|
|
10583
|
+
}
|
|
10584
|
+
async attach(node) {
|
|
10585
|
+
if (!node) {
|
|
10586
|
+
throw new Error("IndexedDBPersistence.attach requires a DignityP2P node");
|
|
10587
|
+
}
|
|
10588
|
+
this.node = node;
|
|
10589
|
+
await this.hydrate();
|
|
10590
|
+
this.changeHandler = (event) => this.persistChange(event);
|
|
10591
|
+
node.on("change", this.changeHandler);
|
|
10592
|
+
}
|
|
10593
|
+
async detach() {
|
|
10594
|
+
if (this.node && this.changeHandler) {
|
|
10595
|
+
this.node.off("change", this.changeHandler);
|
|
10596
|
+
}
|
|
10597
|
+
this.changeHandler = null;
|
|
10598
|
+
this.node = null;
|
|
10599
|
+
}
|
|
10600
|
+
async clear() {
|
|
10601
|
+
await this.runTransaction("readwrite", (store) => new Promise((resolve, reject) => {
|
|
10602
|
+
const request = store.clear();
|
|
10603
|
+
request.onsuccess = () => resolve();
|
|
10604
|
+
request.onerror = () => reject(request.error);
|
|
10605
|
+
}));
|
|
10606
|
+
}
|
|
10607
|
+
};
|
|
10608
|
+
module2.exports = IndexedDBPersistence2;
|
|
10609
|
+
}
|
|
10610
|
+
});
|
|
10611
|
+
|
|
10348
10612
|
// src/index.js
|
|
10349
10613
|
var DignityP2P = require_dignity_p2p();
|
|
10350
10614
|
var createDefaultSignalingPool = require_create_default_signaling_pool();
|
|
@@ -10355,6 +10619,7 @@ var {
|
|
|
10355
10619
|
InMemoryNetworkHub,
|
|
10356
10620
|
InMemoryNetworkAdapter
|
|
10357
10621
|
} = require_in_memory_network();
|
|
10622
|
+
var IndexedDBPersistence = require_indexeddb_persistence();
|
|
10358
10623
|
var {
|
|
10359
10624
|
DEFAULT_CLOUDFLARE_SIGNALING_URLS,
|
|
10360
10625
|
DEFAULT_SIGNALING_FALLBACK_URLS
|
|
@@ -10373,6 +10638,7 @@ module.exports = {
|
|
|
10373
10638
|
PeerJSSignalingProvider,
|
|
10374
10639
|
InMemoryNetworkHub,
|
|
10375
10640
|
InMemoryNetworkAdapter,
|
|
10641
|
+
IndexedDBPersistence,
|
|
10376
10642
|
DEFAULT_CLOUDFLARE_SIGNALING_URLS,
|
|
10377
10643
|
DEFAULT_SIGNALING_FALLBACK_URLS,
|
|
10378
10644
|
VDF,
|