teleportxr 1.0.94 → 1.0.95
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
CHANGED
|
@@ -125,6 +125,13 @@ class Client {
|
|
|
125
125
|
else
|
|
126
126
|
{
|
|
127
127
|
this.webRtcConnected=false;
|
|
128
|
+
// Rebaseline the connect-attempt clock so hasWebRtcConnectionTimedOut() gives the
|
|
129
|
+
// low-level WebRtcConnection.reconnect() (webrtcconnection.js) a fresh
|
|
130
|
+
// WEBRTC_CONNECT_TIMEOUT_MS window to recover, instead of comparing against the
|
|
131
|
+
// original connection-start time from potentially many seconds/minutes earlier —
|
|
132
|
+
// which would otherwise make any post-connect ICE failure time out almost instantly
|
|
133
|
+
// on the next UpdateStreaming() tick and force-disconnect the client mid-reconnect.
|
|
134
|
+
this.webRtcConnectionInitiatedAtMs=Date.now();
|
|
128
135
|
if (this.webRtcConnection)
|
|
129
136
|
{
|
|
130
137
|
try { this.webRtcConnection.stopSceneAudio(); } catch (e) {}
|
|
@@ -111,14 +111,22 @@ class GeometryService {
|
|
|
111
111
|
// this client should stream node uid.
|
|
112
112
|
var res = GeometryService.GetOrCreateTrackedResource(uid);
|
|
113
113
|
var index = clientIDToIndex.get(this.clientID);
|
|
114
|
+
if (res.clientNeeds.get(index)) {
|
|
115
|
+
// Already streaming for this client — nothing to do.
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
114
118
|
res.clientNeeds.set(index, true);
|
|
115
119
|
// Add to the list of nodes this client should eventually receive:
|
|
116
120
|
this.nodesToStreamEventually.add(uid);
|
|
117
121
|
}
|
|
118
122
|
UnstreamNode(uid) {
|
|
119
123
|
var index = clientIDToIndex.get(this.clientID);
|
|
120
|
-
|
|
121
|
-
|
|
124
|
+
var res = GeometryService.trackedResources.get(uid);
|
|
125
|
+
if (res) {
|
|
126
|
+
if (!res.clientNeeds.get(index)) {
|
|
127
|
+
// Already unstreamed for this client — nothing to do.
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
122
130
|
res.clientNeeds.set(index, false);
|
|
123
131
|
}
|
|
124
132
|
// Should certainly be in this set:
|
|
@@ -226,6 +226,14 @@ class WebRtcConnection extends EventEmitter
|
|
|
226
226
|
const pc = this.peerConnection;
|
|
227
227
|
try
|
|
228
228
|
{
|
|
229
|
+
// Hand the client the same ICE server list (including any TURN entries) this
|
|
230
|
+
// connection is using, so the client doesn't need TURN credentials hardcoded
|
|
231
|
+
// or locally configured — this server is the single source of truth. Sent
|
|
232
|
+
// before the offer so it's ready by the time the client builds its
|
|
233
|
+
// RTCConfiguration.
|
|
234
|
+
const iceServersMessage = JSON.stringify({"teleport-signal-type": "ice-servers", "iceServers": this.iceServers});
|
|
235
|
+
this.sendConfigMessage(this.id, iceServersMessage);
|
|
236
|
+
|
|
229
237
|
const offer = await pc.createOffer();
|
|
230
238
|
if (this.peerConnection !== pc) return;
|
|
231
239
|
// Rename each node-audio m-line's mid to the emitting node's uid
|
package/package.json
CHANGED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// Regression tests for GeometryService.StreamNode / UnstreamNode idempotency.
|
|
3
|
+
//
|
|
4
|
+
// Before this fix, UnstreamNode unconditionally rewrote the clientNeeds bit,
|
|
5
|
+
// deleted from nodesToStreamEventually/streamedNodes, and logged — even when
|
|
6
|
+
// the node was already unstreamed for this client. A caller that re-evaluates
|
|
7
|
+
// a streaming decision every tick (e.g. distance-based visibility in
|
|
8
|
+
// teleport-nodejs-server-example's CustomClient.ProcessNodePoses) would then
|
|
9
|
+
// log and touch shared state on every tick it stayed on one side of the
|
|
10
|
+
// threshold, rather than only on the actual transition. StreamNode had the
|
|
11
|
+
// same gap in the other direction. Both now return early — no state writes,
|
|
12
|
+
// no log — when the call would not change anything.
|
|
13
|
+
|
|
14
|
+
const test = require('node:test');
|
|
15
|
+
const assert = require('node:assert');
|
|
16
|
+
const { GeometryService } = require('../client/geometry_service');
|
|
17
|
+
|
|
18
|
+
function makeService(clientID) {
|
|
19
|
+
// Reset the static trackedResources map between tests so uids don't leak
|
|
20
|
+
// across cases.
|
|
21
|
+
GeometryService.trackedResources = new Map();
|
|
22
|
+
return new GeometryService(clientID);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
test('UnstreamNode logs and clears state on the real transition, then is silent on repeat calls', () => {
|
|
26
|
+
const svc = makeService(201);
|
|
27
|
+
svc.StreamNode(31n);
|
|
28
|
+
const res = GeometryService.GetOrCreateTrackedResource(31n);
|
|
29
|
+
assert.ok(res.IsNeededByClient(201));
|
|
30
|
+
|
|
31
|
+
const originalLog = console.log;
|
|
32
|
+
const logs = [];
|
|
33
|
+
console.log = (...args) => logs.push(args.join(''));
|
|
34
|
+
try {
|
|
35
|
+
svc.UnstreamNode(31n);
|
|
36
|
+
assert.ok(!res.IsNeededByClient(201));
|
|
37
|
+
assert.strictEqual(logs.length, 1, 'the first UnstreamNode call for a streamed node must log once');
|
|
38
|
+
|
|
39
|
+
svc.UnstreamNode(31n);
|
|
40
|
+
svc.UnstreamNode(31n);
|
|
41
|
+
assert.strictEqual(logs.length, 1,
|
|
42
|
+
'repeated UnstreamNode calls while already unstreamed must not log again');
|
|
43
|
+
} finally {
|
|
44
|
+
console.log = originalLog;
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test('UnstreamNode is a no-op on the redundant path: it must not re-touch nodesToStreamEventually/streamedNodes', () => {
|
|
49
|
+
const svc = makeService(202);
|
|
50
|
+
svc.StreamNode(33n);
|
|
51
|
+
svc.UnstreamNode(33n);
|
|
52
|
+
|
|
53
|
+
// Simulate some other subsystem re-adding bookkeeping for this uid after
|
|
54
|
+
// the real unstream, so a later no-op UnstreamNode call would visibly
|
|
55
|
+
// disturb it if the guard were missing.
|
|
56
|
+
svc.nodesToStreamEventually.add(33n);
|
|
57
|
+
svc.streamedNodes.set(33n, 1);
|
|
58
|
+
|
|
59
|
+
svc.UnstreamNode(33n);
|
|
60
|
+
assert.ok(svc.nodesToStreamEventually.has(33n),
|
|
61
|
+
'a redundant UnstreamNode call (already unstreamed) must not delete from nodesToStreamEventually');
|
|
62
|
+
assert.ok(svc.streamedNodes.has(33n),
|
|
63
|
+
'a redundant UnstreamNode call (already unstreamed) must not delete from streamedNodes');
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test('StreamNode is idempotent: a second call for an already-needed node does not resurrect nodesToStreamEventually', () => {
|
|
67
|
+
const svc = makeService(203);
|
|
68
|
+
svc.StreamNode(41n);
|
|
69
|
+
assert.ok(svc.nodesToStreamEventually.has(41n));
|
|
70
|
+
|
|
71
|
+
// Simulate the resource having been picked up and removed from the
|
|
72
|
+
// "eventually" queue by UpdateNodesToStream, as happens in production.
|
|
73
|
+
svc.nodesToStreamEventually.delete(41n);
|
|
74
|
+
|
|
75
|
+
// A redundant StreamNode call (client still needs it) must not re-add it —
|
|
76
|
+
// only the real transition (need false -> true) should do that.
|
|
77
|
+
svc.StreamNode(41n);
|
|
78
|
+
assert.ok(!svc.nodesToStreamEventually.has(41n),
|
|
79
|
+
'redundant StreamNode call must not resurrect nodesToStreamEventually for an already-needed node');
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test('StreamNode after UnstreamNode is a real transition and does re-add to nodesToStreamEventually', () => {
|
|
83
|
+
const svc = makeService(204);
|
|
84
|
+
svc.StreamNode(43n);
|
|
85
|
+
svc.UnstreamNode(43n);
|
|
86
|
+
svc.nodesToStreamEventually.delete(43n); // UnstreamNode already does this; assert the starting state.
|
|
87
|
+
assert.ok(!svc.nodesToStreamEventually.has(43n));
|
|
88
|
+
|
|
89
|
+
svc.StreamNode(43n);
|
|
90
|
+
assert.ok(svc.nodesToStreamEventually.has(43n),
|
|
91
|
+
'StreamNode must still re-add the uid on an actual false -> true transition');
|
|
92
|
+
const res = GeometryService.GetOrCreateTrackedResource(43n);
|
|
93
|
+
assert.ok(res.IsNeededByClient(204));
|
|
94
|
+
});
|
|
@@ -95,6 +95,25 @@ test('ClientManager.disconnectClient removes client from map', () => {
|
|
|
95
95
|
assert.strictEqual(cm.clients.has(42), false, 'client must be removed from the map');
|
|
96
96
|
});
|
|
97
97
|
|
|
98
|
+
test('streamingConnectionStateChanged rebaselines the connect-attempt clock on post-connect failure', () => {
|
|
99
|
+
// Simulate a client that connected a long time ago (well past the timeout window),
|
|
100
|
+
// then later has its ICE connection fail. Before the fix, hasWebRtcConnectionTimedOut()
|
|
101
|
+
// would compare against the original, stale webRtcConnectionInitiatedAtMs and report an
|
|
102
|
+
// immediate timeout, force-disconnecting the client before its own reconnect logic
|
|
103
|
+
// (WebRtcConnection.reconnect(), in webrtcconnection.js) gets a chance to run.
|
|
104
|
+
const c = makeStubClient({
|
|
105
|
+
webRtcConnectionInitiatedAtMs: Date.now() - 100000, // original connect attempt, long ago
|
|
106
|
+
});
|
|
107
|
+
c.webRtcConnected = true;
|
|
108
|
+
c.webRtcConnectedAtMs = Date.now() - 90000;
|
|
109
|
+
|
|
110
|
+
c.streamingConnectionStateChanged('failed');
|
|
111
|
+
|
|
112
|
+
assert.strictEqual(c.webRtcConnected, false, 'webRtcConnected must be false after a failure');
|
|
113
|
+
assert.strictEqual(c.hasWebRtcConnectionTimedOut(), false,
|
|
114
|
+
'must not report a timeout immediately after rebaselining, so reconnect gets a fresh window');
|
|
115
|
+
});
|
|
116
|
+
|
|
98
117
|
test('ClientManager.UpdateStreaming removes timed-out clients', async () => {
|
|
99
118
|
const cm = new ClientManager();
|
|
100
119
|
const clientsRemoved = [];
|