freertc 0.1.1 → 0.1.3

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/bin/freertc.mjs CHANGED
@@ -34,7 +34,7 @@ Examples:
34
34
 
35
35
  function runInProject(command, args, { bootstrap = false } = {}) {
36
36
  if (bootstrap) {
37
- ensureProjectFiles(PROJECT_ROOT);
37
+ ensureProjectFiles(PROJECT_ROOT, { updateExisting: true });
38
38
  }
39
39
 
40
40
  const result = spawnSync(command, args, {
@@ -87,7 +87,7 @@ if (subcommand === 'init' || subcommand === 'install') {
87
87
  }
88
88
 
89
89
  if (subcommand === 'deploy') {
90
- ensureProjectFiles(PROJECT_ROOT);
90
+ ensureProjectFiles(PROJECT_ROOT, { updateExisting: true });
91
91
  requireWranglerConfig();
92
92
  const wrangler = resolveWranglerCommand(PROJECT_ROOT);
93
93
  runInProject(wrangler.command, [...wrangler.baseArgs, 'deploy', '--env', 'production', ...rest]);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "freertc",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "Cloudflare Worker signaling relay for WebRTC peers with D1 storage.",
5
5
  "keywords": [
6
6
  "webrtc",
@@ -59,28 +59,42 @@ export function resolveProjectRoot(startDir = process.cwd()) {
59
59
  );
60
60
  }
61
61
 
62
- export function ensureProjectFiles(projectRoot) {
62
+ function filesMatch(sourcePath, targetPath) {
63
+ if (!fs.existsSync(sourcePath) || !fs.existsSync(targetPath)) {
64
+ return false;
65
+ }
66
+
67
+ const source = fs.readFileSync(sourcePath);
68
+ const target = fs.readFileSync(targetPath);
69
+ return source.equals(target);
70
+ }
71
+
72
+ export function ensureProjectFiles(projectRoot, { updateExisting = false } = {}) {
63
73
  const targetRoot = path.resolve(projectRoot);
64
74
  if (targetRoot === PACKAGE_ROOT) {
65
75
  return [];
66
76
  }
67
77
 
68
- const copied = [];
78
+ const synced = [];
69
79
 
70
80
  for (const [sourceRelativePath, targetRelativePath] of PROJECT_FILE_MAPPINGS) {
71
81
  const sourcePath = path.join(PACKAGE_ROOT, sourceRelativePath);
72
82
  const targetPath = path.join(targetRoot, targetRelativePath);
73
83
 
74
- if (fs.existsSync(targetPath) || !fs.existsSync(sourcePath)) {
84
+ if (!fs.existsSync(sourcePath)) {
85
+ continue;
86
+ }
87
+
88
+ if (fs.existsSync(targetPath) && (!updateExisting || filesMatch(sourcePath, targetPath))) {
75
89
  continue;
76
90
  }
77
91
 
78
92
  fs.mkdirSync(path.dirname(targetPath), { recursive: true });
79
93
  fs.copyFileSync(sourcePath, targetPath);
80
- copied.push(targetRelativePath);
94
+ synced.push(targetRelativePath);
81
95
  }
82
96
 
83
- return copied;
97
+ return synced;
84
98
  }
85
99
 
86
100
  export function resolveWranglerCommand(cwd = process.cwd()) {
@@ -402,7 +402,7 @@ async function main() {
402
402
  const forcedMode = modeFromArgs(process.argv);
403
403
 
404
404
  try {
405
- const copiedFiles = ensureProjectFiles(ROOT);
405
+ const copiedFiles = ensureProjectFiles(ROOT, { updateExisting: true });
406
406
 
407
407
  console.log(`\n${PROJECT_NAME} Wrangler Install Wizard\n`);
408
408
  console.log(`Using project root: ${ROOT}`);
@@ -416,7 +416,7 @@ async function main() {
416
416
  }
417
417
 
418
418
  if (copiedFiles.length > 0) {
419
- console.log('Copied package files into this project:');
419
+ console.log('Synced package-managed files into this project:');
420
420
  for (const file of copiedFiles) {
421
421
  console.log(` - ${file}`);
422
422
  }
@@ -447,7 +447,7 @@ async function main() {
447
447
  if (copiedFiles.length === 0) {
448
448
  console.log('Required worker files already exist in this project.');
449
449
  } else {
450
- console.log('Project bootstrapped from the published freertc package.');
450
+ console.log('Project files refreshed from the published freertc package.');
451
451
  }
452
452
 
453
453
  resolveWrangler();
package/src/index.js CHANGED
@@ -227,15 +227,29 @@ async function forwardToRelay(relayUrl, message, selfRelayId) {
227
227
  const ws = resp.webSocket;
228
228
  ws.accept();
229
229
 
230
- // Outbound Worker WebSocket: send immediately after accept(), no open event needed
231
- const relayPeerId = selfRelayId || "relay-bridge";
232
- ws.send(JSON.stringify({
233
- psp_version: PSP_VERSION, type: "announce", network: message.network,
234
- from: relayPeerId, message_id: crypto.randomUUID(),
235
- timestamp: Date.now(), ttl_ms: 10_000, body: { capabilities: { relay: true } }
236
- }));
237
- ws.send(JSON.stringify(message));
238
- ws.close();
230
+ await new Promise((resolve) => {
231
+ const relayPeerId = selfRelayId || "relay-bridge";
232
+ const closeTimer = setTimeout(() => {
233
+ try { ws.close(); } catch {}
234
+ resolve();
235
+ }, 250);
236
+
237
+ const finish = () => {
238
+ clearTimeout(closeTimer);
239
+ resolve();
240
+ };
241
+
242
+ ws.addEventListener("error", finish, { once: true });
243
+ ws.addEventListener("close", finish, { once: true });
244
+
245
+ // Outbound Worker WebSocket: send immediately after accept(), no open event needed.
246
+ ws.send(JSON.stringify({
247
+ psp_version: PSP_VERSION, type: "announce", network: message.network,
248
+ from: relayPeerId, message_id: crypto.randomUUID(),
249
+ timestamp: Date.now(), ttl_ms: 10_000, body: { capabilities: { relay: true } }
250
+ }));
251
+ ws.send(JSON.stringify(message));
252
+ });
239
253
  } catch {}
240
254
  }
241
255
 
@@ -260,6 +274,41 @@ async function listRelays(db) {
260
274
  return (result.results || []).map(r => ({ url: r.url, name: r.name }));
261
275
  }
262
276
 
277
+ function mergeDiscoveredPeers(...peerGroups) {
278
+ const merged = new Map();
279
+
280
+ for (const peers of peerGroups) {
281
+ if (!Array.isArray(peers)) continue;
282
+ for (const peer of peers) {
283
+ const peerId = peer?.peer_id;
284
+ if (typeof peerId !== "string" || !peerId) continue;
285
+
286
+ const existing = merged.get(peerId);
287
+ const nextTimestamp = Number(peer?.timestamp || 0);
288
+ const existingTimestamp = Number(existing?.timestamp || 0);
289
+ if (!existing || nextTimestamp >= existingTimestamp) {
290
+ merged.set(peerId, peer);
291
+ }
292
+ }
293
+ }
294
+
295
+ return Array.from(merged.values()).sort((left, right) => left.peer_id.localeCompare(right.peer_id));
296
+ }
297
+
298
+ function sendPeerList(socket, network, peers, to = null, from = "bootstrap-relay") {
299
+ socket.send(JSON.stringify({
300
+ psp_version: PSP_VERSION,
301
+ type: "peer_list",
302
+ network,
303
+ from,
304
+ to,
305
+ message_id: crypto.randomUUID(),
306
+ timestamp: Date.now(),
307
+ ttl_ms: DEFAULT_TTL_MS,
308
+ body: { peers }
309
+ }));
310
+ }
311
+
263
312
  // Broadcast peer list to all connected peers in a network
264
313
  async function broadcastPeerList(db, network) {
265
314
  const sockets = networkSubscribers.get(network);
@@ -279,23 +328,9 @@ async function broadcastPeerList(db, network) {
279
328
  session_id: row.session_id,
280
329
  timestamp: row.updated_at_ms
281
330
  }));
282
-
283
- const message = {
284
- psp_version: PSP_VERSION,
285
- type: "peer_list",
286
- network,
287
- from: "bootstrap-relay",
288
- to: null,
289
- message_id: crypto.randomUUID(),
290
- timestamp: Date.now(),
291
- ttl_ms: DEFAULT_TTL_MS,
292
- body: { peers }
293
- };
294
-
295
- const payload = JSON.stringify(message);
296
331
  for (const socket of sockets) {
297
332
  try {
298
- socket.send(payload);
333
+ sendPeerList(socket, network, peers);
299
334
  } catch (e) {
300
335
  sockets.delete(socket);
301
336
  }
@@ -566,36 +601,36 @@ async function handleClientMessage(socket, rawData, env, ctx, prevPeerKey = null
566
601
  }
567
602
 
568
603
  } else if (type === "discover") {
569
- // Local peers first
604
+ let localPeers = [];
570
605
  if (db) {
571
- broadcastPeerList(db, network).catch(() => {});
606
+ localPeers = await findPeers(db, network, peerId);
572
607
  }
573
- // Fan out to all known peer relays, exchanging relay lists bidirectionally
608
+
609
+ let remotePeers = [];
574
610
  if (env.RELAY_URL && env.DB) {
575
- ctx.waitUntil((async () => {
576
- const selfRelayId = env.RELAY_PEER_ID || "relay-bridge";
577
- const selfUrl = normalizeRelayUrl(env.RELAY_URL);
578
- const allRelays = await listRelays(env.DB);
579
- const remoteUrls = allRelays.map(r => r.url).filter(u => u !== selfUrl);
580
- if (!remoteUrls.length) return;
611
+ const selfRelayId = env.RELAY_PEER_ID || "relay-bridge";
612
+ const selfUrl = normalizeRelayUrl(env.RELAY_URL);
613
+ const allRelays = await listRelays(env.DB);
614
+ const remoteUrls = allRelays.map(r => r.url).filter(u => u !== selfUrl);
581
615
 
616
+ if (remoteUrls.length) {
582
617
  const results = await Promise.all(
583
618
  remoteUrls.map(u => queryRelayForPeers(u, network, selfRelayId, env.DB, allRelays))
584
619
  );
585
- const remotePeers = results.flat();
586
- if (!remotePeers.length) return;
587
-
588
- const message = {
589
- psp_version: PSP_VERSION, type: "peer_list", network,
590
- from: selfRelayId, to: peerId,
591
- message_id: crypto.randomUUID(), timestamp: Date.now(),
592
- ttl_ms: DEFAULT_TTL_MS,
593
- body: { peers: remotePeers }
594
- };
595
- try { socket.send(JSON.stringify(message)); } catch {}
596
- })());
620
+ remotePeers = results.flat();
621
+ }
597
622
  }
598
623
 
624
+ try {
625
+ sendPeerList(
626
+ socket,
627
+ network,
628
+ mergeDiscoveredPeers(localPeers, remotePeers).filter(peer => peer.peer_id !== peerId),
629
+ peerId,
630
+ env.RELAY_PEER_ID || "bootstrap-relay"
631
+ );
632
+ } catch {}
633
+
599
634
  } else if (type === "ext" && message.body?.action === "relay_list") {
600
635
  // Remote relay is sharing its known relay list — cache any new entries
601
636
  if (db) {