@yz-social/webrtc 0.1.0 → 0.1.1

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
@@ -181,40 +181,44 @@ export class WebRTC {
181
181
  if (!this.transferSignals) return; // Just keep collecting until the next call to respond();
182
182
  this.sendPending();
183
183
  }
184
- followupTimer = null;
184
+ //followupTimer = null;
185
185
  sendPending(force = false) { // Send over any signals we have, and process the response.
186
186
  this.lastOutboundSignal = Date.now();
187
- clearTimeout(this.followupTimer);
187
+ //clearTimeout(this.followupTimer);
188
188
  this.transferSerializer = this.transferSerializer.then(() => {
189
189
  const signals = this.collectPendingSignals();
190
190
  if (!force && !signals.length) return null; // A stack of pending signals got rolled together and pending is now empty.
191
191
  this.lastOutboundSend = Date.now();
192
192
  this.log('sending', signals.length, 'signals');
193
193
  return this.transferSignals(signals).then(async response => {
194
- clearTimeout(this.followupTimer);
194
+ //clearTimeout(this.followupTimer);
195
195
  this.lastResponse = Date.now();
196
196
  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);
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
+ // Note: if we bring this back, stop after closed!
201
+ // this.log('************** nothing new to send', this.pc.connectionState, ' ************************');
202
+ // this.sendPending(true);
203
+ // }, 500);
203
204
  });
204
205
  });
205
206
  }
206
207
  transferSerializer = Promise.resolve();
207
208
 
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;
209
+ setupChannel(dc) { // Arrange for the data channel promise to resolve open, and do other setup.
210
+ // Called by explicit createChannel ('ours' opened) and also by ondatachannel ('theirs' opened).
211
+ const { label, readyState } = dc;
212
+ const isTheirs = readyState === 'open'; // Came via ondatachannel.
213
+ this.log('setupChannel:', label, dc.id, readyState, 'negotiated:', dc.negotiated);
214
+ const kind = isTheirs ? 'Theirs' : 'Ours';
212
215
  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]);
216
+ dc.onopen = async () => { // Idempotent (except for logging), if we do not bash dataChannePromises[label] multiple times.
217
+ this.log('channel onopen:', label, dc.id, readyState, 'negotiated:', dc.negotiated);
218
+ this[this.restrictablePromiseKey()][label]?.resolve(dc);
219
+ this[this.restrictablePromiseKey(kind)][label]?.resolve(dc);
216
220
  };
217
- if (dc.readyState === 'open') dc.onopen();
221
+ if (isTheirs) dc.onopen();
218
222
  return dc;
219
223
  }
220
224
  ondatachannel(dc) {
@@ -229,10 +233,18 @@ export class WebRTC {
229
233
  return this.setupChannel(this.pc.createDataChannel(name, {negotiated, id, ...options}));
230
234
  }
231
235
  dataChannelPromises = {};
232
- getDataChannelPromise(name = 'data') { // Promise to resolve when opened, WITHOUT actually creating one.
236
+ dataChannelOursPromises = {};
237
+ dataChannelTheirsPromises = {};
238
+ restrictablePromiseKey(restriction = '') { // The property in which store dataChannel promises of the specified restriction.
239
+ return `dataChannel${restriction}Promises`;
240
+ }
241
+ getDataChannelPromise(name = 'data', restriction = '') { // Promise to resolve when opened, WITHOUT actually creating one.
242
+ // The application can restrict this to being only a channel of 'ours' or 'theirs' (see setupChannel), which is useful for
243
+ // non-negotiated channels.
244
+ const key = this.restrictablePromiseKey(restriction);
233
245
  const {promise, resolve, reject} = Promise.withResolvers();
234
246
  Object.assign(promise, {resolve, reject});
235
- return this.dataChannelPromises[name] = promise;
247
+ return this[key][name] = promise;
236
248
  }
237
249
 
238
250
  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.1",
4
4
  "description": "Streamlined portable webrtc management of p2p and client2server.",
5
5
  "keywords": [
6
6
  "webrtc",
@@ -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,17 @@ 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
+ // maximums
79
+ // 1 channel pair, without negotiated on first signal: nodejs:83, firefox:150+, safari:85+ but gets confused with closing, chrome/edge:50(?)
80
+ // 50 works across the board with one channel pair
81
+ // On Safari (only), anything more than 32 pair starts to loose messages on the SECOND channel.
82
+ const nPairs = includeSecondChannel ? 32 : 62; //32;
72
83
  beforeAll(async function () {
73
84
  const start = Date.now();
74
- console.log('start setup');
85
+ console.log(new Date(), 'start setup', nPairs, 'pairs');
75
86
  for (let index = 0; index < nPairs; index++) {
76
87
  await setup({index});
77
88
  }
@@ -81,8 +92,10 @@ describe("WebRTC", function () {
81
92
  for (let index = 0; index < nPairs; index++) {
82
93
  it(`connects ${index}.`, function () {
83
94
  const {A, B} = connections[index];
84
- expect(A.data.readyState).toBe('open');
85
- expect(B.data.readyState).toBe('open');
95
+ expect(A.ours.readyState).toBe('open');
96
+ expect(B.ours.readyState).toBe('open');
97
+ expect(A.theirs.readyState).toBe('open');
98
+ expect(B.theirs.readyState).toBe('open');
86
99
  });
87
100
  it(`receives ${index}.`, async function () {
88
101
  const {A, B} = connections[index];
@@ -143,6 +156,7 @@ describe("WebRTC", function () {
143
156
  }
144
157
  describe("one side opens", function () {
145
158
  describe('non-negotiated', function () {
159
+ beforeAll(function () {console.log('one-sided non-negotiated'); });
146
160
  standardBehavior(async function ({index}) {
147
161
  const {A, B, bothOpen} = makePair({index});
148
162
  A.createChannel('data', {negotiated: false});
@@ -150,6 +164,7 @@ describe("WebRTC", function () {
150
164
  }, {includeConflictCheck: false, includeSecondChannel: false});
151
165
  });
152
166
  describe("negotiated on first signal", function () {
167
+ beforeAll(function () {console.log('one-sided negotiated'); });
153
168
  standardBehavior(async function ({index}) {
154
169
  const {A, B, bothOpen} = makePair({index});
155
170
  // There isn't really a direct, automated way to have one side open another with negotiated:true,
@@ -173,6 +188,7 @@ describe("WebRTC", function () {
173
188
  describe("simultaneous two-sided", function () {
174
189
  describe("negotiated single full-duplex-channel", function () {
175
190
  describe("impolite first", function () {
191
+ beforeAll(function () {console.log('two-sided negotiated impolite-first'); });
176
192
  standardBehavior(async function ({index}) {
177
193
  const {A, B, bothOpen} = makePair({index});
178
194
  A.createChannel("data", {negotiated: true});
@@ -181,6 +197,7 @@ describe("WebRTC", function () {
181
197
  });
182
198
  });
183
199
  describe("polite first", function () {
200
+ beforeAll(function () {console.log('two-sided negotiated polite-first');});
184
201
  standardBehavior(async function ({index}) {
185
202
  const {A, B, bothOpen} = makePair({index});
186
203
  B.createChannel("data", {negotiated: true});
@@ -190,18 +207,21 @@ describe("WebRTC", function () {
190
207
  });
191
208
  });
192
209
  describe("non-negotiated dual half-duplex channels", function () {
193
- const delay = 10;
210
+ const delay = 200;
211
+ const debug = false;
194
212
  describe("impolite first", function () {
213
+ beforeAll(function () {console.log('two-sided non-negotiated impolite-first');});
195
214
  standardBehavior(async function ({index}) {
196
- const {A, B, bothOpen} = makePair({delay, index});
215
+ const {A, B, bothOpen} = makePair({delay, index, debug});
197
216
  A.createChannel("data", {negotiated: false});
198
217
  B.createChannel("data", {negotiated: false});
199
218
  await bothOpen;
200
219
  });
201
220
  });
202
221
  describe("polite first", function () {
222
+ beforeAll(function () {console.log('two-sided non-negotiated polite-first');});
203
223
  standardBehavior(async function ({index}) {
204
- const {A, B, bothOpen} = makePair({delay, index});
224
+ const {A, B, bothOpen} = makePair({delay, index, debug});
205
225
  B.createChannel("data", {negotiated: false});
206
226
  A.createChannel("data", {negotiated: false});
207
227
  await bothOpen;