dignity.js 0.7.1 → 0.8.1

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.
@@ -338,5 +338,61 @@ log('keys upgraded:', bob.getPeerIdentityState('alice')?.publicKey?.signingPubli
338
338
 
339
339
  await alice.stop();
340
340
  await bob.stop();`
341
+ },
342
+ {
343
+ id: 'cqrs-replica',
344
+ title: 'CQRS — publisher + query replica',
345
+ description: 'Domain events from a tiered PeerGroup hydrate a read-only DignityQueryReplica.',
346
+ code: `const { DignityP2P, DignityQueryReplica, InMemoryNetworkHub, InMemoryNetworkAdapter } = dignity;
347
+
348
+ const hub = new InMemoryNetworkHub();
349
+ const security = helpers.fastSecurity();
350
+
351
+ const publisher = new DignityP2P({
352
+ nodeId: 'publisher',
353
+ networkAdapter: new InMemoryNetworkAdapter(hub),
354
+ security
355
+ });
356
+ const reader = new DignityP2P({
357
+ nodeId: 'reader',
358
+ networkAdapter: new InMemoryNetworkAdapter(hub),
359
+ security
360
+ });
361
+
362
+ helpers.track(publisher, reader);
363
+ await publisher.start();
364
+ await reader.start();
365
+
366
+ await publisher.joinPeerGroup('feed:demo', {
367
+ role: 'publisher',
368
+ tiered: true,
369
+ liveCap: 100,
370
+ domainEvents: true,
371
+ fanout: 2,
372
+ maxActivePeers: 4
373
+ });
374
+
375
+ const replica = new DignityQueryReplica(reader, {
376
+ groupId: 'feed:demo',
377
+ collections: ['posts'],
378
+ publisherId: 'publisher'
379
+ });
380
+ await replica.start({ bootstrapPeerIds: ['publisher'] });
381
+ await helpers.sleep(40);
382
+
383
+ await publisher.create('posts', { text: 'CQRS demo post' }, {
384
+ id: 'p1',
385
+ peerGroupId: 'feed:demo'
386
+ });
387
+ await helpers.sleep(60);
388
+
389
+ const record = replica.read('posts', 'p1');
390
+ log('replica read:', JSON.stringify(record?.data));
391
+ log('chain ok:', replica.verifyChain().ok);
392
+ log('stats:', JSON.stringify(replica.getViewStats()));
393
+
394
+ await replica.stop();
395
+ await publisher.stop();
396
+ await reader.stop();`
341
397
  }
342
398
  ];
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.7.1 — REST-like P2P object API for decentralized JavaScript applications." />
6
+ <meta name="description" content="dignity.js v0.8.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.7.1</span>
19
+ <span class="site-header__version">v0.8.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>
@@ -47,6 +47,7 @@
47
47
  <a href="#object-api">Object API</a>
48
48
  <a href="#discovery">Room discovery</a>
49
49
  <a href="#peer-groups">PeerGroup gossip</a>
50
+ <a href="#cqrs-tiers">CQRS tiers</a>
50
51
  <a href="#messaging">Direct messaging</a>
51
52
  <a href="#concurrency">Concurrency</a>
52
53
  <a href="#content-hashes">Content hashes</a>
@@ -96,6 +97,10 @@
96
97
  <h3>Content hashes</h3>
97
98
  <p><code>record.hash</code> — <code>sha512:</code> digests over canonical <code>data</code> via <code>stableStringify</code>.</p>
98
99
  </article>
100
+ <article class="card">
101
+ <h3>CQRS PeerGroups (v0.8+)</h3>
102
+ <p>Live + bulk tiers, signed domain events, and <code>DignityQueryReplica</code> read views for massive audiences.</p>
103
+ </article>
99
104
  </div>
100
105
  </section>
101
106
 
@@ -480,7 +485,7 @@ await node.leaveDiscovery('team:red');</code></pre>
480
485
  bootstrapPeerIds: [hostPeerId],
481
486
  fanout: 3,
482
487
  maxActivePeers: 8,
483
- maxHops: 6,
488
+ maxHops: 64,
484
489
  metadata: { role: 'spectator' }
485
490
  });
486
491
 
@@ -521,6 +526,14 @@ node.getPeerGroupStats();
521
526
  <td><code>record:snapshot</code></td>
522
527
  <td>Late-joiner catch-up</td>
523
528
  </tr>
529
+ <tr>
530
+ <td><code>domain:event</code></td>
531
+ <td>Signed, versioned write event (auto-published on CRUD when <code>domainEvents: true</code>)</td>
532
+ </tr>
533
+ <tr>
534
+ <td><code>domain:checkpoint</code></td>
535
+ <td>Bulk-tier catch-up anchor (<code>lastEventHash</code>, <code>recordCount</code>)</td>
536
+ </tr>
524
537
  <tr>
525
538
  <td>App-defined</td>
526
539
  <td>Custom payloads via <code>publishToPeerGroup</code> (emits <code>peergroupmessage</code>)</td>
@@ -539,6 +552,46 @@ node.getPeerGroupStats();
539
552
  <li>Set <code>relayEnabled: false</code> on a group to receive updates without re-forwarding.</li>
540
553
  </ul>
541
554
 
555
+ <h3 id="cqrs-tiers">CQRS tiers &amp; query replicas (v0.8+)</h3>
556
+ <p>
557
+ For audiences above ~5&nbsp;000 subscribers <em>per publisher</em>, use the command/query split:
558
+ publishers write locally and emit signed domain events; subscribers maintain read-only materialized views.
559
+ </p>
560
+ <ul>
561
+ <li><strong>Live tier</strong> — first <code>liveCap</code> subscribers (default 5&nbsp;000) receive real-time gossip via <code>publishToPeerGroup</code>.</li>
562
+ <li><strong>Bulk tier</strong> — overflow subscribers receive batched updates via <code>publishPeerGroupBulk</code> and elected bulk relays.</li>
563
+ <li><strong>Domain events</strong> — every <code>create</code> / <code>update</code> / <code>remove</code> on a publisher group auto-emits <code>domain:event</code> with hash-chain linking.</li>
564
+ <li><strong>Query replica</strong> — <code>DignityQueryReplica</code> hydrates local <code>read</code> / <code>list</code> views without contacting the owner.</li>
565
+ </ul>
566
+ <div class="code-block">
567
+ <pre><code class="language-javascript">const { DignityP2P, DignityQueryReplica } = dignity;
568
+
569
+ // Publisher (command path)
570
+ await publisher.joinPeerGroup('feed:alice', {
571
+ role: 'publisher',
572
+ tiered: true,
573
+ liveCap: 5000,
574
+ domainEvents: true
575
+ });
576
+ await publisher.create('posts', { text: 'hello' }, {
577
+ id: 'p1',
578
+ peerGroupId: 'feed:alice' // auto-publishes domain:event
579
+ });
580
+
581
+ // Read-only replica (query path)
582
+ const replica = new DignityQueryReplica(reader, {
583
+ groupId: 'feed:alice',
584
+ collections: ['posts'],
585
+ publisherId: 'alice'
586
+ });
587
+ await replica.start({ bootstrapPeerIds: ['alice'] });
588
+ replica.read('posts', 'p1');
589
+ replica.verifyChain(); // hash-chain consistency</code></pre>
590
+ </div>
591
+ <p>
592
+ Try the <a href="./playground/index.html">live playground</a> demo <em>CQRS — publisher + query replica</em>.
593
+ </p>
594
+
542
595
  <h3>Chess spectators</h3>
543
596
  <p>
544
597
  The <a href="./chess/index.html">3D Chess demo</a> mixes both replication modes:
@@ -573,8 +626,8 @@ node.getPeerGroupStats();
573
626
  </tr>
574
627
  <tr>
575
628
  <td><code>maxHops</code></td>
576
- <td>6</td>
577
- <td>Covers large graphs with low diameter</td>
629
+ <td>64</td>
630
+ <td>Covers large graphs (fanout³×⁶⁴) without per-group tuning</td>
578
631
  </tr>
579
632
  <tr>
580
633
  <td><code>globalMaxOpenConnections</code></td>
@@ -591,7 +644,8 @@ node.getPeerGroupStats();
591
644
  </div>
592
645
 
593
646
  <h3>Roadmap</h3>
594
- <p><strong>v0.7.1 (current)</strong> — live docs playground, demo fixes, syntax highlighting.</p>
647
+ <p><strong>v0.8.0 (current)</strong> — CQRS tiered PeerGroups, domain events, DignityQueryReplica, hash chains, bulk relay, maxHops 64.</p>
648
+ <p><strong>v0.7.1</strong> — live docs playground, demo fixes, syntax highlighting.</p>
595
649
  <p><strong>v0.7.0 (shipped)</strong> — credential-derived keys, identity rotation, PeerGroup hardening, stress harness.</p>
596
650
  <p><strong>v0.6.0 (shipped)</strong> — core gossip, chess spectators, unit + e2e tests.</p>
597
651
  <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>
@@ -914,7 +968,7 @@ const pool = createDefaultSignalingPool({
914
968
  <tbody>
915
969
  <tr>
916
970
  <td><code>dignity.js</code></td>
917
- <td><code>DignityP2P</code>, <code>IndexedDBPersistence</code>, signaling providers, in-memory adapters, security utilities</td>
971
+ <td><code>DignityP2P</code>, <code>DignityQueryReplica</code>, <code>IndexedDBPersistence</code>, signaling providers, in-memory adapters, PeerGroup + CQRS helpers (<code>domain-events</code>, <code>peer-group-tiers</code>, <code>bulk-relay</code>), security utilities</td>
918
972
  </tr>
919
973
  <tr>
920
974
  <td><code>dignity.js/react</code></td>
@@ -965,10 +1019,10 @@ const pool = createDefaultSignalingPool({
965
1019
  <section id="development" class="section">
966
1020
  <h2>Development</h2>
967
1021
  <div class="code-block">
968
- <pre><code class="language-bash"># Run tests (237+ passing, ~91% line coverage)
1022
+ <pre><code class="language-bash"># Run tests (289+ passing)
969
1023
  npm test
970
1024
 
971
- # PeerGroup unit + e2e tests only
1025
+ # PeerGroup + CQRS unit + integration tests
972
1026
  npm run test:peer-group
973
1027
 
974
1028
  # Build browser + Node bundles
@@ -985,7 +1039,7 @@ npm run example:chess</code></pre>
985
1039
 
986
1040
  <footer class="site-footer">
987
1041
  <p>
988
- dignity.js v0.7.1 ·
1042
+ dignity.js v0.8.0 ·
989
1043
  <a href="https://github.com/jose-compu/dignity.js/blob/main/LICENSE">Apache 2.0</a> ·
990
1044
  <a href="https://github.com/jose-compu/dignity.js">GitHub</a> ·
991
1045
  <a href="https://www.npmjs.com/package/dignity.js">npm</a>
@@ -1,7 +1,11 @@
1
1
  {
2
2
  "name": "dignity.js",
3
- "version": "0.7.1",
3
+ "version": "0.8.1",
4
4
  "description": "REST-like object API over peer-to-peer replication",
5
+ "lifecycle": {
6
+ "start": "attach network message handler and begin receiving",
7
+ "stop": "leave discovery scopes and detach handler"
8
+ },
5
9
  "resources": {
6
10
  "collections/{collection}/{id}": {
7
11
  "create": {
@@ -11,7 +15,8 @@
11
15
  "id": "optional stable id",
12
16
  "collaborators": "optional peer id list",
13
17
  "broadcastScope": "scoped broadcast password namespace",
14
- "connectToPeers": "optional; defaults to collaborators on PeerJS mesh"
18
+ "connectToPeers": "optional; defaults to collaborators on PeerJS mesh",
19
+ "peerGroupId": "optional; auto-publish signed domain event to this PeerGroup (v0.8+, publisher role)"
15
20
  }
16
21
  },
17
22
  "read": {
@@ -43,7 +48,17 @@
43
48
  },
44
49
  "delete": {
45
50
  "method": "remove(collection, id)",
46
- "authorization": "owner-only"
51
+ "authorization": "owner-only",
52
+ "options": {
53
+ "peerGroupId": "optional; auto-publish domain event when publisher (v0.8+)"
54
+ }
55
+ },
56
+ "transferOwnership": {
57
+ "method": "transferOwnership(collection, id, newOwnerId, options)",
58
+ "authorization": "owner-only",
59
+ "options": {
60
+ "keepAsCollaborator": "default true; previous owner stays collaborator"
61
+ }
47
62
  }
48
63
  },
49
64
  "collections/{collection}": {
@@ -57,24 +72,62 @@
57
72
  "getConnectionStats": "{ openCount, peerIds }",
58
73
  "ensureConnectedToPeers": "connect to many peers before broadcast",
59
74
  "joinDiscovery": "scoped presence; options.bootstrapPeerIds connects before announce",
60
- "broadcastMessage": "custom app messages; options.connectToPeers"
75
+ "leaveDiscovery": "stop heartbeat and remove local presence from scope",
76
+ "listPeers": "list presence entries in a discovery scope",
77
+ "announcePresence": "manual presence heartbeat for a joined scope",
78
+ "broadcastMessage": "custom app messages; options.connectToPeers",
79
+ "sendDirectMessage": "encrypted direct message to targetId",
80
+ "registerPeerPublicKey": "trust peer signing/encryption keys with optional generation",
81
+ "trustPeerPublicKey": "register trusted keys without generation metadata",
82
+ "getPublicKey": "returns this node's public key bundle",
83
+ "unbanPeer": "clear manual or automatic peer ban",
84
+ "getBanInfo": "returns ban expiry and reason for peerId, or null"
85
+ },
86
+ "identity": {
87
+ "getPeerIdentityGeneration": "trusted identity generation for peerId",
88
+ "getPeerIdentityState": "public key + generation state for peerId",
89
+ "adoptDerivedIdentityKeyPair": "install credential-derived keys locally",
90
+ "deriveAndAdoptIdentity": "derive keys from username/password and adopt",
91
+ "broadcastIdentityRotation": "broadcast signed identity:rotate to peers",
92
+ "enrollAndBroadcastColdRecovery": "enroll cold recovery key and announce",
93
+ "revokeAndRotateDerivedIdentity": "compromise recovery rotation helper",
94
+ "rotateDerivedIdentityPassword": "password-change rotation helper",
95
+ "applyPeerIdentityRotation": "apply remote identity:rotate message",
96
+ "applyPeerColdRecoveryEnrollment": "apply remote cold-recovery enrollment"
61
97
  },
62
98
  "peerGroups": {
63
- "joinPeerGroup": "join scalable gossip group (multiplexed); options.fanout, maxActivePeers, bootstrapPeerIds",
99
+ "joinPeerGroup": "join scalable gossip group; options: fanout, maxActivePeers, maxHops (default 64), role, tiered, liveCap, tierMode, domainEvents, publisherId",
64
100
  "leavePeerGroup": "leave gossip group and scoped presence",
65
101
  "listPeerGroupMembers": "list presence in gossip:{groupId} scope",
66
- "publishToPeerGroup": "epidemic publish with bounded fanout; inner types: operation, record:snapshot, custom",
102
+ "publishToPeerGroup": "epidemic publish to live tier when tiered; inner types: operation, record:snapshot, domain:event, custom",
103
+ "publishPeerGroupBulk": "bulk-tier publish (publisher only); batched domain events and checkpoints",
104
+ "publishPeerGroupCheckpoint": "publish domain-event chain checkpoint to bulk tier",
67
105
  "publishRecordToPeerGroup": "publish normalized record snapshot into gossip group",
106
+ "getPeerGroupConfig": "returns joined group config including tier and domainEvents flags",
68
107
  "getPeerGroupStats": "{ joinedGroups, seenGossipCount, openConnectionCount, globalMaxOpenConnections }"
108
+ },
109
+ "cqrs": {
110
+ "DignityQueryReplica": "read-only materialized view from domain events; methods: start, stop, read, list, verifyChain, getViewStats",
111
+ "domainEvents": "operationToDomainEvent, verifyDomainEvent, verifyEventChain, DOMAIN_EVENT_SCHEMA_VERSION",
112
+ "tiers": "assignPeerGroupTier, DEFAULT_LIVE_CAP (5000), filterPeersByTier",
113
+ "bulkRelay": "electBulkRelays, DEFAULT_BULK_RELAY_COUNT"
69
114
  }
70
115
  },
71
116
  "events": {
72
117
  "change": "object create/update/delete/snapshot applied",
73
118
  "conflict": "local or remote version mismatch",
74
- "warning": "orphan-operation, peer-connect-failed, presence failures, content-hash-mismatch",
119
+ "warning": "orphan-operation, peer-connect-failed, presence failures, content-hash-mismatch, domain-event-rejected",
120
+ "domainevent": "signed domain event applied or emitted locally",
121
+ "chainbroken": "domain event hash chain verification failed",
122
+ "bulkrelaychanged": "bulk relay peer set changed for a tiered group",
123
+ "checkpointpublished": "publisher published domain-event checkpoint",
75
124
  "peerdiscovered": "peer joined discovery scope",
125
+ "peergroupjoined": "local node joined a PeerGroup",
126
+ "peergroupleft": "local node left a PeerGroup",
76
127
  "peerleft": "peer left or timed out",
77
- "message": "custom decrypted message received"
128
+ "message": "custom decrypted message received",
129
+ "securityerror": "signature, PoW, or decrypt failure on incoming message",
130
+ "messageignored": "message dropped (wrong target, banned peer, etc.)"
78
131
  },
79
132
  "recordShape": {
80
133
  "active": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dignity.js",
3
- "version": "0.7.1",
3
+ "version": "0.8.1",
4
4
  "description": "P2P object API for decentralized JavaScript applications",
5
5
  "homepage": "https://jose-compu.github.io/dignity.js/",
6
6
  "repository": {
@@ -41,7 +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
+ "test:peer-group": "jest tests/unit/peer-group-gossip.test.js tests/unit/peer-group-helpers.test.js tests/unit/domain-events.test.js tests/unit/peer-group-tiers.test.js tests/unit/query-replica.test.js tests/unit/view-consistency.test.js tests/integration/peer-group-gossip-e2e.test.js tests/integration/cqrs-tiered-peergroup.test.js --runInBand --coverage=false",
45
45
  "test:stress-peer-group": "RUN_STRESS_TESTS=1 jest tests/stress/peer-group-scale.test.js --runInBand --coverage=false",
46
46
  "stress:peer-group": "node scripts/stress-peer-group.js",
47
47
  "test:cloudflare-live": "RUN_CLOUDFLARE_LIVE_TESTS=1 jest tests/integration/cloudflare-signaling-live.test.js --runInBand",