@livedesk/hub 0.1.11 → 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 +3 -0
- 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
|
@@ -2070,6 +2070,9 @@ function buildRemoteFrameBinaryPacket(frameEvent, diagnostics = {}) {
|
|
|
2070
2070
|
nativeEncodableFrameCount: Number(frame.nativeEncodableFrameCount || 0) || 0,
|
|
2071
2071
|
nativeSkippedFrameCount: Number(frame.nativeSkippedFrameCount || 0) || 0,
|
|
2072
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,
|
|
2073
2076
|
nativeCaptureTelemetryAt: frame.nativeCaptureTelemetryAt || '',
|
|
2074
2077
|
droppedByAgent: Number(frame.droppedByAgent || 0) || 0,
|
|
2075
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
|
+
}
|