@yz-social/webrtc 0.1.0 → 0.1.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.
@@ -0,0 +1,27 @@
1
+ name: CI/CD Build and Test
2
+
3
+ on:
4
+ push:
5
+ branches: [ main ]
6
+ pull_request:
7
+ branches: [ main ]
8
+ workflow_dispatch: # Allows manual running
9
+
10
+ jobs:
11
+ build-and-test:
12
+ runs-on: ubuntu-latest # Uses the latest Ubuntu runner
13
+
14
+ steps:
15
+ - name: Checkout Code
16
+ uses: actions/checkout@v5 # Action to check out your repository code
17
+
18
+ - name: Set up Node.js
19
+ uses: actions/setup-node@v4 # Action to set up Node.js environment
20
+ with:
21
+ node-version: '22.x'
22
+
23
+ - name: Install Dependencies
24
+ run: npm i # Some folks recommend 'npm ci', but I don't like the required package lock.
25
+
26
+ - name: Run Tests
27
+ run: npm test # Executes the 'test' script defined in your package.json
package/index.js CHANGED
@@ -18,6 +18,19 @@ export class WebRTC {
18
18
  // https://xirsys.com/pricing/ 500 MB/month (50 GB/month for $33/month)
19
19
  // Also https://www.npmjs.com/package/node-turn or https://meetrix.io/blog/webrtc/coturn/installation.html
20
20
  ];
21
+ cleanup() { // Attempt to allow everything to be garbage-collected.
22
+ if (!this.pc) return;
23
+ this.pc.onicecandidate = this.pc.ondatachannel = this.pc.onnegotiationneeded = this.pc.onconnectionstatechange = null;
24
+ delete this.pc;
25
+ delete this.dataChannelPromises;
26
+ delete this.dataChannelOursPromises;
27
+ delete this.dataChannelTheirsPromises;
28
+ }
29
+
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;
21
34
  constructor({configuration = {iceServers: WebRTC.iceServers}, ...properties}) {
22
35
  Object.assign(this, properties);
23
36
 
@@ -26,14 +39,17 @@ export class WebRTC {
26
39
  promise.resolve = resolve;
27
40
  this.closed = promise;
28
41
  // Safari doesn't fire signalingstatechange for closing activity.
29
- this.pc.addEventListener('connectionstatechange', () => { // Only fires by action from other side.
30
- const state = this.pc.connectionState;
42
+ this.pc.onconnectionstatechange = () => { // Only fires by action from other side.
43
+ const pc = this.pc;
44
+ if (!pc) return null;
45
+ const state = pc.connectionState;
31
46
  if (state === 'connected') return this.signalsReadyResolver?.();
32
47
  if (['new', 'connecting'].includes(state)) return null;
33
48
  // closed, disconnected, failed: resolve this.closed promise.
34
- this.log('connectionstatechange signaling/connection:', this.pc.signalingState, state);
35
- return resolve(this.pc);
36
- });
49
+ this.log('connectionstatechange signaling/connection:', pc.signalingState, state);
50
+ this.cleanup();
51
+ return resolve(pc);
52
+ };
37
53
 
38
54
  this.connectionStartTime = Date.now();
39
55
  this.makingOffer = false;
@@ -62,8 +78,11 @@ export class WebRTC {
62
78
  }
63
79
  async close() {
64
80
  // Do not try to close or wait for data channels. It confuses Safari.
65
- this.pc.close();
66
- this.closed.resolve(this.pc); // We do not automatically receive 'connectionstatechange' when our side explicitly closes. (Only if the other does.)
81
+ const pc = this.pc;
82
+ if (!pc) return null;
83
+ pc.close();
84
+ this.closed.resolve(pc); // We do not automatically receive 'connectionstatechange' when our side explicitly closes. (Only if the other does.)
85
+ this.cleanup();
67
86
  return this.closed;
68
87
  }
69
88
  flog(...rest) {
@@ -80,6 +99,7 @@ export class WebRTC {
80
99
  explicitRollback = typeof(globalThis.process) !== 'undefined';
81
100
  async onSignal({ description, candidate }) {
82
101
  // Most of this and onnegotiationneeded is from https://developer.mozilla.org/en-US/docs/Web/API/WebRTC_API/Perfect_negotiation
102
+ if (!this.pc) return;
83
103
  if (description) {
84
104
  const offerCollision =
85
105
  description.type === "offer" &&
@@ -181,40 +201,44 @@ export class WebRTC {
181
201
  if (!this.transferSignals) return; // Just keep collecting until the next call to respond();
182
202
  this.sendPending();
183
203
  }
184
- followupTimer = null;
204
+ //followupTimer = null;
185
205
  sendPending(force = false) { // Send over any signals we have, and process the response.
186
206
  this.lastOutboundSignal = Date.now();
187
- clearTimeout(this.followupTimer);
207
+ //clearTimeout(this.followupTimer);
188
208
  this.transferSerializer = this.transferSerializer.then(() => {
189
209
  const signals = this.collectPendingSignals();
190
210
  if (!force && !signals.length) return null; // A stack of pending signals got rolled together and pending is now empty.
191
211
  this.lastOutboundSend = Date.now();
192
212
  this.log('sending', signals.length, 'signals');
193
213
  return this.transferSignals(signals).then(async response => {
194
- clearTimeout(this.followupTimer);
214
+ //clearTimeout(this.followupTimer);
195
215
  this.lastResponse = Date.now();
196
216
  await this.onSignals(response);
197
- if (this.pc.connectionState === 'connected') return;
198
- this.followupTimer = setTimeout(() => { // We may have sent everything we had, but still need to poke in order to get more ice from them.
199
- if (this.pc.connectionState === 'connected') return;
200
- this.log('************** nothing new to send', this.pc.connectionState, ' ************************');
201
- this.sendPending(true);
202
- }, 500);
217
+ // if (this.pc.connectionState === 'connected') return;
218
+ // this.followupTimer = setTimeout(() => { // We may have sent everything we had, but still need to poke in order to get more ice from them.
219
+ // if (this.pc.connectionState === 'connected') return;
220
+ // Note: if we bring this back, stop after closed!
221
+ // this.log('************** nothing new to send', this.pc.connectionState, ' ************************');
222
+ // this.sendPending(true);
223
+ // }, 500);
203
224
  });
204
225
  });
205
226
  }
206
227
  transferSerializer = Promise.resolve();
207
228
 
208
- dataChannels = {};
209
- setupChannel(dc) { // Given an open or connecting channel, set it up in a unform way.
210
- this.log('setup:', dc.label, dc.id, dc.readyState, 'negotiated:', dc.negotiated, 'exists:', !!this[dc.label]);
211
- this[dc.label] = this.dataChannels[dc.label] = dc;
229
+ setupChannel(dc) { // Arrange for the data channel promise to resolve open, and do other setup.
230
+ // Called by explicit createChannel ('ours' opened) and also by ondatachannel ('theirs' opened).
231
+ const { label, readyState } = dc;
232
+ const isTheirs = readyState === 'open'; // Came via ondatachannel.
233
+ this.log('setupChannel:', label, dc.id, readyState, 'negotiated:', dc.negotiated);
234
+ const kind = isTheirs ? 'Theirs' : 'Ours';
212
235
  dc.webrtc = this;
213
- dc.onopen = async () => {
214
- this.log('channel onopen:', dc.label, dc.id, dc.readyState, 'negotiated:', dc.negotiated);
215
- this.dataChannelPromises[dc.label]?.resolve(this[dc.label]);
236
+ dc.onopen = async () => { // Idempotent (except for logging), if we do not bash dataChannePromises[label] multiple times.
237
+ this.log('channel onopen:', label, dc.id, readyState, 'negotiated:', dc.negotiated);
238
+ this[this.restrictablePromiseKey()][label]?.resolve(dc);
239
+ this[this.restrictablePromiseKey(kind)][label]?.resolve(dc);
216
240
  };
217
- if (dc.readyState === 'open') dc.onopen();
241
+ if (isTheirs) dc.onopen();
218
242
  return dc;
219
243
  }
220
244
  ondatachannel(dc) {
@@ -229,10 +253,18 @@ export class WebRTC {
229
253
  return this.setupChannel(this.pc.createDataChannel(name, {negotiated, id, ...options}));
230
254
  }
231
255
  dataChannelPromises = {};
232
- getDataChannelPromise(name = 'data') { // Promise to resolve when opened, WITHOUT actually creating one.
256
+ dataChannelOursPromises = {};
257
+ dataChannelTheirsPromises = {};
258
+ restrictablePromiseKey(restriction = '') { // The property in which store dataChannel promises of the specified restriction.
259
+ return `dataChannel${restriction}Promises`;
260
+ }
261
+ getDataChannelPromise(name = 'data', restriction = '') { // Promise to resolve when opened, WITHOUT actually creating one.
262
+ // The application can restrict this to being only a channel of 'ours' or 'theirs' (see setupChannel), which is useful for
263
+ // non-negotiated channels.
264
+ const key = this.restrictablePromiseKey(restriction);
233
265
  const {promise, resolve, reject} = Promise.withResolvers();
234
266
  Object.assign(promise, {resolve, reject});
235
- return this.dataChannelPromises[name] = promise;
267
+ return this[key][name] = promise;
236
268
  }
237
269
 
238
270
  async reportConnection(doLogging = false) { // Update self with latest wrtc stats (and log them if doLogging true). See Object.assign for properties.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yz-social/webrtc",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Streamlined portable webrtc management of p2p and client2server.",
5
5
  "keywords": [
6
6
  "webrtc",
package/spec/portal.js ADDED
@@ -0,0 +1,64 @@
1
+ import process from 'node:process';
2
+ import cluster from 'node:cluster';
3
+ import express from 'express';
4
+ import path from 'path';
5
+ import { fileURLToPath } from 'url';
6
+ import { WebRTC } from '../index.js';
7
+
8
+ // A minimal webrtc web portal server.
9
+ // Peers may POST signals to /join/<n> in order to connect (one at a time!) to a WebRTC node running at id <n>.
10
+ // Two such POSTs are typically required.
11
+ // The WebRTC does nothing except say 'Welcome!' on the data channel opened by the client.
12
+ // For a more complete example, see https://github.com/YZ-social/kdht/blob/main/spec/portal.js
13
+
14
+ const nPortals = parseInt(process.argv[2] || WebRTC.suggestedInstancesLimit);
15
+
16
+ if (cluster.isPrimary) { // Parent process with portal webserver through which clienta can bootstrap
17
+ process.title = 'webrtc-test-portal';
18
+ const app = express();
19
+ const __filename = fileURLToPath(import.meta.url);
20
+ const __dirname = path.dirname(__filename);
21
+ console.log('launching', nPortals, 'portals');
22
+ for (let i = 0; i < nPortals; i++) {
23
+ const worker = cluster.fork();
24
+ worker.on('message', signals => { // Message from a worker, in response to a POST.
25
+ worker.requestResolver?.(signals);
26
+ });
27
+ }
28
+ const workers = Object.values(cluster.workers);
29
+ app.use(express.json());
30
+ app.use(express.static(path.resolve(__dirname, '..'))); // Serve files needed for testing browsers.
31
+ app.post('/join/:to', async (req, res, next) => { // Handler for JSON POST requests that provide an array of signals and get signals back.
32
+ const {params, body} = req;
33
+ // Find the specifed worker, or pick one at random. TODO CLEANUP: Remove. We now use as separate /name/:label to pick one.
34
+ const worker = workers[params.to];
35
+ if (!worker) {
36
+ console.warn('no worker', params.to);
37
+ return res.sendStatus(404);
38
+ }
39
+
40
+ // Pass the POST body to the worker and await the response.
41
+ const promise = new Promise(resolve => worker.requestResolver = resolve);
42
+ worker.send(body, undefined, undefined, error =>
43
+ error && console.log(`Error communicating with portal worker ${worker.id}:`, error));
44
+ let response = await promise;
45
+ delete worker.requestResolver; // Now that we have the response.
46
+
47
+ return res.send(response);
48
+ });
49
+ app.listen(3000);
50
+ } else {
51
+ process.title = 'webrtc-test-bot-' + cluster.worker.id;
52
+ let portal;
53
+ process.on('message', async incomingSignals => { // Signals from a sender through the server.
54
+ const response = await portal.respond(incomingSignals);
55
+ process.send(response);
56
+ });
57
+ function setup() {
58
+ console.log(new Date(), 'launched bot', cluster.worker.id);
59
+ portal = new WebRTC({name: 'portal'});
60
+ portal.getDataChannelPromise('data').then(dc => dc.send('Welcome!'));
61
+ portal.closed.then(setup); // Without any explicit message, this is 15 seconds after the other end goes away.
62
+ }
63
+ setup();
64
+ }
package/spec/test.html CHANGED
@@ -9,6 +9,7 @@
9
9
  <script src="../jasmine-standalone-5.7.1/lib/jasmine-5.7.1/boot1.js"></script>
10
10
  <script type="module">
11
11
  import './webrtcSpec.js';
12
+ import './webrtcCapacitySpec.js';
12
13
  </script>
13
14
  </head>
14
15
  </html>
@@ -0,0 +1,68 @@
1
+ const { describe, it, expect, beforeAll, afterAll, beforeEach, afterEach} = globalThis; // For linters.
2
+ import { WebRTC } from '../index.js';
3
+
4
+ describe("WebRTC capacity", function () {
5
+ let nNodes = 20; // When running all webrtc tests at once, it is important to keep this low. (Memory leak?)
6
+
7
+ // Uncomment this line if running a stand-alone capacity test.
8
+ // (And also likely comment out the import './webrtcSpec.js' in test.html.)
9
+ // nNodes = WebRTC.suggestedInstancesLimit;
10
+
11
+ const isNodeJS = typeof(globalThis.process) !== 'undefined';
12
+ let nodes = [];
13
+ beforeAll(async function () {
14
+
15
+ if (isNodeJS) {
16
+ const { spawn } = await import('node:child_process');
17
+ const path = await import('path');
18
+ const { fileURLToPath } = await import('url');
19
+
20
+ const __filename = fileURLToPath(import.meta.url);
21
+ const __dirname = path.dirname(__filename);
22
+ function echo(data) { data = data.slice(0, -1); console.log(data.toString()); }
23
+ const portalProcess = spawn('node', [path.resolve(__dirname, 'portal.js'), nNodes]);
24
+ portalProcess.stdout.on('data', echo);
25
+ portalProcess.stderr.on('data', echo);
26
+ await new Promise(resolve => setTimeout(resolve, 6e3));
27
+ }
28
+
29
+ console.log(new Date(), 'creating', nNodes, 'nodes');
30
+ for (let index = 0; index < nNodes; index++) {
31
+ const node = nodes[index] = new WebRTC({name: 'node'});
32
+ node.transferSignals = messages => fetch(`http://localhost:3000/join/${index}`, {
33
+ method: 'POST',
34
+ headers: { 'Content-Type': 'application/json', 'Connection': 'close' },
35
+ body: JSON.stringify(messages)
36
+ }).then(response => response.json());
37
+ node.closed.then(() => console.log('closed', index)); // Just for debugging.
38
+ const dataOpened = node.getDataChannelPromise('data')
39
+ .then(dc => node.dataReceived = new Promise(resolve => dc.onmessage = event => resolve(event.data)));
40
+ node.createChannel('data', {negotiated: false});
41
+ await dataOpened;
42
+ console.log('opened', index);
43
+ }
44
+ console.log(new Date(), 'finished setup');
45
+ }, Math.max(20e3, nNodes * (500 + 300)));
46
+ for (let index = 0; index < nNodes; index++) {
47
+ it('opened connection ' + index, function () {
48
+ expect(nodes[index].pc.connectionState).toBe('connected');
49
+ });
50
+ it('got data ' + index, async function () {
51
+ expect(await nodes[index].dataReceived).toBe('Welcome!');
52
+ });
53
+ }
54
+ afterAll(async function () {
55
+ console.log(new Date(), 'starting teardown');
56
+ for (let index = 0; index < nNodes; index++) {
57
+ const node = nodes[index];
58
+ expect(node.pc.connectionState).toBe('connected');
59
+ await node.close().then(pc =>
60
+ expect(pc.connectionState).toBe('closed'));
61
+ delete nodes[index];
62
+ }
63
+ if (isNodeJS) {
64
+ const { exec } = await import('node:child_process');
65
+ exec('pkill webrtc-test-');
66
+ }
67
+ });
68
+ });
@@ -3,11 +3,6 @@ import { WebRTC } from '../index.js';
3
3
 
4
4
  describe("WebRTC", function () {
5
5
  const isBrowser = typeof(process) === 'undefined';
6
- // maximums
7
- // 1 channel pair, without negotiated on first signal: nodejs:83, firefox:150+, safari:85+ but gets confused with closing, chrome/edge:50(?)
8
- // 50 works across the board with one channel pair
9
- // On Safari (only), anything more than 32 pair starts to loose messages on the SECOND channel.
10
- const nPairs = 32; //32;
11
6
  let connections = [];
12
7
  describe("direct in-process signaling", function () {
13
8
  function makePair({debug = false, delay = 0, index = 0} = {}) {
@@ -15,25 +10,13 @@ describe("WebRTC", function () {
15
10
  const configuration = { iceServers: WebRTC.iceServers };
16
11
  const A = new WebRTC({name: `A (impolite) ${index}`, polite: false, debug, configuration});
17
12
  const B = new WebRTC({name: `B (polite) ${index}`, polite: true, debug, configuration});
18
- async function sendingSetup(dc) { // Given an open channel, set up to receive a message and then send a test message.
19
- const webrtc = dc.webrtc;
20
- webrtc.receivedMessageCount = webrtc.sentMessageCount = 0;
21
- webrtc.gotData = new Promise(resolve => {
22
- dc.onmessage = e => {
23
- webrtc.receivedMessageCount++;
24
- webrtc.log('got message on', dc.label, dc.id, e.data);
25
- resolve(e.data);
26
- };
27
- });
28
- webrtc.sentMessageCount++;
29
-
30
13
  // Subtle:
31
14
  //
32
15
  // An explicit createChannel() arranges for the promise created by getDataChannelPromise() of the same name to
33
16
  // resolve on that side of the connection when the channel is open. The other side will internally receive a
34
17
  // datachannel event and we arrange for that side to then also resolve a getDataChannelPromise for that name.
35
18
  //
36
- // If only wide side creates the channel, things work out smoothly. Each side ends up resolving the promise for a
19
+ // If only one side creates the channel, things work out smoothly. Each side ends up resolving the promise for a
37
20
  // channel of the given name. The id is as defaulted or specified if negotiated:true, else an internally determined
38
21
  // unique id is used.
39
22
  //
@@ -41,24 +24,47 @@ describe("WebRTC", function () {
41
24
  // are overlappaing offers. One side backs down and there ends up being one channel with the specified id. (The default
42
25
  // id will be the same on both sides IFF any multiple data channels are created in the same order on both sides.)
43
26
  //
44
- // However, when negotiated:false, and both sides call createChannel simultaneously, things get complex. Counterintuitively,
45
- // RTCPeerConnection arranges for the internall determined id to be unique between the two sides, so both get
27
+ // However, when negotiated:FALSE, and both sides call createChannel with a delay in betwen, things get complex. Counterintuitively,
28
+ // RTCPeerConnection arranges for the internally determined id to be unique between the two sides, so both get
46
29
  // two channels with the same name and different id. There will be two internal open events on each side: first for the
47
- // channel that it created, and second for one that the other side created. There isn't any reason to do this (as
30
+ // channel that it created, and second for one that the other side created. (There likely isn't any reason to do this, as
48
31
  // opposed to specifying negotiated:true, unless the application needs each side to listen on one channel and send
49
- // on the other. However, getDataChannelPromise return a promise based on the specified name alone, and it only
50
- // resolves once. For compatibility with the single-sided case, we resolve the first channel (on each side, which
51
- // are different ids). Thus we must attach onmessage to one channel and send on the other. On both sides, the
52
- // first channel is what the promise resolves to, but after a delay, the second channel is available on webrtc[channelName].
53
-
54
- if (!delay) return dc.send(`Hello from ${webrtc.name}`);
32
+ // on the other.)
33
+ async function receivingSetup(dc) {
34
+ const webrtc = dc.webrtc;
35
+ webrtc.log('got open recieving channel', dc.label, dc.id);
36
+ webrtc.ours = dc;
37
+ webrtc.receivedMessageCount = 0;
38
+ webrtc.gotData = new Promise(resolve => webrtc.gotDataResolver = resolve);
39
+ dc.onmessage = e => {
40
+ webrtc.receivedMessageCount++;
41
+ webrtc.log('got message on', dc.label, dc.id, e.data);
42
+ webrtc.gotDataResolver(e.data);
43
+ };
44
+ return dc;
45
+ }
46
+ async function sendingSetup(dc) {
47
+ const webrtc = dc.webrtc;
48
+ webrtc.log('got open sending channel', dc.label, dc.id);
49
+ webrtc.theirs = dc;
50
+ webrtc.sentMessageCount ||= 0;
51
+ webrtc.sentMessageCount++;
55
52
  await WebRTC.delay(delay);
56
- webrtc.data.send(`Hello from ${webrtc.name}`);
57
- return null;
53
+ dc.send(`Hello from ${webrtc.name}`);
54
+ return dc;
55
+ }
56
+ const promises = [];
57
+ if (!delay) {
58
+ promises.push(A.getDataChannelPromise('data').then(receivingSetup).then(sendingSetup));
59
+ promises.push(B.getDataChannelPromise('data').then(receivingSetup).then(sendingSetup));
60
+ } else {
61
+ promises.push(A.getDataChannelPromise('data', 'Ours').then(receivingSetup));
62
+ promises.push(B.getDataChannelPromise('data', 'Ours').then(receivingSetup));
63
+ promises.push(A.getDataChannelPromise('data', 'Theirs').then(sendingSetup));
64
+ promises.push(B.getDataChannelPromise('data', 'Theirs').then(sendingSetup));
58
65
  }
59
- const aOpen = A.getDataChannelPromise('data').then(sendingSetup);
60
- const bOpen = B.getDataChannelPromise('data').then(sendingSetup);
61
- const direct = false; // Does signal work direct/one-sided to the other? False makes a request that waits for a response.
66
+
67
+ const direct = true; // Does signal work direct/one-sided to the other? False makes a request that waits for a response.
62
68
  if (direct) {
63
69
  A.signal = message => B.onSignal(message);
64
70
  B.signal = message => A.onSignal(message);
@@ -66,12 +72,27 @@ describe("WebRTC", function () {
66
72
  A.transferSignals = messages => B.respond(messages);
67
73
  B.transferSignals = messages => A.respond(messages);
68
74
  }
69
- return connections[index] = {A, B, bothOpen: Promise.all([aOpen, bOpen])};
75
+ return connections[index] = {A, B, bothOpen: Promise.all(promises)};
70
76
  }
71
- function standardBehavior(setup, {includeConflictCheck = isBrowser, includeSecondChannel = true} = {}) {
77
+ function standardBehavior(setup, {includeConflictCheck = isBrowser, includeSecondChannel = false} = {}) {
78
+ // The nPairs does NOT seem to be a reliable way to determine how many webrtc peers can be active in the same Javascript.
79
+ // I have had numbers that work for every one of the cases DESCRIBEd below, and even in combinations,
80
+ // but it seems to get upset when all are run together, and it seemms to depend on the state of the machine or phases of the moon.
81
+ // When it fails, it just locks up with no indication of what is happening.
82
+ // Maybe a memory / memory-leak issue?
83
+ //
84
+ // Some observed behaviors, at some point in time, include:
85
+ // 1 channel pair, without negotiated on first signal: nodejs:83, firefox:150+, safari:85+ but gets confused with closing, chrome/edge:50(?)
86
+ // 50 works across the board with one channel pair
87
+ // On Safari (only), anything more than 32 pair starts to loose messages on the SECOND channel.
88
+ // In NodeJS with includeSecondChannel: 32
89
+ // In NodeJS witout includeSecondChannel: 62, 75, even 85, but not consistently.
90
+ //
91
+ // webrtcCapacitySpec.js may be a better test for capacity.
92
+ const nPairs = 10;
72
93
  beforeAll(async function () {
73
94
  const start = Date.now();
74
- console.log('start setup');
95
+ console.log(new Date(), 'start setup', nPairs, 'pairs');
75
96
  for (let index = 0; index < nPairs; index++) {
76
97
  await setup({index});
77
98
  }
@@ -81,8 +102,10 @@ describe("WebRTC", function () {
81
102
  for (let index = 0; index < nPairs; index++) {
82
103
  it(`connects ${index}.`, function () {
83
104
  const {A, B} = connections[index];
84
- expect(A.data.readyState).toBe('open');
85
- expect(B.data.readyState).toBe('open');
105
+ expect(A.ours.readyState).toBe('open');
106
+ expect(B.ours.readyState).toBe('open');
107
+ expect(A.theirs.readyState).toBe('open');
108
+ expect(B.theirs.readyState).toBe('open');
86
109
  });
87
110
  it(`receives ${index}.`, async function () {
88
111
  const {A, B} = connections[index];
@@ -132,8 +155,9 @@ describe("WebRTC", function () {
132
155
  expect(apc.signalingState).toBe('closed');
133
156
  const bpc = await B.closed; // Waiting for B to notice.
134
157
  await B.close(); // Resources are not necessarilly freed when the other side closes. An explicit close() is needed.
135
- expect(['closed', 'disconnected', 'failed']).toContain(B.pc.connectionState);
136
- expect(bpc.signalingState).toBe('closed');
158
+ expect(['closed', 'disconnected', 'failed']).toContain(bpc.connectionState);
159
+ expect(['closed', 'stable']).toContain(bpc.signalingState);
160
+ delete connections[index];
137
161
  });
138
162
  promises.push(promise);
139
163
  }
@@ -143,6 +167,7 @@ describe("WebRTC", function () {
143
167
  }
144
168
  describe("one side opens", function () {
145
169
  describe('non-negotiated', function () {
170
+ beforeAll(function () {console.log('one-sided non-negotiated'); });
146
171
  standardBehavior(async function ({index}) {
147
172
  const {A, B, bothOpen} = makePair({index});
148
173
  A.createChannel('data', {negotiated: false});
@@ -150,6 +175,7 @@ describe("WebRTC", function () {
150
175
  }, {includeConflictCheck: false, includeSecondChannel: false});
151
176
  });
152
177
  describe("negotiated on first signal", function () {
178
+ beforeAll(function () {console.log('one-sided negotiated'); });
153
179
  standardBehavior(async function ({index}) {
154
180
  const {A, B, bothOpen} = makePair({index});
155
181
  // There isn't really a direct, automated way to have one side open another with negotiated:true,
@@ -173,6 +199,7 @@ describe("WebRTC", function () {
173
199
  describe("simultaneous two-sided", function () {
174
200
  describe("negotiated single full-duplex-channel", function () {
175
201
  describe("impolite first", function () {
202
+ beforeAll(function () {console.log('two-sided negotiated impolite-first'); });
176
203
  standardBehavior(async function ({index}) {
177
204
  const {A, B, bothOpen} = makePair({index});
178
205
  A.createChannel("data", {negotiated: true});
@@ -181,6 +208,7 @@ describe("WebRTC", function () {
181
208
  });
182
209
  });
183
210
  describe("polite first", function () {
211
+ beforeAll(function () {console.log('two-sided negotiated polite-first');});
184
212
  standardBehavior(async function ({index}) {
185
213
  const {A, B, bothOpen} = makePair({index});
186
214
  B.createChannel("data", {negotiated: true});
@@ -190,18 +218,21 @@ describe("WebRTC", function () {
190
218
  });
191
219
  });
192
220
  describe("non-negotiated dual half-duplex channels", function () {
193
- const delay = 10;
221
+ const delay = 200;
222
+ const debug = false;
194
223
  describe("impolite first", function () {
224
+ beforeAll(function () {console.log('two-sided non-negotiated impolite-first');});
195
225
  standardBehavior(async function ({index}) {
196
- const {A, B, bothOpen} = makePair({delay, index});
226
+ const {A, B, bothOpen} = makePair({delay, index, debug});
197
227
  A.createChannel("data", {negotiated: false});
198
228
  B.createChannel("data", {negotiated: false});
199
229
  await bothOpen;
200
230
  });
201
231
  });
202
232
  describe("polite first", function () {
233
+ beforeAll(function () {console.log('two-sided non-negotiated polite-first');});
203
234
  standardBehavior(async function ({index}) {
204
- const {A, B, bothOpen} = makePair({delay, index});
235
+ const {A, B, bothOpen} = makePair({delay, index, debug});
205
236
  B.createChannel("data", {negotiated: false});
206
237
  A.createChannel("data", {negotiated: false});
207
238
  await bothOpen;