teleportxr 1.0.59 → 1.0.60
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.
|
@@ -381,8 +381,13 @@ class GeometryService {
|
|
|
381
381
|
continue;
|
|
382
382
|
}
|
|
383
383
|
}
|
|
384
|
+
// Do NOT mark Sent here. The caller's Send*() path checks
|
|
385
|
+
// isGeometryOpen() and may bail out silently if the geometry channel
|
|
386
|
+
// hasn't finished opening yet; marking Sent eagerly would leave the
|
|
387
|
+
// resource stuck "in flight" until timeout_us (10 s) elapses, even
|
|
388
|
+
// though nothing was actually transmitted. The successful-send path
|
|
389
|
+
// calls EncodedResource(uid), which records the Sent state.
|
|
384
390
|
toSend.push(uid);
|
|
385
|
-
res.Sent(this.clientID, time_now_us);
|
|
386
391
|
}
|
|
387
392
|
return toSend;
|
|
388
393
|
}
|
|
@@ -406,26 +411,11 @@ class GeometryService {
|
|
|
406
411
|
// Get the list of meshes to stream. This is the list of meshes that we should have on the client
|
|
407
412
|
// excluding those that have been sent.
|
|
408
413
|
GetMeshesToSend() {
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
// If it was already received we don't send it:
|
|
415
|
-
if (res.WasAcknowledgedByClient(this.clientID)) continue;
|
|
416
|
-
if (res.WasSentToClient(this.clientID)) {
|
|
417
|
-
var timeSentUs = res.GetTimeSent(this.clientID);
|
|
418
|
-
// If we sent it too long ago with no acknowledgement, we can send it again.
|
|
419
|
-
if (time_now_us - timeSentUs > this.timeout_us) {
|
|
420
|
-
res.Timeout(this.clientID);
|
|
421
|
-
}
|
|
422
|
-
} else {
|
|
423
|
-
// if it hasn't been sent at all to our client, we add its resources.
|
|
424
|
-
resource_uids.push(uid);
|
|
425
|
-
res.Sent(this.clientID, time_now_us);
|
|
426
|
-
}
|
|
427
|
-
}
|
|
428
|
-
return resource_uids;
|
|
414
|
+
// Delegate to GetResourcesToSend so all resource pools share the same
|
|
415
|
+
// "not acknowledged AND (not sent OR sent-but-timed-out)" rule, and the
|
|
416
|
+
// same gating contract — Sent is recorded by EncodedResource() after the
|
|
417
|
+
// transport actually accepts the buffer, not by the picker.
|
|
418
|
+
return this.GetResourcesToSend(this.streamedMeshes);
|
|
429
419
|
}
|
|
430
420
|
EncodedResource(resource_uid) {
|
|
431
421
|
if (!GeometryService.trackedResources.has(resource_uid)) return;
|
package/package.json
CHANGED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// Regression tests for GeometryService.GetResourcesToSend / GetMeshesToSend /
|
|
3
|
+
// GetNodesToSend. Before the fix, the picker eagerly called res.Sent(now) for
|
|
4
|
+
// every uid it returned. Combined with Client.SendNode's "channel not open"
|
|
5
|
+
// silent short-circuit, this caused all initial resources to be marked Sent
|
|
6
|
+
// during the first UpdateStreaming tick (which fires ~150 ms before the
|
|
7
|
+
// geometry data channel finishes opening). On the next tick, every resource
|
|
8
|
+
// looked "in flight" and was skipped, and they then sat idle for the full
|
|
9
|
+
// 10 s timeout_us window before being retried — visible in production as a
|
|
10
|
+
// ~10 s gap between WebRTC connect and the first geometry chunk reaching the
|
|
11
|
+
// client.
|
|
12
|
+
//
|
|
13
|
+
// The fix: GetResourcesToSend / GetMeshesToSend must NOT mark Sent. Sent is
|
|
14
|
+
// recorded by EncodedResource(uid), called from Send*() only after the
|
|
15
|
+
// transport actually accepts the buffer.
|
|
16
|
+
|
|
17
|
+
const test = require('node:test');
|
|
18
|
+
const assert = require('node:assert');
|
|
19
|
+
const { GeometryService } = require('../client/geometry_service');
|
|
20
|
+
|
|
21
|
+
function makeService(clientID) {
|
|
22
|
+
// Reset the static trackedResources map between tests so uids don't leak
|
|
23
|
+
// across cases.
|
|
24
|
+
GeometryService.trackedResources = new Map();
|
|
25
|
+
return new GeometryService(clientID);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function seed(svc, pool, uid) {
|
|
29
|
+
// Ensure the uid is in the static trackedResources map and in the per-client
|
|
30
|
+
// pool, mimicking what AddOrRemoveNodeAndResources / UpdateNodesToStream do.
|
|
31
|
+
GeometryService.GetOrCreateTrackedResource(uid);
|
|
32
|
+
pool.set(uid, 1);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
test('GetResourcesToSend returns uid but does NOT mark it Sent', () => {
|
|
36
|
+
const svc = makeService(101);
|
|
37
|
+
const pool = new Map();
|
|
38
|
+
seed(svc, pool, 9n);
|
|
39
|
+
|
|
40
|
+
const toSend = svc.GetResourcesToSend(pool);
|
|
41
|
+
assert.deepStrictEqual(toSend, [9n]);
|
|
42
|
+
|
|
43
|
+
const res = GeometryService.GetOrCreateTrackedResource(9n);
|
|
44
|
+
// WasSentToClient returns the underlying bitset bit (0 / 1), not a boolean.
|
|
45
|
+
assert.ok(!res.WasSentToClient(101),
|
|
46
|
+
'picker must not record Sent — that is the transport layer\'s job');
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test('GetResourcesToSend keeps returning the same uid across ticks until EncodedResource is called', () => {
|
|
50
|
+
// This is the exact scenario the production stall hit: the geometry data
|
|
51
|
+
// channel is not yet open, SendNode bails out silently, EncodedResource is
|
|
52
|
+
// never invoked. Every subsequent tick must re-offer the uid.
|
|
53
|
+
const svc = makeService(102);
|
|
54
|
+
const pool = new Map();
|
|
55
|
+
seed(svc, pool, 11n);
|
|
56
|
+
|
|
57
|
+
for (let tick = 0; tick < 5; tick++) {
|
|
58
|
+
const toSend = svc.GetResourcesToSend(pool);
|
|
59
|
+
assert.deepStrictEqual(toSend, [11n],
|
|
60
|
+
`tick ${tick}: uid must reappear because no successful send was recorded`);
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test('GetResourcesToSend stops returning a uid once EncodedResource records the send', () => {
|
|
65
|
+
const svc = makeService(103);
|
|
66
|
+
const pool = new Map();
|
|
67
|
+
seed(svc, pool, 13n);
|
|
68
|
+
|
|
69
|
+
assert.deepStrictEqual(svc.GetResourcesToSend(pool), [13n]);
|
|
70
|
+
// Simulate Client.SendNode → sendGeometry returned true → EncodedResource.
|
|
71
|
+
svc.EncodedResource(13n);
|
|
72
|
+
assert.deepStrictEqual(svc.GetResourcesToSend(pool), [],
|
|
73
|
+
'after a successful send the uid must not be re-offered until timeout or Timeout()');
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test('GetResourcesToSend re-offers a uid after timeout_us elapses without acknowledgement', () => {
|
|
77
|
+
const svc = makeService(104);
|
|
78
|
+
svc.timeout_us = 1000; // 1 ms timeout for the test
|
|
79
|
+
const pool = new Map();
|
|
80
|
+
seed(svc, pool, 17n);
|
|
81
|
+
|
|
82
|
+
// First tick: picker returns the uid; transport marks it Sent via EncodedResource.
|
|
83
|
+
assert.deepStrictEqual(svc.GetResourcesToSend(pool), [17n]);
|
|
84
|
+
svc.EncodedResource(17n);
|
|
85
|
+
// Immediately after: still in flight, must not be re-offered.
|
|
86
|
+
assert.deepStrictEqual(svc.GetResourcesToSend(pool), []);
|
|
87
|
+
|
|
88
|
+
// Manually age the Sent timestamp past the timeout window.
|
|
89
|
+
// core.getTimestampUs() returns a plain Number (microtime.now() - start),
|
|
90
|
+
// matching the type used in GetResourcesToSend's arithmetic.
|
|
91
|
+
const res = GeometryService.GetOrCreateTrackedResource(17n);
|
|
92
|
+
res.sent_server_time_us.set(104, res.GetTimeSent(104) - (svc.timeout_us + 1));
|
|
93
|
+
|
|
94
|
+
assert.deepStrictEqual(svc.GetResourcesToSend(pool), [17n],
|
|
95
|
+
'after timeout the uid must be re-offered for retransmission');
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test('GetResourcesToSend skips uids the client has already acknowledged', () => {
|
|
99
|
+
const svc = makeService(105);
|
|
100
|
+
const pool = new Map();
|
|
101
|
+
seed(svc, pool, 19n);
|
|
102
|
+
|
|
103
|
+
svc.EncodedResource(19n);
|
|
104
|
+
svc.ConfirmResource(19n);
|
|
105
|
+
assert.deepStrictEqual(svc.GetResourcesToSend(pool), []);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test('GetMeshesToSend follows the same no-eager-Sent contract', () => {
|
|
109
|
+
// The mesh picker used to mark Sent inline; verify it now defers to the
|
|
110
|
+
// transport like the other resource pools.
|
|
111
|
+
const svc = makeService(106);
|
|
112
|
+
seed(svc, svc.streamedMeshes, 23n);
|
|
113
|
+
|
|
114
|
+
for (let tick = 0; tick < 3; tick++) {
|
|
115
|
+
assert.deepStrictEqual(svc.GetMeshesToSend(), [23n],
|
|
116
|
+
`tick ${tick}: mesh uid must reappear until EncodedResource is called`);
|
|
117
|
+
}
|
|
118
|
+
svc.EncodedResource(23n);
|
|
119
|
+
assert.deepStrictEqual(svc.GetMeshesToSend(), []);
|
|
120
|
+
});
|