@yz-social/webrtc 0.1.10 → 0.1.12

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/index.js CHANGED
@@ -4,14 +4,14 @@ const wrtc = (typeof(process) === 'undefined') ? globalThis : (await import('#wr
4
4
  export class WebRTC {
5
5
  static iceServers = [ // Some default stun and even turn servers.
6
6
 
7
- { urls: 'stun:stun.l.google.com:19302'},
7
+ { urls: 'stun:stun.l.google.com:19302'}, // As of 5/5/26, Edge and Firefox still require iceServers to be non-empty, even for localhost peers.
8
8
  // https://freestun.net/ Currently 50 KBit/s. (2.5 MBit/s fors $9/month)
9
- { urls: 'stun:freestun.net:3478' },
9
+ //{ urls: 'stun:freestun.net:3478' },
10
10
 
11
11
  //{ urls: 'turn:freestun.net:3478', username: 'free', credential: 'free' },
12
12
  // Presumably traffic limited. Can generate new credentials at https://speed.cloudflare.com/turn-creds
13
13
  // Also https://developers.cloudflare.com/calls/ 1 TB/month, and $0.05 /GB after that.
14
- { urls: 'turn:turn.speed.cloudflare.com:50000', username: '826226244cd6e5edb3f55749b796235f420fe5ee78895e0dd7d2baa45e1f7a8f49e9239e78691ab38b72ce016471f7746f5277dcef84ad79fc60f8020b132c73', credential: 'aba9b169546eb6dcc7bfb1cdf34544cf95b5161d602e3b5fa7c8342b2e9802fb' }
14
+ //{ urls: 'turn:turn.speed.cloudflare.com:50000', username: '826226244cd6e5edb3f55749b796235f420fe5ee78895e0dd7d2baa45e1f7a8f49e9239e78691ab38b72ce016471f7746f5277dcef84ad79fc60f8020b132c73', credential: 'aba9b169546eb6dcc7bfb1cdf34544cf95b5161d602e3b5fa7c8342b2e9802fb' }
15
15
 
16
16
  // See also:
17
17
  // https://fastturn.net/ Currently 500MB/month? (25 GB/month for $9/month)
@@ -20,7 +20,7 @@ export class WebRTC {
20
20
  ];
21
21
  cleanup() { // Attempt to allow everything to be garbage-collected.
22
22
  if (!this.pc) return;
23
- this.pc.onicecandidate = this.pc.ondatachannel = this.pc.onnegotiationneeded = this.pc.onconnectionstatechange = this.pc.oniceconnectionstatechange = null;
23
+ this.pc.onicecandidate = this.pc.ondatachannel = this.pc.onnegotiationneeded = this.pc.onconnectionstatechange = this.pc.oniceconnectionstatechange = this.pc.onicecandidateerror = null;
24
24
  delete this.pc;
25
25
  delete this.dataChannelPromises;
26
26
  delete this.dataChannelOursPromises;
@@ -28,9 +28,11 @@ export class WebRTC {
28
28
  }
29
29
 
30
30
  // Number of instances at a time (if previous have been garbage collected), as of 1/27/26:
31
- static suggestedInstancesLimit = globalThis.navigator.vendor?.startsWith('Apple') ? 95 : // Seems to be hardcoded?
32
- globalThis.navigator.userAgent?.includes('Firefox') ? 190 :
33
- 200;
31
+ static suggestedInstancesLimit =
32
+ globalThis.navigator.vendor?.startsWith('Apple') ? 150 : // Safari will open 256, but it won't reliably keep them all open at once.
33
+ globalThis.navigator.userAgent?.includes('Firefox') ? 150 : // 256 on an M5 chip. On an i9 MBP, I can sometimes get more than 150, but not with refresh.
34
+ (typeof(globalThis.process) !== 'undefined') ? 249 : // NodeJS
35
+ 256;
34
36
  constructor({configuration = {iceServers: WebRTC.iceServers}, ...properties}) {
35
37
  Object.assign(this, properties);
36
38
 
@@ -63,9 +65,12 @@ export class WebRTC {
63
65
  this.signal({ candidate: e.candidate });
64
66
  };
65
67
  this.pc.ondatachannel = e => this.ondatachannel(e.channel);
68
+ this.pc.onicecandidateerror = error => {
69
+ const {errorCode, errorText, url, address, port} = error;
70
+ this.flog(`${errorCode}: ${errorText || '(ice failure)'} ${address}:${port} @${url}`);
71
+ };
66
72
  this.pc.oniceconnectionstatechange = () => {
67
73
  if (!this.pc) return;
68
- //this.flog('iceConnectionState', this.pc.iceConnectionState);
69
74
  switch (this.pc.iceConnectionState) {
70
75
  case 'completed':
71
76
  this._resolveIceCompleted?.(true);
@@ -325,11 +330,11 @@ export class WebRTC {
325
330
  return;
326
331
  }
327
332
  const remote = stats.get(candidatePair.remoteCandidateId);
328
- const {protocol, candidateType} = remote;
333
+ const {protocol, candidateType, address} = remote;
329
334
  const now = Date.now();
330
335
  const statsElapsed = now - this.connectionStartTime;
331
336
  Object.assign(this, {stats, transport, candidatePair, remote, protocol, candidateType, statsTime: now, statsElapsed});
332
- if (doLogging) console.info(this.name, 'connected', protocol, candidateType, (statsElapsed/1e3).toFixed(1));
337
+ if (doLogging) console.info(this.name, 'connected', protocol, candidateType, address, (statsElapsed/1e3).toFixed(1));
333
338
  }
334
339
 
335
340
  static getPublicIP(stunServer = "stun:stun.l.google.com:19302") { // Promise external/WAN/public IP addresses for this device.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yz-social/webrtc",
3
- "version": "0.1.10",
3
+ "version": "0.1.12",
4
4
  "description": "Streamlined portable webrtc management of p2p and client2server.",
5
5
  "keywords": [
6
6
  "webrtc",
package/spec/portal.js CHANGED
@@ -13,8 +13,8 @@ import { WebRTC } from '../index.js';
13
13
  // The WebRTC does nothing except say 'Welcome!' on the data channel opened by the client.
14
14
  // For a more complete example, see https://github.com/YZ-social/kdht/blob/main/spec/portal.js
15
15
 
16
- const nPortals = parseInt(process.argv[2] || WebRTC.suggestedInstancesLimit);
17
- const perPortalDelay = parseInt(process.argv[3] || 1e3);
16
+ const nPortals = parseInt(process.argv[2] || 256);
17
+ const perPortalDelay = parseInt(process.argv[3] || 200);
18
18
  const port = parseInt(process.argv[4] || 3000);
19
19
 
20
20
  if (cluster.isPrimary) { // Parent process with portal webserver through which clienta can bootstrap
@@ -23,14 +23,22 @@ if (cluster.isPrimary) { // Parent process with portal webserver through which c
23
23
  const __filename = fileURLToPath(import.meta.url);
24
24
  const __dirname = path.dirname(__filename);
25
25
  console.log('launching', nPortals, 'portals');
26
- for (let i = 0; i < nPortals; i++) {
27
- const worker = cluster.fork();
26
+ const workers = [];
27
+ function makeWorker(i) {
28
+ const worker = workers[i] = cluster.fork();
29
+ console.log(new Date(), 'creating worker', i, 'at', worker.id);
28
30
  worker.on('message', signals => { // Message from a worker, in response to a POST.
29
31
  worker.requestResolver?.(signals);
30
32
  });
33
+ }
34
+ for (let i = 0; i < nPortals; i++) {
35
+ makeWorker(i);
31
36
  await new Promise(resolve => setTimeout(resolve, perPortalDelay));
32
37
  }
33
- const workers = Object.values(cluster.workers);
38
+ cluster.on('exit', (worker, code, signal) => { // Replace the worker at the same index in workers[].
39
+ const index = workers.indexOf(worker);
40
+ makeWorker(index);
41
+ });
34
42
  app.use(logger(':date[iso] :status :method :url :res[content-length] - :response-time ms'));
35
43
  app.use(express.json());
36
44
  app.use(express.static(path.resolve(__dirname, '..'))); // Serve files needed for testing browsers.
@@ -61,7 +69,6 @@ if (cluster.isPrimary) { // Parent process with portal webserver through which c
61
69
  process.send(response);
62
70
  });
63
71
  function setup() {
64
- console.log(new Date(), 'launched bot', cluster.worker.id);
65
72
  portal = new WebRTC({name: 'portal'});
66
73
  portal.getDataChannelPromise('data').then(dc => {
67
74
  console.log(new Date(), 'connected bot', cluster.worker.id);
@@ -69,8 +76,7 @@ if (cluster.isPrimary) { // Parent process with portal webserver through which c
69
76
  });
70
77
  portal.closed.then(() => { // Without any explicit message, this is 15 seconds after the other end goes away.
71
78
  console.log('disconnected', cluster.worker.id);
72
- // Not needed for this test, but for other purposes:
73
- // setup());
79
+ process.exit(0); // cleaner for testing than repeating setup();
74
80
  });
75
81
  }
76
82
  setup();
@@ -6,19 +6,26 @@ function delay(ms) {
6
6
  }
7
7
 
8
8
  describe("WebRTC capacity", function () {
9
- let nNodes = 75; // When running all webrtc tests at once, it is important to keep this low. (Memory leak?)
10
- let perPortalDelay = 1e3;
9
+ let perPortalDelay = 200;
11
10
  let portalSlopDelay = 2e3;
12
11
  let perConnectionDelay = 100;
13
- let connectionSlopDelay = 2e3;
12
+ let connectionSlopDelay = 4e3;
14
13
  let port = 3000;
15
14
  let baseURL = `http://localhost:${port}`;
16
15
  // Alas, I can't seem to get more than about 150-160 nodes through ngrok, even on a machine that can handle 200 directly.
17
16
  //let baseURL = 'https://dorado.ngrok.dev'; // if E.g., node spec/portal.js 200 100; ngrok http 3000 --url https://dorado.ngrok.dev
18
17
 
19
18
  // Uncomment this line if running a stand-alone capacity test.
20
- // (And also likely comment out the import './webrtcSpec.js' in test.html.)
21
- // nNodes = WebRTC.suggestedInstancesLimit;
19
+ // (And also likely comment out the import './webrtcSpec.js' in test.html,
20
+ // and "resignals on restartIce..." tests.)
21
+ let nNodes = WebRTC.suggestedInstancesLimit;
22
+ let reNegotiate = false; // Should we attempt to test restartIce/re-negotiation?
23
+ if (reNegotiate) {
24
+ if (globalThis.navigator.userAgent?.includes('Firefox') ||
25
+ globalThis.navigator.vendor?.startsWith('Apple') ||
26
+ (typeof(globalThis.process) !== 'undefined'))
27
+ nNodes = Math.floor(0.5 * nNodes);
28
+ }
22
29
 
23
30
  const isNodeJS = typeof(globalThis.process) !== 'undefined';
24
31
  const portalIsLocal = isNodeJS && baseURL.startsWith('http://localhost');
@@ -60,18 +67,13 @@ describe("WebRTC capacity", function () {
60
67
  const dataOpened = node.getDataChannelPromise('data')
61
68
  .then(dc => node.dataReceived = new Promise(resolve => dc.onmessage = event => resolve(event.data)));
62
69
  node.createChannel('data', {negotiated: false});
63
- await dataOpened;
70
+ await Promise.race([dataOpened, delay(perConnectionDelay)]);
64
71
  console.log('opened', index);
65
- // if (!portalIsLocal) {
66
- // const maxConnectionsPerNode = 3;
67
- // const maxNgrokConnectionsPerSecond = 120 / 60;
68
- // const secondsPerNode = maxConnectionsPerNode / maxNgrokConnectionsPerSecond;
69
- // await delay(secondsPerNode * 1.5e3); // fudge factor milliseconds/second
70
- // }
71
72
  }
73
+ console.log(new Date(), 'pause after setup');
72
74
  await delay(connectionSlopDelay);
73
75
  console.log(new Date(), 'finished setup');
74
- }, nNodes * perPortalDelay + portalSlopDelay + nNodes * perConnectionDelay + connectionSlopDelay + 1e3);
76
+ }, nNodes * perPortalDelay + portalSlopDelay + 2 * nNodes * perConnectionDelay + 2 * connectionSlopDelay);
75
77
  for (let index = 0; index < nNodes; index++) {
76
78
  it('opened connection ' + index, function () {
77
79
  expect(nodes[index].pc.connectionState).toBe('connected');
@@ -79,17 +81,17 @@ describe("WebRTC capacity", function () {
79
81
  it('got data ' + index, async function () {
80
82
  expect(await nodes[index].dataReceived).toBe('Welcome!');
81
83
  });
82
- it('resignals on restartIce ' + index, async function () {
83
- const node = nodes[index];
84
- expect(node.pc.iceConnectionState).toBe('completed');
85
- node.nFetches = 0;
86
- node.pc.restartIce();
87
- await delay(100); // timing will vary
88
- expect(node.pc.iceConnectionState).not.toBe('completed');
89
- await delay(2e3); // timing will vary
90
- expect(node.pc.iceConnectionState).toBe('completed');
91
- expect(node.nFetches).toBeGreaterThan(0); // We will have re-signalled.
92
- });
84
+ if (reNegotiate) {
85
+ it('resignals on restartIce ' + index, async function () {
86
+ const node = nodes[index];
87
+ expect(['connected', 'completed']).toContain(node.pc.iceConnectionState);
88
+ node.nFetches = 0;
89
+ const promise = node.renegotiate();
90
+ await Promise.race([promise, delay(1e3)]);
91
+ expect(['connected', 'completed']).toContain(node.pc.iceConnectionState);
92
+ expect(node.nFetches).toBeGreaterThan(0); // We will have re-signalled.
93
+ });
94
+ }
93
95
  }
94
96
  afterAll(async function () {
95
97
  console.log(new Date(), 'starting teardown');