dignity.js 0.4.0 → 0.5.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.
Files changed (38) hide show
  1. package/README.md +83 -2
  2. package/dist/dignity.cjs.js +542 -21
  3. package/dist/dignity.cjs.js.map +4 -4
  4. package/dist/dignity.esm.js +542 -21
  5. package/dist/dignity.esm.js.map +3 -3
  6. package/dist/dignity.min.js +18 -18
  7. package/docs/assets/dignity.esm.js +11205 -0
  8. package/docs/assets/favicon.svg +8 -0
  9. package/docs/chess/assets/chess-app.js +58022 -0
  10. package/docs/chess/assets/chess-app.js.map +7 -0
  11. package/docs/chess/assets/chess.css +584 -0
  12. package/docs/chess/favicon.ico +0 -0
  13. package/docs/chess/index.html +16 -0
  14. package/docs/chess/src/App.jsx +128 -0
  15. package/docs/chess/src/components/Board3D.jsx +364 -0
  16. package/docs/chess/src/components/GameView.jsx +847 -0
  17. package/docs/chess/src/components/JoinGate.jsx +68 -0
  18. package/docs/chess/src/components/LinkPanel.jsx +132 -0
  19. package/docs/chess/src/components/Lobby.jsx +154 -0
  20. package/docs/chess/src/components/MovePanel.jsx +123 -0
  21. package/docs/chess/src/lib/audio.js +50 -0
  22. package/docs/chess/src/lib/dignitySetup.js +42 -0
  23. package/docs/chess/src/lib/links.js +124 -0
  24. package/docs/chess/src/lib/localGames.js +160 -0
  25. package/docs/chess/src/lib/p2pDebug.js +192 -0
  26. package/docs/chess/src/main.jsx +5 -0
  27. package/docs/favicon.ico +0 -0
  28. package/docs/index.html +7 -3
  29. package/docs/openapi-like.json +35 -6
  30. package/examples/decentralized-chess-lite.js +52 -30
  31. package/package.json +12 -4
  32. package/src/core/dignity-p2p.js +388 -16
  33. package/src/index.js +6 -0
  34. package/src/network/peerjs-network.js +234 -0
  35. package/src/persistence/indexeddb-persistence.js +2 -0
  36. package/src/react/index.js +143 -1
  37. package/src/signaling/parse-peerjs-url.js +24 -0
  38. package/src/signaling/peerjs-signaling-provider.js +2 -8
@@ -0,0 +1,160 @@
1
+ const SESSIONS_KEY = 'dignity-chess-sessions';
2
+ const COLLECTION = 'chess-matches';
3
+ const DB_NAME = 'dignity';
4
+ const STORE_NAME = 'records';
5
+
6
+ export function loadLocalGameSessions() {
7
+ try {
8
+ const raw = localStorage.getItem(SESSIONS_KEY);
9
+ const parsed = raw ? JSON.parse(raw) : [];
10
+ return Array.isArray(parsed) ? parsed : [];
11
+ } catch (error) {
12
+ return [];
13
+ }
14
+ }
15
+
16
+ export function saveLocalGameSession(session) {
17
+ if (!session?.gameId || !session?.roomKey) {
18
+ return;
19
+ }
20
+
21
+ const sessions = loadLocalGameSessions();
22
+ const index = sessions.findIndex((entry) => entry.gameId === session.gameId);
23
+ const next = {
24
+ ...sessions[index],
25
+ ...session,
26
+ updatedAt: session.updatedAt || Date.now()
27
+ };
28
+
29
+ if (index >= 0) {
30
+ sessions[index] = next;
31
+ } else {
32
+ sessions.unshift(next);
33
+ }
34
+
35
+ const trimmed = sessions
36
+ .sort((a, b) => (b.updatedAt || 0) - (a.updatedAt || 0))
37
+ .slice(0, 40);
38
+
39
+ localStorage.setItem(SESSIONS_KEY, JSON.stringify(trimmed));
40
+ }
41
+
42
+ function loadChessRecordsFromIndexedDB() {
43
+ if (typeof indexedDB === 'undefined') {
44
+ return Promise.resolve([]);
45
+ }
46
+
47
+ return new Promise((resolve) => {
48
+ const request = indexedDB.open(DB_NAME, 1);
49
+
50
+ request.onerror = () => resolve([]);
51
+ request.onsuccess = () => {
52
+ const db = request.result;
53
+ if (!db.objectStoreNames.contains(STORE_NAME)) {
54
+ db.close();
55
+ resolve([]);
56
+ return;
57
+ }
58
+
59
+ const transaction = db.transaction(STORE_NAME, 'readonly');
60
+ const store = transaction.objectStore(STORE_NAME);
61
+ const getAll = store.getAll();
62
+
63
+ getAll.onsuccess = () => {
64
+ const records = (getAll.result || []).filter(
65
+ (record) => record.collection === COLLECTION && !record.deletedAt
66
+ );
67
+ resolve(records);
68
+ db.close();
69
+ };
70
+
71
+ getAll.onerror = () => {
72
+ resolve([]);
73
+ db.close();
74
+ };
75
+ };
76
+ });
77
+ }
78
+
79
+ function mergeSessionWithRecord(session, record) {
80
+ if (!record?.data) {
81
+ return session;
82
+ }
83
+
84
+ return {
85
+ ...session,
86
+ status: record.data.status || session.status || 'waiting',
87
+ winner: record.data.winner ?? session.winner ?? null,
88
+ moveCount: Array.isArray(record.data.moveHistory) ? record.data.moveHistory.length : session.moveCount || 0,
89
+ whitePlayerId: record.data.whitePlayerId || session.whitePlayerId || null,
90
+ blackPlayerId: record.data.blackPlayerId || session.blackPlayerId || null,
91
+ updatedAt: record.updatedAt || session.updatedAt || Date.now()
92
+ };
93
+ }
94
+
95
+ export async function listLocalGames() {
96
+ const sessions = loadLocalGameSessions();
97
+ const records = await loadChessRecordsFromIndexedDB();
98
+ const recordById = new Map(records.map((record) => [record.id, record]));
99
+
100
+ const merged = sessions.map((session) => mergeSessionWithRecord(session, recordById.get(session.gameId)));
101
+
102
+ const active = merged.filter((game) => game.status === 'waiting' || game.status === 'playing');
103
+ const finished = merged.filter((game) => game.status === 'finished');
104
+
105
+ return { active, finished };
106
+ }
107
+
108
+ export function sessionResumeHash(session) {
109
+ const role = session.role === 'host' ? 'host' : 'resume';
110
+ const params = new URLSearchParams({
111
+ game: session.gameId,
112
+ room: session.roomKey,
113
+ role,
114
+ resume: session.resumeToken || ''
115
+ });
116
+
117
+ if (session.hostPeer) {
118
+ params.set('host', session.hostPeer);
119
+ }
120
+ if (session.role === 'host' && session.joinToken) {
121
+ params.set('join', session.joinToken);
122
+ }
123
+ if (session.role === 'host' && session.watchToken) {
124
+ params.set('watch', session.watchToken);
125
+ }
126
+
127
+ return params.toString();
128
+ }
129
+
130
+ export function formatGameStatus(game) {
131
+ if (game.status === 'waiting') {
132
+ return 'Waiting for opponent';
133
+ }
134
+ if (game.status === 'playing') {
135
+ return `${game.moveCount || 0} move(s) · in progress`;
136
+ }
137
+ if (game.winner === 'draw') {
138
+ return 'Draw';
139
+ }
140
+ if (game.winner === 'w') {
141
+ return 'White wins';
142
+ }
143
+ if (game.winner === 'b') {
144
+ return 'Black wins';
145
+ }
146
+ return 'Finished';
147
+ }
148
+
149
+ export function formatRoleLabel(game) {
150
+ if (game.role === 'host') {
151
+ return 'You · White (host)';
152
+ }
153
+ if (game.role === 'join') {
154
+ return 'You · Black';
155
+ }
156
+ if (game.role === 'watch') {
157
+ return 'Spectator';
158
+ }
159
+ return 'Resume';
160
+ }
@@ -0,0 +1,192 @@
1
+ const PREFIX = '[chess-p2p]';
2
+
3
+ function ts() {
4
+ return new Date().toISOString().slice(11, 23);
5
+ }
6
+
7
+ export function p2pLog(label, detail) {
8
+ if (detail === undefined) {
9
+ console.log(`${PREFIX} ${ts()} ${label}`);
10
+ return;
11
+ }
12
+ console.log(`${PREFIX} ${ts()} ${label}`, detail);
13
+ }
14
+
15
+ export function p2pWarn(label, detail) {
16
+ if (detail === undefined) {
17
+ console.warn(`${PREFIX} ${ts()} ${label}`);
18
+ return;
19
+ }
20
+ console.warn(`${PREFIX} ${ts()} ${label}`, detail);
21
+ }
22
+
23
+ export function p2pError(label, detail) {
24
+ if (detail === undefined) {
25
+ console.error(`${PREFIX} ${ts()} ${label}`);
26
+ return;
27
+ }
28
+ console.error(`${PREFIX} ${ts()} ${label}`, detail);
29
+ }
30
+
31
+ export function connectionSnapshot(node) {
32
+ const adapter = node?.networkAdapter;
33
+ if (!adapter) {
34
+ return { adapter: 'none' };
35
+ }
36
+
37
+ const openIds = [];
38
+ if (adapter.connections instanceof Map) {
39
+ for (const [peerId, conn] of adapter.connections.entries()) {
40
+ if (conn?.open) {
41
+ openIds.push(peerId);
42
+ }
43
+ }
44
+ }
45
+
46
+ return {
47
+ signalingUrl: adapter.url || null,
48
+ localPeerId: adapter.nodeId || node?.nodeId || null,
49
+ openConnectionCount: node?.getConnectionStats?.()?.openCount ?? adapter.getOpenConnectionCount?.() ?? openIds.length,
50
+ openPeerIds: node?.getConnectionStats?.()?.peerIds ?? openIds,
51
+ peerJsReady: Boolean(adapter.peer)
52
+ };
53
+ }
54
+
55
+ export function attachNodeDebugListeners(node, context = {}) {
56
+ if (!node || node.__chessDebugAttached) {
57
+ return () => undefined;
58
+ }
59
+
60
+ node.__chessDebugAttached = true;
61
+ const role = context.role || '?';
62
+
63
+ p2pLog(`debug attached (${role})`, {
64
+ nodeId: node.nodeId,
65
+ scope: context.scope,
66
+ gameId: context.gameId
67
+ });
68
+
69
+ const handlers = {
70
+ warning: (event) => p2pWarn('node warning', event),
71
+ securityerror: (event) => p2pError('security error', {
72
+ senderId: event?.senderId,
73
+ code: event?.error?.code,
74
+ message: event?.error?.message
75
+ }),
76
+ messageignored: (event) => p2pWarn('message ignored', event),
77
+ peerdiscovered: (event) => p2pLog('peer discovered', event),
78
+ peerleft: (event) => p2pLog('peer left', event),
79
+ peerbanned: (event) => p2pError('peer banned', event),
80
+ conflict: (event) => p2pWarn('sync conflict', event),
81
+ change: (event) => {
82
+ if (event?.collection === context.collection) {
83
+ p2pLog('game record changed', {
84
+ id: event.id,
85
+ kind: event.kind,
86
+ version: event.version
87
+ });
88
+ }
89
+ },
90
+ message: (event) => {
91
+ if (event?.type === 'claim-seat' || event?.type === 'operation') {
92
+ p2pLog('incoming message', {
93
+ type: event.type,
94
+ senderId: event.senderId,
95
+ payload: event.payload
96
+ });
97
+ }
98
+ }
99
+ };
100
+
101
+ for (const [eventName, handler] of Object.entries(handlers)) {
102
+ node.on(eventName, handler);
103
+ }
104
+
105
+ if (node.networkAdapter?.peer) {
106
+ const peer = node.networkAdapter.peer;
107
+ peer.on('error', (err) => {
108
+ p2pError('PeerJS error', {
109
+ type: err?.type,
110
+ message: err?.message || String(err)
111
+ });
112
+ });
113
+ peer.on('disconnected', () => p2pWarn('PeerJS disconnected from signaling server'));
114
+ peer.on('close', () => p2pWarn('PeerJS peer closed'));
115
+ peer.on('connection', (conn) => {
116
+ p2pLog('PeerJS inbound connection', { from: conn.peer });
117
+ conn.on('open', () => p2pLog('PeerJS data channel open (inbound)', { peer: conn.peer }));
118
+ conn.on('close', () => p2pWarn('PeerJS data channel closed (inbound)', { peer: conn.peer }));
119
+ conn.on('error', (err) => p2pError('PeerJS data channel error (inbound)', {
120
+ peer: conn.peer,
121
+ message: err?.message || String(err)
122
+ }));
123
+ });
124
+ }
125
+
126
+ return () => {
127
+ for (const [eventName, handler] of Object.entries(handlers)) {
128
+ node.off(eventName, handler);
129
+ }
130
+ delete node.__chessDebugAttached;
131
+ };
132
+ }
133
+
134
+ export function dumpJoinState({
135
+ route,
136
+ node,
137
+ nodeId,
138
+ scope,
139
+ status,
140
+ error,
141
+ joined,
142
+ roomConnected,
143
+ connectionCount,
144
+ remoteHostPeer,
145
+ peers,
146
+ game,
147
+ myColor
148
+ }) {
149
+ const snap = connectionSnapshot(node);
150
+ const payload = {
151
+ role: route.role,
152
+ localNodeId: nodeId,
153
+ nodeRunningId: node?.nodeId,
154
+ hostPeerTarget: remoteHostPeer,
155
+ routeHostParam: route.hostPeer,
156
+ gameId: route.gameId,
157
+ scope,
158
+ dignityStatus: status,
159
+ dignityError: error?.message || null,
160
+ joinedDiscovery: joined,
161
+ roomConnected,
162
+ connectionCount,
163
+ peerJs: snap,
164
+ peers: peers.map((p) => ({
165
+ peerId: p.peerId,
166
+ role: p.metadata?.role,
167
+ nickname: p.metadata?.nickname,
168
+ joinToken: p.metadata?.joinToken ? `${p.metadata.joinToken.slice(0, 6)}…` : null
169
+ })),
170
+ game: game ? {
171
+ status: game.data?.status,
172
+ whitePlayerId: game.data?.whitePlayerId,
173
+ blackPlayerId: game.data?.blackPlayerId,
174
+ joinTokenUsed: game.data?.joinTokenUsed,
175
+ joinTokenMatch: route.joinToken === game.data?.joinToken,
176
+ version: game.version
177
+ } : null,
178
+ myColor,
179
+ webrtc: typeof RTCPeerConnection !== 'undefined' ? 'available' : 'missing'
180
+ };
181
+
182
+ p2pLog('STATE SNAPSHOT', payload);
183
+ return payload;
184
+ }
185
+
186
+ export function installGlobalDebug(dumpFn) {
187
+ if (typeof window === 'undefined') {
188
+ return;
189
+ }
190
+ window.__chessP2pDump = dumpFn;
191
+ p2pLog('manual dump: run __chessP2pDump() in console');
192
+ }
@@ -0,0 +1,5 @@
1
+ import React from 'react';
2
+ import { createRoot } from 'react-dom/client';
3
+ import App from './App.jsx';
4
+
5
+ createRoot(document.getElementById('root')).render(<App />);
Binary file
package/docs/index.html CHANGED
@@ -3,8 +3,10 @@
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.4.0 — REST-like P2P object API for decentralized JavaScript applications." />
6
+ <meta name="description" content="dignity.js v0.5.1 — REST-like P2P object API for decentralized JavaScript applications." />
7
7
  <title>dignity.js · Documentation</title>
8
+ <link rel="icon" href="./assets/favicon.svg" type="image/svg+xml" />
9
+ <link rel="icon" href="./favicon.ico" sizes="32x32" />
8
10
  <link rel="stylesheet" href="./assets/highlight/github.min.css" media="(prefers-color-scheme: light)" />
9
11
  <link rel="stylesheet" href="./assets/highlight/github-dark.min.css" media="(prefers-color-scheme: dark)" />
10
12
  <link rel="stylesheet" href="./assets/styles.css" />
@@ -14,11 +16,12 @@
14
16
  <a class="site-header__brand" href="#overview">
15
17
  <img src="./assets/dignity-logo.svg" alt="" width="344" height="80" />
16
18
  <!-- <span>dignity.js</span> -->
17
- <span class="site-header__version">v0.4.0</span>
19
+ <span class="site-header__version">v0.5.1</span>
18
20
  </a>
19
21
  <div class="site-header__links">
20
22
  <a href="https://www.npmjs.com/package/dignity.js" target="_blank" rel="noopener noreferrer">npm</a>
21
23
  <a href="https://github.com/jose-compu/dignity.js" target="_blank" rel="noopener noreferrer">GitHub</a>
24
+ <a href="./chess/index.html">3D Chess</a>
22
25
  <a href="./openapi-like.json">API JSON</a>
23
26
  </div>
24
27
  <button class="menu-toggle" type="button" aria-label="Toggle navigation">Menu</button>
@@ -46,6 +49,7 @@
46
49
  <a href="#persistence">IndexedDB persistence</a>
47
50
  <a href="#react">React hooks</a>
48
51
  <a href="#signaling">Signaling</a>
52
+ <a href="./chess/index.html">3D Chess demo</a>
49
53
  </nav>
50
54
  </div>
51
55
  <div class="sidebar__group">
@@ -621,7 +625,7 @@ npm run example:chess</code></pre>
621
625
 
622
626
  <footer class="site-footer">
623
627
  <p>
624
- dignity.js v0.4.0 ·
628
+ dignity.js v0.5.1 ·
625
629
  <a href="https://github.com/jose-compu/dignity.js/blob/main/LICENSE">Apache 2.0</a> ·
626
630
  <a href="https://github.com/jose-compu/dignity.js">GitHub</a> ·
627
631
  <a href="https://www.npmjs.com/package/dignity.js">npm</a>
@@ -1,28 +1,44 @@
1
1
  {
2
2
  "name": "dignity.js",
3
- "version": "0.4.0",
3
+ "version": "0.5.1",
4
4
  "description": "REST-like object API over peer-to-peer replication",
5
5
  "resources": {
6
6
  "collections/{collection}/{id}": {
7
7
  "create": {
8
8
  "method": "create(collection, data, options)",
9
- "owner": "actor that creates the object"
9
+ "owner": "actor that creates the object",
10
+ "options": {
11
+ "id": "optional stable id",
12
+ "collaborators": "optional peer id list",
13
+ "broadcastScope": "scoped broadcast password namespace",
14
+ "connectToPeers": "optional; defaults to collaborators on PeerJS mesh"
15
+ }
10
16
  },
11
17
  "read": {
12
18
  "method": "read(collection, id)"
13
19
  },
14
20
  "update": {
15
21
  "method": "update(collection, id, patch, options)",
16
- "authorization": "owner-only",
22
+ "authorization": "owner or collaborator",
17
23
  "options": {
18
24
  "expectedVersion": "optional number; throws VERSION_CONFLICT when mismatched",
19
- "broadcastScope": "optional scoped broadcast password namespace"
25
+ "broadcastScope": "optional scoped broadcast password namespace",
26
+ "collaborators": "owner may replace collaborator list",
27
+ "connectToPeers": "optional; defaults to owner + collaborators"
20
28
  }
21
29
  },
22
30
  "updateWithRetry": {
23
31
  "method": "updateWithRetry(collection, id, patchFn, options)",
24
32
  "description": "read-modify-write helper with automatic retry on version conflicts"
25
33
  },
34
+ "pushRecordSnapshot": {
35
+ "method": "pushRecordSnapshot(collection, id, options)",
36
+ "description": "broadcast full record for late joiners who missed the initial create"
37
+ },
38
+ "getRecordPeerIds": {
39
+ "method": "getRecordPeerIds(collection, id, options)",
40
+ "description": "returns owner + collaborator peer ids for connectToPeers"
41
+ },
26
42
  "delete": {
27
43
  "method": "remove(collection, id)",
28
44
  "authorization": "owner-only"
@@ -32,11 +48,19 @@
32
48
  "list": {
33
49
  "method": "list(collection, options)"
34
50
  }
51
+ },
52
+ "peers": {
53
+ "connectToPeer": "open PeerJS data channel to peer id",
54
+ "getConnectionStats": "{ openCount, peerIds }",
55
+ "ensureConnectedToPeers": "connect to many peers before broadcast",
56
+ "joinDiscovery": "scoped presence; options.bootstrapPeerIds connects before announce",
57
+ "broadcastMessage": "custom app messages; options.connectToPeers"
35
58
  }
36
59
  },
37
60
  "events": {
38
- "change": "object create/update/delete applied",
61
+ "change": "object create/update/delete/snapshot applied",
39
62
  "conflict": "local or remote version mismatch",
63
+ "warning": "orphan-operation, peer-connect-failed, presence failures",
40
64
  "peerdiscovered": "peer joined discovery scope",
41
65
  "peerleft": "peer left or timed out",
42
66
  "message": "custom decrypted message received"
@@ -56,7 +80,12 @@
56
80
  "hooks": [
57
81
  "useDignity",
58
82
  "useCollection",
59
- "usePeers"
83
+ "useObject",
84
+ "usePeers",
85
+ "useDiscovery",
86
+ "useConnectionStats",
87
+ "useRoom",
88
+ "useMessages"
60
89
  ]
61
90
  },
62
91
  "signaling": {
@@ -11,6 +11,7 @@ function initialBoard() {
11
11
 
12
12
  async function runDemo() {
13
13
  const hub = new InMemoryNetworkHub();
14
+ const scope = 'room:chess-lite';
14
15
 
15
16
  const host = new DignityP2P({
16
17
  nodeId: 'host',
@@ -21,8 +22,27 @@ async function runDemo() {
21
22
  }
22
23
  });
23
24
 
24
- const observer = new DignityP2P({
25
- nodeId: 'observer',
25
+ await host.start();
26
+ await host.joinDiscovery(scope, {
27
+ metadata: { nickname: 'host', role: 'owner' },
28
+ heartbeatIntervalMs: 100000,
29
+ ttlMs: 30000
30
+ });
31
+
32
+ // Host creates before joiner is online — joiner never receives the create op.
33
+ await host.create(
34
+ 'matches',
35
+ {
36
+ type: 'chess-lite',
37
+ board: initialBoard(),
38
+ moveHistory: [],
39
+ status: 'waiting'
40
+ },
41
+ { id: 'match-1', broadcastScope: scope }
42
+ );
43
+
44
+ const joiner = new DignityP2P({
45
+ nodeId: 'joiner',
26
46
  networkAdapter: new InMemoryNetworkAdapter(hub),
27
47
  security: {
28
48
  appPassword: 'demo-shared-password',
@@ -30,38 +50,40 @@ async function runDemo() {
30
50
  }
31
51
  });
32
52
 
33
- await host.start();
34
- await observer.start();
35
- await host.joinDiscovery('room:chess-lite', {
36
- metadata: { nickname: 'host', role: 'owner' },
37
- heartbeatIntervalMs: 100000,
38
- ttlMs: 30000
39
- });
40
- await observer.joinDiscovery('room:chess-lite', {
41
- metadata: { nickname: 'observer', role: 'viewer' },
53
+ await joiner.start();
54
+ await joiner.joinDiscovery(scope, {
55
+ metadata: { nickname: 'joiner', role: 'player' },
56
+ bootstrapPeerIds: ['host'],
42
57
  heartbeatIntervalMs: 100000,
43
58
  ttlMs: 30000
44
59
  });
45
60
 
46
- console.log(
47
- '\nPeers visible from host in room:chess-lite:',
48
- host.listPeers('room:chess-lite', { includeSelf: false }).map((peer) => peer.peerId)
49
- );
61
+ console.log('\nJoiner before snapshot:', joiner.read('matches', 'match-1'));
50
62
 
51
- await host.create(
63
+ const warnings = [];
64
+ joiner.on('warning', (event) => warnings.push(event));
65
+
66
+ await host.update(
52
67
  'matches',
68
+ 'match-1',
69
+ { status: 'playing', blackPlayerId: 'joiner' },
53
70
  {
54
- type: 'chess-lite',
55
- board: initialBoard(),
56
- moveHistory: []
57
- },
58
- { id: 'match-1', broadcastScope: 'room:chess-lite' }
71
+ broadcastScope: scope,
72
+ collaborators: ['host', 'joiner']
73
+ }
59
74
  );
60
75
 
76
+ console.log('\nJoiner after update (still missing create):', joiner.read('matches', 'match-1'));
77
+ console.log('Orphan warnings:', warnings.filter((event) => event.type === 'orphan-operation').length);
78
+
79
+ await host.pushRecordSnapshot('matches', 'match-1', {
80
+ broadcastScope: scope,
81
+ connectToPeers: ['joiner']
82
+ });
83
+
61
84
  const scriptedMoves = [
62
85
  { from: 'a2', to: 'a4', piece: 'whitePawnA' },
63
- { from: 'a7', to: 'a5', piece: 'blackPawnA' },
64
- { from: 'e1', to: 'e2', piece: 'whiteKing' }
86
+ { from: 'a7', to: 'a5', piece: 'blackPawnA' }
65
87
  ];
66
88
 
67
89
  for (const move of scriptedMoves) {
@@ -77,23 +99,23 @@ async function runDemo() {
77
99
  moveHistory,
78
100
  lastMove: move
79
101
  },
80
- { broadcastScope: 'room:chess-lite' }
102
+ { broadcastScope: scope }
81
103
  );
82
104
  }
83
105
 
84
106
  const hostState = host.read('matches', 'match-1');
85
- const observerState = observer.read('matches', 'match-1');
107
+ const joinerState = joiner.read('matches', 'match-1');
86
108
 
87
109
  console.log('\nHost state:');
88
110
  console.log(JSON.stringify(hostState.data, null, 2));
89
111
 
90
- console.log('\nObserver replicated state:');
91
- console.log(JSON.stringify(observerState.data, null, 2));
112
+ console.log('\nJoiner replicated state (after snapshot + moves):');
113
+ console.log(JSON.stringify(joinerState.data, null, 2));
92
114
 
93
- await host.leaveDiscovery('room:chess-lite');
94
- await observer.leaveDiscovery('room:chess-lite');
115
+ await host.leaveDiscovery(scope);
116
+ await joiner.leaveDiscovery(scope);
95
117
  await host.stop();
96
- await observer.stop();
118
+ await joiner.stop();
97
119
  }
98
120
 
99
121
  runDemo().catch((error) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dignity.js",
3
- "version": "0.4.0",
3
+ "version": "0.5.1",
4
4
  "description": "P2P object API for decentralized JavaScript applications",
5
5
  "main": "dist/dignity.cjs.js",
6
6
  "module": "dist/dignity.esm.js",
@@ -31,8 +31,13 @@
31
31
  "test:cloudflare-live": "RUN_CLOUDFLARE_LIVE_TESTS=1 jest tests/integration/cloudflare-signaling-live.test.js --runInBand",
32
32
  "test:pow-calibrate": "jest tests/unit/sloth-vdf-timing.test.js --runInBand",
33
33
  "build": "node scripts/build.js",
34
- "docs:serve": "npx http-server docs -p 4173 -o",
35
- "docs:check": "node -e \"const fs=require('fs');['docs/index.html','docs/assets/highlight/highlight.min.js','docs/assets/highlight/github.min.css','docs/assets/highlight/github-dark.min.css'].forEach(p=>fs.accessSync(p));\"",
34
+ "build:chess": "node scripts/build-chess-demo.js",
35
+ "docs:favicon": "node scripts/generate-favicon.js",
36
+ "docs:build": "npm run build:chess",
37
+ "docs:dev": "node scripts/serve-docs.js",
38
+ "docs:serve": "node scripts/serve-docs.js",
39
+ "docs:stop": "node scripts/stop-docs.js",
40
+ "docs:check": "node -e \"const fs=require('fs');['docs/index.html','docs/favicon.ico','docs/chess/index.html','docs/chess/favicon.ico','docs/assets/favicon.svg','docs/chess/assets/chess-app.js','docs/assets/highlight/highlight.min.js','docs/assets/highlight/github.min.css','docs/assets/highlight/github-dark.min.css'].forEach(p=>fs.accessSync(p));\"",
36
41
  "example:tictactoe": "node examples/decentralized-tictactoe.js",
37
42
  "example:chess": "node examples/decentralized-chess-lite.js",
38
43
  "prepublishOnly": "npm test && npm run build"
@@ -55,12 +60,15 @@
55
60
  },
56
61
  "devDependencies": {
57
62
  "@testing-library/react": "^16.3.0",
63
+ "chess.js": "^1.4.0",
58
64
  "esbuild": "^0.28.0",
59
65
  "fake-indexeddb": "^6.0.0",
66
+ "http-server": "^14.1.1",
60
67
  "jest": "^29.7.0",
61
68
  "jest-environment-jsdom": "^29.7.0",
62
69
  "react": "^19.1.0",
63
- "react-dom": "^19.1.0"
70
+ "react-dom": "^19.1.0",
71
+ "three": "^0.184.0"
64
72
  },
65
73
  "dependencies": {
66
74
  "peerjs": "^1.5.5",