sa2kit 1.6.34 → 1.6.35

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,146 @@
1
+ import { F as FireworkKind, a as FireworkLaunchPayload } from '../../types-Cgk9zWhO.mjs';
2
+
3
+ interface FireworksUserIdentity {
4
+ userId: string;
5
+ nickname?: string;
6
+ avatarUrl?: string;
7
+ }
8
+ interface DanmakuBroadcastEvent {
9
+ id: string;
10
+ roomId: string;
11
+ text: string;
12
+ color?: string;
13
+ kind?: FireworkKind;
14
+ user: FireworksUserIdentity;
15
+ timestamp: number;
16
+ }
17
+ interface FireworkBroadcastEvent {
18
+ id: string;
19
+ roomId: string;
20
+ payload: FireworkLaunchPayload;
21
+ user: FireworksUserIdentity;
22
+ timestamp: number;
23
+ }
24
+ type FireworksClientMessage = {
25
+ type: 'join';
26
+ roomId: string;
27
+ user: FireworksUserIdentity;
28
+ } | {
29
+ type: 'leave';
30
+ } | {
31
+ type: 'danmaku.send';
32
+ payload: {
33
+ text: string;
34
+ color?: string;
35
+ kind?: FireworkKind;
36
+ };
37
+ } | {
38
+ type: 'firework.launch';
39
+ payload: FireworkLaunchPayload;
40
+ } | {
41
+ type: 'ping';
42
+ ts?: number;
43
+ };
44
+ type FireworksServerMessage = {
45
+ type: 'joined';
46
+ roomId: string;
47
+ self: FireworksUserIdentity;
48
+ onlineCount: number;
49
+ } | {
50
+ type: 'room.snapshot';
51
+ roomId: string;
52
+ users: FireworksUserIdentity[];
53
+ danmakuHistory: DanmakuBroadcastEvent[];
54
+ fireworkHistory: FireworkBroadcastEvent[];
55
+ } | {
56
+ type: 'room.user_joined';
57
+ roomId: string;
58
+ user: FireworksUserIdentity;
59
+ onlineCount: number;
60
+ } | {
61
+ type: 'room.user_left';
62
+ roomId: string;
63
+ userId: string;
64
+ onlineCount: number;
65
+ } | {
66
+ type: 'danmaku.broadcast';
67
+ roomId: string;
68
+ event: DanmakuBroadcastEvent;
69
+ } | {
70
+ type: 'firework.broadcast';
71
+ roomId: string;
72
+ event: FireworkBroadcastEvent;
73
+ } | {
74
+ type: 'pong';
75
+ ts: number;
76
+ } | {
77
+ type: 'error';
78
+ code: string;
79
+ message: string;
80
+ };
81
+ interface FireworksConnectionTransport {
82
+ send: (encodedMessage: string) => void;
83
+ close?: (code?: number, reason?: string) => void;
84
+ }
85
+ interface FireworksConnection {
86
+ id: string;
87
+ handleMessage: (raw: unknown) => void;
88
+ disconnect: (reason?: string) => void;
89
+ send: (message: FireworksServerMessage) => void;
90
+ }
91
+ interface FireworksRoomHubOptions {
92
+ maxUsersPerRoom?: number;
93
+ historyLimit?: number;
94
+ maxDanmakuLength?: number;
95
+ logger?: Pick<Console, 'info' | 'warn' | 'error'>;
96
+ }
97
+ interface FireworksHubStats {
98
+ rooms: number;
99
+ connections: number;
100
+ }
101
+ interface WsLikeSocket {
102
+ send: (data: string) => void;
103
+ on: (event: 'message' | 'close' | 'error', listener: (...args: unknown[]) => void) => void;
104
+ }
105
+
106
+ declare class FireworksRoomHub {
107
+ private readonly rooms;
108
+ private readonly sessions;
109
+ private readonly maxUsersPerRoom;
110
+ private readonly historyLimit;
111
+ private readonly maxDanmakuLength;
112
+ private readonly logger?;
113
+ constructor(options?: FireworksRoomHubOptions);
114
+ connect(transport: FireworksConnectionTransport, id?: string): FireworksConnection;
115
+ disconnect(connectionId: string, reason?: string): void;
116
+ getStats(): {
117
+ rooms: number;
118
+ connections: number;
119
+ };
120
+ getRoomOnlineCount(roomId: string): number;
121
+ private handleRawMessage;
122
+ private joinRoom;
123
+ private leaveRoom;
124
+ private handleDanmaku;
125
+ private handleFirework;
126
+ private createRoom;
127
+ private sendToConnection;
128
+ private broadcastToRoom;
129
+ private sendError;
130
+ }
131
+
132
+ interface BindWsSocketOptions {
133
+ connectionId?: string;
134
+ onConnected?: (connection: FireworksConnection) => void;
135
+ onDisconnected?: (connectionId: string) => void;
136
+ }
137
+ /**
138
+ * Bind a ws-like socket to FireworksRoomHub.
139
+ *
140
+ * Example (ws package):
141
+ * const hub = new FireworksRoomHub();
142
+ * wss.on('connection', (socket) => bindWsSocket(hub, socket));
143
+ */
144
+ declare function bindWsSocket(hub: FireworksRoomHub, socket: WsLikeSocket, options?: BindWsSocketOptions): FireworksConnection;
145
+
146
+ export { type BindWsSocketOptions, type DanmakuBroadcastEvent, type FireworkBroadcastEvent, type FireworksClientMessage, type FireworksConnection, type FireworksConnectionTransport, type FireworksHubStats, FireworksRoomHub, type FireworksRoomHubOptions, type FireworksServerMessage, type FireworksUserIdentity, type WsLikeSocket, bindWsSocket };
@@ -0,0 +1,146 @@
1
+ import { F as FireworkKind, a as FireworkLaunchPayload } from '../../types-Cgk9zWhO.js';
2
+
3
+ interface FireworksUserIdentity {
4
+ userId: string;
5
+ nickname?: string;
6
+ avatarUrl?: string;
7
+ }
8
+ interface DanmakuBroadcastEvent {
9
+ id: string;
10
+ roomId: string;
11
+ text: string;
12
+ color?: string;
13
+ kind?: FireworkKind;
14
+ user: FireworksUserIdentity;
15
+ timestamp: number;
16
+ }
17
+ interface FireworkBroadcastEvent {
18
+ id: string;
19
+ roomId: string;
20
+ payload: FireworkLaunchPayload;
21
+ user: FireworksUserIdentity;
22
+ timestamp: number;
23
+ }
24
+ type FireworksClientMessage = {
25
+ type: 'join';
26
+ roomId: string;
27
+ user: FireworksUserIdentity;
28
+ } | {
29
+ type: 'leave';
30
+ } | {
31
+ type: 'danmaku.send';
32
+ payload: {
33
+ text: string;
34
+ color?: string;
35
+ kind?: FireworkKind;
36
+ };
37
+ } | {
38
+ type: 'firework.launch';
39
+ payload: FireworkLaunchPayload;
40
+ } | {
41
+ type: 'ping';
42
+ ts?: number;
43
+ };
44
+ type FireworksServerMessage = {
45
+ type: 'joined';
46
+ roomId: string;
47
+ self: FireworksUserIdentity;
48
+ onlineCount: number;
49
+ } | {
50
+ type: 'room.snapshot';
51
+ roomId: string;
52
+ users: FireworksUserIdentity[];
53
+ danmakuHistory: DanmakuBroadcastEvent[];
54
+ fireworkHistory: FireworkBroadcastEvent[];
55
+ } | {
56
+ type: 'room.user_joined';
57
+ roomId: string;
58
+ user: FireworksUserIdentity;
59
+ onlineCount: number;
60
+ } | {
61
+ type: 'room.user_left';
62
+ roomId: string;
63
+ userId: string;
64
+ onlineCount: number;
65
+ } | {
66
+ type: 'danmaku.broadcast';
67
+ roomId: string;
68
+ event: DanmakuBroadcastEvent;
69
+ } | {
70
+ type: 'firework.broadcast';
71
+ roomId: string;
72
+ event: FireworkBroadcastEvent;
73
+ } | {
74
+ type: 'pong';
75
+ ts: number;
76
+ } | {
77
+ type: 'error';
78
+ code: string;
79
+ message: string;
80
+ };
81
+ interface FireworksConnectionTransport {
82
+ send: (encodedMessage: string) => void;
83
+ close?: (code?: number, reason?: string) => void;
84
+ }
85
+ interface FireworksConnection {
86
+ id: string;
87
+ handleMessage: (raw: unknown) => void;
88
+ disconnect: (reason?: string) => void;
89
+ send: (message: FireworksServerMessage) => void;
90
+ }
91
+ interface FireworksRoomHubOptions {
92
+ maxUsersPerRoom?: number;
93
+ historyLimit?: number;
94
+ maxDanmakuLength?: number;
95
+ logger?: Pick<Console, 'info' | 'warn' | 'error'>;
96
+ }
97
+ interface FireworksHubStats {
98
+ rooms: number;
99
+ connections: number;
100
+ }
101
+ interface WsLikeSocket {
102
+ send: (data: string) => void;
103
+ on: (event: 'message' | 'close' | 'error', listener: (...args: unknown[]) => void) => void;
104
+ }
105
+
106
+ declare class FireworksRoomHub {
107
+ private readonly rooms;
108
+ private readonly sessions;
109
+ private readonly maxUsersPerRoom;
110
+ private readonly historyLimit;
111
+ private readonly maxDanmakuLength;
112
+ private readonly logger?;
113
+ constructor(options?: FireworksRoomHubOptions);
114
+ connect(transport: FireworksConnectionTransport, id?: string): FireworksConnection;
115
+ disconnect(connectionId: string, reason?: string): void;
116
+ getStats(): {
117
+ rooms: number;
118
+ connections: number;
119
+ };
120
+ getRoomOnlineCount(roomId: string): number;
121
+ private handleRawMessage;
122
+ private joinRoom;
123
+ private leaveRoom;
124
+ private handleDanmaku;
125
+ private handleFirework;
126
+ private createRoom;
127
+ private sendToConnection;
128
+ private broadcastToRoom;
129
+ private sendError;
130
+ }
131
+
132
+ interface BindWsSocketOptions {
133
+ connectionId?: string;
134
+ onConnected?: (connection: FireworksConnection) => void;
135
+ onDisconnected?: (connectionId: string) => void;
136
+ }
137
+ /**
138
+ * Bind a ws-like socket to FireworksRoomHub.
139
+ *
140
+ * Example (ws package):
141
+ * const hub = new FireworksRoomHub();
142
+ * wss.on('connection', (socket) => bindWsSocket(hub, socket));
143
+ */
144
+ declare function bindWsSocket(hub: FireworksRoomHub, socket: WsLikeSocket, options?: BindWsSocketOptions): FireworksConnection;
145
+
146
+ export { type BindWsSocketOptions, type DanmakuBroadcastEvent, type FireworkBroadcastEvent, type FireworksClientMessage, type FireworksConnection, type FireworksConnectionTransport, type FireworksHubStats, FireworksRoomHub, type FireworksRoomHubOptions, type FireworksServerMessage, type FireworksUserIdentity, type WsLikeSocket, bindWsSocket };
@@ -0,0 +1,338 @@
1
+ 'use strict';
2
+
3
+ require('../../chunk-DGUM43GV.js');
4
+
5
+ // src/mikuFireworks3D/server/FireworksRoomHub.ts
6
+ var DEFAULT_MAX_USERS_PER_ROOM = 200;
7
+ var DEFAULT_HISTORY_LIMIT = 50;
8
+ var DEFAULT_MAX_DANMAKU_LENGTH = 64;
9
+ var FireworksRoomHub = class {
10
+ constructor(options) {
11
+ this.rooms = /* @__PURE__ */ new Map();
12
+ this.sessions = /* @__PURE__ */ new Map();
13
+ this.maxUsersPerRoom = options?.maxUsersPerRoom ?? DEFAULT_MAX_USERS_PER_ROOM;
14
+ this.historyLimit = options?.historyLimit ?? DEFAULT_HISTORY_LIMIT;
15
+ this.maxDanmakuLength = options?.maxDanmakuLength ?? DEFAULT_MAX_DANMAKU_LENGTH;
16
+ this.logger = options?.logger;
17
+ }
18
+ connect(transport, id) {
19
+ const connectionId = id ?? createId("conn");
20
+ const session = {
21
+ id: connectionId,
22
+ transport
23
+ };
24
+ this.sessions.set(connectionId, session);
25
+ return {
26
+ id: connectionId,
27
+ handleMessage: (raw) => this.handleRawMessage(connectionId, raw),
28
+ disconnect: (reason) => this.disconnect(connectionId, reason),
29
+ send: (message) => this.sendToConnection(connectionId, message)
30
+ };
31
+ }
32
+ disconnect(connectionId, reason) {
33
+ const session = this.sessions.get(connectionId);
34
+ if (!session) {
35
+ return;
36
+ }
37
+ this.leaveRoom(session, false);
38
+ this.sessions.delete(connectionId);
39
+ if (reason) {
40
+ this.logger?.info?.(`[FireworksRoomHub] disconnected ${connectionId}: ${reason}`);
41
+ }
42
+ }
43
+ getStats() {
44
+ return {
45
+ rooms: this.rooms.size,
46
+ connections: this.sessions.size
47
+ };
48
+ }
49
+ getRoomOnlineCount(roomId) {
50
+ return this.rooms.get(roomId)?.connections.size ?? 0;
51
+ }
52
+ handleRawMessage(connectionId, raw) {
53
+ const message = parseClientMessage(raw);
54
+ if (!message.ok) {
55
+ this.sendError(connectionId, "BAD_MESSAGE", message.error);
56
+ return;
57
+ }
58
+ const session = this.sessions.get(connectionId);
59
+ if (!session) {
60
+ return;
61
+ }
62
+ const clientMessage = message.value;
63
+ switch (clientMessage.type) {
64
+ case "join": {
65
+ this.joinRoom(session, clientMessage.roomId, clientMessage.user);
66
+ return;
67
+ }
68
+ case "leave": {
69
+ this.leaveRoom(session, true);
70
+ return;
71
+ }
72
+ case "danmaku.send": {
73
+ this.handleDanmaku(session, clientMessage.payload);
74
+ return;
75
+ }
76
+ case "firework.launch": {
77
+ this.handleFirework(session, clientMessage.payload);
78
+ return;
79
+ }
80
+ case "ping": {
81
+ this.sendToConnection(connectionId, {
82
+ type: "pong",
83
+ ts: clientMessage.ts ?? Date.now()
84
+ });
85
+ return;
86
+ }
87
+ default: {
88
+ this.sendError(connectionId, "UNSUPPORTED_MESSAGE", "Unsupported message type.");
89
+ }
90
+ }
91
+ }
92
+ joinRoom(session, roomId, user) {
93
+ const normalizedRoomId = roomId.trim();
94
+ const normalizedUserId = user.userId.trim();
95
+ if (!normalizedRoomId) {
96
+ this.sendError(session.id, "ROOM_ID_REQUIRED", "roomId is required.");
97
+ return;
98
+ }
99
+ if (!normalizedUserId) {
100
+ this.sendError(session.id, "USER_ID_REQUIRED", "user.userId is required.");
101
+ return;
102
+ }
103
+ if (session.roomId && session.roomId !== normalizedRoomId) {
104
+ this.leaveRoom(session, true);
105
+ }
106
+ const room = this.rooms.get(normalizedRoomId) ?? this.createRoom(normalizedRoomId);
107
+ if (room.connections.size >= this.maxUsersPerRoom && !room.connections.has(session.id)) {
108
+ this.sendError(session.id, "ROOM_FULL", "Room is full.");
109
+ return;
110
+ }
111
+ const identity = {
112
+ userId: normalizedUserId,
113
+ nickname: user.nickname,
114
+ avatarUrl: user.avatarUrl
115
+ };
116
+ const wasMember = room.connections.has(session.id);
117
+ room.connections.add(session.id);
118
+ room.usersByConnection.set(session.id, identity);
119
+ session.roomId = normalizedRoomId;
120
+ session.user = identity;
121
+ this.sendToConnection(session.id, {
122
+ type: "joined",
123
+ roomId: normalizedRoomId,
124
+ self: identity,
125
+ onlineCount: room.connections.size
126
+ });
127
+ this.sendToConnection(session.id, {
128
+ type: "room.snapshot",
129
+ roomId: normalizedRoomId,
130
+ users: Array.from(room.usersByConnection.values()),
131
+ danmakuHistory: [...room.danmakuHistory],
132
+ fireworkHistory: [...room.fireworkHistory]
133
+ });
134
+ if (!wasMember) {
135
+ this.broadcastToRoom(normalizedRoomId, {
136
+ type: "room.user_joined",
137
+ roomId: normalizedRoomId,
138
+ user: identity,
139
+ onlineCount: room.connections.size
140
+ }, session.id);
141
+ }
142
+ }
143
+ leaveRoom(session, notify) {
144
+ const roomId = session.roomId;
145
+ if (!roomId) {
146
+ return;
147
+ }
148
+ const room = this.rooms.get(roomId);
149
+ if (!room) {
150
+ session.roomId = void 0;
151
+ session.user = void 0;
152
+ return;
153
+ }
154
+ const leavingUserId = room.usersByConnection.get(session.id)?.userId;
155
+ room.connections.delete(session.id);
156
+ room.usersByConnection.delete(session.id);
157
+ session.roomId = void 0;
158
+ session.user = void 0;
159
+ if (notify && leavingUserId) {
160
+ this.broadcastToRoom(roomId, {
161
+ type: "room.user_left",
162
+ roomId,
163
+ userId: leavingUserId,
164
+ onlineCount: room.connections.size
165
+ });
166
+ }
167
+ if (room.connections.size === 0) {
168
+ this.rooms.delete(roomId);
169
+ }
170
+ }
171
+ handleDanmaku(session, payload) {
172
+ if (!session.roomId || !session.user) {
173
+ this.sendError(session.id, "NOT_JOINED", "Join a room before sending danmaku.");
174
+ return;
175
+ }
176
+ const room = this.rooms.get(session.roomId);
177
+ if (!room) {
178
+ this.sendError(session.id, "ROOM_NOT_FOUND", "Room does not exist.");
179
+ return;
180
+ }
181
+ const text = payload.text.trim().slice(0, this.maxDanmakuLength);
182
+ if (!text) {
183
+ this.sendError(session.id, "EMPTY_DANMAKU", "Danmaku text cannot be empty.");
184
+ return;
185
+ }
186
+ const event = {
187
+ id: `${session.roomId}-d-${room.seq++}`,
188
+ roomId: session.roomId,
189
+ text,
190
+ color: payload.color,
191
+ kind: payload.kind,
192
+ user: session.user,
193
+ timestamp: Date.now()
194
+ };
195
+ room.danmakuHistory.push(event);
196
+ trimHistory(room.danmakuHistory, this.historyLimit);
197
+ this.broadcastToRoom(session.roomId, {
198
+ type: "danmaku.broadcast",
199
+ roomId: session.roomId,
200
+ event
201
+ });
202
+ }
203
+ handleFirework(session, payload) {
204
+ if (!session.roomId || !session.user) {
205
+ this.sendError(session.id, "NOT_JOINED", "Join a room before launching fireworks.");
206
+ return;
207
+ }
208
+ const room = this.rooms.get(session.roomId);
209
+ if (!room) {
210
+ this.sendError(session.id, "ROOM_NOT_FOUND", "Room does not exist.");
211
+ return;
212
+ }
213
+ const event = {
214
+ id: `${session.roomId}-f-${room.seq++}`,
215
+ roomId: session.roomId,
216
+ payload,
217
+ user: session.user,
218
+ timestamp: Date.now()
219
+ };
220
+ room.fireworkHistory.push(event);
221
+ trimHistory(room.fireworkHistory, this.historyLimit);
222
+ this.broadcastToRoom(session.roomId, {
223
+ type: "firework.broadcast",
224
+ roomId: session.roomId,
225
+ event
226
+ });
227
+ }
228
+ createRoom(roomId) {
229
+ const room = {
230
+ id: roomId,
231
+ connections: /* @__PURE__ */ new Set(),
232
+ usersByConnection: /* @__PURE__ */ new Map(),
233
+ danmakuHistory: [],
234
+ fireworkHistory: [],
235
+ seq: 1
236
+ };
237
+ this.rooms.set(roomId, room);
238
+ return room;
239
+ }
240
+ sendToConnection(connectionId, message) {
241
+ const session = this.sessions.get(connectionId);
242
+ if (!session) {
243
+ return;
244
+ }
245
+ try {
246
+ session.transport.send(JSON.stringify(message));
247
+ } catch (error) {
248
+ this.logger?.warn?.("[FireworksRoomHub] failed to send message", error);
249
+ }
250
+ }
251
+ broadcastToRoom(roomId, message, excludeConnectionId) {
252
+ const room = this.rooms.get(roomId);
253
+ if (!room) {
254
+ return;
255
+ }
256
+ for (const connectionId of room.connections) {
257
+ if (excludeConnectionId && connectionId === excludeConnectionId) {
258
+ continue;
259
+ }
260
+ this.sendToConnection(connectionId, message);
261
+ }
262
+ }
263
+ sendError(connectionId, code, message) {
264
+ this.sendToConnection(connectionId, {
265
+ type: "error",
266
+ code,
267
+ message
268
+ });
269
+ }
270
+ };
271
+ function createId(prefix) {
272
+ return `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
273
+ }
274
+ function trimHistory(history, limit) {
275
+ if (history.length <= limit) {
276
+ return;
277
+ }
278
+ history.splice(0, history.length - limit);
279
+ }
280
+ function parseClientMessage(raw) {
281
+ if (typeof raw === "string") {
282
+ try {
283
+ const parsed = JSON.parse(raw);
284
+ return { ok: true, value: parsed };
285
+ } catch {
286
+ return { ok: false, error: "Invalid JSON payload." };
287
+ }
288
+ }
289
+ if (isObject(raw)) {
290
+ return { ok: true, value: raw };
291
+ }
292
+ return { ok: false, error: "Unsupported message payload." };
293
+ }
294
+ function isObject(value) {
295
+ return typeof value === "object" && value !== null;
296
+ }
297
+
298
+ // src/mikuFireworks3D/server/wsAdapter.ts
299
+ function bindWsSocket(hub, socket, options) {
300
+ const connection = hub.connect(
301
+ {
302
+ send: (encodedMessage) => {
303
+ socket.send(encodedMessage);
304
+ }
305
+ },
306
+ options?.connectionId
307
+ );
308
+ options?.onConnected?.(connection);
309
+ socket.on("message", (raw) => {
310
+ connection.handleMessage(normalizeWsRawData(raw));
311
+ });
312
+ socket.on("close", () => {
313
+ connection.disconnect("socket_closed");
314
+ options?.onDisconnected?.(connection.id);
315
+ });
316
+ socket.on("error", () => {
317
+ connection.disconnect("socket_error");
318
+ options?.onDisconnected?.(connection.id);
319
+ });
320
+ return connection;
321
+ }
322
+ function normalizeWsRawData(raw) {
323
+ if (typeof raw === "string") {
324
+ return raw;
325
+ }
326
+ if (raw instanceof ArrayBuffer) {
327
+ return new TextDecoder().decode(new Uint8Array(raw));
328
+ }
329
+ if (ArrayBuffer.isView(raw)) {
330
+ return new TextDecoder().decode(raw);
331
+ }
332
+ return "";
333
+ }
334
+
335
+ exports.FireworksRoomHub = FireworksRoomHub;
336
+ exports.bindWsSocket = bindWsSocket;
337
+ //# sourceMappingURL=index.js.map
338
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/mikuFireworks3D/server/FireworksRoomHub.ts","../../../src/mikuFireworks3D/server/wsAdapter.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["import type {\n DanmakuBroadcastEvent,\n FireworkBroadcastEvent,\n FireworksClientMessage,\n FireworksConnection,\n FireworksConnectionTransport,\n FireworksRoomHubOptions,\n FireworksServerMessage,\n FireworksUserIdentity,\n} from './types';\n\ninterface Session {\n id: string;\n transport: FireworksConnectionTransport;\n roomId?: string;\n user?: FireworksUserIdentity;\n}\n\ninterface RoomState {\n id: string;\n connections: Set<string>;\n usersByConnection: Map<string, FireworksUserIdentity>;\n danmakuHistory: DanmakuBroadcastEvent[];\n fireworkHistory: FireworkBroadcastEvent[];\n seq: number;\n}\n\nconst DEFAULT_MAX_USERS_PER_ROOM = 200;\nconst DEFAULT_HISTORY_LIMIT = 50;\nconst DEFAULT_MAX_DANMAKU_LENGTH = 64;\n\nexport class FireworksRoomHub {\n private readonly rooms = new Map<string, RoomState>();\n private readonly sessions = new Map<string, Session>();\n private readonly maxUsersPerRoom: number;\n private readonly historyLimit: number;\n private readonly maxDanmakuLength: number;\n private readonly logger?: Pick<Console, 'info' | 'warn' | 'error'>;\n\n constructor(options?: FireworksRoomHubOptions) {\n this.maxUsersPerRoom = options?.maxUsersPerRoom ?? DEFAULT_MAX_USERS_PER_ROOM;\n this.historyLimit = options?.historyLimit ?? DEFAULT_HISTORY_LIMIT;\n this.maxDanmakuLength = options?.maxDanmakuLength ?? DEFAULT_MAX_DANMAKU_LENGTH;\n this.logger = options?.logger;\n }\n\n connect(transport: FireworksConnectionTransport, id?: string): FireworksConnection {\n const connectionId = id ?? createId('conn');\n const session: Session = {\n id: connectionId,\n transport,\n };\n\n this.sessions.set(connectionId, session);\n\n return {\n id: connectionId,\n handleMessage: (raw) => this.handleRawMessage(connectionId, raw),\n disconnect: (reason) => this.disconnect(connectionId, reason),\n send: (message) => this.sendToConnection(connectionId, message),\n };\n }\n\n disconnect(connectionId: string, reason?: string): void {\n const session = this.sessions.get(connectionId);\n if (!session) {\n return;\n }\n\n this.leaveRoom(session, false);\n this.sessions.delete(connectionId);\n\n if (reason) {\n this.logger?.info?.(`[FireworksRoomHub] disconnected ${connectionId}: ${reason}`);\n }\n }\n\n getStats(): { rooms: number; connections: number } {\n return {\n rooms: this.rooms.size,\n connections: this.sessions.size,\n };\n }\n\n getRoomOnlineCount(roomId: string): number {\n return this.rooms.get(roomId)?.connections.size ?? 0;\n }\n\n private handleRawMessage(connectionId: string, raw: unknown): void {\n const message = parseClientMessage(raw);\n if (!message.ok) {\n this.sendError(connectionId, 'BAD_MESSAGE', message.error);\n return;\n }\n\n const session = this.sessions.get(connectionId);\n if (!session) {\n return;\n }\n\n const clientMessage = message.value;\n\n switch (clientMessage.type) {\n case 'join': {\n this.joinRoom(session, clientMessage.roomId, clientMessage.user);\n return;\n }\n case 'leave': {\n this.leaveRoom(session, true);\n return;\n }\n case 'danmaku.send': {\n this.handleDanmaku(session, clientMessage.payload);\n return;\n }\n case 'firework.launch': {\n this.handleFirework(session, clientMessage.payload);\n return;\n }\n case 'ping': {\n this.sendToConnection(connectionId, {\n type: 'pong',\n ts: clientMessage.ts ?? Date.now(),\n });\n return;\n }\n default: {\n this.sendError(connectionId, 'UNSUPPORTED_MESSAGE', 'Unsupported message type.');\n }\n }\n }\n\n private joinRoom(session: Session, roomId: string, user: FireworksUserIdentity): void {\n const normalizedRoomId = roomId.trim();\n const normalizedUserId = user.userId.trim();\n\n if (!normalizedRoomId) {\n this.sendError(session.id, 'ROOM_ID_REQUIRED', 'roomId is required.');\n return;\n }\n if (!normalizedUserId) {\n this.sendError(session.id, 'USER_ID_REQUIRED', 'user.userId is required.');\n return;\n }\n\n if (session.roomId && session.roomId !== normalizedRoomId) {\n this.leaveRoom(session, true);\n }\n\n const room = this.rooms.get(normalizedRoomId) ?? this.createRoom(normalizedRoomId);\n if (room.connections.size >= this.maxUsersPerRoom && !room.connections.has(session.id)) {\n this.sendError(session.id, 'ROOM_FULL', 'Room is full.');\n return;\n }\n\n const identity: FireworksUserIdentity = {\n userId: normalizedUserId,\n nickname: user.nickname,\n avatarUrl: user.avatarUrl,\n };\n\n const wasMember = room.connections.has(session.id);\n room.connections.add(session.id);\n room.usersByConnection.set(session.id, identity);\n\n session.roomId = normalizedRoomId;\n session.user = identity;\n\n this.sendToConnection(session.id, {\n type: 'joined',\n roomId: normalizedRoomId,\n self: identity,\n onlineCount: room.connections.size,\n });\n\n this.sendToConnection(session.id, {\n type: 'room.snapshot',\n roomId: normalizedRoomId,\n users: Array.from(room.usersByConnection.values()),\n danmakuHistory: [...room.danmakuHistory],\n fireworkHistory: [...room.fireworkHistory],\n });\n\n if (!wasMember) {\n this.broadcastToRoom(normalizedRoomId, {\n type: 'room.user_joined',\n roomId: normalizedRoomId,\n user: identity,\n onlineCount: room.connections.size,\n }, session.id);\n }\n }\n\n private leaveRoom(session: Session, notify: boolean): void {\n const roomId = session.roomId;\n if (!roomId) {\n return;\n }\n\n const room = this.rooms.get(roomId);\n if (!room) {\n session.roomId = undefined;\n session.user = undefined;\n return;\n }\n\n const leavingUserId = room.usersByConnection.get(session.id)?.userId;\n\n room.connections.delete(session.id);\n room.usersByConnection.delete(session.id);\n\n session.roomId = undefined;\n session.user = undefined;\n\n if (notify && leavingUserId) {\n this.broadcastToRoom(roomId, {\n type: 'room.user_left',\n roomId,\n userId: leavingUserId,\n onlineCount: room.connections.size,\n });\n }\n\n if (room.connections.size === 0) {\n this.rooms.delete(roomId);\n }\n }\n\n private handleDanmaku(\n session: Session,\n payload: {\n text: string;\n color?: string;\n kind?: 'normal' | 'miku' | 'avatar';\n }\n ): void {\n if (!session.roomId || !session.user) {\n this.sendError(session.id, 'NOT_JOINED', 'Join a room before sending danmaku.');\n return;\n }\n\n const room = this.rooms.get(session.roomId);\n if (!room) {\n this.sendError(session.id, 'ROOM_NOT_FOUND', 'Room does not exist.');\n return;\n }\n\n const text = payload.text.trim().slice(0, this.maxDanmakuLength);\n if (!text) {\n this.sendError(session.id, 'EMPTY_DANMAKU', 'Danmaku text cannot be empty.');\n return;\n }\n\n const event: DanmakuBroadcastEvent = {\n id: `${session.roomId}-d-${room.seq++}`,\n roomId: session.roomId,\n text,\n color: payload.color,\n kind: payload.kind,\n user: session.user,\n timestamp: Date.now(),\n };\n\n room.danmakuHistory.push(event);\n trimHistory(room.danmakuHistory, this.historyLimit);\n\n this.broadcastToRoom(session.roomId, {\n type: 'danmaku.broadcast',\n roomId: session.roomId,\n event,\n });\n }\n\n private handleFirework(session: Session, payload: FireworkBroadcastEvent['payload']): void {\n if (!session.roomId || !session.user) {\n this.sendError(session.id, 'NOT_JOINED', 'Join a room before launching fireworks.');\n return;\n }\n\n const room = this.rooms.get(session.roomId);\n if (!room) {\n this.sendError(session.id, 'ROOM_NOT_FOUND', 'Room does not exist.');\n return;\n }\n\n const event: FireworkBroadcastEvent = {\n id: `${session.roomId}-f-${room.seq++}`,\n roomId: session.roomId,\n payload,\n user: session.user,\n timestamp: Date.now(),\n };\n\n room.fireworkHistory.push(event);\n trimHistory(room.fireworkHistory, this.historyLimit);\n\n this.broadcastToRoom(session.roomId, {\n type: 'firework.broadcast',\n roomId: session.roomId,\n event,\n });\n }\n\n private createRoom(roomId: string): RoomState {\n const room: RoomState = {\n id: roomId,\n connections: new Set(),\n usersByConnection: new Map(),\n danmakuHistory: [],\n fireworkHistory: [],\n seq: 1,\n };\n\n this.rooms.set(roomId, room);\n return room;\n }\n\n private sendToConnection(connectionId: string, message: FireworksServerMessage): void {\n const session = this.sessions.get(connectionId);\n if (!session) {\n return;\n }\n\n try {\n session.transport.send(JSON.stringify(message));\n } catch (error) {\n this.logger?.warn?.('[FireworksRoomHub] failed to send message', error);\n }\n }\n\n private broadcastToRoom(roomId: string, message: FireworksServerMessage, excludeConnectionId?: string): void {\n const room = this.rooms.get(roomId);\n if (!room) {\n return;\n }\n\n for (const connectionId of room.connections) {\n if (excludeConnectionId && connectionId === excludeConnectionId) {\n continue;\n }\n this.sendToConnection(connectionId, message);\n }\n }\n\n private sendError(connectionId: string, code: string, message: string): void {\n this.sendToConnection(connectionId, {\n type: 'error',\n code,\n message,\n });\n }\n}\n\nfunction createId(prefix: string): string {\n return `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;\n}\n\nfunction trimHistory<T>(history: T[], limit: number): void {\n if (history.length <= limit) {\n return;\n }\n history.splice(0, history.length - limit);\n}\n\nfunction parseClientMessage(raw: unknown):\n | { ok: true; value: FireworksClientMessage }\n | { ok: false; error: string } {\n if (typeof raw === 'string') {\n try {\n const parsed = JSON.parse(raw) as FireworksClientMessage;\n return { ok: true, value: parsed };\n } catch {\n return { ok: false, error: 'Invalid JSON payload.' };\n }\n }\n\n if (isObject(raw)) {\n return { ok: true, value: raw as FireworksClientMessage };\n }\n\n return { ok: false, error: 'Unsupported message payload.' };\n}\n\nfunction isObject(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null;\n}\n","import type { FireworksRoomHub } from './FireworksRoomHub';\nimport type { FireworksConnection, WsLikeSocket } from './types';\n\nexport interface BindWsSocketOptions {\n connectionId?: string;\n onConnected?: (connection: FireworksConnection) => void;\n onDisconnected?: (connectionId: string) => void;\n}\n\n/**\n * Bind a ws-like socket to FireworksRoomHub.\n *\n * Example (ws package):\n * const hub = new FireworksRoomHub();\n * wss.on('connection', (socket) => bindWsSocket(hub, socket));\n */\nexport function bindWsSocket(\n hub: FireworksRoomHub,\n socket: WsLikeSocket,\n options?: BindWsSocketOptions\n): FireworksConnection {\n const connection = hub.connect(\n {\n send: (encodedMessage) => {\n socket.send(encodedMessage);\n },\n },\n options?.connectionId\n );\n\n options?.onConnected?.(connection);\n\n socket.on('message', (raw) => {\n connection.handleMessage(normalizeWsRawData(raw));\n });\n\n socket.on('close', () => {\n connection.disconnect('socket_closed');\n options?.onDisconnected?.(connection.id);\n });\n\n socket.on('error', () => {\n connection.disconnect('socket_error');\n options?.onDisconnected?.(connection.id);\n });\n\n return connection;\n}\n\nfunction normalizeWsRawData(raw: unknown): string {\n if (typeof raw === 'string') {\n return raw;\n }\n\n if (raw instanceof ArrayBuffer) {\n return new TextDecoder().decode(new Uint8Array(raw));\n }\n\n if (ArrayBuffer.isView(raw)) {\n return new TextDecoder().decode(raw);\n }\n\n return '';\n}\n"]}