teleportxr 1.0.59 → 1.0.61

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
@@ -280,10 +280,16 @@ class Client {
280
280
  return;
281
281
  }
282
282
  var msg =new message.AcknowledgementMessage();
283
- // data arrives from the WebSocket signaling channel as a Node Buffer (Uint8Array view over a pooled
284
- // ArrayBuffer), so we must wrap its underlying buffer using byteOffset/byteLength rather than passing
285
- // the Buffer directly to DataView.
286
- var dataView =new DataView(data.buffer,data.byteOffset,data.byteLength);
283
+ // ReceiveAcknowledgement is reached from two transports with different argument shapes:
284
+ // - WebRTC data channels (receivedMessageReliable / receivedMessageUnreliable) deliver
285
+ // event.data as a raw ArrayBuffer; ArrayBuffer.isView(data) is false and data.buffer
286
+ // is undefined, so we must construct the DataView from `data` directly.
287
+ // - The WebSocket signaling fallback (receiveReliableBinaryMessage) delivers a Node
288
+ // Buffer, which is a Uint8Array view over a pooled ArrayBuffer; we must wrap its
289
+ // backing buffer using byteOffset/byteLength so we don't read pool neighbours.
290
+ var dataView =ArrayBuffer.isView(data)
291
+ ?new DataView(data.buffer,data.byteOffset,data.byteLength)
292
+ :new DataView(data,0,data.byteLength);
287
293
  core.decodeFromDataView(msg,dataView,0);
288
294
  if(msg.uint64_ackId==this.currentOriginState.ackId)
289
295
  {
@@ -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
- var resource_uids = [];
410
- let time_now_us = core.getTimestampUs();
411
- for (const [uid, count] of this.streamedMeshes) {
412
- //is mesh streamed
413
- var res = GeometryService.GetOrCreateTrackedResource(uid);
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
@@ -16,7 +16,7 @@
16
16
  },
17
17
  "name": "teleportxr",
18
18
  "description": "Teleport Spatial Server on node.js",
19
- "version": "1.0.59",
19
+ "version": "1.0.61",
20
20
  "repository": {
21
21
  "type": "git",
22
22
  "url": "git+https://github.com/simul/teleport-nodejs.git"
@@ -3,53 +3,59 @@ const core= require("../core/core.js");
3
3
  const command= require("./command");
4
4
  const node= require("../scene/node.js");
5
5
 
6
+ // Mirrors core.decodeFromDataView so the two implementations stay in sync.
7
+ // The previous version had four latent bugs that only escaped notice because
8
+ // nothing imports this function (every real caller uses core.decodeFromDataView):
9
+ // 1. `Object.entries[key]=value` assigned a property on the `Object.entries`
10
+ // *function object* itself instead of writing back to `obj`, so decoded
11
+ // fields were silently dropped on the floor.
12
+ // 2. The multi-byte getters were called without an endian argument, defaulting
13
+ // to big-endian and disagreeing with the writer's little-endian output.
14
+ // 3. The "struct" branch referenced an undefined `value` local; the recursion
15
+ // would have thrown immediately had it ever been reached.
16
+ // 4. `SizeOfType` was referenced bare but is not imported into this file —
17
+ // only the `core` namespace is — so the very first iteration would have
18
+ // thrown ReferenceError.
6
19
  function decodeFromDataView(obj,dataView,byteOffset){
7
- for (let key of Object.keys(obj)) {
20
+ for (let [key, sub_obj] of Object.entries(obj)) {
8
21
  let first_underscore=key.search('_');
9
22
  var name=key.substring(first_underscore+1,key.end);
10
23
  var type=key.substring(0,first_underscore);
11
- var [sz,tp]=SizeOfType(type);
24
+ var [sz,tp]=core.SizeOfType(type);
12
25
  if(tp=="uint8")
13
26
  {
14
- var value=dataView.getUint8(byteOffset);
15
- Object.entries[key]=value;
27
+ obj[key]=dataView.getUint8(byteOffset);
16
28
  }
17
29
  else if(tp=="int8")
18
30
  {
19
- var value=dataView.getInt8(byteOffset);
20
- Object.entries[key]=value;
31
+ obj[key]=dataView.getInt8(byteOffset);
21
32
  }
22
33
  else if(tp=="uint32")
23
34
  {
24
- var value=dataView.getUint32(byteOffset);
25
- Object.entries[key]=value;
35
+ obj[key]=dataView.getUint32(byteOffset,core.endian);
26
36
  }
27
37
  else if(tp=="int32")
28
38
  {
29
- var value=dataView.getInt32(byteOffset);
30
- Object.entries[key]=value;
39
+ obj[key]=dataView.getInt32(byteOffset,core.endian);
31
40
  }
32
41
  else if(tp=="float32")
33
42
  {
34
- var value=dataView.getFloat32(byteOffset);
35
- Object.entries[key]=value;
43
+ obj[key]=dataView.getFloat32(byteOffset,core.endian);
36
44
  }
37
45
  else if(tp=="int64")
38
46
  {
39
- var value=dataView.getBigInt64(byteOffset);
40
- Object.entries[key]=value;
47
+ obj[key]=dataView.getBigInt64(byteOffset,core.endian);
41
48
  }
42
49
  else if(tp=="uint64")
43
50
  {
44
- var value=dataView.getBigUint64(byteOffset);
45
- Object.entries[key]=value;
51
+ obj[key]=dataView.getBigUint64(byteOffset,core.endian);
46
52
  }
47
53
  else if(tp=="struct")
48
54
  {
49
- sz=decodeFromDataView(value,dataView,byteOffset)-byteOffset;
55
+ sz=decodeFromDataView(sub_obj,dataView,byteOffset)-byteOffset;
50
56
  }
51
57
  byteOffset+=sz;
52
- console.log(byteOffset+": "+sz+" bytes\t\t"+tp+" "+name+" "+value.toString());
58
+ console.log(byteOffset+": "+sz+" bytes\t\t"+tp+" "+name+" "+obj[key]);
53
59
  }
54
60
  return byteOffset;
55
61
  }
@@ -0,0 +1,92 @@
1
+ 'use strict';
2
+ // Regression tests for Client.ReceiveAcknowledgement. The handler is called
3
+ // from three transports:
4
+ // * receivedMessageReliable / receivedMessageUnreliable (WebRTC data channels)
5
+ // deliver event.data as a raw ArrayBuffer.
6
+ // * receiveReliableBinaryMessage (WebSocket signaling fallback) delivers a
7
+ // Node Buffer (Uint8Array view over a pooled ArrayBuffer).
8
+ // A previous patch ("Fix buffer type error.") switched the DataView ctor to
9
+ // `new DataView(data.buffer, data.byteOffset, data.byteLength)` which fixed the
10
+ // Buffer path but crashed the WebRTC path because ArrayBuffer has no .buffer
11
+ // property. The crash exits the server with status 1, forcing reconnects and a
12
+ // 10 s+ stall in initial resource transfer. These tests pin both shapes.
13
+
14
+ const test = require('node:test');
15
+ const assert = require('node:assert');
16
+ const { Client } = require('../client/client');
17
+ const message = require('../protocol/message');
18
+
19
+ // Build a 17-byte AcknowledgementMessage buffer (sizeof === 17). Field
20
+ // layout (little-endian) per Message + AcknowledgementMessage:
21
+ // [0..1) uint8 messagePayloadType (= MessagePayloadType.Acknowledgement)
22
+ // [1..9) int64 timestamp
23
+ // [9..17) uint64 ackId
24
+ // The current decodeFromDataView implementation has an unrelated bug that
25
+ // prevents the decoded values from being assigned back onto `msg`, so the
26
+ // downstream ackId comparison branches won't actually fire — but that is fine
27
+ // for this regression: the test only asserts the handler does not throw and
28
+ // returns normally, which is the property the crash violated.
29
+ function makeAckArrayBuffer() {
30
+ const ab = new ArrayBuffer(message.AcknowledgementMessage.sizeof());
31
+ const dv = new DataView(ab);
32
+ dv.setUint8(0, message.MessagePayloadType.Acknowledgement);
33
+ dv.setBigInt64(1, 0n, true);
34
+ dv.setBigUint64(9, 42n, true);
35
+ return ab;
36
+ }
37
+
38
+ function makeStubClient() {
39
+ const c = Object.create(Client.prototype);
40
+ c.clientID = 1;
41
+ c.currentOriginState = { ackId: 0n, acknowledged: false, resendCount: 0 };
42
+ c.currentLightingState = { ackId: 0n, acknowledged: false, resendCount: 0 };
43
+ return c;
44
+ }
45
+
46
+ test('ReceiveAcknowledgement accepts a raw ArrayBuffer (WebRTC path)', () => {
47
+ const c = makeStubClient();
48
+ const ab = makeAckArrayBuffer();
49
+ // Sanity-check: this is the exact shape WebRTC delivers — a bare
50
+ // ArrayBuffer with no .buffer property. Before the fix, the DataView ctor
51
+ // threw "First argument to DataView constructor must be an ArrayBuffer"
52
+ // because data.buffer was undefined.
53
+ assert.strictEqual(ArrayBuffer.isView(ab), false);
54
+ assert.strictEqual(ab.buffer, undefined);
55
+ assert.doesNotThrow(() => c.ReceiveAcknowledgement(ab));
56
+ });
57
+
58
+ test('ReceiveAcknowledgement accepts a Node Buffer (signaling path)', () => {
59
+ const c = makeStubClient();
60
+ const ab = makeAckArrayBuffer();
61
+ // ws delivers binary frames as Node Buffer. Buffer is a Uint8Array view,
62
+ // so ArrayBuffer.isView is true and .buffer / .byteOffset / .byteLength
63
+ // must be used to construct the DataView.
64
+ const buf = Buffer.from(ab);
65
+ assert.strictEqual(ArrayBuffer.isView(buf), true);
66
+ assert.doesNotThrow(() => c.ReceiveAcknowledgement(buf));
67
+ });
68
+
69
+ test('ReceiveAcknowledgement accepts a Uint8Array view at non-zero offset', () => {
70
+ // Defensive: simulate a pooled Buffer where byteOffset > 0. If the handler
71
+ // were to ignore byteOffset and read from offset 0 of the pool, it would
72
+ // see unrelated bytes from a neighbouring allocation.
73
+ const c = makeStubClient();
74
+ const ackSize = message.AcknowledgementMessage.sizeof();
75
+ const pool = new ArrayBuffer(64 + ackSize);
76
+ const dv = new DataView(pool);
77
+ dv.setUint8(64, message.MessagePayloadType.Acknowledgement);
78
+ dv.setBigInt64(65, 0n, true);
79
+ dv.setBigUint64(73, 7n, true);
80
+ const view = new Uint8Array(pool, 64, ackSize);
81
+ assert.strictEqual(view.byteOffset, 64);
82
+ assert.doesNotThrow(() => c.ReceiveAcknowledgement(view));
83
+ });
84
+
85
+ test('ReceiveAcknowledgement rejects a malformed packet without throwing', () => {
86
+ const c = makeStubClient();
87
+ const tooSmall = new ArrayBuffer(3);
88
+ // Should log and return early, not throw.
89
+ assert.doesNotThrow(() => c.ReceiveAcknowledgement(tooSmall));
90
+ const tooBig = new ArrayBuffer(message.AcknowledgementMessage.sizeof() + 1);
91
+ assert.doesNotThrow(() => c.ReceiveAcknowledgement(tooBig));
92
+ });
@@ -0,0 +1,115 @@
1
+ 'use strict';
2
+ // Regression tests for the decodeFromDataView duplicate that lives in
3
+ // protocol/message.js. The duplicate had four latent bugs (object-assignment
4
+ // via Object.entries, missing endian on multi-byte getters, undefined `value`
5
+ // in the struct branch, and a missing SizeOfType import). Tests are written
6
+ // against the function as exercised through a small re-export so the bugfix
7
+ // stays load-bearing even though no production caller imports it today.
8
+
9
+ const test = require('node:test');
10
+ const assert = require('node:assert');
11
+ const Module = require('node:module');
12
+
13
+ // Reach into protocol/message.js's module scope to grab the (non-exported)
14
+ // decodeFromDataView. Re-loading the module and reading its compiled wrapper
15
+ // is overkill; instead, require() the file and use a tiny trick: the function
16
+ // is referenced by class methods on classes that ARE exported. Since none of
17
+ // the exported classes call it either, we evaluate the module file once and
18
+ // pull the symbol from its source via Function constructor.
19
+ function loadLocalDecode() {
20
+ const fs = require('node:fs');
21
+ const path = require('node:path');
22
+ const src = fs.readFileSync(
23
+ path.join(__dirname, '..', 'protocol', 'message.js'),
24
+ 'utf8'
25
+ );
26
+ // Stand up a fake module context that mirrors the file's top-level requires
27
+ // but exposes decodeFromDataView. Tack a `module.exports.decodeFromDataView`
28
+ // assignment onto the end and evaluate via Module._compile.
29
+ const m = new Module(require.resolve('../protocol/message.js'));
30
+ m.filename = require.resolve('../protocol/message.js');
31
+ m.paths = Module._nodeModulePaths(m.filename);
32
+ m._compile(
33
+ src + '\nmodule.exports.decodeFromDataView = decodeFromDataView;\n',
34
+ m.filename
35
+ );
36
+ return m.exports.decodeFromDataView;
37
+ }
38
+
39
+ const decodeFromDataView = loadLocalDecode();
40
+ const core = require('../core/core.js');
41
+
42
+ test('decodeFromDataView writes uint8 fields back onto the target object', () => {
43
+ const obj = { uint8_messagePayloadType: 0 };
44
+ const ab = new ArrayBuffer(1);
45
+ new DataView(ab).setUint8(0, 12);
46
+ decodeFromDataView(obj, new DataView(ab), 0);
47
+ assert.strictEqual(obj.uint8_messagePayloadType, 12);
48
+ });
49
+
50
+ test('decodeFromDataView reads multi-byte fields in the writer\'s endianness', () => {
51
+ // The writer (core.encodeIntoDataView) uses core.endian (little-endian).
52
+ // Pre-fix, the local decoder omitted the endian argument and would have
53
+ // read big-endian, so 42n encoded LE as 2a 00 00 00 00 00 00 00 would
54
+ // decode as 0x2a00000000000000n. Pin the round-trip.
55
+ const obj = { uint64_ackId: 0n };
56
+ const ab = new ArrayBuffer(8);
57
+ new DataView(ab).setBigUint64(0, 42n, core.endian);
58
+ decodeFromDataView(obj, new DataView(ab), 0);
59
+ assert.strictEqual(obj.uint64_ackId, 42n);
60
+ });
61
+
62
+ test('decodeFromDataView round-trips a full Message-shaped payload', () => {
63
+ // The Message base class is { uint8_messagePayloadType, int64_timestamp }
64
+ // plus one trailing uint64. Build the layout directly so we don't depend
65
+ // on the (separately tested) core encoder.
66
+ const obj = {
67
+ uint8_messagePayloadType: 0,
68
+ int64_timestamp: 0n,
69
+ uint64_ackId: 0n,
70
+ };
71
+ const ab = new ArrayBuffer(17);
72
+ const dv = new DataView(ab);
73
+ dv.setUint8(0, 12);
74
+ dv.setBigInt64(1, -7n, core.endian);
75
+ dv.setBigUint64(9, 0xdeadbeefn, core.endian);
76
+ const endOffset = decodeFromDataView(obj, new DataView(ab), 0);
77
+ assert.strictEqual(endOffset, 17);
78
+ assert.strictEqual(obj.uint8_messagePayloadType, 12);
79
+ assert.strictEqual(obj.int64_timestamp, -7n);
80
+ assert.strictEqual(obj.uint64_ackId, 0xdeadbeefn);
81
+ });
82
+
83
+ test('decodeFromDataView recurses into nested struct fields', () => {
84
+ // "struct" is the catch-all branch in SizeOfType — anything not in the
85
+ // primitive list is treated as a nested object whose own keys follow the
86
+ // type_name convention. Pre-fix, this branch referenced an undefined
87
+ // `value` and would have thrown TypeError on the very first struct field.
88
+ const obj = {
89
+ struct_inner: { uint8_kind: 0, uint32_value: 0 },
90
+ uint8_trailer: 0,
91
+ };
92
+ const ab = new ArrayBuffer(6);
93
+ const dv = new DataView(ab);
94
+ dv.setUint8(0, 3);
95
+ dv.setUint32(1, 0x11223344, core.endian);
96
+ dv.setUint8(5, 0xff);
97
+ const endOffset = decodeFromDataView(obj, new DataView(ab), 0);
98
+ assert.strictEqual(endOffset, 6);
99
+ assert.strictEqual(obj.struct_inner.uint8_kind, 3);
100
+ assert.strictEqual(obj.struct_inner.uint32_value, 0x11223344);
101
+ assert.strictEqual(obj.uint8_trailer, 0xff);
102
+ });
103
+
104
+ test('decodeFromDataView does not mutate Object.entries itself', () => {
105
+ // Pin the most embarrassing of the pre-fix bugs: `Object.entries[key]=value`
106
+ // wrote properties onto the global Object.entries function. After the fix,
107
+ // decoding must leave Object.entries untouched.
108
+ const before = Object.getOwnPropertyNames(Object.entries);
109
+ const obj = { uint8_x: 0 };
110
+ const ab = new ArrayBuffer(1);
111
+ new DataView(ab).setUint8(0, 99);
112
+ decodeFromDataView(obj, new DataView(ab), 0);
113
+ const after = Object.getOwnPropertyNames(Object.entries);
114
+ assert.deepStrictEqual(after, before);
115
+ });
@@ -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
+ });