@air-jam/server 0.1.0

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.
@@ -0,0 +1,749 @@
1
+ // src/index.ts
2
+ import {
3
+ controllerInputSchema,
4
+ controllerJoinSchema,
5
+ controllerLeaveSchema,
6
+ controllerStateSchema,
7
+ controllerSystemSchema,
8
+ ErrorCode,
9
+ hostJoinAsChildSchema,
10
+ hostRegisterSystemSchema,
11
+ hostRegistrationSchema,
12
+ systemLaunchGameSchema
13
+ } from "@air-jam/sdk/protocol";
14
+ import Color from "color";
15
+ import cors from "cors";
16
+ import express from "express";
17
+ import { createServer } from "http";
18
+ import { Server } from "socket.io";
19
+ import { v4 as uuidv4 } from "uuid";
20
+
21
+ // src/services/auth-service.ts
22
+ import { and, eq } from "drizzle-orm";
23
+
24
+ // src/db.ts
25
+ import * as dotenv from "dotenv";
26
+ import { boolean, pgTable, text, timestamp } from "drizzle-orm/pg-core";
27
+ import { drizzle } from "drizzle-orm/postgres-js";
28
+ import postgres from "postgres";
29
+ dotenv.config();
30
+ var apiKeys = pgTable("api_keys", {
31
+ id: text("id").primaryKey(),
32
+ gameId: text("game_id").notNull().unique(),
33
+ // One API key per game
34
+ key: text("key").notNull().unique(),
35
+ isActive: boolean("is_active").default(true).notNull(),
36
+ createdAt: timestamp("created_at").defaultNow().notNull(),
37
+ lastUsedAt: timestamp("last_used_at")
38
+ });
39
+ var connectionString = process.env.DATABASE_URL;
40
+ var client = connectionString ? postgres(connectionString) : null;
41
+ var db = client ? drizzle(client) : null;
42
+
43
+ // src/services/auth-service.ts
44
+ var AuthService = class {
45
+ masterKey;
46
+ databaseUrl;
47
+ isDevMode;
48
+ constructor() {
49
+ this.masterKey = process.env.AIR_JAM_MASTER_KEY;
50
+ this.databaseUrl = process.env.DATABASE_URL;
51
+ this.isDevMode = !this.masterKey && !this.databaseUrl;
52
+ if (this.isDevMode) {
53
+ console.log(
54
+ "[server] Running in development mode - authentication disabled"
55
+ );
56
+ } else if (this.masterKey && !this.databaseUrl) {
57
+ console.log(
58
+ "[server] Running with master key authentication (no database required)"
59
+ );
60
+ } else if (this.databaseUrl) {
61
+ console.log("[server] Running with database authentication");
62
+ }
63
+ }
64
+ /**
65
+ * Verify an API key
66
+ * Returns verification result with optional error message
67
+ * In dev mode, always returns success
68
+ */
69
+ async verifyApiKey(apiKey) {
70
+ if (this.isDevMode) {
71
+ return { isVerified: true };
72
+ }
73
+ if (!apiKey) {
74
+ return {
75
+ isVerified: false,
76
+ error: "Unauthorized: Invalid or Missing API Key"
77
+ };
78
+ }
79
+ if (this.masterKey && apiKey === this.masterKey) {
80
+ return { isVerified: true };
81
+ }
82
+ if (!this.databaseUrl || !db) {
83
+ return {
84
+ isVerified: false,
85
+ error: "Unauthorized: Invalid or Missing API Key"
86
+ };
87
+ }
88
+ try {
89
+ const [keyRecord] = await db.select().from(apiKeys).where(and(eq(apiKeys.key, apiKey), eq(apiKeys.isActive, true))).limit(1);
90
+ if (keyRecord) {
91
+ db.update(apiKeys).set({ lastUsedAt: /* @__PURE__ */ new Date() }).where(eq(apiKeys.id, keyRecord.id)).catch(
92
+ (err) => console.error("[server] Failed to update lastUsedAt", err)
93
+ );
94
+ return { isVerified: true };
95
+ }
96
+ return {
97
+ isVerified: false,
98
+ error: "Unauthorized: Invalid or Missing API Key"
99
+ };
100
+ } catch (error) {
101
+ console.error("[server] Database error during key verification", error);
102
+ return {
103
+ isVerified: false,
104
+ error: "Internal Server Error"
105
+ };
106
+ }
107
+ }
108
+ };
109
+ var authService = new AuthService();
110
+
111
+ // src/services/room-manager.ts
112
+ var RoomManager = class {
113
+ rooms = /* @__PURE__ */ new Map();
114
+ hostIndex = /* @__PURE__ */ new Map();
115
+ controllerIndex = /* @__PURE__ */ new Map();
116
+ /**
117
+ * Get a room by ID
118
+ */
119
+ getRoom(roomId) {
120
+ return this.rooms.get(roomId);
121
+ }
122
+ /**
123
+ * Create or update a room
124
+ */
125
+ setRoom(roomId, session) {
126
+ this.rooms.set(roomId, session);
127
+ }
128
+ /**
129
+ * Delete a room
130
+ */
131
+ deleteRoom(roomId) {
132
+ this.rooms.delete(roomId);
133
+ }
134
+ /**
135
+ * Get room ID by host socket ID
136
+ */
137
+ getRoomByHostId(socketId) {
138
+ return this.hostIndex.get(socketId);
139
+ }
140
+ /**
141
+ * Associate a host socket with a room
142
+ */
143
+ setHostRoom(socketId, roomId) {
144
+ this.hostIndex.set(socketId, roomId);
145
+ }
146
+ /**
147
+ * Remove host association
148
+ */
149
+ deleteHost(socketId) {
150
+ this.hostIndex.delete(socketId);
151
+ }
152
+ /**
153
+ * Get controller info by socket ID
154
+ */
155
+ getControllerInfo(socketId) {
156
+ return this.controllerIndex.get(socketId);
157
+ }
158
+ /**
159
+ * Associate a controller socket with room and controller ID
160
+ */
161
+ setController(socketId, entry) {
162
+ this.controllerIndex.set(socketId, entry);
163
+ }
164
+ /**
165
+ * Remove controller association
166
+ */
167
+ deleteController(socketId) {
168
+ this.controllerIndex.delete(socketId);
169
+ }
170
+ /**
171
+ * Get the active host socket ID based on focus
172
+ */
173
+ getActiveHostId(session) {
174
+ return session.focus === "GAME" && session.childHostSocketId ? session.childHostSocketId : session.masterHostSocketId;
175
+ }
176
+ /**
177
+ * Remove a room and clean up all associations
178
+ */
179
+ removeRoom(roomId, io2, reason) {
180
+ const session = this.rooms.get(roomId);
181
+ if (!session) return;
182
+ io2.to(roomId).emit("server:hostLeft", { roomId, reason });
183
+ session.controllers.forEach((controller) => {
184
+ this.controllerIndex.delete(controller.socketId);
185
+ });
186
+ this.hostIndex.delete(session.masterHostSocketId);
187
+ if (session.childHostSocketId) {
188
+ this.hostIndex.delete(session.childHostSocketId);
189
+ }
190
+ this.rooms.delete(roomId);
191
+ }
192
+ /**
193
+ * Get all rooms (for debugging/monitoring)
194
+ */
195
+ getAllRooms() {
196
+ return this.rooms;
197
+ }
198
+ };
199
+ var roomManager = new RoomManager();
200
+
201
+ // src/index.ts
202
+ var PORT = Number(process.env.PORT ?? 4e3);
203
+ var app = express();
204
+ app.use(cors());
205
+ app.use(express.json());
206
+ app.get("/health", (_, res) => {
207
+ res.json({ ok: true });
208
+ });
209
+ var httpServer = createServer(app);
210
+ var io = new Server(httpServer, {
211
+ cors: {
212
+ origin: "*"
213
+ },
214
+ pingInterval: 2e3,
215
+ pingTimeout: 5e3
216
+ });
217
+ var emitError = (socketId, payload) => {
218
+ io.to(socketId).emit("server:error", payload);
219
+ };
220
+ io.on(
221
+ "connection",
222
+ (socket) => {
223
+ socket.on(
224
+ "host:registerSystem",
225
+ async (payload, callback) => {
226
+ const parsed = hostRegisterSystemSchema.safeParse(payload);
227
+ if (!parsed.success) {
228
+ callback({
229
+ ok: false,
230
+ message: parsed.error.message,
231
+ code: ErrorCode.INVALID_PAYLOAD
232
+ });
233
+ return;
234
+ }
235
+ const { roomId, apiKey } = parsed.data;
236
+ const verification = await authService.verifyApiKey(apiKey);
237
+ if (!verification.isVerified) {
238
+ console.warn(
239
+ `[server] Unauthorized host registration attempt for room ${roomId}`
240
+ );
241
+ callback({
242
+ ok: false,
243
+ message: verification.error,
244
+ code: ErrorCode.INVALID_API_KEY
245
+ });
246
+ return;
247
+ }
248
+ let session = roomManager.getRoom(roomId);
249
+ if (session) {
250
+ session.masterHostSocketId = socket.id;
251
+ roomManager.setRoom(roomId, session);
252
+ } else {
253
+ console.log(`[server] Creating room ${roomId}`);
254
+ session = {
255
+ roomId,
256
+ masterHostSocketId: socket.id,
257
+ focus: "SYSTEM",
258
+ controllers: /* @__PURE__ */ new Map(),
259
+ maxPlayers: 32,
260
+ // Default increased to 32 to allow for observers/queue
261
+ gameState: "paused"
262
+ };
263
+ roomManager.setRoom(roomId, session);
264
+ }
265
+ roomManager.setHostRoom(socket.id, roomId);
266
+ socket.join(roomId);
267
+ callback({ ok: true, roomId });
268
+ io.to(roomId).emit("server:roomReady", { roomId });
269
+ }
270
+ );
271
+ socket.on(
272
+ "system:launchGame",
273
+ (payload, callback) => {
274
+ const parsed = systemLaunchGameSchema.safeParse(payload);
275
+ if (!parsed.success) {
276
+ callback({
277
+ ok: false,
278
+ message: parsed.error.message,
279
+ code: ErrorCode.INVALID_PAYLOAD
280
+ });
281
+ return;
282
+ }
283
+ const { roomId, gameUrl } = parsed.data;
284
+ const session = roomManager.getRoom(roomId);
285
+ if (!session) {
286
+ callback({
287
+ ok: false,
288
+ message: "Room not found",
289
+ code: ErrorCode.ROOM_NOT_FOUND
290
+ });
291
+ return;
292
+ }
293
+ if (session.masterHostSocketId !== socket.id) {
294
+ callback({
295
+ ok: false,
296
+ message: "Unauthorized: Not System Host",
297
+ code: ErrorCode.UNAUTHORIZED
298
+ });
299
+ return;
300
+ }
301
+ if (session.childHostSocketId) {
302
+ callback({
303
+ ok: false,
304
+ message: "Game already active",
305
+ code: ErrorCode.ALREADY_CONNECTED
306
+ });
307
+ return;
308
+ }
309
+ if (session.joinToken && !session.childHostSocketId) {
310
+ callback({ ok: true, joinToken: session.joinToken });
311
+ return;
312
+ }
313
+ const joinToken = uuidv4();
314
+ session.joinToken = joinToken;
315
+ session.activeControllerUrl = gameUrl;
316
+ console.log(`[server] Launching game in room ${roomId}`);
317
+ io.to(roomId).emit("client:loadUi", { url: gameUrl });
318
+ callback({ ok: true, joinToken });
319
+ }
320
+ );
321
+ socket.on(
322
+ "host:joinAsChild",
323
+ (payload, callback) => {
324
+ const parsed = hostJoinAsChildSchema.safeParse(payload);
325
+ if (!parsed.success) {
326
+ callback({
327
+ ok: false,
328
+ message: parsed.error.message,
329
+ code: ErrorCode.INVALID_PAYLOAD
330
+ });
331
+ return;
332
+ }
333
+ const { roomId, joinToken } = parsed.data;
334
+ const session = roomManager.getRoom(roomId);
335
+ if (!session) {
336
+ callback({
337
+ ok: false,
338
+ message: "Room not found",
339
+ code: ErrorCode.ROOM_NOT_FOUND
340
+ });
341
+ return;
342
+ }
343
+ if (session.joinToken !== joinToken) {
344
+ console.warn(
345
+ `[server] Invalid join token for room ${roomId}. Expected ${session.joinToken}, got ${joinToken}`
346
+ );
347
+ callback({
348
+ ok: false,
349
+ message: "Invalid Join Token",
350
+ code: ErrorCode.INVALID_TOKEN
351
+ });
352
+ return;
353
+ }
354
+ console.log(`[server] Game host joined room ${roomId}`);
355
+ session.childHostSocketId = socket.id;
356
+ session.focus = "GAME";
357
+ roomManager.setHostRoom(socket.id, roomId);
358
+ socket.join(roomId);
359
+ setTimeout(() => {
360
+ session.controllers.forEach((c) => {
361
+ const notice = {
362
+ controllerId: c.controllerId,
363
+ nickname: c.nickname,
364
+ player: c.playerProfile
365
+ };
366
+ socket.emit("server:controllerJoined", notice);
367
+ });
368
+ const statePayload = {
369
+ roomId,
370
+ state: {
371
+ gameState: session.gameState
372
+ }
373
+ };
374
+ socket.emit("server:state", statePayload);
375
+ }, 100);
376
+ callback({ ok: true, roomId });
377
+ }
378
+ );
379
+ socket.on("system:closeGame", (payload) => {
380
+ const { roomId } = payload;
381
+ const session = roomManager.getRoom(roomId);
382
+ if (!session) {
383
+ return;
384
+ }
385
+ if (session.masterHostSocketId !== socket.id) {
386
+ return;
387
+ }
388
+ console.log(`[server] Closing game in room ${roomId}`);
389
+ if (session.childHostSocketId) {
390
+ const childSocket = io.sockets.sockets.get(session.childHostSocketId);
391
+ if (childSocket) {
392
+ childSocket.disconnect(true);
393
+ }
394
+ }
395
+ session.focus = "SYSTEM";
396
+ session.childHostSocketId = void 0;
397
+ session.joinToken = void 0;
398
+ session.activeControllerUrl = void 0;
399
+ io.to(roomId).emit("client:unloadUi");
400
+ });
401
+ socket.on(
402
+ "host:register",
403
+ async (payload, callback) => {
404
+ const parsed = hostRegistrationSchema.safeParse(payload);
405
+ if (!parsed.success) {
406
+ callback({
407
+ ok: false,
408
+ message: parsed.error.message,
409
+ code: ErrorCode.INVALID_PAYLOAD
410
+ });
411
+ return;
412
+ }
413
+ const { roomId, maxPlayers } = parsed.data;
414
+ let session = roomManager.getRoom(roomId);
415
+ if (session) {
416
+ session.masterHostSocketId = socket.id;
417
+ session.focus = "SYSTEM";
418
+ } else {
419
+ console.log(`[server] Creating standalone room ${roomId}`);
420
+ session = {
421
+ roomId,
422
+ masterHostSocketId: socket.id,
423
+ focus: "SYSTEM",
424
+ controllers: /* @__PURE__ */ new Map(),
425
+ maxPlayers,
426
+ gameState: "paused"
427
+ };
428
+ roomManager.setRoom(roomId, session);
429
+ }
430
+ roomManager.setHostRoom(socket.id, roomId);
431
+ socket.join(roomId);
432
+ callback({ ok: true, roomId });
433
+ io.to(roomId).emit("server:roomReady", { roomId });
434
+ }
435
+ );
436
+ socket.on("controller:join", (payload, callback) => {
437
+ const parsed = controllerJoinSchema.safeParse(payload);
438
+ if (!parsed.success) {
439
+ callback({
440
+ ok: false,
441
+ message: parsed.error.message,
442
+ code: ErrorCode.INVALID_PAYLOAD
443
+ });
444
+ return;
445
+ }
446
+ const { roomId, controllerId, nickname } = parsed.data;
447
+ const session = roomManager.getRoom(roomId);
448
+ if (!session) {
449
+ callback({
450
+ ok: false,
451
+ message: "Room not found",
452
+ code: ErrorCode.ROOM_NOT_FOUND
453
+ });
454
+ emitError(socket.id, {
455
+ code: ErrorCode.ROOM_NOT_FOUND,
456
+ message: "Room not found"
457
+ });
458
+ return;
459
+ }
460
+ if (session.controllers.size >= session.maxPlayers) {
461
+ callback({
462
+ ok: false,
463
+ message: "Room full",
464
+ code: ErrorCode.ROOM_FULL
465
+ });
466
+ emitError(socket.id, {
467
+ code: ErrorCode.ROOM_FULL,
468
+ message: "Room is full"
469
+ });
470
+ return;
471
+ }
472
+ const existing = session.controllers.get(controllerId);
473
+ if (existing) {
474
+ roomManager.deleteController(existing.socketId);
475
+ }
476
+ const PLAYER_COLORS = [
477
+ "#38bdf8",
478
+ "#a78bfa",
479
+ "#f472b6",
480
+ "#34d399",
481
+ "#fbbf24",
482
+ "#60a5fa",
483
+ "#c084fc",
484
+ "#fb7185",
485
+ "#4ade80",
486
+ "#f87171",
487
+ "#22d3ee",
488
+ "#a855f7",
489
+ "#ec4899",
490
+ "#10b981",
491
+ "#f59e0b",
492
+ "#3b82f6",
493
+ "#8b5cf6",
494
+ "#ef4444",
495
+ "#14b8a6",
496
+ "#f97316"
497
+ ];
498
+ const colorHex = PLAYER_COLORS[session.controllers.size % PLAYER_COLORS.length];
499
+ let color;
500
+ try {
501
+ color = Color(colorHex).hex();
502
+ } catch {
503
+ color = Color("#38bdf8").hex();
504
+ }
505
+ const playerProfile = {
506
+ id: controllerId,
507
+ label: nickname ?? `Player ${session.controllers.size}`,
508
+ color
509
+ };
510
+ const controllerSession = {
511
+ controllerId,
512
+ nickname,
513
+ socketId: socket.id,
514
+ playerProfile
515
+ };
516
+ session.controllers.set(controllerId, controllerSession);
517
+ roomManager.setController(socket.id, { roomId, controllerId });
518
+ socket.join(roomId);
519
+ const notice = {
520
+ controllerId,
521
+ nickname,
522
+ player: playerProfile
523
+ };
524
+ io.to(roomManager.getActiveHostId(session)).emit(
525
+ "server:controllerJoined",
526
+ notice
527
+ );
528
+ callback({ ok: true, controllerId, roomId });
529
+ const welcomePayload = {
530
+ controllerId,
531
+ roomId,
532
+ player: playerProfile
533
+ };
534
+ socket.emit("server:welcome", welcomePayload);
535
+ const statePayload = {
536
+ roomId,
537
+ state: {
538
+ gameState: session.gameState
539
+ }
540
+ };
541
+ socket.emit("server:state", statePayload);
542
+ if (session.activeControllerUrl) {
543
+ socket.emit("client:loadUi", { url: session.activeControllerUrl });
544
+ }
545
+ console.log(
546
+ `[server] Controller joined room ${roomId} (${session.controllers.size}/${session.maxPlayers} players)`
547
+ );
548
+ });
549
+ socket.on("controller:leave", (payload) => {
550
+ const parsed = controllerLeaveSchema.safeParse(payload);
551
+ if (!parsed.success) {
552
+ return;
553
+ }
554
+ const { roomId, controllerId } = parsed.data;
555
+ const session = roomManager.getRoom(roomId);
556
+ if (!session) {
557
+ return;
558
+ }
559
+ session.controllers.delete(controllerId);
560
+ roomManager.deleteController(socket.id);
561
+ const notice = { controllerId };
562
+ io.to(roomManager.getActiveHostId(session)).emit(
563
+ "server:controllerLeft",
564
+ notice
565
+ );
566
+ socket.leave(roomId);
567
+ });
568
+ socket.on("controller:input", (payload) => {
569
+ const result = controllerInputSchema.safeParse(payload);
570
+ if (!result.success) {
571
+ return;
572
+ }
573
+ const { roomId } = result.data;
574
+ const session = roomManager.getRoom(roomId);
575
+ if (!session) {
576
+ return;
577
+ }
578
+ const targetHostId = roomManager.getActiveHostId(session);
579
+ if (targetHostId) {
580
+ io.to(targetHostId).emit("server:input", result.data);
581
+ }
582
+ });
583
+ socket.on("controller:system", (payload) => {
584
+ const parsed = controllerSystemSchema.safeParse(payload);
585
+ if (!parsed.success) {
586
+ return;
587
+ }
588
+ const { roomId, command } = parsed.data;
589
+ const session = roomManager.getRoom(roomId);
590
+ if (!session) {
591
+ return;
592
+ }
593
+ if (command === "exit") {
594
+ console.log(`[server] Controller exit request in room ${roomId}`);
595
+ if (session.childHostSocketId) {
596
+ const childSocket = io.sockets.sockets.get(session.childHostSocketId);
597
+ if (childSocket) {
598
+ childSocket.disconnect(true);
599
+ }
600
+ }
601
+ session.focus = "SYSTEM";
602
+ session.childHostSocketId = void 0;
603
+ session.joinToken = void 0;
604
+ session.activeControllerUrl = void 0;
605
+ session.gameState = "paused";
606
+ io.to(roomId).emit("client:unloadUi");
607
+ if (session.masterHostSocketId) {
608
+ io.to(session.masterHostSocketId).emit("server:closeChild");
609
+ }
610
+ } else if (command === "toggle_pause") {
611
+ session.gameState = session.gameState === "playing" ? "paused" : "playing";
612
+ const statePayload = {
613
+ roomId,
614
+ state: {
615
+ gameState: session.gameState
616
+ }
617
+ };
618
+ io.to(roomId).emit("server:state", statePayload);
619
+ }
620
+ });
621
+ socket.on("host:system", (payload) => {
622
+ const parsed = controllerSystemSchema.safeParse(payload);
623
+ if (!parsed.success) {
624
+ return;
625
+ }
626
+ const { roomId, command } = parsed.data;
627
+ const session = roomManager.getRoom(roomId);
628
+ if (!session) {
629
+ return;
630
+ }
631
+ if (command === "toggle_pause") {
632
+ session.gameState = session.gameState === "playing" ? "paused" : "playing";
633
+ const statePayload = {
634
+ roomId,
635
+ state: {
636
+ gameState: session.gameState
637
+ }
638
+ };
639
+ io.to(roomId).emit("server:state", statePayload);
640
+ }
641
+ });
642
+ socket.on("host:state", (payload) => {
643
+ const result = controllerStateSchema.safeParse(payload);
644
+ if (!result.success) return;
645
+ const { roomId, state } = result.data;
646
+ const session = roomManager.getRoom(roomId);
647
+ if (session) {
648
+ if (state.gameState) {
649
+ session.gameState = state.gameState;
650
+ }
651
+ session.controllers.forEach((c) => {
652
+ io.to(c.socketId).emit("server:state", result.data);
653
+ });
654
+ if (session.masterHostSocketId) {
655
+ io.to(session.masterHostSocketId).emit("server:state", result.data);
656
+ }
657
+ if (session.childHostSocketId) {
658
+ io.to(session.childHostSocketId).emit("server:state", result.data);
659
+ }
660
+ }
661
+ });
662
+ socket.on("host:signal", (payload) => {
663
+ const roomId = roomManager.getRoomByHostId(socket.id);
664
+ if (!roomId) return;
665
+ const session = roomManager.getRoom(roomId);
666
+ if (!session) return;
667
+ if (payload.targetId) {
668
+ const controller = session.controllers.get(payload.targetId);
669
+ if (controller) {
670
+ io.to(controller.socketId).emit("server:signal", payload);
671
+ }
672
+ } else {
673
+ socket.to(roomId).emit("server:signal", payload);
674
+ }
675
+ });
676
+ socket.on("host:play_sound", (payload) => {
677
+ const { roomId, targetControllerId, soundId, volume, loop } = payload;
678
+ const session = roomManager.getRoom(roomId);
679
+ if (!session) return;
680
+ const message = { id: soundId, volume, loop };
681
+ if (targetControllerId) {
682
+ const controller = session.controllers.get(targetControllerId);
683
+ if (controller) {
684
+ io.to(controller.socketId).emit("server:playSound", message);
685
+ }
686
+ } else {
687
+ socket.to(roomId).emit("server:playSound", message);
688
+ }
689
+ });
690
+ socket.on("controller:play_sound", (payload) => {
691
+ const { roomId, soundId, volume, loop } = payload;
692
+ const session = roomManager.getRoom(roomId);
693
+ if (!session) return;
694
+ io.to(roomManager.getActiveHostId(session)).emit("server:playSound", {
695
+ id: soundId,
696
+ volume,
697
+ loop
698
+ });
699
+ });
700
+ socket.on("disconnect", () => {
701
+ const roomId = roomManager.getRoomByHostId(socket.id);
702
+ if (roomId) {
703
+ const session = roomManager.getRoom(roomId);
704
+ if (!session) {
705
+ roomManager.deleteHost(socket.id);
706
+ return;
707
+ }
708
+ if (socket.id === session.childHostSocketId) {
709
+ console.log(`[server] Game host disconnected from room ${roomId}`);
710
+ session.childHostSocketId = void 0;
711
+ session.focus = "SYSTEM";
712
+ session.joinToken = void 0;
713
+ session.activeControllerUrl = void 0;
714
+ io.to(roomId).emit("client:unloadUi");
715
+ } else if (socket.id === session.masterHostSocketId) {
716
+ console.log(`[server] Host disconnected from room ${roomId}`);
717
+ setTimeout(() => {
718
+ const currentSession = roomManager.getRoom(roomId);
719
+ if (currentSession && currentSession.masterHostSocketId === socket.id) {
720
+ console.log(`[server] Removing room ${roomId}`);
721
+ roomManager.removeRoom(roomId, io, "Host disconnected");
722
+ }
723
+ }, 3e3);
724
+ }
725
+ roomManager.deleteHost(socket.id);
726
+ return;
727
+ }
728
+ const controller = roomManager.getControllerInfo(socket.id);
729
+ if (controller) {
730
+ const session = roomManager.getRoom(controller.roomId);
731
+ if (session) {
732
+ session.controllers.delete(controller.controllerId);
733
+ const notice = {
734
+ controllerId: controller.controllerId
735
+ };
736
+ io.to(roomManager.getActiveHostId(session)).emit(
737
+ "server:controllerLeft",
738
+ notice
739
+ );
740
+ }
741
+ roomManager.deleteController(socket.id);
742
+ }
743
+ });
744
+ }
745
+ );
746
+ httpServer.listen(PORT, () => {
747
+ console.log(`[air-jam] server listening on http://localhost:${PORT}`);
748
+ });
749
+ //# sourceMappingURL=chunk-NWAVO5AJ.js.map