@yz-social/webrtc 0.0.5 → 0.1.0

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,58 @@
1
+ describe('Player', function() {
2
+ let player;
3
+ let song;
4
+
5
+ beforeEach(function() {
6
+ player = new Player();
7
+ song = new Song();
8
+ });
9
+
10
+ it('should be able to play a Song', function() {
11
+ player.play(song);
12
+ expect(player.currentlyPlayingSong).toEqual(song);
13
+
14
+ // demonstrates use of custom matcher
15
+ expect(player).toBePlaying(song);
16
+ });
17
+
18
+ describe('when song has been paused', function() {
19
+ beforeEach(function() {
20
+ player.play(song);
21
+ player.pause();
22
+ });
23
+
24
+ it('should indicate that the song is currently paused', function() {
25
+ expect(player.isPlaying).toBeFalsy();
26
+
27
+ // demonstrates use of 'not' with a custom matcher
28
+ expect(player).not.toBePlaying(song);
29
+ });
30
+
31
+ it('should be possible to resume', function() {
32
+ player.resume();
33
+ expect(player.isPlaying).toBeTruthy();
34
+ expect(player.currentlyPlayingSong).toEqual(song);
35
+ });
36
+ });
37
+
38
+ // demonstrates use of spies to intercept and test method calls
39
+ it('tells the current song if the user has made it a favorite', function() {
40
+ spyOn(song, 'persistFavoriteStatus');
41
+
42
+ player.play(song);
43
+ player.makeFavorite();
44
+
45
+ expect(song.persistFavoriteStatus).toHaveBeenCalledWith(true);
46
+ });
47
+
48
+ //demonstrates use of expected exceptions
49
+ describe('#resume', function() {
50
+ it('should throw an exception if song is already playing', function() {
51
+ player.play(song);
52
+
53
+ expect(function() {
54
+ player.resume();
55
+ }).toThrowError('song is already playing');
56
+ });
57
+ });
58
+ });
@@ -0,0 +1,15 @@
1
+ beforeEach(function () {
2
+ jasmine.addMatchers({
3
+ toBePlaying: function () {
4
+ return {
5
+ compare: function (actual, expected) {
6
+ const player = actual;
7
+
8
+ return {
9
+ pass: player.currentlyPlayingSong === expected && player.isPlaying
10
+ };
11
+ }
12
+ };
13
+ }
14
+ });
15
+ });
@@ -0,0 +1,22 @@
1
+ class Player {
2
+ play(song) {
3
+ this.currentlyPlayingSong = song;
4
+ this.isPlaying = true;
5
+ }
6
+
7
+ pause() {
8
+ this.isPlaying = false;
9
+ }
10
+
11
+ resume() {
12
+ if (this.isPlaying) {
13
+ throw new Error('song is already playing');
14
+ }
15
+
16
+ this.isPlaying = true;
17
+ }
18
+
19
+ makeFavorite() {
20
+ this.currentlyPlayingSong.persistFavoriteStatus(true);
21
+ }
22
+ }
@@ -0,0 +1,6 @@
1
+ class Song {
2
+ persistFavoriteStatus(value) {
3
+ // something complicated
4
+ throw new Error('not yet implemented');
5
+ }
6
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yz-social/webrtc",
3
- "version": "0.0.5",
3
+ "version": "0.1.0",
4
4
  "description": "Streamlined portable webrtc management of p2p and client2server.",
5
5
  "keywords": [
6
6
  "webrtc",
@@ -20,10 +20,7 @@
20
20
  "./router": "./routes/index.js"
21
21
  },
22
22
  "scripts": {
23
- "test": "(npm run test-server &); npm run test-only; npm run stop-server",
24
- "test-only": "npx jasmine",
25
- "test-server": "node testServer.js",
26
- "stop-server": "pkill webrtcTestServer"
23
+ "test": "npx jasmine"
27
24
  },
28
25
  "dependencies": {
29
26
  "@roamhq/wrtc": "^0.9.1"
package/spec/test.html ADDED
@@ -0,0 +1,14 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <link rel="stylesheet" href="../jasmine-standalone-5.7.1/lib/jasmine-5.7.1/jasmine.css">
5
+ <script src="../jasmine-standalone-5.7.1/lib/jasmine-5.7.1/jasmine.js"></script>
6
+ <script src="../jasmine-standalone-5.7.1/lib/jasmine-5.7.1/jasmine-html.js"></script>
7
+ <script src="../jasmine-standalone-5.7.1/lib/jasmine-5.7.1/boot0.js"></script>
8
+ <!-- optional: include a file here that configures the Jasmine env -->
9
+ <script src="../jasmine-standalone-5.7.1/lib/jasmine-5.7.1/boot1.js"></script>
10
+ <script type="module">
11
+ import './webrtcSpec.js';
12
+ </script>
13
+ </head>
14
+ </html>
@@ -1,158 +1,213 @@
1
1
  const { describe, it, expect, beforeAll, afterAll, beforeEach, afterEach} = globalThis; // For linters.
2
- import { WebRTC, WebRTCBase } from '../index.js';
3
-
4
- class DirectSignaling extends WebRTCBase {
5
- signal(type, message) { // Just invoke the method directly on the otherSide.
6
- this.otherSide[type](message);
7
- }
8
- signals = [];
9
- }
10
- function delay(ms) {
11
- return new Promise(resolve => setTimeout(resolve, ms));
12
- }
13
- async function fetchSignals(url, signalsToSend) {
14
- const response = await fetch(url, {
15
- method: 'POST',
16
- headers: { 'Content-Type': 'application/json' },
17
- body: JSON.stringify(signalsToSend)
18
- });
19
- return await response.json();
20
- }
2
+ import { WebRTC } from '../index.js';
21
3
 
22
4
  describe("WebRTC", function () {
23
-
24
- describe("connection between two peers on the same computer", function () {
25
- const channelName = 'test';
26
- function test(Kind, connect) {
27
- describe(Kind.name, function () {
28
- let a = {}, b = {}; let calledCloseA = 0, calledCloseB = 0;
29
- beforeAll(async function () {
30
- const debug = false;
31
- a.connection = Kind.ensure({serviceLabel: 'A'+Kind.name, debug});
32
- b.connection = Kind.ensure({serviceLabel: 'B'+Kind.name, debug});
33
-
34
- await connect(a, b);
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
+ let connections = [];
12
+ describe("direct in-process signaling", function () {
13
+ function makePair({debug = false, delay = 0, index = 0} = {}) {
14
+ //const configuration = { iceServers: [] };
15
+ const configuration = { iceServers: WebRTC.iceServers };
16
+ const A = new WebRTC({name: `A (impolite) ${index}`, polite: false, debug, configuration});
17
+ 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++;
35
29
 
36
- a.testChannel = await a.dataChannelPromise;
37
- b.testChannel = await b.dataChannelPromise;
38
- a.testChannel.addEventListener('close', () => { calledCloseA++; });
39
- b.testChannel.addEventListener('close', () => { calledCloseB++; });
30
+ // Subtle:
31
+ //
32
+ // An explicit createChannel() arranges for the promise created by getDataChannelPromise() of the same name to
33
+ // resolve on that side of the connection when the channel is open. The other side will internally receive a
34
+ // datachannel event and we arrange for that side to then also resolve a getDataChannelPromise for that name.
35
+ //
36
+ // If only wide side creates the channel, things work out smoothly. Each side ends up resolving the promise for a
37
+ // channel of the given name. The id is as defaulted or specified if negotiated:true, else an internally determined
38
+ // unique id is used.
39
+ //
40
+ // Things are also smooth if negotiated:true and both sides call createChannel simultaneously, such that there
41
+ // are overlappaing offers. One side backs down and there ends up being one channel with the specified id. (The default
42
+ // id will be the same on both sides IFF any multiple data channels are created in the same order on both sides.)
43
+ //
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
46
+ // 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
48
+ // 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].
40
53
 
41
- await a.connection.reportConnection();
42
- await b.connection.reportConnection();
43
- });
44
- afterAll(async function () {
45
- a.connection.close();
46
- expect(a.connection.peer.connectionState).toBe('new');
47
- await delay(10); // Yield to allow the other side to close.
48
- expect(b.connection.peer.connectionState).toBe('new');
49
- expect(calledCloseA).toBe(1);
50
- expect(calledCloseB).toBe(1);
51
- });
52
- it("changes state appropriately.", async function () {
53
- expect(await a.dataChannelPromise).toBeTruthy();
54
- expect(await b.dataChannelPromise).toBeTruthy();
55
- expect(a.connection.peer.connectionState).toBe('connected');
56
- expect(b.connection.peer.connectionState).toBe('connected');
57
- expect(a.connection.protocol).toBe(b.connection.protocol);
58
- expect(a.connection.protocol).toBe('udp');
59
- // In this case, since both are on the same machine:
60
- expect(a.connection.candidateType).toBe(b.connection.candidateType);
61
- expect(a.connection.candidateType).toBe('host');
62
-
63
- });
64
- it("sends data over raw channel.", async function () {
65
- const aReceived = new Promise(resolve => a.testChannel.onmessage = event => resolve(event.data));
66
- const bReceived = new Promise(resolve => b.testChannel.onmessage = event => resolve(event.data));
67
- a.testChannel.send("forty-two");
68
- b.testChannel.send("17");
69
- expect(await aReceived).toBe("17");
70
- expect(await bReceived).toBe("forty-two");
71
- });
72
- describe("second channel", function () {
73
- beforeAll(async function () {
74
- const name2 = 'channel2';
75
- a.promise2 = a.connection.ensureDataChannel(name2);
76
- b.promise2 = b.connection.ensureDataChannel(name2);
77
- a.channel2 = await a.promise2;
78
- b.channel2 = await b.promise2;
54
+ if (!delay) return dc.send(`Hello from ${webrtc.name}`);
55
+ await WebRTC.delay(delay);
56
+ webrtc.data.send(`Hello from ${webrtc.name}`);
57
+ return null;
58
+ }
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.
62
+ if (direct) {
63
+ A.signal = message => B.onSignal(message);
64
+ B.signal = message => A.onSignal(message);
65
+ } else {
66
+ A.transferSignals = messages => B.respond(messages);
67
+ B.transferSignals = messages => A.respond(messages);
68
+ }
69
+ return connections[index] = {A, B, bothOpen: Promise.all([aOpen, bOpen])};
70
+ }
71
+ function standardBehavior(setup, {includeConflictCheck = isBrowser, includeSecondChannel = true} = {}) {
72
+ beforeAll(async function () {
73
+ const start = Date.now();
74
+ console.log('start setup');
75
+ for (let index = 0; index < nPairs; index++) {
76
+ await setup({index});
77
+ }
78
+ //await Promise.all(connections.map(connection => connection.bothOpen));
79
+ console.log('end setup', Date.now() - start);
80
+ }, nPairs * 1e3);
81
+ for (let index = 0; index < nPairs; index++) {
82
+ it(`connects ${index}.`, function () {
83
+ const {A, B} = connections[index];
84
+ expect(A.data.readyState).toBe('open');
85
+ expect(B.data.readyState).toBe('open');
86
+ });
87
+ it(`receives ${index}.`, async function () {
88
+ const {A, B} = connections[index];
89
+ await B.gotData;
90
+ expect(B.receivedMessageCount).toBe(A.sentMessageCount);
91
+ await A.gotData;
92
+ expect(A.receivedMessageCount).toBe(B.sentMessageCount);
93
+ });
94
+ it(`learns of one open ${index}.`, function () {
95
+ const {A, B} = connections[index];
96
+ expect(A.sentMessageCount).toBe(1);
97
+ expect(B.sentMessageCount).toBe(1);
98
+ });
99
+ if (includeSecondChannel) {
100
+ it(`handles second channel ${index}.`, async function () {
101
+ const {A, B} = connections[index];
102
+ const aOpen = A.getDataChannelPromise('second');
103
+ const bOpen = B.getDataChannelPromise('second');
104
+ const a = A.createChannel('second', {negotiated: true});
105
+ const b = B.createChannel('second', {negotiated: true});
106
+ const dca = await aOpen;
107
+ let gotit = new Promise(resolve => {
108
+ dca.onmessage = event => resolve(event.data);
109
+ });
110
+ const dcb = await bOpen;
111
+ const start = Date.now();
112
+ dcb.send('message');
113
+ expect(await Promise.race([gotit, WebRTC.delay(2e3, 'timeout')])).toBe('message');
79
114
  });
80
- afterAll(async function () {
81
- a.channel2.close();
115
+ }
116
+ if (includeConflictCheck) {
117
+ it(`politely ignores a conflict ${index}.`, function () {
118
+ const {A, B} = connections[index];
119
+ expect(A.rolledBack).toBeFalsy();
120
+ expect(B.rolledBack).toBeTruthy(); // timing dependent, but useful for debugging
121
+ });
122
+ }
123
+ }
124
+ afterAll(async function () {
125
+ const start = Date.now();
126
+ console.log('start teardown');
127
+ const promises = [];
128
+ for (let index = 0; index < nPairs; index++) {
129
+ const {A, B} = connections[index];
130
+ let promise = A.close().then(async apc => {
131
+ expect(apc.connectionState).toBe('closed'); // Only on the side that explicitly closed.
132
+ expect(apc.signalingState).toBe('closed');
133
+ const bpc = await B.closed; // Waiting for B to notice.
134
+ 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');
82
137
  });
83
- it("handles data.", async function () {
84
- const aReceived = new Promise(resolve => a.channel2.onmessage = event => resolve(event.data));
85
- const bReceived = new Promise(resolve => b.channel2.onmessage = event => resolve(event.data));
86
- a.channel2.send("red");
87
- b.channel2.send("blue");
88
- expect(await aReceived).toBe("blue");
89
- expect(await bReceived).toBe("red");
90
- }, 10e3);
91
- });
92
- });
138
+ promises.push(promise);
139
+ }
140
+ await Promise.all(promises);
141
+ console.log('end teardown', Date.now() - start);
142
+ }, Math.max(30e3, 1e3 * nPairs));
93
143
  }
94
- test(DirectSignaling, async (a, b) => {
95
- a.connection.otherSide = b.connection;
96
- b.connection.otherSide = a.connection;
97
- a.dataChannelPromise = a.connection.ensureDataChannel(channelName);
98
- // Give the other side an empty list of signals (else we get two offers and no answer),
99
- // but yield for the a's offer to be directly transmitted.
100
- b.dataChannelPromise = b.connection.ensureDataChannel(channelName, {}, await []);
101
- });
102
- test(WebRTC, async (a, b) => {
103
- a.dataChannelPromise = a.connection.ensureDataChannel(channelName);
104
- await a.connection.signalsReady; // await for offer
105
- b.dataChannelPromise = b.connection.ensureDataChannel(channelName, {}, a.connection.signals);
106
- await b.connection.signalsReady; // await answer
107
- await a.connection.connectVia(signals => b.connection.respond(signals)); // Transmits signals back and forth.
108
- });
109
- });
110
-
111
- describe("connection to server", function () {
112
- describe("using NG ICE", function () {
113
- let connection, dataChannel;
114
- beforeAll(async function () {
115
- connection = WebRTC.ensure({serviceLabel: 'Client'});
116
- const ready = connection.signalsReady;
117
- const dataChannelPromise = connection.ensureDataChannel('echo');
118
- await ready;
119
- connection.connectVia(signals => fetchSignals("http://localhost:3000/test/data/echo/foo", signals));
120
- dataChannel = await dataChannelPromise;
121
- });
122
- afterAll(function () {
123
- connection.close();
144
+ describe("one side opens", function () {
145
+ describe('non-negotiated', function () {
146
+ standardBehavior(async function ({index}) {
147
+ const {A, B, bothOpen} = makePair({index});
148
+ A.createChannel('data', {negotiated: false});
149
+ await bothOpen;
150
+ }, {includeConflictCheck: false, includeSecondChannel: false});
124
151
  });
125
- it("sends and receives data", async function () {
126
- const echoPromise = new Promise(resolve => dataChannel.onmessage = event => resolve(event.data));
127
- dataChannel.send('hello');
128
- expect(await echoPromise).toBe('hello');
152
+ describe("negotiated on first signal", function () {
153
+ standardBehavior(async function ({index}) {
154
+ const {A, B, bothOpen} = makePair({index});
155
+ // There isn't really a direct, automated way to have one side open another with negotiated:true,
156
+ // because the receiving RTCPeerConnection does not fire 'datachannel' when the sender was negotiated:true.
157
+ // However, what the app can do is wake up and create an explicit createChannel when it first receives an
158
+ // unsolicited offer.
159
+ let awake = false;
160
+ A.signal = async msg => {
161
+ if (!awake) {
162
+ awake = true;
163
+ B.createChannel("data", {negotiated: true});
164
+ }
165
+ B.onSignal(msg);
166
+ };
167
+ A.createChannel('data', {negotiated: true});
168
+ await bothOpen;
169
+ }, {includeConflictCheck: false});
129
170
  });
130
171
  });
131
- });
132
172
 
133
- describe("capacity", function () {
134
- const pairs = [];
135
- const nPairs = 76; // NodeJS fails after 152 webrtc peers (76 pairs) in series. In parallel, it is only up through 67 pairs.
136
- beforeAll(async function () {
137
- const promises = [];
138
- for (let i = 0; i < nPairs; i++) {
139
- const a = {}, b = {};
140
- a.connection = new WebRTC({label: `initiator-${i}`});
141
- b.connection = new WebRTC({label: `receiver-${i}`});
142
- let aPromise = a.connection.signalsReady;
143
- a.dataChannelPromise = a.connection.ensureDataChannel('capacity');
144
- const aSignals = await aPromise;
145
- await a.connection.connectVia(signals => b.connection.respond(signals))
146
- .then(() => pairs.push([a, b]));
147
- }
148
- await Promise.all(promises);
149
- });
150
- afterAll(async function () {
151
- for (const [a] of pairs) a.connection.close();
152
- await delay(2e3);
153
- });
154
- it("is pretty big.", function () {
155
- expect(pairs.length).toBe(nPairs);
173
+ describe("simultaneous two-sided", function () {
174
+ describe("negotiated single full-duplex-channel", function () {
175
+ describe("impolite first", function () {
176
+ standardBehavior(async function ({index}) {
177
+ const {A, B, bothOpen} = makePair({index});
178
+ A.createChannel("data", {negotiated: true});
179
+ B.createChannel("data", {negotiated: true});
180
+ await bothOpen;
181
+ });
182
+ });
183
+ describe("polite first", function () {
184
+ standardBehavior(async function ({index}) {
185
+ const {A, B, bothOpen} = makePair({index});
186
+ B.createChannel("data", {negotiated: true});
187
+ A.createChannel("data", {negotiated: true});
188
+ await bothOpen;
189
+ });
190
+ });
191
+ });
192
+ describe("non-negotiated dual half-duplex channels", function () {
193
+ const delay = 10;
194
+ describe("impolite first", function () {
195
+ standardBehavior(async function ({index}) {
196
+ const {A, B, bothOpen} = makePair({delay, index});
197
+ A.createChannel("data", {negotiated: false});
198
+ B.createChannel("data", {negotiated: false});
199
+ await bothOpen;
200
+ });
201
+ });
202
+ describe("polite first", function () {
203
+ standardBehavior(async function ({index}) {
204
+ const {A, B, bothOpen} = makePair({delay, index});
205
+ B.createChannel("data", {negotiated: false});
206
+ A.createChannel("data", {negotiated: false});
207
+ await bothOpen;
208
+ });
209
+ });
210
+ });
156
211
  });
157
212
  });
158
213
  });
@@ -1,144 +0,0 @@
1
- import { WebRTCPeerEvents } from './peerevents.js';
2
-
3
- // Data Channels:
4
- // -
5
- // - Send/receive, including the break-up of long messages and re-assembly.
6
-
7
- export class WebRTCDataChannels extends WebRTCPeerEvents {
8
-
9
- // Negotiated channels use specific integers on both sides, starting with this number.
10
- // We do not start at zero because the non-negotiated channels (as used on server relays) generate their
11
- // own ids starting with 0, and we don't want to conflict.
12
- // The spec says these can go to 65,534, but I find that starting greater than the value here gives errors.
13
- // As of 7/6/25, current evergreen browsers work with 1000 base, but Firefox fails in our case (10 negotatiated channels)
14
- // if any ids are 256 or higher.
15
- static BASE_CHANNEL_ID = 125;
16
- channelId = this.constructor.BASE_CHANNEL_ID;
17
- get hasStartedConnecting() {
18
- return this.channelId > this.constructor.BASE_CHANNEL_ID;
19
- }
20
- async ensureDataChannel(channelName, channelOptions = {}, signals = null) { // Return a promise for an open data channel on this connection.
21
- // Much of this machinery is for ealing with the first data channel on either side in the case that the connection as a whole
22
- // has not yet been made.
23
-
24
- const hasStartedConnecting = this.hasStartedConnecting; // True for first channel only. Must ask before incrementing id.
25
- const id = this.channelId++; // This and everything leading up to it must be synchronous on this instance, so that id assignment is deterministic.
26
- const negotiated = this.multiplex && hasStartedConnecting;
27
- const allowOtherSideToCreate = !hasStartedConnecting /*!negotiated*/ && !!signals; // Only the 0th with signals waits passively.
28
- // signals can be nullish, in which case the real signals will have to be assigned later. This allows the data channel to be started (and to consume
29
- // a channelId) synchronously, but the promise won't resolve until the real signals are supplied later. This is
30
- // useful in multiplexing an ordered series of data channels on an ANSWER connection, where the data channels must
31
- // match up with an OFFER connection on a peer. This works because of the wonderful happenstance that answer connections
32
- // getDataChannelPromise (which doesn't require the connection to yet be open) rather than createDataChannel (which would
33
- // require the connection to already be open).
34
- const useSignals = !hasStartedConnecting && signals?.length;
35
- const options = negotiated ? {id, negotiated, ...channelOptions} : channelOptions;
36
- this.log({label: this.label, channelName, multiplex: this.multiplex, hasStartedConnecting, id, negotiated, allowOtherSideToCreate, useSignals, options});
37
- if (hasStartedConnecting) {
38
- await this.connected; // Before creating promise.
39
- // I sometimes encounter a bug in Safari in which ONE of the channels created soon after connection gets stuck in
40
- // the connecting readyState and never opens. Experimentally, this seems to be robust.
41
- //
42
- // Note to self: If it should turn out that we still have problems, try serializing the calls to peer.createDataChannel
43
- // so that there isn't more than one channel opening at a time.
44
- await new Promise(resolve => setTimeout(resolve, 100));
45
- } else if (useSignals) {
46
- this.signals = signals;
47
- }
48
- const promise = allowOtherSideToCreate ?
49
- this.getDataChannelPromise(channelName) :
50
- this.createDataChannel(channelName, options);
51
- return await promise;
52
- }
53
-
54
- createDataChannel(label = "data", channelOptions = {}) { // Promise resolves when the channel is open (which will be after any needed negotiation).
55
- return new Promise(resolve => {
56
- this.log('create data-channel', label, channelOptions);
57
- let channel = this.peer.createDataChannel(label, channelOptions);
58
- this.noteChannel(channel, 'explicit'); // Noted even before opened.
59
- // The channel may have already been opened on the other side. In this case, all browsers fire the open event anyway,
60
- // but wrtc (i.e., on nodeJS) does not. So we have to explicitly check.
61
- switch (channel.readyState) {
62
- case 'open':
63
- setTimeout(() => resolve(channel), 10);
64
- break;
65
- case 'connecting':
66
- channel.onopen = _ => resolve(channel);
67
- break;
68
- default:
69
- throw new Error(`Unexpected readyState ${channel.readyState} for data channel ${label}.`);
70
- }
71
- });
72
- }
73
- waitingChannels = {}; // channelName => resolvers
74
- getDataChannelPromise(label = "data") { // Resolves to an open data channel.
75
- return new Promise(resolve => {
76
- this.log('promise data-channel', label);
77
- this.waitingChannels[label] = resolve;
78
- });
79
- }
80
- resetPeer() { // Reset a 'connected' property that promised to resolve when opened, and track incoming datachannels.
81
- super.resetPeer();
82
- this.connected = new Promise(resolve => { // this.connected is a promise that resolves when we are.
83
- this.peer.addEventListener('connectionstatechange', event => {
84
- if (this.peer.connectionState === 'connected') {
85
- resolve(true);
86
- }
87
- });
88
- });
89
- this.peer.addEventListener('datachannel', event => { // Resolve promise made with getDataChannelPromise().
90
- const channel = event.channel;
91
- const label = channel.label;
92
- const waiting = this.waitingChannels[label];
93
- this.noteChannel(channel, 'datachannel event', waiting); // Regardless of whether we are waiting.
94
- if (!waiting) return; // Might not be explicitly waiting. E.g., routers.
95
- delete this.waitingChannels[label];
96
- waiting(channel);
97
- });
98
- }
99
-
100
- // TODO: (Optionaly?) Set up generic jsonrpc on each channel, along with serialization and fragmentation/assembly for long messages. (See collections/serializer)
101
-
102
-
103
- // We need to know if there are open data channels. There is a proposal and even an accepted PR for RTCPeerConnection.getDataChannels(),
104
- // https://github.com/w3c/webrtc-extensions/issues/110
105
- // but it hasn't been deployed everywhere yet. So we'll need to keep our own count.
106
- // Alas, a count isn't enough, because we can open stuff, and the other side can open stuff, but if it happens to be
107
- // the same "negotiated" id, it isn't really a different channel. (https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/datachannel_event
108
- dataChannels = new Map(); // channelName => Open channel objects.
109
- reportChannels() { // Return a report string useful for debugging.
110
- const entries = Array.from(this.dataChannels.entries());
111
- const kv = entries.map(([k, v]) => `${k}:${v.id}`);
112
- return `${this.dataChannels.size}/${kv.join(', ')}`;
113
- }
114
- noteChannel(channel, source, waiting) { // Bookkeep open channel and return it.
115
- // Emperically, with multiplex false: // 18 occurrences, with id=null|0|1 as for eventchannel or createDataChannel
116
- // Apparently, without negotiation, id is initially null (regardless of options.id), and then assigned to a free value during opening
117
- const key = channel.label; //fixme channel.id === null ? 1 : channel.id;
118
- const existing = this.dataChannels.get(key);
119
- this.log('got data-channel', source, key, channel.readyState, 'existing:', existing, 'waiting:', waiting);
120
- this.dataChannels.set(key, channel);
121
- channel.addEventListener('close', event => { // Close whole connection when no more data channels or streams.
122
- this.dataChannels.delete(key);
123
- // If there's nothing open, close the connection.
124
- if (this.dataChannels.size) return;
125
- if (this.peer.getSenders().length) return;
126
- this.close();
127
- });
128
- return channel;
129
- }
130
- close(removeConnection = true) {
131
- this.channelId = this.constructor.BASE_CHANNEL_ID;
132
- super.close();
133
- if (removeConnection) this.constructor.connections.delete(this.serviceLabel);
134
- // If the webrtc implementation closes the data channels before the peer itself, then this.dataChannels will be empty.
135
- // But if not (e.g., status 'failed' or 'disconnected' on Safari), then let us explicitly close them so that Synchronizers know to clean up.
136
- for (const channel of this.dataChannels.values()) {
137
- if (channel.readyState !== 'open') continue;
138
- // It appears that in Safari (18.5) for a call to channel.close() with the connection already internally closed, Safari
139
- // will set channel.readyState to 'closing', but NOT fire the closed or closing event. So we have to dispatch it ourselves.
140
- //channel.close();
141
- channel.dispatchEvent(new Event('close'));
142
- }
143
- }
144
- }