@zero-server/webrtc 0.9.7
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/LICENSE +21 -0
- package/README.md +37 -0
- package/index.d.ts +2 -0
- package/index.js +53 -0
- package/lib/auth/index.js +1 -0
- package/lib/debug.js +372 -0
- package/lib/errors.js +1 -0
- package/lib/middleware/index.js +1 -0
- package/lib/observe/index.js +1 -0
- package/lib/webrtc/bot.js +361 -0
- package/lib/webrtc/cli.js +182 -0
- package/lib/webrtc/cluster.js +350 -0
- package/lib/webrtc/e2ee.js +282 -0
- package/lib/webrtc/ice.js +370 -0
- package/lib/webrtc/index.js +132 -0
- package/lib/webrtc/joinToken.js +116 -0
- package/lib/webrtc/observe.js +229 -0
- package/lib/webrtc/peer.js +116 -0
- package/lib/webrtc/room.js +171 -0
- package/lib/webrtc/sdp.js +508 -0
- package/lib/webrtc/sfu/index.js +201 -0
- package/lib/webrtc/sfu/livekit.js +301 -0
- package/lib/webrtc/sfu/mediasoup.js +317 -0
- package/lib/webrtc/sfu/memory.js +204 -0
- package/lib/webrtc/signaling.js +546 -0
- package/lib/webrtc/stun.js +492 -0
- package/lib/webrtc/turn/codec.js +370 -0
- package/lib/webrtc/turn/credentials.js +141 -0
- package/lib/webrtc/turn/server.js +633 -0
- package/lib/ws/index.js +1 -0
- package/package.json +62 -0
- package/types/app.d.ts +223 -0
- package/types/auth.d.ts +520 -0
- package/types/body.d.ts +14 -0
- package/types/cli.d.ts +2 -0
- package/types/cluster.d.ts +75 -0
- package/types/env.d.ts +80 -0
- package/types/errors.d.ts +316 -0
- package/types/fetch.d.ts +43 -0
- package/types/grpc.d.ts +432 -0
- package/types/index.d.ts +396 -0
- package/types/lifecycle.d.ts +60 -0
- package/types/middleware.d.ts +320 -0
- package/types/observe.d.ts +304 -0
- package/types/orm.d.ts +1887 -0
- package/types/request.d.ts +109 -0
- package/types/response.d.ts +157 -0
- package/types/router.d.ts +78 -0
- package/types/sse.d.ts +78 -0
- package/types/webrtc.d.ts +501 -0
- package/types/websocket.d.ts +126 -0
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module webrtc/observe
|
|
3
|
+
* @description Optional metrics + tracing wiring for a {@link SignalingHub}.
|
|
4
|
+
* Pass a `MetricsRegistry`, a `Tracer`, or both; the binder
|
|
5
|
+
* subscribes to the hub's lifecycle events and exports the six
|
|
6
|
+
* standard `zs_webrtc_*` Prometheus series plus per-operation
|
|
7
|
+
* OpenTelemetry-compatible spans.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
'use strict';
|
|
11
|
+
|
|
12
|
+
// --- Helpers ---
|
|
13
|
+
|
|
14
|
+
/** Extract the first `a=ice-ufrag:` value from an SDP blob. */
|
|
15
|
+
function _extractUfrag(sdp)
|
|
16
|
+
{
|
|
17
|
+
const m = /^a=ice-ufrag:([^\r\n]+)/m.exec(sdp);
|
|
18
|
+
return m ? m[1].trim() : null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// --- Metrics binder ---
|
|
22
|
+
|
|
23
|
+
function _registerMetrics(registry)
|
|
24
|
+
{
|
|
25
|
+
return {
|
|
26
|
+
peersActive: registry.gauge({
|
|
27
|
+
name: 'zs_webrtc_peers_active',
|
|
28
|
+
help: 'Number of WebRTC peers currently joined per room.',
|
|
29
|
+
labels: ['room'],
|
|
30
|
+
}),
|
|
31
|
+
roomsActive: registry.gauge({
|
|
32
|
+
name: 'zs_webrtc_rooms_active',
|
|
33
|
+
help: 'Number of WebRTC rooms with at least one peer.',
|
|
34
|
+
}),
|
|
35
|
+
signalingMessages: registry.counter({
|
|
36
|
+
name: 'zs_webrtc_signaling_messages_total',
|
|
37
|
+
help: 'WebRTC signaling messages by type, direction, and outcome.',
|
|
38
|
+
labels: ['type', 'direction', 'result'],
|
|
39
|
+
}),
|
|
40
|
+
offerDuration: registry.histogram({
|
|
41
|
+
name: 'zs_webrtc_offer_duration_ms',
|
|
42
|
+
help: 'End-to-end latency between an offer and its matching answer, in ms.',
|
|
43
|
+
labels: ['room'],
|
|
44
|
+
buckets: [5, 10, 25, 50, 100, 250, 500, 1000, 2500, 5000, 10000],
|
|
45
|
+
}),
|
|
46
|
+
joinFailures: registry.counter({
|
|
47
|
+
name: 'zs_webrtc_join_failures_total',
|
|
48
|
+
help: 'WebRTC room joins rejected by the hub.',
|
|
49
|
+
labels: ['reason'],
|
|
50
|
+
}),
|
|
51
|
+
iceRestart: registry.counter({
|
|
52
|
+
name: 'zs_webrtc_ice_restart_total',
|
|
53
|
+
help: 'Detected ICE restarts (ufrag rotation) per room.',
|
|
54
|
+
labels: ['room'],
|
|
55
|
+
}),
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function _bindMetrics(hub, m)
|
|
60
|
+
{
|
|
61
|
+
/** Outstanding offer timestamps: `offererPeerId` -> ms epoch. */
|
|
62
|
+
const offerStart = new Map();
|
|
63
|
+
/** Last seen ufrag per peer.id (for ICE-restart detection). */
|
|
64
|
+
const lastUfrag = new Map();
|
|
65
|
+
|
|
66
|
+
hub.on('signal', ({ peer, type }) =>
|
|
67
|
+
{
|
|
68
|
+
m.signalingMessages.inc({ type, direction: 'in', result: 'ok' });
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
hub.on('join', ({ peer, room }) =>
|
|
72
|
+
{
|
|
73
|
+
const before = m.peersActive.get({ room: room.name });
|
|
74
|
+
m.peersActive.inc({ room: room.name });
|
|
75
|
+
// Room newly non-empty?
|
|
76
|
+
if (before === 0) m.roomsActive.inc();
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
hub.on('leave', ({ peer, room }) =>
|
|
80
|
+
{
|
|
81
|
+
m.peersActive.dec({ room: room.name });
|
|
82
|
+
if (m.peersActive.get({ room: room.name }) <= 0)
|
|
83
|
+
m.roomsActive.dec();
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
hub.on('joinFailed', ({ reason }) =>
|
|
87
|
+
{
|
|
88
|
+
m.joinFailures.inc({ reason: reason || 'UNKNOWN' });
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
hub.on('offer', ({ peer, sdp, room }) =>
|
|
92
|
+
{
|
|
93
|
+
offerStart.set(peer.id, Date.now());
|
|
94
|
+
|
|
95
|
+
// ICE-restart detection: compare ufrag against last-known for this peer.
|
|
96
|
+
const ufrag = _extractUfrag(sdp);
|
|
97
|
+
if (ufrag)
|
|
98
|
+
{
|
|
99
|
+
const prev = lastUfrag.get(peer.id);
|
|
100
|
+
if (prev && prev !== ufrag)
|
|
101
|
+
m.iceRestart.inc({ room: room.name });
|
|
102
|
+
lastUfrag.set(peer.id, ufrag);
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
hub.on('answer', ({ peer, target, room }) =>
|
|
107
|
+
{
|
|
108
|
+
// The offer was sent by the original target (now answering back to it).
|
|
109
|
+
const startedAt = offerStart.get(target.id);
|
|
110
|
+
if (startedAt !== undefined)
|
|
111
|
+
{
|
|
112
|
+
m.offerDuration.observe({ room: room.name }, Date.now() - startedAt);
|
|
113
|
+
offerStart.delete(target.id);
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// --- Tracing binder ---
|
|
119
|
+
|
|
120
|
+
function _bindTracing(hub, tracer)
|
|
121
|
+
{
|
|
122
|
+
hub.on('signal', ({ peer, type }) =>
|
|
123
|
+
{
|
|
124
|
+
const span = tracer.startSpan('webrtc.signal', {
|
|
125
|
+
kind: 'server',
|
|
126
|
+
attributes: { 'peer.id': peer.id, 'rtc.type': type },
|
|
127
|
+
});
|
|
128
|
+
span.setOk();
|
|
129
|
+
span.end();
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
hub.on('join', ({ peer, room }) =>
|
|
133
|
+
{
|
|
134
|
+
const span = tracer.startSpan('webrtc.join', {
|
|
135
|
+
kind: 'server',
|
|
136
|
+
attributes: { 'peer.id': peer.id, 'room.id': room.name },
|
|
137
|
+
});
|
|
138
|
+
span.setOk();
|
|
139
|
+
span.end();
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
hub.on('joinFailed', ({ peer, reason, room }) =>
|
|
143
|
+
{
|
|
144
|
+
const span = tracer.startSpan('webrtc.join', {
|
|
145
|
+
kind: 'server',
|
|
146
|
+
attributes: { 'peer.id': peer.id, 'room.id': room || '', 'rtc.error': reason },
|
|
147
|
+
});
|
|
148
|
+
span.setError(reason);
|
|
149
|
+
span.end();
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
hub.on('offer', ({ peer, target, room }) =>
|
|
153
|
+
{
|
|
154
|
+
const span = tracer.startSpan('webrtc.publish', {
|
|
155
|
+
kind: 'producer',
|
|
156
|
+
attributes: { 'peer.id': peer.id, 'room.id': room.name, 'rtc.target': target.id },
|
|
157
|
+
});
|
|
158
|
+
span.setOk();
|
|
159
|
+
span.end();
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
hub.on('publishFailed', ({ peer, reason, room }) =>
|
|
163
|
+
{
|
|
164
|
+
const span = tracer.startSpan('webrtc.publish', {
|
|
165
|
+
kind: 'producer',
|
|
166
|
+
attributes: { 'peer.id': peer.id, 'room.id': room || '', 'rtc.error': reason },
|
|
167
|
+
});
|
|
168
|
+
span.setError(reason);
|
|
169
|
+
span.end();
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
hub.on('answer', ({ peer, target, room }) =>
|
|
173
|
+
{
|
|
174
|
+
const span = tracer.startSpan('webrtc.subscribe', {
|
|
175
|
+
kind: 'consumer',
|
|
176
|
+
attributes: { 'peer.id': peer.id, 'room.id': room.name, 'rtc.target': target.id },
|
|
177
|
+
});
|
|
178
|
+
span.setOk();
|
|
179
|
+
span.end();
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
hub.on('subscribeFailed', ({ peer, reason, room }) =>
|
|
183
|
+
{
|
|
184
|
+
const span = tracer.startSpan('webrtc.subscribe', {
|
|
185
|
+
kind: 'consumer',
|
|
186
|
+
attributes: { 'peer.id': peer.id, 'room.id': room || '', 'rtc.error': reason },
|
|
187
|
+
});
|
|
188
|
+
span.setError(reason);
|
|
189
|
+
span.end();
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// --- Public API ---
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Wire a {@link SignalingHub} to a metrics registry and / or a tracer.
|
|
197
|
+
*
|
|
198
|
+
* Registers six standard Prometheus series under the `zs_webrtc_` prefix:
|
|
199
|
+
*
|
|
200
|
+
* - `zs_webrtc_peers_active{room}` (gauge)
|
|
201
|
+
* - `zs_webrtc_rooms_active` (gauge)
|
|
202
|
+
* - `zs_webrtc_signaling_messages_total{type,direction,result}` (counter)
|
|
203
|
+
* - `zs_webrtc_offer_duration_ms{room}` (histogram)
|
|
204
|
+
* - `zs_webrtc_join_failures_total{reason}` (counter)
|
|
205
|
+
* - `zs_webrtc_ice_restart_total{room}` (counter)
|
|
206
|
+
*
|
|
207
|
+
* Emits spans named `webrtc.join`, `webrtc.signal`, `webrtc.publish`, and
|
|
208
|
+
* `webrtc.subscribe`, each annotated with `peer.id` and `room.id`.
|
|
209
|
+
*
|
|
210
|
+
* @section Observability
|
|
211
|
+
*
|
|
212
|
+
* @param {SignalingHub} hub - The hub to instrument.
|
|
213
|
+
* @param {object} opts
|
|
214
|
+
* @param {MetricsRegistry} [opts.metrics] - Prometheus-compatible registry.
|
|
215
|
+
* @param {Tracer} [opts.tracer] - Tracer for span emission.
|
|
216
|
+
* @returns {SignalingHub} The same hub, for chaining.
|
|
217
|
+
*
|
|
218
|
+
* @example | Plug into an existing app
|
|
219
|
+
* const hub = new SignalingHub();
|
|
220
|
+
* bindObservability(hub, { metrics: app.metrics() });
|
|
221
|
+
*/
|
|
222
|
+
function bindObservability(hub, opts = {})
|
|
223
|
+
{
|
|
224
|
+
if (opts.metrics) _bindMetrics(hub, _registerMetrics(opts.metrics));
|
|
225
|
+
if (opts.tracer) _bindTracing(hub, opts.tracer);
|
|
226
|
+
return hub;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
module.exports = { bindObservability };
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module webrtc/peer
|
|
3
|
+
* @description Per-connection state machine for a WebRTC signaling peer.
|
|
4
|
+
*
|
|
5
|
+
* A `Peer` wraps a transport object (typically a `WebSocketConnection`
|
|
6
|
+
* but any duck-typed `{ send, on, close }` works - see the unit tests
|
|
7
|
+
* for a minimal in-memory transport) and exposes the JSEP message
|
|
8
|
+
* types as ordinary events. The hub is responsible for routing.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
'use strict';
|
|
12
|
+
|
|
13
|
+
let _peerCounter = 0;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* JSEP signaling state strings, matching RFC 8829 §4.1.
|
|
17
|
+
* @enum {string}
|
|
18
|
+
*/
|
|
19
|
+
const PEER_STATE = Object.freeze({
|
|
20
|
+
STABLE: 'stable',
|
|
21
|
+
HAVE_LOCAL_OFFER: 'have-local-offer',
|
|
22
|
+
HAVE_REMOTE_OFFER: 'have-remote-offer',
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* One signaling peer attached to a `SignalingHub` via some transport.
|
|
27
|
+
*
|
|
28
|
+
* @class
|
|
29
|
+
* @section Peers
|
|
30
|
+
*
|
|
31
|
+
* @example
|
|
32
|
+
* hub.on('join', ({ peer, room }) => {
|
|
33
|
+
* peer.send('welcome', { roomSize: room.size });
|
|
34
|
+
* peer.on('mute', ev => audit('mute', peer.id, ev.kind));
|
|
35
|
+
* });
|
|
36
|
+
*/
|
|
37
|
+
class Peer
|
|
38
|
+
{
|
|
39
|
+
/**
|
|
40
|
+
* @constructor
|
|
41
|
+
* @param {object} transport - Anything with `send(string)`, `on('message'|'close', cb)`, `close(code?, reason?)`.
|
|
42
|
+
* @param {object} [info] - Connection metadata.
|
|
43
|
+
* @param {*} [info.user] - Authenticated user object (if any).
|
|
44
|
+
* @param {string} [info.ip] - Remote IP for audit / rate limits.
|
|
45
|
+
*/
|
|
46
|
+
constructor(transport, info = {})
|
|
47
|
+
{
|
|
48
|
+
/** @type {string} Globally-unique peer id within a hub. */
|
|
49
|
+
this.id = 'peer_' + (++_peerCounter) + '_' + Date.now().toString(36);
|
|
50
|
+
|
|
51
|
+
/** @type {*} Authenticated user object (if any). */
|
|
52
|
+
this.user = info.user || null;
|
|
53
|
+
|
|
54
|
+
/** @type {string|null} Remote IP for audit / rate limits. */
|
|
55
|
+
this.ip = info.ip || null;
|
|
56
|
+
|
|
57
|
+
/** @type {object} Underlying transport (WS connection or mock). */
|
|
58
|
+
this.transport = transport;
|
|
59
|
+
|
|
60
|
+
/** @type {string} Current JSEP state. */
|
|
61
|
+
this.state = PEER_STATE.STABLE;
|
|
62
|
+
|
|
63
|
+
/** @type {import('./room')|null} Current room membership. */
|
|
64
|
+
this.room = null;
|
|
65
|
+
|
|
66
|
+
/** @type {number} Count of malformed frames received - rate-limit material. */
|
|
67
|
+
this.errors = 0;
|
|
68
|
+
|
|
69
|
+
/** @type {number} ms timestamp the peer was created. */
|
|
70
|
+
this.connectedAt = Date.now();
|
|
71
|
+
|
|
72
|
+
/** @type {boolean} */
|
|
73
|
+
this.closed = false;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Send a JSON envelope to this peer. `type` is added to `payload` and
|
|
78
|
+
* the result is serialised once. Silently drops sends after close.
|
|
79
|
+
* @param {string} type
|
|
80
|
+
* @param {object} [payload]
|
|
81
|
+
*/
|
|
82
|
+
send(type, payload)
|
|
83
|
+
{
|
|
84
|
+
if (this.closed) return;
|
|
85
|
+
const frame = Object.assign({ type }, payload || {});
|
|
86
|
+
try { this.transport.send(JSON.stringify(frame)); }
|
|
87
|
+
catch { /* transport may already be closed; ignore */ }
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Send a typed error frame. Callers SHOULD use this rather than throwing,
|
|
92
|
+
* because exceptions escape the message handler and tear down the process.
|
|
93
|
+
* @param {string} code
|
|
94
|
+
* @param {string} message
|
|
95
|
+
*/
|
|
96
|
+
sendError(code, message)
|
|
97
|
+
{
|
|
98
|
+
this.send('error', { code, message });
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Close the underlying transport with a WebSocket-style code and reason.
|
|
103
|
+
* Defaults to 1000 (normal closure).
|
|
104
|
+
* @param {number} [code=1000]
|
|
105
|
+
* @param {string} [reason='']
|
|
106
|
+
*/
|
|
107
|
+
close(code = 1000, reason = '')
|
|
108
|
+
{
|
|
109
|
+
if (this.closed) return;
|
|
110
|
+
this.closed = true;
|
|
111
|
+
try { this.transport.close(code, reason); }
|
|
112
|
+
catch { /* already closed */ }
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
module.exports = { Peer, PEER_STATE };
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module webrtc/room
|
|
3
|
+
* @description Room / channel abstraction for the WebRTC signaling hub.
|
|
4
|
+
*
|
|
5
|
+
* A `Room` holds a set of `Peer`s plus a list of `require()` policy gates
|
|
6
|
+
* that decide whether a given peer may join. Membership is in-process;
|
|
7
|
+
* the cluster adapter (PR 8) will fan room state out via pub/sub.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
'use strict';
|
|
11
|
+
|
|
12
|
+
const { SignalingError } = require('../errors');
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* One signaling room.
|
|
16
|
+
*
|
|
17
|
+
* Constructed lazily by `SignalingHub#room(name)`. Application code never
|
|
18
|
+
* calls `new Room()` directly.
|
|
19
|
+
*
|
|
20
|
+
* @class
|
|
21
|
+
* @section Rooms
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* hub.room('lobby').open();
|
|
25
|
+
* hub.room('boardroom')
|
|
26
|
+
* .require(peer => peer.user && peer.user.role === 'exec')
|
|
27
|
+
* .canPublish(peer => peer.user.isHost);
|
|
28
|
+
*/
|
|
29
|
+
class Room
|
|
30
|
+
{
|
|
31
|
+
/**
|
|
32
|
+
* @constructor
|
|
33
|
+
* @param {string} name - Room name, used as routing key.
|
|
34
|
+
* @param {object} [opts]
|
|
35
|
+
* @param {import('./signaling').SignalingHub} [opts.hub] - Owning hub.
|
|
36
|
+
*/
|
|
37
|
+
constructor(name, opts = {})
|
|
38
|
+
{
|
|
39
|
+
if (typeof name !== 'string' || name.length === 0)
|
|
40
|
+
throw new SignalingError('Room name must be a non-empty string');
|
|
41
|
+
|
|
42
|
+
/** @type {string} */
|
|
43
|
+
this.name = name;
|
|
44
|
+
|
|
45
|
+
/** @type {import('./signaling').SignalingHub|null} */
|
|
46
|
+
this.hub = opts.hub || null;
|
|
47
|
+
|
|
48
|
+
/** @type {Set<import('./peer').Peer>} */
|
|
49
|
+
this._peers = new Set();
|
|
50
|
+
|
|
51
|
+
/** @type {Array<(peer:import('./peer').Peer) => boolean>} */
|
|
52
|
+
this._gates = [];
|
|
53
|
+
|
|
54
|
+
/** @type {((peer:import('./peer').Peer) => boolean)|null} */
|
|
55
|
+
this._canPublish = null;
|
|
56
|
+
|
|
57
|
+
/** @type {((peer:import('./peer').Peer) => boolean)|null} */
|
|
58
|
+
this._canSubscribe = null;
|
|
59
|
+
|
|
60
|
+
/** @type {boolean} `true` once `.open()` has been called. */
|
|
61
|
+
this.isOpen = false;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// -- Configuration (fluent) --
|
|
65
|
+
|
|
66
|
+
/** Mark the room as public. Returns `this` for chaining. */
|
|
67
|
+
open()
|
|
68
|
+
{
|
|
69
|
+
this.isOpen = true;
|
|
70
|
+
return this;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Add a policy gate. Called on every join; first falsy return rejects.
|
|
75
|
+
* @param {(peer:import('./peer').Peer) => boolean | Promise<boolean>} fn
|
|
76
|
+
* @returns {Room}
|
|
77
|
+
*/
|
|
78
|
+
require(fn)
|
|
79
|
+
{
|
|
80
|
+
if (typeof fn !== 'function')
|
|
81
|
+
throw new SignalingError('Room.require(fn) requires a function');
|
|
82
|
+
this._gates.push(fn);
|
|
83
|
+
return this;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Set the publish-permission check. Hub calls this before relaying offers.
|
|
88
|
+
* @param {(peer:import('./peer').Peer) => boolean} fn
|
|
89
|
+
*/
|
|
90
|
+
canPublish(fn) { this._canPublish = fn; return this; }
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Set the subscribe-permission check. Hub calls this before relaying answers.
|
|
94
|
+
* @param {(peer:import('./peer').Peer) => boolean} fn
|
|
95
|
+
*/
|
|
96
|
+
canSubscribe(fn) { this._canSubscribe = fn; return this; }
|
|
97
|
+
|
|
98
|
+
// -- Membership --
|
|
99
|
+
|
|
100
|
+
/** Current member count. */
|
|
101
|
+
get size() { return this._peers.size; }
|
|
102
|
+
|
|
103
|
+
/** @returns {import('./peer').Peer[]} */
|
|
104
|
+
peers() { return Array.from(this._peers); }
|
|
105
|
+
|
|
106
|
+
/** @returns {boolean} */
|
|
107
|
+
has(peer) { return this._peers.has(peer); }
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Evaluate every `require()` gate against the candidate peer.
|
|
111
|
+
* @param {import('./peer').Peer} peer
|
|
112
|
+
* @returns {boolean}
|
|
113
|
+
*/
|
|
114
|
+
canJoin(peer)
|
|
115
|
+
{
|
|
116
|
+
for (const gate of this._gates)
|
|
117
|
+
{
|
|
118
|
+
try { if (!gate(peer)) return false; }
|
|
119
|
+
catch { return false; }
|
|
120
|
+
}
|
|
121
|
+
return true;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** Internal - hub uses this; do not call from application code. */
|
|
125
|
+
_add(peer)
|
|
126
|
+
{
|
|
127
|
+
this._peers.add(peer);
|
|
128
|
+
peer.room = this;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** Internal - hub uses this; do not call from application code. */
|
|
132
|
+
_remove(peer)
|
|
133
|
+
{
|
|
134
|
+
if (!this._peers.has(peer)) return;
|
|
135
|
+
this._peers.delete(peer);
|
|
136
|
+
if (peer.room === this) peer.room = null;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// -- Fan-out --
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Send a `{type, ...payload}` JSON frame to every peer in the room.
|
|
143
|
+
* @param {string} type
|
|
144
|
+
* @param {object} [payload]
|
|
145
|
+
* @param {string} [exceptPeerId] - Optional peer id to skip (e.g. the originator).
|
|
146
|
+
*/
|
|
147
|
+
broadcast(type, payload, exceptPeerId)
|
|
148
|
+
{
|
|
149
|
+
for (const p of this._peers)
|
|
150
|
+
{
|
|
151
|
+
if (exceptPeerId && p.id === exceptPeerId) continue;
|
|
152
|
+
p.send(type, payload);
|
|
153
|
+
}
|
|
154
|
+
if (this.hub && this.hub._cluster)
|
|
155
|
+
this.hub._cluster.fanoutRoom(this.name, type, payload, exceptPeerId);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** Kick every peer with code 1001 (going-away) and unregister from the hub. */
|
|
159
|
+
close(reason = 'room-closed')
|
|
160
|
+
{
|
|
161
|
+
for (const p of Array.from(this._peers))
|
|
162
|
+
{
|
|
163
|
+
p.send('bye', { reason });
|
|
164
|
+
p.close(1001, reason);
|
|
165
|
+
}
|
|
166
|
+
this._peers.clear();
|
|
167
|
+
if (this.hub) this.hub._removeRoom(this);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
module.exports = { Room };
|