@conference-kit/signaling-server 0.0.1 → 0.0.2

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.
Files changed (3) hide show
  1. package/README.md +51 -0
  2. package/package.json +1 -1
  3. package/src/index.ts +192 -5
package/README.md ADDED
@@ -0,0 +1,51 @@
1
+ # @conference-kit/signaling-server
2
+
3
+ A lightweight Bun WebSocket signaling server for conferencing: presence, mesh signaling, broadcasts, waiting rooms, and host controls for admits/rejects and raised hands.
4
+
5
+ ## Install & run
6
+
7
+ ```bash
8
+ npm install
9
+ npm run dev # bun run src/index.ts
10
+ # or build
11
+ npm run build # outputs dist/index.js
12
+ ```
13
+
14
+ Defaults: binds `ws://0.0.0.0:8787`.
15
+
16
+ ## Connecting clients
17
+
18
+ Clients connect via WebSocket query params:
19
+
20
+ - `peerId` (required): unique ID for the peer.
21
+ - `room` (optional): room namespace for presence and broadcasts.
22
+ - `host=1` (optional): mark this peer as host (receives waiting list + control signals).
23
+ - `waitingRoom=1` (optional): non-hosts wait for host admission when enabled.
24
+
25
+ ## Messages
26
+
27
+ - `presence`: broadcast to room on join/leave with `peers` roster.
28
+ - `signal`: relay payloads between peers (`{type:"signal", to, data}` inbound → `{type:"signal", from, data}` outbound).
29
+ - `broadcast`: room-wide app messages (`{type:"broadcast", data}` inbound → `{type:"broadcast", from, room, data}` outbound).
30
+ - `control`: server-mediated actions:
31
+ - hosts send `{action:"admit", data:{peerId}}` or `{action:"reject", data:{peerId}}`.
32
+ - everyone can raise/lower hands: `{action:"raise-hand"}` or `{action:"hand-lowered"}` routed to hosts.
33
+ - server notifies hosts with `{action:"waiting-list", data:{waiting:string[]}}` and notifies peers with `{action:"waiting"|"admitted"|"rejected"}`.
34
+
35
+ ## Waiting room flow
36
+
37
+ 1. Non-host connects with `waitingRoom=1`; server enqueues and sends them `waiting`.
38
+ 2. Hosts in the same room receive `waiting-list` snapshots.
39
+ 3. Host sends `admit` or `reject`.
40
+ 4. Admit: peer is subscribed to the room, gets a `presence` join, and the host list updates. Reject: peer receives `rejected` and the socket closes.
41
+
42
+ ## Topology
43
+
44
+ - Presence and broadcasts are room-scoped (`room:<name>` topics).
45
+ - Signals are peer-scoped (`peer:<peerId>` topics).
46
+ - State is kept in memory maps (`roomMembers`, `waitingRooms`, `clients`).
47
+
48
+ ## Customizing
49
+
50
+ - Update `HOST`/`PORT` in `src/index.ts` if you need a different bind.
51
+ - Extend the `control` switch to add new server-mediated actions as needed.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@conference-kit/signaling-server",
3
- "version": "0.0.1",
3
+ "version": "0.0.2",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
package/src/index.ts CHANGED
@@ -8,6 +8,7 @@ type ServerWebSocket<T> = {
8
8
  unsubscribe(topic: string): void;
9
9
  publish(topic: string, data: string): void;
10
10
  send(data: string): void;
11
+ close(code?: number): void;
11
12
  };
12
13
 
13
14
  declare const Bun: {
@@ -26,7 +27,13 @@ declare const Bun: {
26
27
  }) => { port: number };
27
28
  };
28
29
 
29
- type ClientMeta = { peerId: string; room?: string | null };
30
+ type ClientMeta = {
31
+ peerId: string;
32
+ room?: string | null;
33
+ isHost?: boolean;
34
+ waitingRoom?: boolean;
35
+ admitted?: boolean;
36
+ };
30
37
 
31
38
  type SignalPayload = {
32
39
  type: "signal";
@@ -39,12 +46,25 @@ type BroadcastPayload = {
39
46
  data: unknown;
40
47
  };
41
48
 
42
- type IncomingMessage = SignalPayload | BroadcastPayload;
49
+ type ControlPayload = {
50
+ type: "control";
51
+ action: string;
52
+ data?: unknown;
53
+ };
54
+
55
+ type IncomingMessage = SignalPayload | BroadcastPayload | ControlPayload;
43
56
 
44
57
  type OutgoingMessage =
45
58
  | { type: "signal"; from: string; data: unknown }
46
59
  | { type: "broadcast"; from: string; room?: string | null; data: unknown }
47
60
  | PresenceMessage
61
+ | {
62
+ type: "control";
63
+ from: string;
64
+ room?: string | null;
65
+ action: string;
66
+ data?: unknown;
67
+ }
48
68
  | { type: "error"; message: string };
49
69
 
50
70
  type PresenceMessage = {
@@ -58,7 +78,12 @@ type PresenceMessage = {
58
78
  function parseMessage(raw: string): IncomingMessage | null {
59
79
  try {
60
80
  const parsed = JSON.parse(raw);
61
- if (parsed && (parsed.type === "signal" || parsed.type === "broadcast"))
81
+ if (
82
+ parsed &&
83
+ (parsed.type === "signal" ||
84
+ parsed.type === "broadcast" ||
85
+ parsed.type === "control")
86
+ )
62
87
  return parsed;
63
88
  return null;
64
89
  } catch (error) {
@@ -71,6 +96,45 @@ const HOST = "0.0.0.0";
71
96
  const PORT = 8787;
72
97
 
73
98
  const roomMembers = new Map<string, Set<string>>();
99
+ const waitingRooms = new Map<string, Set<string>>();
100
+ const clients = new Map<string, ServerWebSocket<ClientMeta>>();
101
+
102
+ function getHosts(room?: string | null) {
103
+ if (!room) return [] as ServerWebSocket<ClientMeta>[];
104
+ const hosts: ServerWebSocket<ClientMeta>[] = [];
105
+ for (const ws of clients.values()) {
106
+ if (ws.data.room === room && ws.data.isHost) hosts.push(ws);
107
+ }
108
+ return hosts;
109
+ }
110
+
111
+ function sendControl(
112
+ target: ServerWebSocket<ClientMeta>,
113
+ payload: {
114
+ action: string;
115
+ data?: unknown;
116
+ room?: string | null;
117
+ from?: string;
118
+ }
119
+ ) {
120
+ const message: OutgoingMessage = {
121
+ type: "control",
122
+ from: payload.from ?? "server",
123
+ room: payload.room,
124
+ action: payload.action,
125
+ data: payload.data,
126
+ };
127
+ target.send(JSON.stringify(message));
128
+ }
129
+
130
+ function broadcastToHosts(
131
+ room: string | null | undefined,
132
+ payload: { action: string; data?: unknown }
133
+ ) {
134
+ getHosts(room).forEach((hostWs) =>
135
+ sendControl(hostWs, { action: payload.action, data: payload.data, room })
136
+ );
137
+ }
74
138
 
75
139
  // Signaling server with simple room presence
76
140
  const liveServer = Bun.serve<ClientMeta>({
@@ -80,12 +144,22 @@ const liveServer = Bun.serve<ClientMeta>({
80
144
  const url = new URL(req.url);
81
145
  const peerId = url.searchParams.get("peerId");
82
146
  const room = url.searchParams.get("room");
147
+ const isHost = url.searchParams.get("host") === "1";
148
+ const waitingRoom = url.searchParams.get("waitingRoom") === "1";
83
149
 
84
150
  if (!peerId) {
85
151
  return new Response("peerId query param is required", { status: 400 });
86
152
  }
87
153
 
88
- const upgraded = bunServer.upgrade(req, { data: { peerId, room } });
154
+ const upgraded = bunServer.upgrade(req, {
155
+ data: {
156
+ peerId,
157
+ room,
158
+ isHost,
159
+ waitingRoom,
160
+ admitted: isHost ? true : !waitingRoom,
161
+ },
162
+ });
89
163
  if (!upgraded) {
90
164
  return new Response("WebSocket upgrade failed", { status: 500 });
91
165
  }
@@ -95,8 +169,26 @@ const liveServer = Bun.serve<ClientMeta>({
95
169
  websocket: {
96
170
  open(ws: ServerWebSocket<ClientMeta>) {
97
171
  const { peerId, room } = ws.data;
172
+ clients.set(peerId, ws);
98
173
  ws.subscribe(`peer:${peerId}`);
99
174
  if (room) {
175
+ if (ws.data.waitingRoom && !ws.data.isHost) {
176
+ const waiters = waitingRooms.get(room) ?? new Set<string>();
177
+ waiters.add(peerId);
178
+ waitingRooms.set(room, waiters);
179
+ broadcastToHosts(room, {
180
+ action: "waiting-list",
181
+ data: { waiting: Array.from(waiters) },
182
+ });
183
+ sendControl(ws, {
184
+ action: "waiting",
185
+ room,
186
+ data: { position: waiters.size },
187
+ });
188
+ return;
189
+ }
190
+
191
+ ws.data.admitted = true;
100
192
  ws.subscribe(`room:${room}`);
101
193
  const set = roomMembers.get(room) ?? new Set<string>();
102
194
  set.add(peerId);
@@ -108,9 +200,18 @@ const liveServer = Bun.serve<ClientMeta>({
108
200
  peers: Array.from(set),
109
201
  action: "join",
110
202
  };
111
- // Publish to everyone in the room and also send directly so the new peer gets an immediate roster snapshot.
112
203
  ws.publish(`room:${room}`, JSON.stringify(presence));
113
204
  ws.send(JSON.stringify(presence));
205
+
206
+ // send waiting list snapshot to host
207
+ const pending = waitingRooms.get(room);
208
+ if (ws.data.isHost && pending && pending.size) {
209
+ sendControl(ws, {
210
+ action: "waiting-list",
211
+ room,
212
+ data: { waiting: Array.from(pending) },
213
+ });
214
+ }
114
215
  }
115
216
  },
116
217
  message(
@@ -151,11 +252,97 @@ const liveServer = Bun.serve<ClientMeta>({
151
252
  }
152
253
  return;
153
254
  }
255
+
256
+ if (parsed.type === "control") {
257
+ const room = ws.data.room;
258
+ if (parsed.action === "admit" && ws.data.isHost && room) {
259
+ const targetId = (parsed.data as any)?.peerId as string | undefined;
260
+ if (!targetId) return;
261
+ const waiters = waitingRooms.get(room);
262
+ const targetWs = targetId ? clients.get(targetId) : undefined;
263
+ if (waiters && waiters.has(targetId) && targetWs) {
264
+ waiters.delete(targetId);
265
+ waitingRooms.set(room, waiters);
266
+ targetWs.data.admitted = true;
267
+ targetWs.subscribe(`room:${room}`);
268
+ const set = roomMembers.get(room) ?? new Set<string>();
269
+ set.add(targetId);
270
+ roomMembers.set(room, set);
271
+ const presence: PresenceMessage = {
272
+ type: "presence",
273
+ room,
274
+ peerId: targetId,
275
+ peers: Array.from(set),
276
+ action: "join",
277
+ };
278
+ targetWs.publish(`room:${room}`, JSON.stringify(presence));
279
+ targetWs.send(JSON.stringify(presence));
280
+ broadcastToHosts(room, {
281
+ action: "waiting-list",
282
+ data: { waiting: Array.from(waiters) },
283
+ });
284
+ sendControl(targetWs, {
285
+ action: "admitted",
286
+ room,
287
+ from: ws.data.peerId,
288
+ });
289
+ }
290
+ return;
291
+ }
292
+
293
+ if (parsed.action === "reject" && ws.data.isHost && room) {
294
+ const targetId = (parsed.data as any)?.peerId as string | undefined;
295
+ if (!targetId) return;
296
+ const waiters = waitingRooms.get(room);
297
+ const targetWs = targetId ? clients.get(targetId) : undefined;
298
+ if (waiters && waiters.has(targetId) && targetWs) {
299
+ waiters.delete(targetId);
300
+ waitingRooms.set(room, waiters);
301
+ sendControl(targetWs, {
302
+ action: "rejected",
303
+ room,
304
+ from: ws.data.peerId,
305
+ });
306
+ targetWs.close();
307
+ broadcastToHosts(room, {
308
+ action: "waiting-list",
309
+ data: { waiting: Array.from(waiters) },
310
+ });
311
+ }
312
+ return;
313
+ }
314
+
315
+ if (parsed.action === "raise-hand" && room) {
316
+ broadcastToHosts(room, {
317
+ action: "raise-hand",
318
+ data: { peerId: ws.data.peerId },
319
+ });
320
+ return;
321
+ }
322
+
323
+ if (parsed.action === "hand-lowered" && room) {
324
+ broadcastToHosts(room, {
325
+ action: "hand-lowered",
326
+ data: { peerId: ws.data.peerId },
327
+ });
328
+ return;
329
+ }
330
+ }
154
331
  },
155
332
  close(ws: ServerWebSocket<ClientMeta>) {
156
333
  const { peerId, room } = ws.data;
157
334
  ws.unsubscribe(`peer:${peerId}`);
335
+ clients.delete(peerId);
158
336
  if (room) {
337
+ const waiters = waitingRooms.get(room);
338
+ if (waiters && waiters.delete(peerId)) {
339
+ waitingRooms.set(room, waiters);
340
+ broadcastToHosts(room, {
341
+ action: "waiting-list",
342
+ data: { waiting: Array.from(waiters) },
343
+ });
344
+ return;
345
+ }
159
346
  ws.unsubscribe(`room:${room}`);
160
347
  const set = roomMembers.get(room);
161
348
  if (set) {