@livedesk/hub 0.1.10 → 0.1.12
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/package.json +1 -1
- package/src/remote-hub.js +6 -0
- package/src/server.js +7 -3
- package/src/transport/udp-rendezvous.js +402 -78
package/package.json
CHANGED
package/src/remote-hub.js
CHANGED
|
@@ -3881,6 +3881,9 @@ export function createRemoteHub(options = {}) {
|
|
|
3881
3881
|
nativeEncodableFrameCount: Number.isFinite(Number(message.nativeEncodableFrameCount)) ? Number(message.nativeEncodableFrameCount) : 0,
|
|
3882
3882
|
nativeSkippedFrameCount: Number.isFinite(Number(message.nativeSkippedFrameCount)) ? Number(message.nativeSkippedFrameCount) : 0,
|
|
3883
3883
|
nativeEncodedFrameCount: Number.isFinite(Number(message.nativeEncodedFrameCount)) ? Number(message.nativeEncodedFrameCount) : 0,
|
|
3884
|
+
nativeEncodeFramesInFlight: Number.isFinite(Number(message.nativeEncodeFramesInFlight)) ? Number(message.nativeEncodeFramesInFlight) : 0,
|
|
3885
|
+
nativeBackpressureSkippedFrameCount: Number.isFinite(Number(message.nativeBackpressureSkippedFrameCount)) ? Number(message.nativeBackpressureSkippedFrameCount) : 0,
|
|
3886
|
+
nativeIdleSkippedFrameCount: Number.isFinite(Number(message.nativeIdleSkippedFrameCount)) ? Number(message.nativeIdleSkippedFrameCount) : 0,
|
|
3884
3887
|
nativeCaptureTelemetryAt: safeString(message.nativeCaptureTelemetryAt, 80),
|
|
3885
3888
|
droppedByAgent: Number.isFinite(Number(message.droppedByAgent)) ? Number(message.droppedByAgent) : 0,
|
|
3886
3889
|
hardwareEncoder: safeString(message.hardwareEncoder, 80),
|
|
@@ -4258,6 +4261,9 @@ export function createRemoteHub(options = {}) {
|
|
|
4258
4261
|
nativeEncodableFrameCount: Number.isFinite(Number(message.nativeEncodableFrameCount)) ? Number(message.nativeEncodableFrameCount) : 0,
|
|
4259
4262
|
nativeSkippedFrameCount: Number.isFinite(Number(message.nativeSkippedFrameCount)) ? Number(message.nativeSkippedFrameCount) : 0,
|
|
4260
4263
|
nativeEncodedFrameCount: Number.isFinite(Number(message.nativeEncodedFrameCount)) ? Number(message.nativeEncodedFrameCount) : 0,
|
|
4264
|
+
nativeEncodeFramesInFlight: Number.isFinite(Number(message.nativeEncodeFramesInFlight)) ? Number(message.nativeEncodeFramesInFlight) : 0,
|
|
4265
|
+
nativeBackpressureSkippedFrameCount: Number.isFinite(Number(message.nativeBackpressureSkippedFrameCount)) ? Number(message.nativeBackpressureSkippedFrameCount) : 0,
|
|
4266
|
+
nativeIdleSkippedFrameCount: Number.isFinite(Number(message.nativeIdleSkippedFrameCount)) ? Number(message.nativeIdleSkippedFrameCount) : 0,
|
|
4261
4267
|
nativeCaptureTelemetryAt: safeString(message.nativeCaptureTelemetryAt, 80),
|
|
4262
4268
|
droppedByAgent: Number.isFinite(Number(message.droppedByAgent)) ? Number(message.droppedByAgent) : 0,
|
|
4263
4269
|
hubIngressDropped: Number.isFinite(Number(message.hubIngressDropped)) ? Number(message.hubIngressDropped) : 0,
|
package/src/server.js
CHANGED
|
@@ -63,10 +63,11 @@ runtimeManager.emit('runtime.started', { role: runtimeRole, pid: process.pid });
|
|
|
63
63
|
const frameBackpressureBytes = readPositiveIntegerEnv('LIVEDESK_FRAME_WS_BACKPRESSURE_BYTES', 8 * 1024 * 1024);
|
|
64
64
|
// ScreenCaptureKit and the native Agent handoff can release two bounded
|
|
65
65
|
// eight-frame batches together. Keep the Wall lane large enough to retain the
|
|
66
|
-
// complete dependent H.264 burst
|
|
67
|
-
//
|
|
66
|
+
// complete dependent H.264 burst. Control is normally a single target, but its
|
|
67
|
+
// macOS Agent handoff can release eight dependent H.264 frames together; keep
|
|
68
|
+
// that complete bounded burst instead of stalling until the next recovery key.
|
|
68
69
|
const frameClientQueuePackets = readPositiveIntegerEnv('LIVEDESK_FRAME_WS_QUEUE_PACKETS', 16);
|
|
69
|
-
const controlFrameClientQueuePackets = readPositiveIntegerEnv('LIVEDESK_CONTROL_FRAME_WS_QUEUE_PACKETS',
|
|
70
|
+
const controlFrameClientQueuePackets = readPositiveIntegerEnv('LIVEDESK_CONTROL_FRAME_WS_QUEUE_PACKETS', 8);
|
|
70
71
|
const frameStreamStopGraceMs = readPositiveIntegerEnv('LIVEDESK_FRAME_STREAM_STOP_GRACE_MS', 1500);
|
|
71
72
|
const mode5FrameBackpressureBytes = readPositiveIntegerEnv('LIVEDESK_MODE5_WS_BACKPRESSURE_BYTES', 2 * 1024 * 1024);
|
|
72
73
|
const mode5FrameQueuePackets = readPositiveIntegerEnv('LIVEDESK_MODE5_WS_QUEUE_PACKETS', 2);
|
|
@@ -2069,6 +2070,9 @@ function buildRemoteFrameBinaryPacket(frameEvent, diagnostics = {}) {
|
|
|
2069
2070
|
nativeEncodableFrameCount: Number(frame.nativeEncodableFrameCount || 0) || 0,
|
|
2070
2071
|
nativeSkippedFrameCount: Number(frame.nativeSkippedFrameCount || 0) || 0,
|
|
2071
2072
|
nativeEncodedFrameCount: Number(frame.nativeEncodedFrameCount || 0) || 0,
|
|
2073
|
+
nativeEncodeFramesInFlight: Number(frame.nativeEncodeFramesInFlight || 0) || 0,
|
|
2074
|
+
nativeBackpressureSkippedFrameCount: Number(frame.nativeBackpressureSkippedFrameCount || 0) || 0,
|
|
2075
|
+
nativeIdleSkippedFrameCount: Number(frame.nativeIdleSkippedFrameCount || 0) || 0,
|
|
2072
2076
|
nativeCaptureTelemetryAt: frame.nativeCaptureTelemetryAt || '',
|
|
2073
2077
|
droppedByAgent: Number(frame.droppedByAgent || 0) || 0,
|
|
2074
2078
|
hubIngressDropped: Number(frame.hubIngressDropped || 0) || 0,
|
|
@@ -1,78 +1,402 @@
|
|
|
1
|
-
import dgram from 'node:dgram';
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
1
|
+
import dgram from 'node:dgram';
|
|
2
|
+
import {
|
|
3
|
+
decodeRendezvousMessage,
|
|
4
|
+
encodeRendezvousMessage,
|
|
5
|
+
UDP_MAX_DATAGRAM_BYTES,
|
|
6
|
+
UDP_P2P_PROTOCOL
|
|
7
|
+
} from './udp-protocol.js';
|
|
8
|
+
|
|
9
|
+
const DEFAULT_LIMITS = Object.freeze({
|
|
10
|
+
roomTtlMs: 60_000,
|
|
11
|
+
sourceTtlMs: 120_000,
|
|
12
|
+
sweepIntervalMs: 10_000,
|
|
13
|
+
statusIntervalMs: 60_000,
|
|
14
|
+
maxRooms: 4_096,
|
|
15
|
+
maxSources: 8_192,
|
|
16
|
+
maxRoomsPerSource: 128,
|
|
17
|
+
perSourceRate: 20,
|
|
18
|
+
perSourceBurst: 40,
|
|
19
|
+
globalRate: 1_000,
|
|
20
|
+
globalBurst: 2_000
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
function safeText(value, max = 160) {
|
|
24
|
+
return String(value || '').replace(/[\r\n\t]/g, ' ').trim().slice(0, max);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function normalizePort(value, fallback) {
|
|
28
|
+
const port = Number(value);
|
|
29
|
+
return Number.isInteger(port) && port >= 0 && port <= 65535 ? port : fallback;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function normalizeInteger(value, fallback, min = 1, max = Number.MAX_SAFE_INTEGER) {
|
|
33
|
+
const number = Number(value);
|
|
34
|
+
return Number.isInteger(number) && number >= min && number <= max ? number : fallback;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function normalizeRate(value, fallback) {
|
|
38
|
+
const number = Number(value);
|
|
39
|
+
return Number.isFinite(number) && number > 0 ? number : fallback;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function createTokenBucket(capacity, now) {
|
|
43
|
+
return { tokens: capacity, updatedAt: now };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function consumeToken(bucket, rate, capacity, now) {
|
|
47
|
+
const elapsedMs = Math.max(0, now - bucket.updatedAt);
|
|
48
|
+
bucket.tokens = Math.min(capacity, bucket.tokens + (elapsedMs / 1_000) * rate);
|
|
49
|
+
bucket.updatedAt = Math.max(bucket.updatedAt, now);
|
|
50
|
+
if (bucket.tokens < 1) return false;
|
|
51
|
+
bucket.tokens -= 1;
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function createUdpRendezvousServer({
|
|
56
|
+
host = '0.0.0.0',
|
|
57
|
+
port = 5199,
|
|
58
|
+
log = () => {},
|
|
59
|
+
version = 'dev',
|
|
60
|
+
now = () => Date.now(),
|
|
61
|
+
roomTtlMs = DEFAULT_LIMITS.roomTtlMs,
|
|
62
|
+
sourceTtlMs = DEFAULT_LIMITS.sourceTtlMs,
|
|
63
|
+
sweepIntervalMs = DEFAULT_LIMITS.sweepIntervalMs,
|
|
64
|
+
statusIntervalMs = DEFAULT_LIMITS.statusIntervalMs,
|
|
65
|
+
maxRooms = DEFAULT_LIMITS.maxRooms,
|
|
66
|
+
maxSources = DEFAULT_LIMITS.maxSources,
|
|
67
|
+
maxRoomsPerSource = DEFAULT_LIMITS.maxRoomsPerSource,
|
|
68
|
+
perSourceRate = DEFAULT_LIMITS.perSourceRate,
|
|
69
|
+
perSourceBurst = DEFAULT_LIMITS.perSourceBurst,
|
|
70
|
+
globalRate = DEFAULT_LIMITS.globalRate,
|
|
71
|
+
globalBurst = DEFAULT_LIMITS.globalBurst
|
|
72
|
+
} = {}) {
|
|
73
|
+
const socket = dgram.createSocket('udp4');
|
|
74
|
+
const rooms = new Map();
|
|
75
|
+
const unpairedRooms = new Map();
|
|
76
|
+
const sources = new Map();
|
|
77
|
+
const evictableSources = new Map();
|
|
78
|
+
const limits = Object.freeze({
|
|
79
|
+
roomTtlMs: normalizeInteger(roomTtlMs, DEFAULT_LIMITS.roomTtlMs),
|
|
80
|
+
sourceTtlMs: normalizeInteger(sourceTtlMs, DEFAULT_LIMITS.sourceTtlMs),
|
|
81
|
+
sweepIntervalMs: normalizeInteger(sweepIntervalMs, DEFAULT_LIMITS.sweepIntervalMs),
|
|
82
|
+
statusIntervalMs: normalizeInteger(statusIntervalMs, DEFAULT_LIMITS.statusIntervalMs, 0),
|
|
83
|
+
maxRooms: normalizeInteger(maxRooms, DEFAULT_LIMITS.maxRooms),
|
|
84
|
+
maxSources: normalizeInteger(maxSources, DEFAULT_LIMITS.maxSources),
|
|
85
|
+
maxRoomsPerSource: normalizeInteger(maxRoomsPerSource, DEFAULT_LIMITS.maxRoomsPerSource),
|
|
86
|
+
perSourceRate: normalizeRate(perSourceRate, DEFAULT_LIMITS.perSourceRate),
|
|
87
|
+
perSourceBurst: normalizeRate(perSourceBurst, DEFAULT_LIMITS.perSourceBurst),
|
|
88
|
+
globalRate: normalizeRate(globalRate, DEFAULT_LIMITS.globalRate),
|
|
89
|
+
globalBurst: normalizeRate(globalBurst, DEFAULT_LIMITS.globalBurst)
|
|
90
|
+
});
|
|
91
|
+
const counters = {
|
|
92
|
+
receivedDatagrams: 0,
|
|
93
|
+
receivedBytes: 0,
|
|
94
|
+
acceptedRegistrations: 0,
|
|
95
|
+
peerNotifications: 0,
|
|
96
|
+
sentBytes: 0,
|
|
97
|
+
sendErrors: 0,
|
|
98
|
+
invalidDatagrams: 0,
|
|
99
|
+
oversizeDatagrams: 0,
|
|
100
|
+
globalRateDrops: 0,
|
|
101
|
+
sourceRateDrops: 0,
|
|
102
|
+
sourceCapacityDrops: 0,
|
|
103
|
+
sourceRoomLimitDrops: 0,
|
|
104
|
+
roomCapacityDrops: 0,
|
|
105
|
+
tokenMismatchDrops: 0,
|
|
106
|
+
expiredRooms: 0,
|
|
107
|
+
evictedRooms: 0,
|
|
108
|
+
expiredSources: 0,
|
|
109
|
+
evictedSources: 0,
|
|
110
|
+
sweeps: 0,
|
|
111
|
+
socketErrors: 0
|
|
112
|
+
};
|
|
113
|
+
const globalBucket = createTokenBucket(limits.globalBurst, now());
|
|
114
|
+
const serviceVersion = safeText(version, 64) || 'dev';
|
|
115
|
+
let started = false;
|
|
116
|
+
let closing = false;
|
|
117
|
+
let startedAt = 0;
|
|
118
|
+
let boundPort = normalizePort(port, 5199);
|
|
119
|
+
let sweepTimer = null;
|
|
120
|
+
let statusTimer = null;
|
|
121
|
+
let lastError = '';
|
|
122
|
+
|
|
123
|
+
function sourceKey(rinfo) {
|
|
124
|
+
return safeText(rinfo?.address, 128);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function deleteRoom(roomId, reason = '') {
|
|
128
|
+
const room = rooms.get(roomId);
|
|
129
|
+
if (!room) return false;
|
|
130
|
+
rooms.delete(roomId);
|
|
131
|
+
unpairedRooms.delete(roomId);
|
|
132
|
+
const owner = sources.get(room.ownerSource);
|
|
133
|
+
if (owner) {
|
|
134
|
+
owner.activeRooms = Math.max(0, owner.activeRooms - 1);
|
|
135
|
+
if (owner.activeRooms === 0) {
|
|
136
|
+
evictableSources.delete(owner.key);
|
|
137
|
+
evictableSources.set(owner.key, owner);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
if (reason === 'expired') counters.expiredRooms += 1;
|
|
141
|
+
if (reason === 'capacity') counters.evictedRooms += 1;
|
|
142
|
+
return true;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function sweep(at = now()) {
|
|
146
|
+
for (const [roomId, room] of rooms) {
|
|
147
|
+
if (at - room.updatedAt >= limits.roomTtlMs) deleteRoom(roomId, 'expired');
|
|
148
|
+
}
|
|
149
|
+
for (const [key, source] of sources) {
|
|
150
|
+
if (source.activeRooms === 0 && at - source.lastSeenAt >= limits.sourceTtlMs) {
|
|
151
|
+
sources.delete(key);
|
|
152
|
+
evictableSources.delete(key);
|
|
153
|
+
counters.expiredSources += 1;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
counters.sweeps += 1;
|
|
157
|
+
return getStatus();
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function getOrCreateSource(rinfo, at) {
|
|
161
|
+
const key = sourceKey(rinfo);
|
|
162
|
+
if (!key) return null;
|
|
163
|
+
let source = sources.get(key);
|
|
164
|
+
if (!source) {
|
|
165
|
+
if (sources.size >= limits.maxSources) {
|
|
166
|
+
const oldestEvictableSource = evictableSources.keys().next().value;
|
|
167
|
+
if (!oldestEvictableSource) {
|
|
168
|
+
counters.sourceCapacityDrops += 1;
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
evictableSources.delete(oldestEvictableSource);
|
|
172
|
+
sources.delete(oldestEvictableSource);
|
|
173
|
+
counters.evictedSources += 1;
|
|
174
|
+
}
|
|
175
|
+
source = {
|
|
176
|
+
key,
|
|
177
|
+
bucket: createTokenBucket(limits.perSourceBurst, at),
|
|
178
|
+
lastSeenAt: at,
|
|
179
|
+
activeRooms: 0
|
|
180
|
+
};
|
|
181
|
+
sources.set(key, source);
|
|
182
|
+
evictableSources.set(key, source);
|
|
183
|
+
}
|
|
184
|
+
source.lastSeenAt = at;
|
|
185
|
+
if (source.activeRooms === 0) {
|
|
186
|
+
evictableSources.delete(key);
|
|
187
|
+
evictableSources.set(key, source);
|
|
188
|
+
}
|
|
189
|
+
if (!consumeToken(source.bucket, limits.perSourceRate, limits.perSourceBurst, at)) {
|
|
190
|
+
counters.sourceRateDrops += 1;
|
|
191
|
+
return null;
|
|
192
|
+
}
|
|
193
|
+
return source;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function send(message, address, targetPort) {
|
|
197
|
+
let payload;
|
|
198
|
+
try {
|
|
199
|
+
payload = encodeRendezvousMessage(message);
|
|
200
|
+
} catch (error) {
|
|
201
|
+
counters.sendErrors += 1;
|
|
202
|
+
lastError = safeText(error?.message || error, 240);
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
counters.peerNotifications += 1;
|
|
206
|
+
counters.sentBytes += payload.length;
|
|
207
|
+
socket.send(payload, targetPort, address, error => {
|
|
208
|
+
if (!error) return;
|
|
209
|
+
counters.sendErrors += 1;
|
|
210
|
+
lastError = safeText(error?.message || error, 240);
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function createRoom(roomId, token, source, at) {
|
|
215
|
+
if (source.activeRooms >= limits.maxRoomsPerSource) {
|
|
216
|
+
counters.sourceRoomLimitDrops += 1;
|
|
217
|
+
return null;
|
|
218
|
+
}
|
|
219
|
+
if (rooms.size >= limits.maxRooms) {
|
|
220
|
+
const oldestUnpairedRoomId = unpairedRooms.keys().next().value;
|
|
221
|
+
if (oldestUnpairedRoomId) {
|
|
222
|
+
deleteRoom(oldestUnpairedRoomId, 'capacity');
|
|
223
|
+
} else {
|
|
224
|
+
counters.roomCapacityDrops += 1;
|
|
225
|
+
return null;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
const room = {
|
|
229
|
+
token,
|
|
230
|
+
ownerSource: source.key,
|
|
231
|
+
peers: new Map(),
|
|
232
|
+
createdAt: at,
|
|
233
|
+
updatedAt: at
|
|
234
|
+
};
|
|
235
|
+
rooms.set(roomId, room);
|
|
236
|
+
unpairedRooms.set(roomId, room);
|
|
237
|
+
source.activeRooms += 1;
|
|
238
|
+
evictableSources.delete(source.key);
|
|
239
|
+
return room;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function onMessage(payload, rinfo) {
|
|
243
|
+
const at = now();
|
|
244
|
+
counters.receivedDatagrams += 1;
|
|
245
|
+
counters.receivedBytes += Buffer.isBuffer(payload) ? payload.length : 0;
|
|
246
|
+
if (!Buffer.isBuffer(payload)) {
|
|
247
|
+
counters.invalidDatagrams += 1;
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
if (payload.length > UDP_MAX_DATAGRAM_BYTES) {
|
|
251
|
+
counters.oversizeDatagrams += 1;
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
if (!consumeToken(globalBucket, limits.globalRate, limits.globalBurst, at)) {
|
|
255
|
+
counters.globalRateDrops += 1;
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
const source = getOrCreateSource(rinfo, at);
|
|
259
|
+
if (!source) return;
|
|
260
|
+
const message = decodeRendezvousMessage(payload);
|
|
261
|
+
if (message?.type !== 'rendezvous.register') {
|
|
262
|
+
counters.invalidDatagrams += 1;
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
const roomId = safeText(message.roomId, 160);
|
|
266
|
+
const role = safeText(message.role, 16).toLowerCase();
|
|
267
|
+
const token = safeText(message.token, 256);
|
|
268
|
+
if (!roomId || !token || !['hub', 'client'].includes(role)) {
|
|
269
|
+
counters.invalidDatagrams += 1;
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
let room = rooms.get(roomId);
|
|
273
|
+
if (room && at - room.updatedAt >= limits.roomTtlMs) {
|
|
274
|
+
deleteRoom(roomId, 'expired');
|
|
275
|
+
room = null;
|
|
276
|
+
}
|
|
277
|
+
if (room && room.token !== token) {
|
|
278
|
+
counters.tokenMismatchDrops += 1;
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
if (!room) {
|
|
282
|
+
room = createRoom(roomId, token, source, at);
|
|
283
|
+
if (!room) return;
|
|
284
|
+
}
|
|
285
|
+
room.updatedAt = at;
|
|
286
|
+
room.peers.set(role, { address: rinfo.address, port: rinfo.port });
|
|
287
|
+
counters.acceptedRegistrations += 1;
|
|
288
|
+
if (room.peers.size < 2) {
|
|
289
|
+
unpairedRooms.delete(roomId);
|
|
290
|
+
unpairedRooms.set(roomId, room);
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
unpairedRooms.delete(roomId);
|
|
294
|
+
const hub = room.peers.get('hub');
|
|
295
|
+
const client = room.peers.get('client');
|
|
296
|
+
if (!hub || !client) return;
|
|
297
|
+
send({
|
|
298
|
+
type: 'rendezvous.peer',
|
|
299
|
+
roomId,
|
|
300
|
+
role: 'client',
|
|
301
|
+
host: client.address,
|
|
302
|
+
port: client.port
|
|
303
|
+
}, hub.address, hub.port);
|
|
304
|
+
send({
|
|
305
|
+
type: 'rendezvous.peer',
|
|
306
|
+
roomId,
|
|
307
|
+
role: 'hub',
|
|
308
|
+
host: hub.address,
|
|
309
|
+
port: hub.port
|
|
310
|
+
}, client.address, client.port);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
socket.on('message', onMessage);
|
|
314
|
+
socket.on('error', error => {
|
|
315
|
+
counters.socketErrors += 1;
|
|
316
|
+
lastError = safeText(error?.message || error, 240);
|
|
317
|
+
log(`UDP rendezvous error: ${lastError}`);
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
function getStatus() {
|
|
321
|
+
const current = now();
|
|
322
|
+
return {
|
|
323
|
+
service: 'livedesk-udp-rendezvous',
|
|
324
|
+
version: serviceVersion,
|
|
325
|
+
protocol: UDP_P2P_PROTOCOL,
|
|
326
|
+
healthy: started && !closing,
|
|
327
|
+
started,
|
|
328
|
+
host,
|
|
329
|
+
port: boundPort,
|
|
330
|
+
uptimeMs: startedAt ? Math.max(0, current - startedAt) : 0,
|
|
331
|
+
rooms: rooms.size,
|
|
332
|
+
pairedRooms: rooms.size - unpairedRooms.size,
|
|
333
|
+
unpairedRooms: unpairedRooms.size,
|
|
334
|
+
sources: sources.size,
|
|
335
|
+
evictableSources: evictableSources.size,
|
|
336
|
+
limits: { ...limits },
|
|
337
|
+
counters: { ...counters },
|
|
338
|
+
lastError
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function logStatus(reason) {
|
|
343
|
+
log(`UDP rendezvous status ${JSON.stringify({ reason, ...getStatus() })}`);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function startTimers() {
|
|
347
|
+
sweepTimer = setInterval(() => sweep(), limits.sweepIntervalMs);
|
|
348
|
+
sweepTimer.unref?.();
|
|
349
|
+
if (limits.statusIntervalMs > 0) {
|
|
350
|
+
statusTimer = setInterval(() => logStatus('periodic'), limits.statusIntervalMs);
|
|
351
|
+
statusTimer.unref?.();
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function stopTimers() {
|
|
356
|
+
if (sweepTimer) clearInterval(sweepTimer);
|
|
357
|
+
if (statusTimer) clearInterval(statusTimer);
|
|
358
|
+
sweepTimer = null;
|
|
359
|
+
statusTimer = null;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
async function start() {
|
|
363
|
+
if (started) return { host, port: boundPort };
|
|
364
|
+
await new Promise((resolve, reject) => {
|
|
365
|
+
const onError = error => { socket.off('listening', onListening); reject(error); };
|
|
366
|
+
const onListening = () => { socket.off('error', onError); resolve(); };
|
|
367
|
+
socket.once('error', onError);
|
|
368
|
+
socket.once('listening', onListening);
|
|
369
|
+
socket.bind(boundPort, host);
|
|
370
|
+
});
|
|
371
|
+
started = true;
|
|
372
|
+
closing = false;
|
|
373
|
+
startedAt = now();
|
|
374
|
+
boundPort = socket.address().port;
|
|
375
|
+
startTimers();
|
|
376
|
+
log(`UDP rendezvous listening on udp://${host}:${boundPort} version=${serviceVersion}`);
|
|
377
|
+
log(`UDP rendezvous limits ${JSON.stringify(limits)}`);
|
|
378
|
+
return { host, port: boundPort };
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
async function close() {
|
|
382
|
+
closing = true;
|
|
383
|
+
stopTimers();
|
|
384
|
+
rooms.clear();
|
|
385
|
+
unpairedRooms.clear();
|
|
386
|
+
sources.clear();
|
|
387
|
+
evictableSources.clear();
|
|
388
|
+
if (started) {
|
|
389
|
+
await new Promise(resolve => socket.close(() => resolve()));
|
|
390
|
+
started = false;
|
|
391
|
+
}
|
|
392
|
+
closing = false;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
return {
|
|
396
|
+
start,
|
|
397
|
+
close,
|
|
398
|
+
getStatus,
|
|
399
|
+
sweepNow: () => sweep(),
|
|
400
|
+
logStatus: (reason = 'requested') => logStatus(safeText(reason, 48) || 'requested')
|
|
401
|
+
};
|
|
402
|
+
}
|