dignity.js 0.5.4 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +25 -3
- package/dist/dignity.cjs.js +513 -17
- package/dist/dignity.cjs.js.map +4 -4
- package/dist/dignity.esm.js +513 -17
- package/dist/dignity.esm.js.map +3 -3
- package/dist/dignity.min.js +27 -27
- package/docs/assets/dignity.esm.js +477 -51
- package/docs/index.html +346 -6
- package/docs/openapi-like.json +9 -1
- package/examples/decentralized-chess-lite.js +2 -2
- package/package.json +2 -1
- package/src/core/dignity-p2p.js +473 -16
- package/src/gossip/peer-group.js +64 -0
- package/src/index.js +13 -1
- package/src/network/in-memory-network.js +42 -0
- package/src/network/peerjs-network.js +28 -0
- package/src/security/message-security-service.js +10 -2
package/docs/index.html
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="UTF-8" />
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
-
<meta name="description" content="dignity.js v0.
|
|
6
|
+
<meta name="description" content="dignity.js v0.6.0 — REST-like P2P object API for decentralized JavaScript applications." />
|
|
7
7
|
<title>dignity.js · Documentation</title>
|
|
8
8
|
<link rel="icon" href="./assets/favicon.svg" type="image/svg+xml" />
|
|
9
9
|
<link rel="icon" href="./favicon.ico" sizes="32x32" />
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
<a class="site-header__brand" href="#overview">
|
|
17
17
|
<img src="./assets/dignity-logo.svg" alt="" width="344" height="80" />
|
|
18
18
|
<!-- <span>dignity.js</span> -->
|
|
19
|
-
<span class="site-header__version">v0.
|
|
19
|
+
<span class="site-header__version">v0.6.0</span>
|
|
20
20
|
</a>
|
|
21
21
|
<div class="site-header__links">
|
|
22
22
|
<a href="https://www.npmjs.com/package/dignity.js" target="_blank" rel="noopener noreferrer">npm</a>
|
|
@@ -34,6 +34,7 @@
|
|
|
34
34
|
<nav>
|
|
35
35
|
<a href="#overview">Overview</a>
|
|
36
36
|
<a href="#installation">Installation</a>
|
|
37
|
+
<a href="#tutorial">Tutorial</a>
|
|
37
38
|
<a href="#quick-start">Quick start</a>
|
|
38
39
|
<a href="#browser">Browser usage</a>
|
|
39
40
|
</nav>
|
|
@@ -43,8 +44,10 @@
|
|
|
43
44
|
<nav>
|
|
44
45
|
<a href="#object-api">Object API</a>
|
|
45
46
|
<a href="#discovery">Room discovery</a>
|
|
47
|
+
<a href="#peer-groups">PeerGroup gossip</a>
|
|
46
48
|
<a href="#messaging">Direct messaging</a>
|
|
47
49
|
<a href="#concurrency">Concurrency</a>
|
|
50
|
+
<a href="#content-hashes">Content hashes</a>
|
|
48
51
|
<a href="#security">Security model</a>
|
|
49
52
|
<a href="#persistence">IndexedDB persistence</a>
|
|
50
53
|
<a href="#react">React hooks</a>
|
|
@@ -87,6 +90,10 @@
|
|
|
87
90
|
<h3>Browser-first</h3>
|
|
88
91
|
<p>IIFE, ESM, and CJS bundles. Optional IndexedDB persistence and React hooks.</p>
|
|
89
92
|
</article>
|
|
93
|
+
<article class="card">
|
|
94
|
+
<h3>Content hashes</h3>
|
|
95
|
+
<p><code>record.hash</code> — <code>sha512:</code> digests over canonical <code>data</code> via <code>stableStringify</code>.</p>
|
|
96
|
+
</article>
|
|
90
97
|
</div>
|
|
91
98
|
</section>
|
|
92
99
|
|
|
@@ -102,6 +109,136 @@
|
|
|
102
109
|
</div>
|
|
103
110
|
</section>
|
|
104
111
|
|
|
112
|
+
<section id="tutorial" class="section">
|
|
113
|
+
<h2>Beginner tutorial</h2>
|
|
114
|
+
<p class="lead">
|
|
115
|
+
New to P2P? Follow these eight short lessons. The full walkthrough is also in
|
|
116
|
+
<a href="https://github.com/jose-compu/dignity.js/blob/main/TUTORIAL.md" target="_blank" rel="noopener noreferrer"><code>TUTORIAL.md</code></a>
|
|
117
|
+
in the repository.
|
|
118
|
+
</p>
|
|
119
|
+
|
|
120
|
+
<div class="card-grid">
|
|
121
|
+
<article class="card">
|
|
122
|
+
<h3>Lesson 1</h3>
|
|
123
|
+
<p>Two peers, one shared object — <code>InMemoryNetworkHub</code> basics.</p>
|
|
124
|
+
</article>
|
|
125
|
+
<article class="card">
|
|
126
|
+
<h3>Lesson 2</h3>
|
|
127
|
+
<p>Rooms — <code>joinDiscovery</code> and <code>broadcastScope</code>.</p>
|
|
128
|
+
</article>
|
|
129
|
+
<article class="card">
|
|
130
|
+
<h3>Lesson 3–5</h3>
|
|
131
|
+
<p>Ownership, collaborators, and version conflicts.</p>
|
|
132
|
+
</article>
|
|
133
|
+
<article class="card">
|
|
134
|
+
<h3>Lesson 6–8</h3>
|
|
135
|
+
<p>Browser/PeerJS, PeerGroup spectators, IndexedDB persistence.</p>
|
|
136
|
+
</article>
|
|
137
|
+
</div>
|
|
138
|
+
|
|
139
|
+
<h3>Lesson 1 — Hello, two peers</h3>
|
|
140
|
+
<p>A <strong>node</strong> is one participant. The in-memory adapter wires peers inside one process (tests and learning):</p>
|
|
141
|
+
<div class="code-block">
|
|
142
|
+
<pre><code class="language-javascript">const { DignityP2P, InMemoryNetworkHub, InMemoryNetworkAdapter } = require('dignity.js');
|
|
143
|
+
|
|
144
|
+
const hub = new InMemoryNetworkHub();
|
|
145
|
+
const security = { appPassword: 'my-team-secret', powSteps: 22 };
|
|
146
|
+
|
|
147
|
+
const alice = new DignityP2P({ nodeId: 'alice', networkAdapter: new InMemoryNetworkAdapter(hub), security });
|
|
148
|
+
const bob = new DignityP2P({ nodeId: 'bob', networkAdapter: new InMemoryNetworkAdapter(hub), security });
|
|
149
|
+
|
|
150
|
+
await alice.start();
|
|
151
|
+
await bob.start();
|
|
152
|
+
|
|
153
|
+
await alice.create('notes', { title: 'Hello P2P' }, { id: 'note-1' });
|
|
154
|
+
console.log(bob.read('notes', 'note-1')); // replicated automatically</code></pre>
|
|
155
|
+
</div>
|
|
156
|
+
|
|
157
|
+
<h3>Lesson 2 — Find peers in a room</h3>
|
|
158
|
+
<p>Use <strong>scoped discovery</strong> so peers see who is online before collaborating:</p>
|
|
159
|
+
<div class="code-block">
|
|
160
|
+
<pre><code class="language-javascript">await alice.joinDiscovery('lobby', { metadata: { nickname: 'Alice' } });
|
|
161
|
+
await bob.joinDiscovery('lobby', { bootstrapPeerIds: ['alice'] });
|
|
162
|
+
|
|
163
|
+
console.log(alice.listPeers('lobby').map((p) => p.peerId));
|
|
164
|
+
|
|
165
|
+
await alice.create('notes', { title: 'Room note' }, {
|
|
166
|
+
id: 'note-2',
|
|
167
|
+
broadcastScope: 'lobby'
|
|
168
|
+
});</code></pre>
|
|
169
|
+
</div>
|
|
170
|
+
|
|
171
|
+
<h3>Lesson 3 — Only the owner can edit</h3>
|
|
172
|
+
<p>The peer that calls <code>create</code> becomes <code>ownerId</code>. Others can read but not update unless you add them as collaborators:</p>
|
|
173
|
+
<div class="code-block">
|
|
174
|
+
<pre><code class="language-javascript">await alice.create('games', { score: 0 }, { id: 'g1' });
|
|
175
|
+
await alice.update('games', 'g1', { score: 10 }); // OK
|
|
176
|
+
await bob.update('games', 'g1', { score: 999 }); // rejected — Bob is not owner
|
|
177
|
+
|
|
178
|
+
alice.on('change', (e) => console.log(e.kind, e.collection, e.id));</code></pre>
|
|
179
|
+
</div>
|
|
180
|
+
|
|
181
|
+
<h3>Lesson 4 — Invite a collaborator</h3>
|
|
182
|
+
<div class="code-block">
|
|
183
|
+
<pre><code class="language-javascript">await alice.update('docs', 'd1', { body: 'shared' }, {
|
|
184
|
+
collaborators: ['alice', 'bob']
|
|
185
|
+
});
|
|
186
|
+
await bob.update('docs', 'd1', { body: 'Bob edited' }); // OK now</code></pre>
|
|
187
|
+
</div>
|
|
188
|
+
|
|
189
|
+
<h3>Lesson 5 — Versions and conflicts</h3>
|
|
190
|
+
<p>Each update increments <code>version</code>. Use <code>expectedVersion</code> or <code>updateWithRetry</code> to avoid overwriting concurrent edits (see <a href="#concurrency">Concurrency</a>).</p>
|
|
191
|
+
|
|
192
|
+
<h3>Lesson 6 — Real browsers (PeerJS)</h3>
|
|
193
|
+
<p>Swap the in-memory adapter for <code>createPeerJSNetworkAdapter</code> and a signaling server. Open the <a href="./chess/index.html">3D Chess demo</a> in two tabs to see live WebRTC sync.</p>
|
|
194
|
+
|
|
195
|
+
<h3>Lesson 7 — Small teams vs big audiences</h3>
|
|
196
|
+
<div class="table-wrap">
|
|
197
|
+
<table>
|
|
198
|
+
<thead>
|
|
199
|
+
<tr>
|
|
200
|
+
<th>Scenario</th>
|
|
201
|
+
<th>Mode</th>
|
|
202
|
+
<th>API</th>
|
|
203
|
+
</tr>
|
|
204
|
+
</thead>
|
|
205
|
+
<tbody>
|
|
206
|
+
<tr>
|
|
207
|
+
<td>2–8 players co-editing</td>
|
|
208
|
+
<td>Direct mesh</td>
|
|
209
|
+
<td><code>create</code> / <code>update</code> + <code>connectToPeers</code></td>
|
|
210
|
+
</tr>
|
|
211
|
+
<tr>
|
|
212
|
+
<td>Spectators, public feeds</td>
|
|
213
|
+
<td>PeerGroup gossip</td>
|
|
214
|
+
<td><code>joinPeerGroup</code> + <code>publishRecordToPeerGroup</code></td>
|
|
215
|
+
</tr>
|
|
216
|
+
</tbody>
|
|
217
|
+
</table>
|
|
218
|
+
</div>
|
|
219
|
+
<div class="code-block">
|
|
220
|
+
<pre><code class="language-javascript">// Spectator watches a chess game (read-only epidemic feed)
|
|
221
|
+
await spectator.joinPeerGroup('spectate:chess:game-42', { bootstrapPeerIds: ['white-host'] });
|
|
222
|
+
await white.publishRecordToPeerGroup('spectate:chess:game-42', 'chess-matches', 'game-42');</code></pre>
|
|
223
|
+
</div>
|
|
224
|
+
<p>Details: <a href="#peer-groups">PeerGroup gossip</a>.</p>
|
|
225
|
+
|
|
226
|
+
<h3>Lesson 8 — Survive page reload</h3>
|
|
227
|
+
<p>Attach <code>IndexedDBPersistence</code> in the browser so objects persist locally (see <a href="#persistence">IndexedDB persistence</a>).</p>
|
|
228
|
+
|
|
229
|
+
<div class="callout callout--warn">
|
|
230
|
+
<strong>Common mistakes</strong>
|
|
231
|
+
<ul>
|
|
232
|
+
<li>Mismatched <code>appPassword</code> between peers — messages fail to decrypt.</li>
|
|
233
|
+
<li>Skipping <code>await node.start()</code> — nothing replicates.</li>
|
|
234
|
+
<li>Using the in-memory adapter in production — use PeerJS instead.</li>
|
|
235
|
+
<li>Keeping the default password — always set a strong <code>appPassword</code>.</li>
|
|
236
|
+
</ul>
|
|
237
|
+
</div>
|
|
238
|
+
|
|
239
|
+
<p><strong>Next:</strong> run <code>node examples/decentralized-tictactoe.js</code> or read the full <a href="https://github.com/jose-compu/dignity.js/blob/main/TUTORIAL.md" target="_blank" rel="noopener noreferrer">TUTORIAL.md</a>.</p>
|
|
240
|
+
</section>
|
|
241
|
+
|
|
105
242
|
<section id="quick-start" class="section">
|
|
106
243
|
<h2>Quick start</h2>
|
|
107
244
|
<p>Create a node, join a room, and replicate an object between two peers:</p>
|
|
@@ -153,7 +290,7 @@ await bob.stop();</code></pre>
|
|
|
153
290
|
</div>
|
|
154
291
|
<div class="callout">
|
|
155
292
|
<strong>In-memory adapter</strong>
|
|
156
|
-
<code>InMemoryNetworkAdapter</code> is for tests and local demos. Production browser apps use
|
|
293
|
+
<code>InMemoryNetworkAdapter</code> is for tests and local demos. Production browser apps use <code>createPeerJSNetworkAdapter</code> (see the <a href="./chess/index.html">3D Chess demo</a>).
|
|
157
294
|
</div>
|
|
158
295
|
</section>
|
|
159
296
|
|
|
@@ -275,6 +412,182 @@ await node.leaveDiscovery('team:red');</code></pre>
|
|
|
275
412
|
</div>
|
|
276
413
|
</section>
|
|
277
414
|
|
|
415
|
+
<section id="peer-groups" class="section">
|
|
416
|
+
<h2>PeerGroup gossip (scalable PubSub)</h2>
|
|
417
|
+
<p>
|
|
418
|
+
Scale object-update distribution to <strong>millions of subscribers</strong> per published
|
|
419
|
+
object while each peer keeps a <em>small, bounded</em> number of active transports.
|
|
420
|
+
Think decentralized Twitter: you follow 200 accounts (200 multiplexed gossip groups);
|
|
421
|
+
a popular account can have millions of followers sharing relay load.
|
|
422
|
+
</p>
|
|
423
|
+
|
|
424
|
+
<h3>Two replication modes</h3>
|
|
425
|
+
<p>Apps can mix both modes on the same node.</p>
|
|
426
|
+
<div class="table-wrap">
|
|
427
|
+
<table>
|
|
428
|
+
<thead>
|
|
429
|
+
<tr>
|
|
430
|
+
<th>Mode</th>
|
|
431
|
+
<th>Use case</th>
|
|
432
|
+
<th>API</th>
|
|
433
|
+
</tr>
|
|
434
|
+
</thead>
|
|
435
|
+
<tbody>
|
|
436
|
+
<tr>
|
|
437
|
+
<td>Direct mesh</td>
|
|
438
|
+
<td>Chess players, small teams, owner + collaborators</td>
|
|
439
|
+
<td><code>create</code> / <code>update</code> with <code>connectToPeers</code> (unchanged)</td>
|
|
440
|
+
</tr>
|
|
441
|
+
<tr>
|
|
442
|
+
<td>PeerGroup gossip</td>
|
|
443
|
+
<td>Spectators, public feeds, high-fanout read replicas</td>
|
|
444
|
+
<td><code>joinPeerGroup</code> + <code>publishToPeerGroup</code></td>
|
|
445
|
+
</tr>
|
|
446
|
+
</tbody>
|
|
447
|
+
</table>
|
|
448
|
+
</div>
|
|
449
|
+
|
|
450
|
+
<h3>Architecture</h3>
|
|
451
|
+
<div class="code-block">
|
|
452
|
+
<pre><code>Publisher --fanout=3--> Peer A --relay--> Peer D ...
|
|
453
|
+
\-------> Peer B --relay--> Peer E ...
|
|
454
|
+
\------> Peer C --relay--> Peer F ...</code></pre>
|
|
455
|
+
</div>
|
|
456
|
+
<ul>
|
|
457
|
+
<li><strong>Presence</strong> — each group reuses scoped <code>joinDiscovery('gossip:{groupId}')</code> as a membership directory.</li>
|
|
458
|
+
<li><strong>Transport</strong> — <code>sendToPeers(envelope, peerIds)</code>; not full-mesh broadcast.</li>
|
|
459
|
+
<li><strong>Relay</strong> — subscribers re-forward to <code>fanout</code> random peers until <code>maxHops</code>.</li>
|
|
460
|
+
<li><strong>Dedup</strong> — <code>gossipId</code> cache (TTL-bounded) stops infinite loops.</li>
|
|
461
|
+
<li><strong>Connection budget</strong> — <code>maxActivePeers</code> per group + <code>globalMaxOpenConnections</code> per node.</li>
|
|
462
|
+
</ul>
|
|
463
|
+
|
|
464
|
+
<h3>Core API</h3>
|
|
465
|
+
<div class="code-block">
|
|
466
|
+
<pre><code class="language-javascript">await node.joinPeerGroup('spectate:chess:game-1', {
|
|
467
|
+
bootstrapPeerIds: [hostPeerId],
|
|
468
|
+
fanout: 3,
|
|
469
|
+
maxActivePeers: 8,
|
|
470
|
+
maxHops: 6,
|
|
471
|
+
metadata: { role: 'spectator' }
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
await node.publishToPeerGroup('spectate:chess:game-1', 'record:snapshot', {
|
|
475
|
+
collectionName: 'chess-matches',
|
|
476
|
+
record
|
|
477
|
+
}, { fanout: 3 });
|
|
478
|
+
|
|
479
|
+
// Convenience wrapper for local records
|
|
480
|
+
await node.publishRecordToPeerGroup('feed:alice', 'posts', 'post-1');
|
|
481
|
+
|
|
482
|
+
// Custom app events
|
|
483
|
+
await node.publishToPeerGroup('feed:alice', 'timeline:update', { id: 'post-1' });
|
|
484
|
+
|
|
485
|
+
await node.leavePeerGroup('spectate:chess:game-1');
|
|
486
|
+
|
|
487
|
+
node.listPeerGroupMembers('spectate:chess:game-1');
|
|
488
|
+
node.getPeerGroupStats();
|
|
489
|
+
// { joinedGroups, seenGossipCount, openConnectionCount, globalMaxOpenConnections }</code></pre>
|
|
490
|
+
</div>
|
|
491
|
+
|
|
492
|
+
<h3>Inner message types</h3>
|
|
493
|
+
<p>Relayed inside <code>peer-group:gossip</code>:</p>
|
|
494
|
+
<div class="table-wrap">
|
|
495
|
+
<table>
|
|
496
|
+
<thead>
|
|
497
|
+
<tr>
|
|
498
|
+
<th>Type</th>
|
|
499
|
+
<th>Purpose</th>
|
|
500
|
+
</tr>
|
|
501
|
+
</thead>
|
|
502
|
+
<tbody>
|
|
503
|
+
<tr>
|
|
504
|
+
<td><code>operation</code></td>
|
|
505
|
+
<td>Replicated CRUD operation</td>
|
|
506
|
+
</tr>
|
|
507
|
+
<tr>
|
|
508
|
+
<td><code>record:snapshot</code></td>
|
|
509
|
+
<td>Late-joiner catch-up</td>
|
|
510
|
+
</tr>
|
|
511
|
+
<tr>
|
|
512
|
+
<td>App-defined</td>
|
|
513
|
+
<td>Custom payloads via <code>publishToPeerGroup</code> (emits <code>peergroupmessage</code>)</td>
|
|
514
|
+
</tr>
|
|
515
|
+
</tbody>
|
|
516
|
+
</table>
|
|
517
|
+
</div>
|
|
518
|
+
|
|
519
|
+
<h3>Security</h3>
|
|
520
|
+
<ul>
|
|
521
|
+
<li>Group-scoped encryption via existing <code>broadcastScope = gossip:{groupId}</code>.</li>
|
|
522
|
+
<li>Signatures and PoW verified before relay (same <code>MessageSecurityService</code> path).</li>
|
|
523
|
+
<li>Invalid gossip rejected; banned peers ignored.</li>
|
|
524
|
+
<li><code>record:snapshot</code> gossip carries <code>record.hash</code> (<code>sha512:</code> over canonical <code>data</code>). Receivers recompute locally and <strong>reject</strong> snapshots whose hash does not match payload data (emits <code>content-hash-mismatch</code>).</li>
|
|
525
|
+
<li><code>gossipId</code> dedup cache prevents replay loops across relays.</li>
|
|
526
|
+
<li>Set <code>relayEnabled: false</code> on a group to receive updates without re-forwarding.</li>
|
|
527
|
+
</ul>
|
|
528
|
+
|
|
529
|
+
<h3>Chess spectators</h3>
|
|
530
|
+
<p>
|
|
531
|
+
The <a href="./chess/index.html">3D Chess demo</a> mixes both replication modes:
|
|
532
|
+
</p>
|
|
533
|
+
<ul>
|
|
534
|
+
<li><strong>Players</strong> — direct mesh between white/black (low latency).</li>
|
|
535
|
+
<li><strong>Spectators</strong> — <code>joinPeerGroup('spectate:chess:{gameId}')</code>.</li>
|
|
536
|
+
<li><strong>Moves</strong> — mover updates on the direct path; also <code>publishRecordToPeerGroup</code> for spectators.</li>
|
|
537
|
+
<li><strong>New spectator</strong> — epidemic relay + host snapshot on join; host does not O(N) connect.</li>
|
|
538
|
+
</ul>
|
|
539
|
+
|
|
540
|
+
<h3>Defaults</h3>
|
|
541
|
+
<div class="table-wrap">
|
|
542
|
+
<table>
|
|
543
|
+
<thead>
|
|
544
|
+
<tr>
|
|
545
|
+
<th>Option</th>
|
|
546
|
+
<th>Default</th>
|
|
547
|
+
<th>Rationale</th>
|
|
548
|
+
</tr>
|
|
549
|
+
</thead>
|
|
550
|
+
<tbody>
|
|
551
|
+
<tr>
|
|
552
|
+
<td><code>fanout</code></td>
|
|
553
|
+
<td>3</td>
|
|
554
|
+
<td>Classic gossip fanout</td>
|
|
555
|
+
</tr>
|
|
556
|
+
<tr>
|
|
557
|
+
<td><code>maxActivePeers</code></td>
|
|
558
|
+
<td>8</td>
|
|
559
|
+
<td>Keeps local PeerJS channels small</td>
|
|
560
|
+
</tr>
|
|
561
|
+
<tr>
|
|
562
|
+
<td><code>maxHops</code></td>
|
|
563
|
+
<td>6</td>
|
|
564
|
+
<td>Covers large graphs with low diameter</td>
|
|
565
|
+
</tr>
|
|
566
|
+
<tr>
|
|
567
|
+
<td><code>globalMaxOpenConnections</code></td>
|
|
568
|
+
<td>32</td>
|
|
569
|
+
<td>Hard cap per node</td>
|
|
570
|
+
</tr>
|
|
571
|
+
<tr>
|
|
572
|
+
<td><code>gossipIdTtlMs</code></td>
|
|
573
|
+
<td>5 min</td>
|
|
574
|
+
<td>Dedup window</td>
|
|
575
|
+
</tr>
|
|
576
|
+
</tbody>
|
|
577
|
+
</table>
|
|
578
|
+
</div>
|
|
579
|
+
|
|
580
|
+
<h3>Roadmap</h3>
|
|
581
|
+
<p><strong>v0.6.0 (shipped)</strong> — core gossip, chess spectators, unit + e2e tests.</p>
|
|
582
|
+
<p><strong>v0.6.x polish</strong> — <code>subscribeObjectFeed</code> wrapper, connection LRU trim, IndexedDB joined-group persistence, React <code>usePeerGroup</code> hook, 50+ node integration test.</p>
|
|
583
|
+
<p><strong>Future</strong> — publisher rate limits, partial snapshot / delta feeds, cross-group fan-in metrics.</p>
|
|
584
|
+
<p>
|
|
585
|
+
Tracking:
|
|
586
|
+
<a href="https://github.com/jose-compu/dignity.js/issues/54" target="_blank" rel="noopener noreferrer">#54 PeerGroup core</a>,
|
|
587
|
+
<a href="https://github.com/jose-compu/dignity.js/issues/55" target="_blank" rel="noopener noreferrer">#55 Chess spectators</a>.
|
|
588
|
+
</p>
|
|
589
|
+
</section>
|
|
590
|
+
|
|
278
591
|
<section id="messaging" class="section">
|
|
279
592
|
<h2>Direct secure messaging</h2>
|
|
280
593
|
<p>Send end-to-end encrypted messages to a specific peer:</p>
|
|
@@ -309,6 +622,25 @@ await node.updateWithRetry('games', 'g1', (current) => ({
|
|
|
309
622
|
</div>
|
|
310
623
|
</section>
|
|
311
624
|
|
|
625
|
+
<section id="content-hashes" class="section">
|
|
626
|
+
<h2>Record content hashes</h2>
|
|
627
|
+
<p>
|
|
628
|
+
Active records from <code>create</code>, <code>read</code>, <code>list</code>, <code>update</code>,
|
|
629
|
+
and <code>pushRecordSnapshot</code> include a <code>hash</code> field:
|
|
630
|
+
</p>
|
|
631
|
+
<div class="code-block">
|
|
632
|
+
<pre><code class="language-javascript">const record = await node.create('notes', { title: 'hello' }, { id: 'n1' });
|
|
633
|
+
console.log(record.hash); // sha512:<128-char hex></code></pre>
|
|
634
|
+
</div>
|
|
635
|
+
<ul>
|
|
636
|
+
<li>Algorithm: <code>sha512</code> via <code>tweetnacl.hash</code> (browser and Node).</li>
|
|
637
|
+
<li>Scope: canonical <code>record.data</code> only — not <code>id</code>, owner, timestamps, collaborators, or version.</li>
|
|
638
|
+
<li>Canonicalization: <code>stableStringify</code> so object key order does not change the digest.</li>
|
|
639
|
+
<li>Replication: hashes are set on create/update and travel with snapshots.</li>
|
|
640
|
+
<li>Integrity: <code>restoreRecord</code> recomputes locally; mismatched remote hashes emit <code>warning</code> with <code>type: 'content-hash-mismatch'</code>. Gossip snapshots use strict mode and are rejected on mismatch.</li>
|
|
641
|
+
</ul>
|
|
642
|
+
</section>
|
|
643
|
+
|
|
312
644
|
<section id="security" class="section">
|
|
313
645
|
<h2>Security model</h2>
|
|
314
646
|
<p>All security features are enabled by default. Configure via the <code>security</code> constructor option.</p>
|
|
@@ -547,7 +879,7 @@ const pool = createDefaultSignalingPool({
|
|
|
547
879
|
</tr>
|
|
548
880
|
<tr>
|
|
549
881
|
<td><span class="tag tag--event">warning</span></td>
|
|
550
|
-
<td><code>{ type, ... }</code> — non-fatal issues (heartbeat, persistence, etc.)</td>
|
|
882
|
+
<td><code>{ type, ... }</code> — non-fatal issues (heartbeat, persistence, <code>content-hash-mismatch</code>, <code>orphan-operation</code>, etc.)</td>
|
|
551
883
|
</tr>
|
|
552
884
|
</tbody>
|
|
553
885
|
</table>
|
|
@@ -600,6 +932,11 @@ const pool = createDefaultSignalingPool({
|
|
|
600
932
|
<td>Replicated move history with compact board model</td>
|
|
601
933
|
<td><code>npm run example:chess</code></td>
|
|
602
934
|
</tr>
|
|
935
|
+
<tr>
|
|
936
|
+
<td><a href="./chess/index.html"><code>docs/chess/</code></a></td>
|
|
937
|
+
<td>3D browser chess — PeerJS mesh, dual-signed resume links, IndexedDB, React hooks</td>
|
|
938
|
+
<td><code>npm run build:chess</code> then <code>npm run docs:serve</code></td>
|
|
939
|
+
</tr>
|
|
603
940
|
</tbody>
|
|
604
941
|
</table>
|
|
605
942
|
</div>
|
|
@@ -608,9 +945,12 @@ const pool = createDefaultSignalingPool({
|
|
|
608
945
|
<section id="development" class="section">
|
|
609
946
|
<h2>Development</h2>
|
|
610
947
|
<div class="code-block">
|
|
611
|
-
<pre><code class="language-bash"># Run tests (
|
|
948
|
+
<pre><code class="language-bash"># Run tests (237+ passing, ~91% line coverage)
|
|
612
949
|
npm test
|
|
613
950
|
|
|
951
|
+
# PeerGroup unit + e2e tests only
|
|
952
|
+
npm run test:peer-group
|
|
953
|
+
|
|
614
954
|
# Build browser + Node bundles
|
|
615
955
|
npm run build
|
|
616
956
|
|
|
@@ -625,7 +965,7 @@ npm run example:chess</code></pre>
|
|
|
625
965
|
|
|
626
966
|
<footer class="site-footer">
|
|
627
967
|
<p>
|
|
628
|
-
dignity.js v0.
|
|
968
|
+
dignity.js v0.6.0 ·
|
|
629
969
|
<a href="https://github.com/jose-compu/dignity.js/blob/main/LICENSE">Apache 2.0</a> ·
|
|
630
970
|
<a href="https://github.com/jose-compu/dignity.js">GitHub</a> ·
|
|
631
971
|
<a href="https://www.npmjs.com/package/dignity.js">npm</a>
|
package/docs/openapi-like.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dignity.js",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.4",
|
|
4
4
|
"description": "REST-like object API over peer-to-peer replication",
|
|
5
5
|
"resources": {
|
|
6
6
|
"collections/{collection}/{id}": {
|
|
@@ -58,6 +58,14 @@
|
|
|
58
58
|
"ensureConnectedToPeers": "connect to many peers before broadcast",
|
|
59
59
|
"joinDiscovery": "scoped presence; options.bootstrapPeerIds connects before announce",
|
|
60
60
|
"broadcastMessage": "custom app messages; options.connectToPeers"
|
|
61
|
+
},
|
|
62
|
+
"peerGroups": {
|
|
63
|
+
"joinPeerGroup": "join scalable gossip group (multiplexed); options.fanout, maxActivePeers, bootstrapPeerIds",
|
|
64
|
+
"leavePeerGroup": "leave gossip group and scoped presence",
|
|
65
|
+
"listPeerGroupMembers": "list presence in gossip:{groupId} scope",
|
|
66
|
+
"publishToPeerGroup": "epidemic publish with bounded fanout; inner types: operation, record:snapshot, custom",
|
|
67
|
+
"publishRecordToPeerGroup": "publish normalized record snapshot into gossip group",
|
|
68
|
+
"getPeerGroupStats": "{ joinedGroups, seenGossipCount, openConnectionCount, globalMaxOpenConnections }"
|
|
61
69
|
}
|
|
62
70
|
},
|
|
63
71
|
"events": {
|
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
* Run this example with:
|
|
5
5
|
* npm run example:chess
|
|
6
6
|
*
|
|
7
|
-
* For the browser
|
|
8
|
-
* docs/chess/
|
|
7
|
+
* For the browser 3D demo (PeerJS, dual-signed resume, IndexedDB), see:
|
|
8
|
+
* docs/chess/ — rebuild with: npm run build:chess
|
|
9
9
|
*/
|
|
10
10
|
const { DignityP2P, InMemoryNetworkHub, InMemoryNetworkAdapter } = require('../src');
|
|
11
11
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dignity.js",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "P2P object API for decentralized JavaScript applications",
|
|
5
5
|
"homepage": "https://jose-compu.github.io/dignity.js/",
|
|
6
6
|
"repository": {
|
|
@@ -41,6 +41,7 @@
|
|
|
41
41
|
"scripts": {
|
|
42
42
|
"test": "jest --coverage",
|
|
43
43
|
"test:unit": "jest tests/unit --runInBand",
|
|
44
|
+
"test:peer-group": "jest tests/unit/peer-group-gossip.test.js tests/unit/peer-group-helpers.test.js tests/integration/peer-group-gossip-e2e.test.js --runInBand --coverage=false",
|
|
44
45
|
"test:cloudflare-live": "RUN_CLOUDFLARE_LIVE_TESTS=1 jest tests/integration/cloudflare-signaling-live.test.js --runInBand",
|
|
45
46
|
"test:pow-calibrate": "RUN_POW_CALIBRATE=1 jest tests/unit/sloth-vdf-timing.test.js --runInBand --coverage=false",
|
|
46
47
|
"build": "node scripts/build.js",
|