teleportxr 1.0.55 → 1.0.57
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/client/client.js +35 -21
- package/connections/webrtcconnection.js +45 -9
- package/connections/webrtcconnectionmanager.js +2 -1
- package/package.json +3 -2
- package/test/test_client_send_node_gate.js +129 -0
- package/test/test_data_channels_open_callback.js +69 -0
- package/test/test_webrtcconnection_send_gate.js +90 -0
package/client/client.js
CHANGED
|
@@ -73,19 +73,29 @@ class Client {
|
|
|
73
73
|
console.log("[T+"+this.elapsedMsSinceStart()+"ms] Connection state is "+newState.toString());
|
|
74
74
|
if(newState=="connected")
|
|
75
75
|
{
|
|
76
|
-
//this.webRtcConnection.sendGeometry("test");
|
|
77
76
|
this.webRtcConnected=true;
|
|
78
77
|
this.webRtcConnectedAtMs=Date.now();
|
|
79
|
-
console.log("[T+"+this.elapsedMsSinceStart()+"ms] WebRTC CONNECTED for client "+this.clientID+" —
|
|
80
|
-
//
|
|
81
|
-
//
|
|
82
|
-
|
|
78
|
+
console.log("[T+"+this.elapsedMsSinceStart()+"ms] WebRTC CONNECTED for client "+this.clientID+" — awaiting data channels.");
|
|
79
|
+
// Do NOT call UpdateStreaming() here. The PeerConnection reaching
|
|
80
|
+
// 'connected' does not guarantee that the geometry/reliable data
|
|
81
|
+
// channels are in 'open' state yet; sends issued now will throw
|
|
82
|
+
// InvalidStateError. Instead, UpdateStreaming is triggered from the
|
|
83
|
+
// dataChannelsOpen callback registered in StartStreaming().
|
|
83
84
|
}
|
|
84
85
|
else
|
|
85
86
|
{
|
|
86
87
|
this.webRtcConnected=false;
|
|
87
88
|
}
|
|
88
89
|
}
|
|
90
|
+
// Invoked once per WebRTC connection, the moment both reliable and geometry
|
|
91
|
+
// data channels are simultaneously in 'open' state. Kicks an immediate
|
|
92
|
+
// streaming tick so the first node/resource batch goes out without waiting
|
|
93
|
+
// for the next periodic interval (up to 1000 ms away).
|
|
94
|
+
onDataChannelsOpen()
|
|
95
|
+
{
|
|
96
|
+
console.log("[T+"+this.elapsedMsSinceStart()+"ms, conn+"+this.elapsedMsSinceConnected()+"ms] data channels open for client "+this.clientID+" — triggering immediate UpdateStreaming tick.");
|
|
97
|
+
setImmediate(this.UpdateStreaming.bind(this));
|
|
98
|
+
}
|
|
89
99
|
receivedMessageReliable(id,pkt)
|
|
90
100
|
{
|
|
91
101
|
var dataView=new DataView(pkt.data,0,1);
|
|
@@ -246,7 +256,8 @@ class Client {
|
|
|
246
256
|
this.clientID
|
|
247
257
|
,this.streamingConnectionStateChanged.bind(this)
|
|
248
258
|
,this.receivedMessageReliable.bind(this)
|
|
249
|
-
,this.receivedMessageUnreliable.bind(this)
|
|
259
|
+
,this.receivedMessageUnreliable.bind(this)
|
|
260
|
+
,this.onDataChannelsOpen.bind(this));
|
|
250
261
|
//.then(
|
|
251
262
|
// function(value) {myDisplayer(value);},
|
|
252
263
|
// function(error) {myDisplayer(error);}
|
|
@@ -486,21 +497,21 @@ class Client {
|
|
|
486
497
|
const MAX_NODE_SIZE=500;
|
|
487
498
|
const buffer = new ArrayBuffer(MAX_NODE_SIZE);
|
|
488
499
|
const nodeSize=node_encoder.encodeNode(node,buffer);
|
|
489
|
-
|
|
490
|
-
const view2 = new DataView(buffer, 0, nodeSize);
|
|
491
|
-
console.log("Sending node "+uid+" "+node.name+" to Client "+this.clientID+", size: "+nodeSize+" bytes");
|
|
500
|
+
const view2 = new DataView(buffer, 0, nodeSize);
|
|
492
501
|
if(!this.webRtcConnection)
|
|
493
502
|
{
|
|
494
503
|
console.error("this.webRtcConnection is null");
|
|
495
504
|
return;
|
|
496
505
|
}
|
|
497
|
-
if(!this.webRtcConnection.
|
|
506
|
+
if(!this.webRtcConnection.isGeometryOpen())
|
|
498
507
|
{
|
|
499
|
-
|
|
500
|
-
|
|
508
|
+
// Channel not yet open; leave the resource unmarked so it retries on
|
|
509
|
+
// the next UpdateStreaming tick without waiting for the timeout.
|
|
501
510
|
return;
|
|
502
511
|
}
|
|
503
|
-
this.
|
|
512
|
+
console.log("Sending node "+uid+" "+node.name+" to Client "+this.clientID+", size: "+nodeSize+" bytes");
|
|
513
|
+
if(this.webRtcConnection.sendGeometry(view2))
|
|
514
|
+
this.geometryService.EncodedResource(uid);
|
|
504
515
|
}
|
|
505
516
|
SendGenericResource(uid)
|
|
506
517
|
{
|
|
@@ -513,21 +524,21 @@ class Client {
|
|
|
513
524
|
const MAX_BUFFER_SIZE=resource.encodedSize();;
|
|
514
525
|
const buffer = new ArrayBuffer(MAX_BUFFER_SIZE);
|
|
515
526
|
const resourceSize=resource_encoder.EncodeResource(resource,buffer);
|
|
516
|
-
this.geometryService.EncodedResource(uid);
|
|
517
527
|
const view2 = new DataView(buffer, 0, resourceSize);
|
|
518
|
-
console.log("Sending resource "+uid+" "+resource.url+" to Client "+this.clientID+", size: "+resourceSize+" bytes");
|
|
519
528
|
if(!this.webRtcConnection)
|
|
520
529
|
{
|
|
521
530
|
console.error("this.webRtcConnection is null");
|
|
522
531
|
return;
|
|
523
532
|
}
|
|
524
|
-
if(!this.webRtcConnection.
|
|
533
|
+
if(!this.webRtcConnection.isGeometryOpen())
|
|
525
534
|
{
|
|
526
|
-
|
|
527
|
-
|
|
535
|
+
// Channel not yet open; leave the resource unmarked so it retries on
|
|
536
|
+
// the next UpdateStreaming tick without waiting for the timeout.
|
|
528
537
|
return;
|
|
529
538
|
}
|
|
530
|
-
this.
|
|
539
|
+
console.log("Sending resource "+uid+" "+resource.url+" to Client "+this.clientID+", size: "+resourceSize+" bytes");
|
|
540
|
+
if(this.webRtcConnection.sendGeometry(view2))
|
|
541
|
+
this.geometryService.EncodedResource(uid);
|
|
531
542
|
}
|
|
532
543
|
SendMesh(uid)
|
|
533
544
|
{
|
|
@@ -571,10 +582,13 @@ class Client {
|
|
|
571
582
|
offset+=core.UID_SIZE;
|
|
572
583
|
}
|
|
573
584
|
}
|
|
585
|
+
// Kick WebRTC negotiation off first so the createOffer / setLocalDescription
|
|
586
|
+
// / ICE gathering pipeline overlaps the signaling round-trip for the ack
|
|
587
|
+
// below. The C++ client buffers offers and candidates that arrive while
|
|
588
|
+
// it is still processing the ack, so ordering is safe.
|
|
589
|
+
this.StartStreaming();
|
|
574
590
|
var acknowledgeHandshakeCommand=new command.AcknowledgeHandshakeCommand;
|
|
575
591
|
this.SendCommand(acknowledgeHandshakeCommand);
|
|
576
|
-
// And now, setup is complete. On the next geometry update, we can send nodes/resources.
|
|
577
|
-
this.StartStreaming();
|
|
578
592
|
}
|
|
579
593
|
receiveReliableBinaryMessage(data){
|
|
580
594
|
const messageType=data[0];
|
|
@@ -56,7 +56,9 @@ class WebRtcConnection extends EventEmitter
|
|
|
56
56
|
this.messageReceivedReliableCb =options.messageReceivedReliable;
|
|
57
57
|
this.messageReceivedUnreliableCb =options.messageReceivedUnreliable;
|
|
58
58
|
this.connectionStateChangedCb=options.connectionStateChanged;
|
|
59
|
+
this.dataChannelsOpenCb =options.dataChannelsOpen;
|
|
59
60
|
this.sendConfigMessage =options.sendConfigMessage;
|
|
61
|
+
this._dataChannelsOpenFired =false;
|
|
60
62
|
|
|
61
63
|
this._onIceConnectionStateChange = this.onIceConnectionStateChange.bind(this);
|
|
62
64
|
this._onIceGatheringStateChange = this.onIceGatheringStateChange.bind(this);
|
|
@@ -348,25 +350,58 @@ class WebRtcConnection extends EventEmitter
|
|
|
348
350
|
this.state = 'closed';
|
|
349
351
|
this.emit('closed');
|
|
350
352
|
};
|
|
351
|
-
|
|
353
|
+
isGeometryOpen() {
|
|
354
|
+
return !!(this.geometryDataChannel && this.geometryDataChannel.readyState === 'open');
|
|
355
|
+
}
|
|
356
|
+
isReliableOpen() {
|
|
357
|
+
return !!(this.reliableDataChannel && this.reliableDataChannel.readyState === 'open');
|
|
358
|
+
}
|
|
359
|
+
// Invoked from each data channel's onopen. Fires the dataChannelsOpen callback
|
|
360
|
+
// exactly once per PeerConnection, the moment both reliable and geometry
|
|
361
|
+
// channels are in 'open' state. This lets Client.UpdateStreaming run as soon
|
|
362
|
+
// as the channels can accept traffic rather than waiting for the next
|
|
363
|
+
// periodic tick (up to 1 s away).
|
|
364
|
+
_handleDataChannelOpen(label) {
|
|
365
|
+
if (this._dataChannelsOpenFired)
|
|
366
|
+
return;
|
|
367
|
+
if (!this.isGeometryOpen() || !this.isReliableOpen())
|
|
368
|
+
return;
|
|
369
|
+
this._dataChannelsOpenFired = true;
|
|
370
|
+
if (this.dataChannelsOpenCb)
|
|
371
|
+
{
|
|
372
|
+
try { this.dataChannelsOpenCb(); }
|
|
373
|
+
catch (e) { console.error('dataChannelsOpenCb threw: '+e.message); }
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
// Returns true if the buffer was handed to the underlying transport, false if the
|
|
377
|
+
// channel was not in the 'open' state or send() threw. Callers must use the
|
|
378
|
+
// return value to gate any "this resource has been transmitted" bookkeeping;
|
|
379
|
+
// otherwise a dropped send leaves the resource marked Sent and it won't be
|
|
380
|
+
// retried until geometry_service.timeout_us elapses (default 10 s).
|
|
381
|
+
sendGeometry(buffer) {
|
|
382
|
+
if (!this.isGeometryOpen())
|
|
383
|
+
return false;
|
|
352
384
|
try {
|
|
353
|
-
|
|
385
|
+
this.geometryDataChannel.send(buffer);
|
|
386
|
+
return true;
|
|
354
387
|
}
|
|
355
388
|
catch(exception) {
|
|
356
|
-
|
|
389
|
+
console.error('datachannel.sendGeometry exception: '+exception.message);
|
|
390
|
+
return false;
|
|
357
391
|
}
|
|
358
|
-
}
|
|
359
|
-
isReliableOpen() {
|
|
360
|
-
return !!(this.reliableDataChannel && this.reliableDataChannel.readyState === 'open');
|
|
361
392
|
}
|
|
362
|
-
|
|
393
|
+
sendReliable(buffer) {
|
|
394
|
+
if (!this.isReliableOpen())
|
|
395
|
+
return false;
|
|
363
396
|
try {
|
|
364
397
|
this.reliableDataChannel.send(buffer);
|
|
398
|
+
return true;
|
|
365
399
|
}
|
|
366
400
|
catch(exception) {
|
|
367
|
-
|
|
401
|
+
console.error('datachannel.sendReliable exception: '+exception.message);
|
|
402
|
+
return false;
|
|
368
403
|
}
|
|
369
|
-
|
|
404
|
+
}
|
|
370
405
|
beforeOffer() {
|
|
371
406
|
|
|
372
407
|
this.videoDataChannel = this.createDataChannel("video",20);
|
|
@@ -425,6 +460,7 @@ class WebRtcConnection extends EventEmitter
|
|
|
425
460
|
|
|
426
461
|
dc.onopen = (event) => {
|
|
427
462
|
console.log('datachannel '+label+' open');
|
|
463
|
+
this._handleDataChannelOpen(label);
|
|
428
464
|
//dc.send('XXXX');
|
|
429
465
|
};
|
|
430
466
|
|
|
@@ -37,7 +37,7 @@ class WebRtcConnectionManager
|
|
|
37
37
|
this.deleteConnection(connection);
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
createConnection(clientID,connectionStateChangedCb,messageReceivedReliableCb,messageReceivedUnreliableCb)
|
|
40
|
+
createConnection(clientID,connectionStateChangedCb,messageReceivedReliableCb,messageReceivedUnreliableCb,dataChannelsOpenCb)
|
|
41
41
|
{
|
|
42
42
|
const existing = this.connections.get(clientID);
|
|
43
43
|
if (existing)
|
|
@@ -52,6 +52,7 @@ class WebRtcConnectionManager
|
|
|
52
52
|
options.messageReceivedReliable =messageReceivedReliableCb;
|
|
53
53
|
options.messageReceivedUnreliable =messageReceivedUnreliableCb;
|
|
54
54
|
options.connectionStateChanged =connectionStateChangedCb;
|
|
55
|
+
options.dataChannelsOpen =dataChannelsOpenCb;
|
|
55
56
|
const connection = new WebRtcConnection(clientID,options);
|
|
56
57
|
// We will not add a "closed" listener, because only the client object will be permitted to close its connection.
|
|
57
58
|
//this.createConnection = (clientID) => this.closedListeners.set(connection, this.closedListener);
|
package/package.json
CHANGED
|
@@ -11,11 +11,12 @@
|
|
|
11
11
|
"ws": "^8.13.0"
|
|
12
12
|
},
|
|
13
13
|
"scripts": {
|
|
14
|
-
"start": "node server.js"
|
|
14
|
+
"start": "node server.js",
|
|
15
|
+
"test": "node --test test/"
|
|
15
16
|
},
|
|
16
17
|
"name": "teleportxr",
|
|
17
18
|
"description": "Teleport Spatial Server on node.js",
|
|
18
|
-
"version": "1.0.
|
|
19
|
+
"version": "1.0.57",
|
|
19
20
|
"repository": {
|
|
20
21
|
"type": "git",
|
|
21
22
|
"url": "git+https://github.com/simul/teleport-nodejs.git"
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// Regression tests for Client.SendNode / Client.SendGenericResource. Before the
|
|
3
|
+
// fix, EncodedResource(uid) was called unconditionally before sendGeometry, so
|
|
4
|
+
// a failed send still marked the resource as transmitted and the
|
|
5
|
+
// geometry_service refused to retry it until its 10 s timeout elapsed.
|
|
6
|
+
|
|
7
|
+
const test = require('node:test');
|
|
8
|
+
const assert = require('node:assert');
|
|
9
|
+
const { Client } = require('../client/client');
|
|
10
|
+
const resources = require('../scene/resources');
|
|
11
|
+
|
|
12
|
+
function makeStubClient(opts) {
|
|
13
|
+
const c = Object.create(Client.prototype);
|
|
14
|
+
c.clientID = 7;
|
|
15
|
+
c.scene = {
|
|
16
|
+
GetNode: () => ({
|
|
17
|
+
name: 'stub-node',
|
|
18
|
+
encodeIntoDataView: (_dv, off) => off + 1,
|
|
19
|
+
}),
|
|
20
|
+
};
|
|
21
|
+
c.geometryService = {
|
|
22
|
+
EncodedResource: (uid) => { c._encodedCalls.push(uid); },
|
|
23
|
+
};
|
|
24
|
+
c._encodedCalls = [];
|
|
25
|
+
c.webRtcConnection = opts.webRtcConnection;
|
|
26
|
+
return c;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
test('SendNode does not mark resource encoded when geometry channel is not open', () => {
|
|
30
|
+
const c = makeStubClient({
|
|
31
|
+
webRtcConnection: {
|
|
32
|
+
isGeometryOpen: () => false,
|
|
33
|
+
sendGeometry: () => { throw new Error('sendGeometry must not be called when not open'); },
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
c.SendNode(42n);
|
|
37
|
+
assert.strictEqual(c._encodedCalls.length, 0);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test('SendNode marks resource encoded when sendGeometry returns true', () => {
|
|
41
|
+
const c = makeStubClient({
|
|
42
|
+
webRtcConnection: {
|
|
43
|
+
isGeometryOpen: () => true,
|
|
44
|
+
sendGeometry: () => true,
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
c.SendNode(42n);
|
|
48
|
+
assert.deepStrictEqual(c._encodedCalls, [42n]);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test('SendNode does not mark resource encoded when sendGeometry returns false', () => {
|
|
52
|
+
// Simulates the race where the channel was "open" at the isGeometryOpen()
|
|
53
|
+
// check but transitioned to closing/closed before send() ran (or wrtc
|
|
54
|
+
// returned false synchronously). The resource must remain unmarked so the
|
|
55
|
+
// next UpdateStreaming tick re-attempts it.
|
|
56
|
+
const c = makeStubClient({
|
|
57
|
+
webRtcConnection: {
|
|
58
|
+
isGeometryOpen: () => true,
|
|
59
|
+
sendGeometry: () => false,
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
c.SendNode(42n);
|
|
63
|
+
assert.strictEqual(c._encodedCalls.length, 0);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test('SendNode short-circuits when webRtcConnection is null', () => {
|
|
67
|
+
const c = makeStubClient({ webRtcConnection: null });
|
|
68
|
+
// Must not throw; must not record an encoded resource.
|
|
69
|
+
c.SendNode(42n);
|
|
70
|
+
assert.strictEqual(c._encodedCalls.length, 0);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
// SendGenericResource shares the same gating shape. Build a minimal Resource so
|
|
74
|
+
// the encoder doesn't blow up.
|
|
75
|
+
class StubResource {
|
|
76
|
+
constructor() {
|
|
77
|
+
this.url = 'stub://resource';
|
|
78
|
+
this.uid = 99n;
|
|
79
|
+
}
|
|
80
|
+
encodedSize() { return 64; }
|
|
81
|
+
encodeIntoDataView(_dv, off) { return off + 1; }
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
test('SendGenericResource does not mark resource encoded when channel is not open', () => {
|
|
85
|
+
const stub = new StubResource();
|
|
86
|
+
const originalGet = resources.GetResourceFromUid;
|
|
87
|
+
resources.GetResourceFromUid = () => stub;
|
|
88
|
+
try {
|
|
89
|
+
const c = makeStubClient({
|
|
90
|
+
webRtcConnection: {
|
|
91
|
+
isGeometryOpen: () => false,
|
|
92
|
+
sendGeometry: () => { throw new Error('must not be called'); },
|
|
93
|
+
},
|
|
94
|
+
});
|
|
95
|
+
c.SendGenericResource(99n);
|
|
96
|
+
assert.strictEqual(c._encodedCalls.length, 0);
|
|
97
|
+
}
|
|
98
|
+
finally {
|
|
99
|
+
resources.GetResourceFromUid = originalGet;
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test('SendGenericResource marks resource encoded only after a successful send', () => {
|
|
104
|
+
const stub = new StubResource();
|
|
105
|
+
const originalGet = resources.GetResourceFromUid;
|
|
106
|
+
resources.GetResourceFromUid = () => stub;
|
|
107
|
+
try {
|
|
108
|
+
const c = makeStubClient({
|
|
109
|
+
webRtcConnection: {
|
|
110
|
+
isGeometryOpen: () => true,
|
|
111
|
+
sendGeometry: () => true,
|
|
112
|
+
},
|
|
113
|
+
});
|
|
114
|
+
c.SendGenericResource(99n);
|
|
115
|
+
assert.deepStrictEqual(c._encodedCalls, [99n]);
|
|
116
|
+
|
|
117
|
+
const c2 = makeStubClient({
|
|
118
|
+
webRtcConnection: {
|
|
119
|
+
isGeometryOpen: () => true,
|
|
120
|
+
sendGeometry: () => false,
|
|
121
|
+
},
|
|
122
|
+
});
|
|
123
|
+
c2.SendGenericResource(99n);
|
|
124
|
+
assert.strictEqual(c2._encodedCalls.length, 0);
|
|
125
|
+
}
|
|
126
|
+
finally {
|
|
127
|
+
resources.GetResourceFromUid = originalGet;
|
|
128
|
+
}
|
|
129
|
+
});
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// Regression tests for _handleDataChannelOpen. After both reliable and geometry
|
|
3
|
+
// data channels are simultaneously in 'open' state, the dataChannelsOpenCb must
|
|
4
|
+
// fire exactly once for the lifetime of the PeerConnection — additional channel
|
|
5
|
+
// opens (e.g. the unreliable channel) must not retrigger it.
|
|
6
|
+
|
|
7
|
+
const test = require('node:test');
|
|
8
|
+
const assert = require('node:assert');
|
|
9
|
+
const WebRtcConnection = require('../connections/webrtcconnection');
|
|
10
|
+
|
|
11
|
+
function makeCtx() {
|
|
12
|
+
const ctx = Object.create(WebRtcConnection.prototype);
|
|
13
|
+
ctx._dataChannelsOpenFired = false;
|
|
14
|
+
return ctx;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
test('callback fires once when both required channels reach "open"', () => {
|
|
18
|
+
let calls = 0;
|
|
19
|
+
const ctx = makeCtx();
|
|
20
|
+
ctx.dataChannelsOpenCb = () => { calls++; };
|
|
21
|
+
ctx.geometryDataChannel = { readyState: 'connecting' };
|
|
22
|
+
ctx.reliableDataChannel = { readyState: 'connecting' };
|
|
23
|
+
|
|
24
|
+
// First channel opens — callback must not fire yet.
|
|
25
|
+
ctx.geometryDataChannel.readyState = 'open';
|
|
26
|
+
ctx._handleDataChannelOpen('geometry_unframed');
|
|
27
|
+
assert.strictEqual(calls, 0, 'callback fired with only geometry open');
|
|
28
|
+
|
|
29
|
+
// Second channel opens — callback fires.
|
|
30
|
+
ctx.reliableDataChannel.readyState = 'open';
|
|
31
|
+
ctx._handleDataChannelOpen('reliable');
|
|
32
|
+
assert.strictEqual(calls, 1, 'callback did not fire when both channels open');
|
|
33
|
+
|
|
34
|
+
// Later channels opening must not retrigger.
|
|
35
|
+
ctx._handleDataChannelOpen('unreliable');
|
|
36
|
+
ctx._handleDataChannelOpen('video');
|
|
37
|
+
assert.strictEqual(calls, 1, 'callback fired a second time on later channel open');
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test('callback never fires while only one of the required channels is open', () => {
|
|
41
|
+
let calls = 0;
|
|
42
|
+
const ctx = makeCtx();
|
|
43
|
+
ctx.dataChannelsOpenCb = () => { calls++; };
|
|
44
|
+
ctx.geometryDataChannel = { readyState: 'open' };
|
|
45
|
+
ctx.reliableDataChannel = { readyState: 'connecting' };
|
|
46
|
+
|
|
47
|
+
ctx._handleDataChannelOpen('geometry_unframed');
|
|
48
|
+
ctx._handleDataChannelOpen('video');
|
|
49
|
+
assert.strictEqual(calls, 0);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test('absent dataChannelsOpenCb does not throw when both channels open', () => {
|
|
53
|
+
const ctx = makeCtx();
|
|
54
|
+
ctx.geometryDataChannel = { readyState: 'open' };
|
|
55
|
+
ctx.reliableDataChannel = { readyState: 'open' };
|
|
56
|
+
// Must not throw.
|
|
57
|
+
ctx._handleDataChannelOpen('reliable');
|
|
58
|
+
assert.strictEqual(ctx._dataChannelsOpenFired, true);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test('callback that throws is caught and the fired flag remains set', () => {
|
|
62
|
+
const ctx = makeCtx();
|
|
63
|
+
ctx.dataChannelsOpenCb = () => { throw new Error('handler boom'); };
|
|
64
|
+
ctx.geometryDataChannel = { readyState: 'open' };
|
|
65
|
+
ctx.reliableDataChannel = { readyState: 'open' };
|
|
66
|
+
// Must not propagate the error to the caller.
|
|
67
|
+
ctx._handleDataChannelOpen('reliable');
|
|
68
|
+
assert.strictEqual(ctx._dataChannelsOpenFired, true);
|
|
69
|
+
});
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// Regression tests for the send-gating behaviour added to WebRtcConnection.
|
|
3
|
+
// Before the fix, sendGeometry/sendReliable swallowed InvalidStateError when
|
|
4
|
+
// the data channel's readyState was still 'connecting' but returned no signal
|
|
5
|
+
// to the caller, so client.js was free to mark resources as transmitted even
|
|
6
|
+
// though nothing had been written. The geometry_service then refused to retry
|
|
7
|
+
// them until its 10 s timeout elapsed.
|
|
8
|
+
//
|
|
9
|
+
// These tests don't construct a real RTCPeerConnection; they invoke the
|
|
10
|
+
// methods on a stub object that inherits from WebRtcConnection.prototype.
|
|
11
|
+
|
|
12
|
+
const test = require('node:test');
|
|
13
|
+
const assert = require('node:assert');
|
|
14
|
+
const WebRtcConnection = require('../connections/webrtcconnection');
|
|
15
|
+
|
|
16
|
+
function makeCtx() {
|
|
17
|
+
return Object.create(WebRtcConnection.prototype);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
test('sendGeometry returns false and does not call dc.send when channel is connecting', () => {
|
|
21
|
+
let sendCalls = 0;
|
|
22
|
+
const ctx = makeCtx();
|
|
23
|
+
ctx.geometryDataChannel = {
|
|
24
|
+
readyState: 'connecting',
|
|
25
|
+
send: () => { sendCalls++; },
|
|
26
|
+
};
|
|
27
|
+
const result = ctx.sendGeometry(new Uint8Array([1, 2, 3]));
|
|
28
|
+
assert.strictEqual(result, false);
|
|
29
|
+
assert.strictEqual(sendCalls, 0, 'send() must not be called while readyState is "connecting"');
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test('sendGeometry returns false when geometryDataChannel is missing entirely', () => {
|
|
33
|
+
const ctx = makeCtx();
|
|
34
|
+
assert.strictEqual(ctx.sendGeometry(new Uint8Array([1])), false);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test('sendGeometry returns true when channel is open and forwards the buffer', () => {
|
|
38
|
+
let captured = null;
|
|
39
|
+
const ctx = makeCtx();
|
|
40
|
+
ctx.geometryDataChannel = {
|
|
41
|
+
readyState: 'open',
|
|
42
|
+
send: (b) => { captured = b; },
|
|
43
|
+
};
|
|
44
|
+
const buf = new Uint8Array([4, 5, 6]);
|
|
45
|
+
assert.strictEqual(ctx.sendGeometry(buf), true);
|
|
46
|
+
assert.strictEqual(captured, buf);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test('sendGeometry returns false when an open channel throws inside send()', () => {
|
|
50
|
+
const ctx = makeCtx();
|
|
51
|
+
ctx.geometryDataChannel = {
|
|
52
|
+
readyState: 'open',
|
|
53
|
+
send: () => { throw new Error('simulated wrtc failure'); },
|
|
54
|
+
};
|
|
55
|
+
assert.strictEqual(ctx.sendGeometry(new Uint8Array([1])), false);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test('sendReliable returns false and does not call dc.send when channel is connecting', () => {
|
|
59
|
+
let sendCalls = 0;
|
|
60
|
+
const ctx = makeCtx();
|
|
61
|
+
ctx.reliableDataChannel = {
|
|
62
|
+
readyState: 'connecting',
|
|
63
|
+
send: () => { sendCalls++; },
|
|
64
|
+
};
|
|
65
|
+
assert.strictEqual(ctx.sendReliable(new Uint8Array([1])), false);
|
|
66
|
+
assert.strictEqual(sendCalls, 0);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test('sendReliable returns true when channel is open and forwards the buffer', () => {
|
|
70
|
+
let captured = null;
|
|
71
|
+
const ctx = makeCtx();
|
|
72
|
+
ctx.reliableDataChannel = {
|
|
73
|
+
readyState: 'open',
|
|
74
|
+
send: (b) => { captured = b; },
|
|
75
|
+
};
|
|
76
|
+
const buf = new Uint8Array([7, 8, 9]);
|
|
77
|
+
assert.strictEqual(ctx.sendReliable(buf), true);
|
|
78
|
+
assert.strictEqual(captured, buf);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test('isGeometryOpen reports true only when readyState equals "open"', () => {
|
|
82
|
+
const ctx = makeCtx();
|
|
83
|
+
assert.strictEqual(ctx.isGeometryOpen(), false, 'no channel');
|
|
84
|
+
ctx.geometryDataChannel = { readyState: 'connecting' };
|
|
85
|
+
assert.strictEqual(ctx.isGeometryOpen(), false, 'connecting');
|
|
86
|
+
ctx.geometryDataChannel.readyState = 'open';
|
|
87
|
+
assert.strictEqual(ctx.isGeometryOpen(), true, 'open');
|
|
88
|
+
ctx.geometryDataChannel.readyState = 'closing';
|
|
89
|
+
assert.strictEqual(ctx.isGeometryOpen(), false, 'closing');
|
|
90
|
+
});
|