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,234 @@
1
+ const { DEFAULT_CLOUDFLARE_SIGNALING_URLS } = require('../signaling/default-signaling-config');
2
+ const parsePeerJsServerUrl = require('../signaling/parse-peerjs-url');
3
+
4
+ function resolvePeerImplementation(PeerImpl) {
5
+ if (PeerImpl) {
6
+ return PeerImpl;
7
+ }
8
+
9
+ try {
10
+ const peerjs = require('peerjs');
11
+ return peerjs.Peer || peerjs;
12
+ } catch (error) {
13
+ return null;
14
+ }
15
+ }
16
+
17
+ class PeerJSNetworkAdapter {
18
+ constructor({
19
+ url,
20
+ urls,
21
+ PeerImpl,
22
+ connectTimeoutMs = 12000
23
+ } = {}) {
24
+ this.urls = urls || (url ? [url] : [...DEFAULT_CLOUDFLARE_SIGNALING_URLS]);
25
+ this.url = this.urls[0];
26
+ this.PeerImpl = resolvePeerImplementation(PeerImpl);
27
+ this.connectTimeoutMs = connectTimeoutMs;
28
+ this.nodeId = null;
29
+ this.peer = null;
30
+ this.connections = new Map();
31
+ this.pendingConnections = new Map();
32
+ this.messageHandlers = new Set();
33
+ }
34
+
35
+ async start(nodeId) {
36
+ if (!nodeId) {
37
+ throw new Error('PeerJSNetworkAdapter requires nodeId on start');
38
+ }
39
+
40
+ if (!this.PeerImpl) {
41
+ throw new Error('PeerJS implementation is not available');
42
+ }
43
+
44
+ if (this.peer) {
45
+ await this.stop();
46
+ }
47
+
48
+ let lastError;
49
+ for (const candidateUrl of this.urls) {
50
+ try {
51
+ await this.startWithUrl(nodeId, candidateUrl);
52
+ this.url = candidateUrl;
53
+ return;
54
+ } catch (error) {
55
+ lastError = error;
56
+ }
57
+ }
58
+
59
+ throw lastError || new Error('Unable to connect PeerJS network adapter');
60
+ }
61
+
62
+ async startWithUrl(nodeId, url) {
63
+ this.nodeId = nodeId;
64
+ const server = parsePeerJsServerUrl(url);
65
+
66
+ await new Promise((resolve, reject) => {
67
+ const peer = new this.PeerImpl(nodeId, {
68
+ host: server.host,
69
+ port: server.port,
70
+ path: server.path,
71
+ secure: server.secure,
72
+ key: server.key
73
+ });
74
+
75
+ const timeout = setTimeout(() => {
76
+ peer.destroy?.();
77
+ reject(new Error(`Unable to connect PeerJS network adapter to ${url}`));
78
+ }, this.connectTimeoutMs);
79
+
80
+ peer.on('open', () => {
81
+ clearTimeout(timeout);
82
+ this.peer = peer;
83
+ resolve();
84
+ });
85
+
86
+ peer.on('connection', (connection) => {
87
+ this.attachConnectionHandlers(connection);
88
+ });
89
+
90
+ peer.on('error', (error) => {
91
+ clearTimeout(timeout);
92
+ peer.destroy?.();
93
+ reject(error || new Error(`Unable to connect PeerJS network adapter to ${url}`));
94
+ });
95
+ });
96
+ }
97
+
98
+ attachConnectionHandlers(connection) {
99
+ const remoteId = connection.peer;
100
+ if (!remoteId) {
101
+ return;
102
+ }
103
+
104
+ this.connections.set(remoteId, connection);
105
+
106
+ connection.on('data', (payload) => {
107
+ const deliveries = [];
108
+ for (const handler of this.messageHandlers) {
109
+ deliveries.push(handler(payload));
110
+ }
111
+ return Promise.all(deliveries);
112
+ });
113
+
114
+ connection.on('close', () => {
115
+ this.connections.delete(remoteId);
116
+ });
117
+ }
118
+
119
+ async connectToPeer(remotePeerId) {
120
+ if (!remotePeerId || remotePeerId === this.nodeId) {
121
+ return null;
122
+ }
123
+
124
+ const existing = this.connections.get(remotePeerId);
125
+ if (existing && existing.open) {
126
+ return existing;
127
+ }
128
+
129
+ if (this.pendingConnections.has(remotePeerId)) {
130
+ return this.pendingConnections.get(remotePeerId);
131
+ }
132
+
133
+ if (!this.peer) {
134
+ throw new Error('PeerJS network adapter has not been started');
135
+ }
136
+
137
+ const pending = new Promise((resolve, reject) => {
138
+ const connection = this.peer.connect(remotePeerId, {
139
+ reliable: true,
140
+ serialization: 'json'
141
+ });
142
+
143
+ const timeout = setTimeout(() => {
144
+ reject(new Error(`Unable to connect to peer ${remotePeerId}`));
145
+ }, this.connectTimeoutMs);
146
+
147
+ connection.on('open', () => {
148
+ clearTimeout(timeout);
149
+ this.attachConnectionHandlers(connection);
150
+ resolve(connection);
151
+ });
152
+
153
+ connection.on('error', () => {
154
+ clearTimeout(timeout);
155
+ reject(new Error(`Unable to connect to peer ${remotePeerId}`));
156
+ });
157
+ }).finally(() => {
158
+ this.pendingConnections.delete(remotePeerId);
159
+ });
160
+
161
+ this.pendingConnections.set(remotePeerId, pending);
162
+ return pending;
163
+ }
164
+
165
+ onMessage(handler) {
166
+ this.messageHandlers.add(handler);
167
+ }
168
+
169
+ offMessage(handler) {
170
+ this.messageHandlers.delete(handler);
171
+ }
172
+
173
+ async broadcast(message) {
174
+ if (!this.peer) {
175
+ throw new Error('PeerJS network adapter has not been started');
176
+ }
177
+
178
+ const deliveries = [];
179
+ for (const connection of this.connections.values()) {
180
+ if (connection.open) {
181
+ deliveries.push(connection.send(message));
182
+ }
183
+ }
184
+
185
+ await Promise.all(deliveries);
186
+ }
187
+
188
+ getOpenConnectionCount() {
189
+ return this.listOpenPeerIds().length;
190
+ }
191
+
192
+ listOpenPeerIds() {
193
+ const ids = [];
194
+ for (const [peerId, connection] of this.connections.entries()) {
195
+ if (connection.open) {
196
+ ids.push(peerId);
197
+ }
198
+ }
199
+ return ids;
200
+ }
201
+
202
+ isConnectedTo(remotePeerId) {
203
+ const connection = this.connections.get(remotePeerId);
204
+ return Boolean(connection && connection.open);
205
+ }
206
+
207
+ async stop() {
208
+ for (const connection of this.connections.values()) {
209
+ if (typeof connection.close === 'function') {
210
+ connection.close();
211
+ }
212
+ }
213
+
214
+ this.connections.clear();
215
+ this.pendingConnections.clear();
216
+
217
+ if (this.peer && typeof this.peer.destroy === 'function') {
218
+ this.peer.destroy();
219
+ }
220
+
221
+ this.peer = null;
222
+ this.nodeId = null;
223
+ }
224
+ }
225
+
226
+ function createPeerJSNetworkAdapter(options = {}) {
227
+ return new PeerJSNetworkAdapter(options);
228
+ }
229
+
230
+ module.exports = {
231
+ PeerJSNetworkAdapter,
232
+ createPeerJSNetworkAdapter,
233
+ parsePeerJsServerUrl
234
+ };
@@ -71,6 +71,7 @@ class IndexedDBPersistence {
71
71
  collection,
72
72
  id,
73
73
  ownerId: record.ownerId,
74
+ collaboratorIds: Array.isArray(record.collaboratorIds) ? [...record.collaboratorIds] : [],
74
75
  data: { ...record.data },
75
76
  createdAt: record.createdAt,
76
77
  updatedAt: record.updatedAt,
@@ -140,6 +141,7 @@ class IndexedDBPersistence {
140
141
  this.node.restoreRecord(stored.collection, {
141
142
  id: stored.id,
142
143
  ownerId: stored.ownerId,
144
+ collaboratorIds: stored.collaboratorIds,
143
145
  data: stored.data,
144
146
  createdAt: stored.createdAt,
145
147
  updatedAt: stored.updatedAt,
@@ -107,8 +107,150 @@ function usePeers(node, scope = 'main', options = {}) {
107
107
  return peers;
108
108
  }
109
109
 
110
+ function useObject(node, collectionName, objectId) {
111
+ const [record, setRecord] = useState(null);
112
+
113
+ const refresh = useCallback(() => {
114
+ if (!node || !collectionName || !objectId) {
115
+ setRecord(null);
116
+ return;
117
+ }
118
+
119
+ setRecord(node.read(collectionName, objectId));
120
+ }, [node, collectionName, objectId]);
121
+
122
+ useEffect(() => {
123
+ refresh();
124
+
125
+ if (!node) {
126
+ return undefined;
127
+ }
128
+
129
+ const handleChange = (event) => {
130
+ if (
131
+ event &&
132
+ event.collection === collectionName &&
133
+ event.id === objectId
134
+ ) {
135
+ refresh();
136
+ }
137
+ };
138
+
139
+ node.on('change', handleChange);
140
+ return () => node.off('change', handleChange);
141
+ }, [node, collectionName, objectId, refresh]);
142
+
143
+ return record;
144
+ }
145
+
146
+ function useDiscovery(node, scope = 'main', options = null) {
147
+ const [joined, setJoined] = useState(false);
148
+ const [error, setError] = useState(null);
149
+
150
+ useEffect(() => {
151
+ if (!node || !scope || !options) {
152
+ setJoined(false);
153
+ setError(null);
154
+ return undefined;
155
+ }
156
+
157
+ let cancelled = false;
158
+
159
+ node.joinDiscovery(scope, options)
160
+ .then(() => {
161
+ if (!cancelled) {
162
+ setJoined(true);
163
+ setError(null);
164
+ }
165
+ })
166
+ .catch((joinError) => {
167
+ if (!cancelled) {
168
+ setJoined(false);
169
+ setError(joinError);
170
+ }
171
+ });
172
+
173
+ return () => {
174
+ cancelled = true;
175
+ node.leaveDiscovery(scope).catch(() => undefined);
176
+ setJoined(false);
177
+ };
178
+ }, [node, scope, options]);
179
+
180
+ return { joined, error };
181
+ }
182
+
183
+ function useConnectionStats(node, pollIntervalMs = 2000) {
184
+ const [stats, setStats] = useState({ openCount: 0, peerIds: [] });
185
+
186
+ const refresh = useCallback(() => {
187
+ if (!node || typeof node.getConnectionStats !== 'function') {
188
+ setStats({ openCount: 0, peerIds: [] });
189
+ return;
190
+ }
191
+
192
+ setStats(node.getConnectionStats());
193
+ }, [node]);
194
+
195
+ useEffect(() => {
196
+ refresh();
197
+
198
+ if (!node || !pollIntervalMs) {
199
+ return undefined;
200
+ }
201
+
202
+ const timer = setInterval(refresh, pollIntervalMs);
203
+ return () => clearInterval(timer);
204
+ }, [node, pollIntervalMs, refresh]);
205
+
206
+ return stats;
207
+ }
208
+
209
+ function useRoom(node, scope = 'main', options = null) {
210
+ const peersOptions = options && options.peersOptions ? options.peersOptions : {};
211
+ const { joined, error } = useDiscovery(node, scope, options);
212
+ const peers = usePeers(node, scope, peersOptions);
213
+ const connectionStats = useConnectionStats(node, options?.connectionPollMs ?? 2000);
214
+
215
+ return {
216
+ joined,
217
+ error,
218
+ peers,
219
+ connectionStats
220
+ };
221
+ }
222
+
223
+ function useMessages(node, filter = null) {
224
+ const [messages, setMessages] = useState([]);
225
+
226
+ useEffect(() => {
227
+ if (!node) {
228
+ setMessages([]);
229
+ return undefined;
230
+ }
231
+
232
+ const handleMessage = (message) => {
233
+ if (typeof filter === 'function' && !filter(message)) {
234
+ return;
235
+ }
236
+
237
+ setMessages((current) => [...current, message]);
238
+ };
239
+
240
+ node.on('message', handleMessage);
241
+ return () => node.off('message', handleMessage);
242
+ }, [node, filter]);
243
+
244
+ return messages;
245
+ }
246
+
110
247
  module.exports = {
111
248
  useDignity,
112
249
  useCollection,
113
- usePeers
250
+ usePeers,
251
+ useObject,
252
+ useDiscovery,
253
+ useConnectionStats,
254
+ useRoom,
255
+ useMessages
114
256
  };
@@ -0,0 +1,24 @@
1
+ function parsePeerJsServerUrl(url) {
2
+ const parsed = new URL(url);
3
+ const secure = parsed.protocol === 'wss:';
4
+ const host = parsed.hostname;
5
+ const port = parsed.port ? Number(parsed.port) : secure ? 443 : 80;
6
+ const key = parsed.searchParams.get('key') || 'peerjs';
7
+
8
+ let path = parsed.pathname || '/';
9
+
10
+ // dignity.js URLs describe the final PeerJS websocket path (often ending in /peerjs).
11
+ // The official PeerJS client always appends "peerjs" to `path`, so strip that suffix
12
+ // to avoid connections like /peerjs/peerjs.
13
+ if (path.endsWith('/peerjs')) {
14
+ path = path.slice(0, -'/peerjs'.length) || '/';
15
+ }
16
+
17
+ if (path !== '/' && !path.endsWith('/')) {
18
+ path += '/';
19
+ }
20
+
21
+ return { secure, host, port, path, key };
22
+ }
23
+
24
+ module.exports = parsePeerJsServerUrl;
@@ -1,4 +1,5 @@
1
1
  const WebSocketSignalingProvider = require('./websocket-signaling-provider');
2
+ const parsePeerJsServerUrl = require('./parse-peerjs-url');
2
3
 
3
4
  class PeerJSSignalingProvider {
4
5
  constructor({ id, url, PeerImpl, WebSocketImpl, priority = 0, connectTimeoutMs = 10000 }) {
@@ -30,14 +31,7 @@ class PeerJSSignalingProvider {
30
31
  }
31
32
 
32
33
  parsePeerJsServerUrl() {
33
- const parsed = new URL(this.url);
34
- const secure = parsed.protocol === 'wss:';
35
- const host = parsed.hostname;
36
- const port = parsed.port ? Number(parsed.port) : secure ? 443 : 80;
37
- const path = parsed.pathname || '/';
38
- const key = parsed.searchParams.get('key') || 'peerjs';
39
-
40
- return { secure, host, port, path, key };
34
+ return parsePeerJsServerUrl(this.url);
41
35
  }
42
36
 
43
37
  shouldUseWebSocketFallback() {