javascript-solid-server 0.0.112 → 0.0.114
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 +6 -6
- package/package.json +1 -1
- package/src/webrtc/index.js +194 -7
- package/test/webrtc.test.js +154 -0
package/README.md
CHANGED
|
@@ -107,12 +107,12 @@ Full options: [docs/configuration.md](docs/configuration.md)
|
|
|
107
107
|
|
|
108
108
|
## Comparison
|
|
109
109
|
|
|
110
|
-
| Server |
|
|
111
|
-
|
|
112
|
-
| [JSS](https://github.com/JavaScriptSolidServer/JavaScriptSolidServer) | ~
|
|
113
|
-
| [
|
|
114
|
-
| [
|
|
115
|
-
| [
|
|
110
|
+
| Server | Package | Packages | node_modules |
|
|
111
|
+
|--------|---------|----------|-------------|
|
|
112
|
+
| [JSS](https://github.com/JavaScriptSolidServer/JavaScriptSolidServer) | ~1 MB | ~191 | ~77 MB |
|
|
113
|
+
| [CSS](https://github.com/CommunitySolidServer/CommunitySolidServer) | ~6 MB | ~311 | ~152 MB |
|
|
114
|
+
| [Pivot](https://github.com/solid-contrib/pivot) | ~6 MB | ~311+ | ~152 MB |
|
|
115
|
+
| [NSS](https://github.com/nodeSolidServer/node-solid-server) | ~7 MB | ~670 | ~539 MB |
|
|
116
116
|
|
|
117
117
|
## Performance
|
|
118
118
|
|
package/package.json
CHANGED
package/src/webrtc/index.js
CHANGED
|
@@ -2,13 +2,18 @@
|
|
|
2
2
|
* WebRTC Signaling Server Plugin
|
|
3
3
|
*
|
|
4
4
|
* Lightweight signaling server for WebRTC peer-to-peer connections.
|
|
5
|
-
*
|
|
6
|
-
*
|
|
5
|
+
* Supports two discovery modes:
|
|
6
|
+
*
|
|
7
|
+
* 1. Identity-based — connect to a specific peer by WebID
|
|
8
|
+
* 2. Content-addressed — find peers sharing the same resource hash
|
|
9
|
+
*
|
|
10
|
+
* Relays SDP offers/answers and ICE candidates between peers.
|
|
11
|
+
* The actual media/data flows directly between peers — JSS just introduces them.
|
|
7
12
|
*
|
|
8
13
|
* Usage: jss start --webrtc
|
|
9
14
|
* Endpoint: wss://your.pod/.webrtc
|
|
10
15
|
*
|
|
11
|
-
*
|
|
16
|
+
* Identity-based protocol (JSON over WebSocket):
|
|
12
17
|
* → { type: "offer", to: "<webid>", sdp: "..." }
|
|
13
18
|
* → { type: "answer", to: "<webid>", sdp: "..." }
|
|
14
19
|
* → { type: "candidate", to: "<webid>", candidate: {...} }
|
|
@@ -21,6 +26,14 @@
|
|
|
21
26
|
* ← { type: "peers", you: "<webid>", peers: ["<webid>", ...] }
|
|
22
27
|
* ← { type: "peer-joined", webId: "<webid>" }
|
|
23
28
|
* ← { type: "peer-left", webId: "<webid>" }
|
|
29
|
+
*
|
|
30
|
+
* Content-addressed protocol (JSON over WebSocket):
|
|
31
|
+
* → { type: "announce", resource: "<hash>", offers: [{ sdp: "...", offer_id: "..." }, ...] }
|
|
32
|
+
* → { type: "answer", resource: "<hash>", to: "<peer_id>", offer_id: "...", sdp: "..." }
|
|
33
|
+
* → { type: "leave", resource: "<hash>" }
|
|
34
|
+
* ← { type: "offer", resource: "<hash>", from: "<peer_id>", offer_id: "...", sdp: "..." }
|
|
35
|
+
* ← { type: "answer", resource: "<hash>", from: "<peer_id>", offer_id: "...", sdp: "..." }
|
|
36
|
+
* ← { type: "resource-peers", resource: "<hash>", count: <n> }
|
|
24
37
|
*/
|
|
25
38
|
|
|
26
39
|
import websocket from '@fastify/websocket';
|
|
@@ -28,6 +41,9 @@ import { getWebIdFromRequestAsync } from '../auth/token.js';
|
|
|
28
41
|
|
|
29
42
|
const ALLOWED_TYPES = new Set(['offer', 'answer', 'candidate', 'hangup']);
|
|
30
43
|
const MAX_MESSAGE_SIZE = 64 * 1024; // 64KB
|
|
44
|
+
const MAX_OFFERS_PER_ANNOUNCE = 10;
|
|
45
|
+
const MAX_RESOURCES_PER_PEER = 50;
|
|
46
|
+
const RESOURCE_HASH_RE = /^[a-fA-F0-9]{8,128}$/;
|
|
31
47
|
|
|
32
48
|
/**
|
|
33
49
|
* Register WebRTC signaling routes on Fastify instance
|
|
@@ -39,9 +55,20 @@ const MAX_MESSAGE_SIZE = 64 * 1024; // 64KB
|
|
|
39
55
|
export async function webrtcPlugin(fastify, options = {}) {
|
|
40
56
|
const path = options.path || '/.webrtc';
|
|
41
57
|
|
|
42
|
-
// Instance-scoped peer state
|
|
58
|
+
// Instance-scoped peer state (identity-based)
|
|
43
59
|
const peers = new Map();
|
|
44
60
|
|
|
61
|
+
// Instance-scoped resource state (content-addressed)
|
|
62
|
+
// Map<resourceHash, Map<peerId, socket>>
|
|
63
|
+
const resources = new Map();
|
|
64
|
+
|
|
65
|
+
// Track which resources each peer has joined
|
|
66
|
+
// Map<peerId, Set<resourceHash>>
|
|
67
|
+
const peerResources = new Map();
|
|
68
|
+
|
|
69
|
+
// Peer ID counter for content-addressed mode
|
|
70
|
+
let nextPeerId = 1;
|
|
71
|
+
|
|
45
72
|
// Only register @fastify/websocket if not already registered
|
|
46
73
|
if (!fastify.websocketServer) {
|
|
47
74
|
await fastify.register(websocket);
|
|
@@ -53,6 +80,8 @@ export async function webrtcPlugin(fastify, options = {}) {
|
|
|
53
80
|
socket.close();
|
|
54
81
|
}
|
|
55
82
|
peers.clear();
|
|
83
|
+
resources.clear();
|
|
84
|
+
peerResources.clear();
|
|
56
85
|
});
|
|
57
86
|
|
|
58
87
|
function broadcast(senderWebId, msg) {
|
|
@@ -64,10 +93,138 @@ export async function webrtcPlugin(fastify, options = {}) {
|
|
|
64
93
|
}
|
|
65
94
|
}
|
|
66
95
|
|
|
96
|
+
// --- Content-addressed helpers ---
|
|
97
|
+
|
|
98
|
+
function getResourcePeers(resourceHash) {
|
|
99
|
+
let group = resources.get(resourceHash);
|
|
100
|
+
if (!group) {
|
|
101
|
+
group = new Map();
|
|
102
|
+
resources.set(resourceHash, group);
|
|
103
|
+
}
|
|
104
|
+
return group;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function addPeerToResource(peerId, socket, resourceHash) {
|
|
108
|
+
const group = getResourcePeers(resourceHash);
|
|
109
|
+
group.set(peerId, socket);
|
|
110
|
+
|
|
111
|
+
let tracked = peerResources.get(peerId);
|
|
112
|
+
if (!tracked) {
|
|
113
|
+
tracked = new Set();
|
|
114
|
+
peerResources.set(peerId, tracked);
|
|
115
|
+
}
|
|
116
|
+
tracked.add(resourceHash);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function removePeerFromResource(peerId, resourceHash) {
|
|
120
|
+
const group = resources.get(resourceHash);
|
|
121
|
+
if (group) {
|
|
122
|
+
group.delete(peerId);
|
|
123
|
+
if (group.size === 0) resources.delete(resourceHash);
|
|
124
|
+
}
|
|
125
|
+
const tracked = peerResources.get(peerId);
|
|
126
|
+
if (tracked) {
|
|
127
|
+
tracked.delete(resourceHash);
|
|
128
|
+
if (tracked.size === 0) peerResources.delete(peerId);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function removePeerFromAllResources(peerId) {
|
|
133
|
+
const tracked = peerResources.get(peerId);
|
|
134
|
+
if (!tracked) return;
|
|
135
|
+
for (const hash of tracked) {
|
|
136
|
+
const group = resources.get(hash);
|
|
137
|
+
if (group) {
|
|
138
|
+
group.delete(peerId);
|
|
139
|
+
if (group.size === 0) resources.delete(hash);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
peerResources.delete(peerId);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function handleAnnounce(socket, peerId, msg) {
|
|
146
|
+
const hash = msg.resource;
|
|
147
|
+
if (!hash || typeof hash !== 'string' || !RESOURCE_HASH_RE.test(hash)) {
|
|
148
|
+
socket.send(JSON.stringify({ type: 'error', message: 'Invalid resource hash' }));
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Limit resources per peer
|
|
153
|
+
const tracked = peerResources.get(peerId);
|
|
154
|
+
if (tracked && tracked.size >= MAX_RESOURCES_PER_PEER && !tracked.has(hash)) {
|
|
155
|
+
socket.send(JSON.stringify({ type: 'error', message: 'Too many resources' }));
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const group = getResourcePeers(hash);
|
|
160
|
+
addPeerToResource(peerId, socket, hash);
|
|
161
|
+
|
|
162
|
+
// Relay offers to existing peers in the group
|
|
163
|
+
const offers = Array.isArray(msg.offers) ? msg.offers.slice(0, MAX_OFFERS_PER_ANNOUNCE) : [];
|
|
164
|
+
const existingPeers = [...group.entries()].filter(([id]) => id !== peerId);
|
|
165
|
+
|
|
166
|
+
for (let i = 0; i < offers.length && i < existingPeers.length; i++) {
|
|
167
|
+
const offer = offers[i];
|
|
168
|
+
const [targetId, targetSocket] = existingPeers[i];
|
|
169
|
+
if (targetSocket.readyState !== 1) continue;
|
|
170
|
+
if (typeof offer.sdp !== 'string') continue;
|
|
171
|
+
|
|
172
|
+
const relay = Object.create(null);
|
|
173
|
+
relay.type = 'offer';
|
|
174
|
+
relay.resource = hash;
|
|
175
|
+
relay.from = peerId;
|
|
176
|
+
relay.offer_id = typeof offer.offer_id === 'string' ? offer.offer_id : String(i);
|
|
177
|
+
relay.sdp = offer.sdp;
|
|
178
|
+
try { targetSocket.send(JSON.stringify(relay)); } catch { /* peer gone */ }
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Tell the announcer how many peers are in the group
|
|
182
|
+
socket.send(JSON.stringify({
|
|
183
|
+
type: 'resource-peers',
|
|
184
|
+
resource: hash,
|
|
185
|
+
count: group.size - 1
|
|
186
|
+
}));
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function handleResourceAnswer(socket, peerId, msg) {
|
|
190
|
+
const hash = msg.resource;
|
|
191
|
+
if (!hash || typeof hash !== 'string') return;
|
|
192
|
+
|
|
193
|
+
const group = resources.get(hash);
|
|
194
|
+
if (!group) return;
|
|
195
|
+
|
|
196
|
+
const targetId = msg.to;
|
|
197
|
+
const targetSocket = group.get(targetId);
|
|
198
|
+
if (!targetSocket || targetSocket.readyState !== 1) {
|
|
199
|
+
socket.send(JSON.stringify({ type: 'error', message: 'Peer not in resource group' }));
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const relay = Object.create(null);
|
|
204
|
+
relay.type = 'answer';
|
|
205
|
+
relay.resource = hash;
|
|
206
|
+
relay.from = peerId;
|
|
207
|
+
if (typeof msg.offer_id === 'string') relay.offer_id = msg.offer_id;
|
|
208
|
+
if (typeof msg.sdp === 'string') relay.sdp = msg.sdp;
|
|
209
|
+
try { targetSocket.send(JSON.stringify(relay)); } catch { /* peer gone */ }
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function handleLeave(socket, peerId, msg) {
|
|
213
|
+
const hash = msg.resource;
|
|
214
|
+
if (!hash || typeof hash !== 'string') return;
|
|
215
|
+
removePeerFromResource(peerId, hash);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// --- WebSocket handler ---
|
|
219
|
+
|
|
67
220
|
fastify.get(path, { websocket: true }, async (connection, request) => {
|
|
68
221
|
const socket = connection.socket;
|
|
69
222
|
|
|
70
|
-
// Authenticate the connection
|
|
223
|
+
// Authenticate the connection (support query param for browser WebSocket which can't set headers)
|
|
224
|
+
const queryToken = request.query?.token;
|
|
225
|
+
if (queryToken && !request.headers.authorization) {
|
|
226
|
+
request.headers.authorization = `Bearer ${queryToken}`;
|
|
227
|
+
}
|
|
71
228
|
const { webId } = await getWebIdFromRequestAsync(request);
|
|
72
229
|
if (!webId) {
|
|
73
230
|
socket.send(JSON.stringify({ type: 'error', message: 'Authentication required' }));
|
|
@@ -75,10 +232,16 @@ export async function webrtcPlugin(fastify, options = {}) {
|
|
|
75
232
|
return;
|
|
76
233
|
}
|
|
77
234
|
|
|
235
|
+
// Assign a stable peer ID for content-addressed mode
|
|
236
|
+
const peerId = String(nextPeerId++);
|
|
237
|
+
socket._peerId = peerId;
|
|
238
|
+
|
|
78
239
|
// Register this peer (close old connection if reconnecting)
|
|
79
240
|
const existing = peers.get(webId);
|
|
80
241
|
const isReconnect = !!existing;
|
|
81
242
|
if (existing) {
|
|
243
|
+
// Clean up old connection's resource memberships
|
|
244
|
+
if (existing._peerId) removePeerFromAllResources(existing._peerId);
|
|
82
245
|
peers.delete(webId);
|
|
83
246
|
existing.close();
|
|
84
247
|
}
|
|
@@ -89,6 +252,7 @@ export async function webrtcPlugin(fastify, options = {}) {
|
|
|
89
252
|
socket.send(JSON.stringify({
|
|
90
253
|
type: 'peers',
|
|
91
254
|
you: webId,
|
|
255
|
+
peerId: peerId,
|
|
92
256
|
peers: [...peers.keys()].filter(id => id !== webId)
|
|
93
257
|
}));
|
|
94
258
|
|
|
@@ -113,8 +277,28 @@ export async function webrtcPlugin(fastify, options = {}) {
|
|
|
113
277
|
return;
|
|
114
278
|
}
|
|
115
279
|
|
|
116
|
-
if (!msg.
|
|
117
|
-
socket.send(JSON.stringify({ type: 'error', message: 'Missing "
|
|
280
|
+
if (!msg.type) {
|
|
281
|
+
socket.send(JSON.stringify({ type: 'error', message: 'Missing "type" field' }));
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Content-addressed messages
|
|
286
|
+
if (msg.type === 'announce') {
|
|
287
|
+
handleAnnounce(socket, peerId, msg);
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
if (msg.type === 'answer' && msg.resource) {
|
|
291
|
+
handleResourceAnswer(socket, peerId, msg);
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
if (msg.type === 'leave') {
|
|
295
|
+
handleLeave(socket, peerId, msg);
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// Identity-based messages require "to" field
|
|
300
|
+
if (!msg.to) {
|
|
301
|
+
socket.send(JSON.stringify({ type: 'error', message: 'Missing "to" field' }));
|
|
118
302
|
return;
|
|
119
303
|
}
|
|
120
304
|
|
|
@@ -144,6 +328,9 @@ export async function webrtcPlugin(fastify, options = {}) {
|
|
|
144
328
|
});
|
|
145
329
|
|
|
146
330
|
socket.on('close', () => {
|
|
331
|
+
// Clean up content-addressed resource memberships
|
|
332
|
+
removePeerFromAllResources(peerId);
|
|
333
|
+
|
|
147
334
|
// Only remove if this socket is still the registered one (not replaced by reconnect)
|
|
148
335
|
if (peers.get(webId) === socket) {
|
|
149
336
|
peers.delete(webId);
|
package/test/webrtc.test.js
CHANGED
|
@@ -209,4 +209,158 @@ describe('WebRTC Signaling', () => {
|
|
|
209
209
|
await new Promise(r => setTimeout(r, 50));
|
|
210
210
|
});
|
|
211
211
|
});
|
|
212
|
+
|
|
213
|
+
describe('Content-Addressed Peer Discovery', () => {
|
|
214
|
+
const RESOURCE_HASH = 'a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2';
|
|
215
|
+
|
|
216
|
+
it('should return peer count on announce', async () => {
|
|
217
|
+
const { ws: alice } = await connectAndWait('alice');
|
|
218
|
+
|
|
219
|
+
alice.send(JSON.stringify({
|
|
220
|
+
type: 'announce',
|
|
221
|
+
resource: RESOURCE_HASH,
|
|
222
|
+
offers: []
|
|
223
|
+
}));
|
|
224
|
+
|
|
225
|
+
const msg = await waitForMessage(alice, 'resource-peers');
|
|
226
|
+
assert.strictEqual(msg.resource, RESOURCE_HASH);
|
|
227
|
+
assert.strictEqual(msg.count, 0);
|
|
228
|
+
|
|
229
|
+
alice.close();
|
|
230
|
+
await new Promise(r => setTimeout(r, 50));
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
it('should relay offers between peers sharing a resource', async () => {
|
|
234
|
+
const { ws: alice, peerId: alicePeerId } = await connectAndWait('alice');
|
|
235
|
+
|
|
236
|
+
// Alice announces with no offers (first in group)
|
|
237
|
+
alice.send(JSON.stringify({
|
|
238
|
+
type: 'announce',
|
|
239
|
+
resource: RESOURCE_HASH,
|
|
240
|
+
offers: []
|
|
241
|
+
}));
|
|
242
|
+
await waitForMessage(alice, 'resource-peers');
|
|
243
|
+
|
|
244
|
+
// Bob announces with an offer — should be relayed to Alice
|
|
245
|
+
const offerPromise = waitForMessage(alice, 'offer');
|
|
246
|
+
const { ws: bob, peerId: bobPeerId } = await connectAndWait('bob');
|
|
247
|
+
|
|
248
|
+
bob.send(JSON.stringify({
|
|
249
|
+
type: 'announce',
|
|
250
|
+
resource: RESOURCE_HASH,
|
|
251
|
+
offers: [{ sdp: 'v=0\r\nbob-offer', offer_id: 'offer1' }]
|
|
252
|
+
}));
|
|
253
|
+
|
|
254
|
+
const offer = await offerPromise;
|
|
255
|
+
assert.strictEqual(offer.type, 'offer');
|
|
256
|
+
assert.strictEqual(offer.resource, RESOURCE_HASH);
|
|
257
|
+
assert.strictEqual(offer.from, bobPeerId);
|
|
258
|
+
assert.strictEqual(offer.offer_id, 'offer1');
|
|
259
|
+
assert.ok(offer.sdp.includes('bob-offer'));
|
|
260
|
+
|
|
261
|
+
// Alice answers Bob
|
|
262
|
+
const answerPromise = waitForMessage(bob, 'answer');
|
|
263
|
+
alice.send(JSON.stringify({
|
|
264
|
+
type: 'answer',
|
|
265
|
+
resource: RESOURCE_HASH,
|
|
266
|
+
to: bobPeerId,
|
|
267
|
+
offer_id: 'offer1',
|
|
268
|
+
sdp: 'v=0\r\nalice-answer'
|
|
269
|
+
}));
|
|
270
|
+
|
|
271
|
+
const answer = await answerPromise;
|
|
272
|
+
assert.strictEqual(answer.type, 'answer');
|
|
273
|
+
assert.strictEqual(answer.resource, RESOURCE_HASH);
|
|
274
|
+
assert.strictEqual(answer.from, alicePeerId);
|
|
275
|
+
assert.ok(answer.sdp.includes('alice-answer'));
|
|
276
|
+
|
|
277
|
+
alice.close();
|
|
278
|
+
bob.close();
|
|
279
|
+
await new Promise(r => setTimeout(r, 50));
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
it('should clean up resources on disconnect', async () => {
|
|
283
|
+
const { ws: alice } = await connectAndWait('alice');
|
|
284
|
+
|
|
285
|
+
alice.send(JSON.stringify({
|
|
286
|
+
type: 'announce',
|
|
287
|
+
resource: RESOURCE_HASH,
|
|
288
|
+
offers: []
|
|
289
|
+
}));
|
|
290
|
+
await waitForMessage(alice, 'resource-peers');
|
|
291
|
+
|
|
292
|
+
// Bob joins the resource group
|
|
293
|
+
const { ws: bob } = await connectAndWait('bob');
|
|
294
|
+
bob.send(JSON.stringify({
|
|
295
|
+
type: 'announce',
|
|
296
|
+
resource: RESOURCE_HASH,
|
|
297
|
+
offers: []
|
|
298
|
+
}));
|
|
299
|
+
const bobPeers = await waitForMessage(bob, 'resource-peers');
|
|
300
|
+
assert.strictEqual(bobPeers.count, 1); // alice is there
|
|
301
|
+
|
|
302
|
+
// Alice disconnects
|
|
303
|
+
alice.close();
|
|
304
|
+
await new Promise(r => setTimeout(r, 200));
|
|
305
|
+
|
|
306
|
+
// Charlie joins — should see only bob
|
|
307
|
+
const { ws: charlie } = await connectAndWait('bob'); // reuse bob pod
|
|
308
|
+
charlie.send(JSON.stringify({
|
|
309
|
+
type: 'announce',
|
|
310
|
+
resource: RESOURCE_HASH,
|
|
311
|
+
offers: []
|
|
312
|
+
}));
|
|
313
|
+
const charliePeers = await waitForMessage(charlie, 'resource-peers');
|
|
314
|
+
// bob was reconnected (old connection closed), so count depends on timing
|
|
315
|
+
assert.ok(charliePeers.count >= 0);
|
|
316
|
+
|
|
317
|
+
bob.close();
|
|
318
|
+
charlie.close();
|
|
319
|
+
await new Promise(r => setTimeout(r, 50));
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
it('should handle leave message', async () => {
|
|
323
|
+
const { ws: alice } = await connectAndWait('alice');
|
|
324
|
+
|
|
325
|
+
alice.send(JSON.stringify({
|
|
326
|
+
type: 'announce',
|
|
327
|
+
resource: RESOURCE_HASH,
|
|
328
|
+
offers: []
|
|
329
|
+
}));
|
|
330
|
+
await waitForMessage(alice, 'resource-peers');
|
|
331
|
+
|
|
332
|
+
// Leave the resource group
|
|
333
|
+
alice.send(JSON.stringify({ type: 'leave', resource: RESOURCE_HASH }));
|
|
334
|
+
|
|
335
|
+
// Bob joins — should see 0 peers (alice left)
|
|
336
|
+
const { ws: bob } = await connectAndWait('bob');
|
|
337
|
+
bob.send(JSON.stringify({
|
|
338
|
+
type: 'announce',
|
|
339
|
+
resource: RESOURCE_HASH,
|
|
340
|
+
offers: []
|
|
341
|
+
}));
|
|
342
|
+
const msg = await waitForMessage(bob, 'resource-peers');
|
|
343
|
+
assert.strictEqual(msg.count, 0);
|
|
344
|
+
|
|
345
|
+
alice.close();
|
|
346
|
+
bob.close();
|
|
347
|
+
await new Promise(r => setTimeout(r, 50));
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
it('should reject invalid resource hash', async () => {
|
|
351
|
+
const { ws: alice } = await connectAndWait('alice');
|
|
352
|
+
|
|
353
|
+
alice.send(JSON.stringify({
|
|
354
|
+
type: 'announce',
|
|
355
|
+
resource: 'not-a-hex-hash!',
|
|
356
|
+
offers: []
|
|
357
|
+
}));
|
|
358
|
+
|
|
359
|
+
const err = await waitForMessage(alice, 'error');
|
|
360
|
+
assert.ok(err.message.includes('Invalid resource hash'));
|
|
361
|
+
|
|
362
|
+
alice.close();
|
|
363
|
+
await new Promise(r => setTimeout(r, 50));
|
|
364
|
+
});
|
|
365
|
+
});
|
|
212
366
|
});
|