dignity.js 0.4.0 → 0.5.2
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 +83 -2
- package/dist/dignity.cjs.js +542 -21
- package/dist/dignity.cjs.js.map +4 -4
- package/dist/dignity.esm.js +542 -21
- package/dist/dignity.esm.js.map +3 -3
- package/dist/dignity.min.js +18 -18
- package/docs/assets/dignity.esm.js +11205 -0
- package/docs/assets/favicon.svg +8 -0
- package/docs/chess/assets/chess-app.js +58022 -0
- package/docs/chess/assets/chess-app.js.map +7 -0
- package/docs/chess/assets/chess.css +584 -0
- package/docs/chess/favicon.ico +0 -0
- package/docs/chess/index.html +16 -0
- package/docs/chess/src/App.jsx +128 -0
- package/docs/chess/src/components/Board3D.jsx +364 -0
- package/docs/chess/src/components/GameView.jsx +847 -0
- package/docs/chess/src/components/JoinGate.jsx +68 -0
- package/docs/chess/src/components/LinkPanel.jsx +132 -0
- package/docs/chess/src/components/Lobby.jsx +154 -0
- package/docs/chess/src/components/MovePanel.jsx +123 -0
- package/docs/chess/src/lib/audio.js +50 -0
- package/docs/chess/src/lib/dignitySetup.js +42 -0
- package/docs/chess/src/lib/links.js +124 -0
- package/docs/chess/src/lib/localGames.js +160 -0
- package/docs/chess/src/lib/p2pDebug.js +192 -0
- package/docs/chess/src/main.jsx +5 -0
- package/docs/favicon.ico +0 -0
- package/docs/index.html +7 -3
- package/docs/openapi-like.json +35 -6
- package/examples/decentralized-chess-lite.js +52 -30
- package/package.json +13 -5
- package/src/core/dignity-p2p.js +388 -16
- package/src/index.js +6 -0
- package/src/network/peerjs-network.js +234 -0
- package/src/persistence/indexeddb-persistence.js +2 -0
- package/src/react/index.js +143 -1
- package/src/signaling/parse-peerjs-url.js +24 -0
- 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,
|
package/src/react/index.js
CHANGED
|
@@ -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
|
-
|
|
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() {
|