@fluxstack/live 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.
- package/dist/build/index.d.ts +27 -0
- package/dist/build/index.js +151 -0
- package/dist/build/index.js.map +1 -0
- package/dist/index.d.ts +1400 -0
- package/dist/index.js +3334 -0
- package/dist/index.js.map +1 -0
- package/package.json +50 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,3334 @@
|
|
|
1
|
+
import { randomBytes, createCipheriv, createDecipheriv, createHmac } from 'crypto';
|
|
2
|
+
import { gzipSync, gunzipSync } from 'zlib';
|
|
3
|
+
import { EventEmitter } from 'events';
|
|
4
|
+
|
|
5
|
+
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
6
|
+
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
7
|
+
}) : x)(function(x) {
|
|
8
|
+
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
9
|
+
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
// src/rooms/RoomEventBus.ts
|
|
13
|
+
function createTypedRoomEventBus() {
|
|
14
|
+
const subscriptions = /* @__PURE__ */ new Map();
|
|
15
|
+
const getKey = (roomType, roomId, event) => `${roomType}:${roomId}:${event}`;
|
|
16
|
+
const getRoomKey = (roomType, roomId) => `${roomType}:${roomId}`;
|
|
17
|
+
return {
|
|
18
|
+
on(roomType, roomId, event, componentId, handler) {
|
|
19
|
+
const key = getKey(roomType, roomId, event);
|
|
20
|
+
if (!subscriptions.has(key)) {
|
|
21
|
+
subscriptions.set(key, /* @__PURE__ */ new Set());
|
|
22
|
+
}
|
|
23
|
+
const subscription = {
|
|
24
|
+
roomType,
|
|
25
|
+
roomId,
|
|
26
|
+
event,
|
|
27
|
+
handler,
|
|
28
|
+
componentId
|
|
29
|
+
};
|
|
30
|
+
subscriptions.get(key).add(subscription);
|
|
31
|
+
return () => {
|
|
32
|
+
subscriptions.get(key)?.delete(subscription);
|
|
33
|
+
if (subscriptions.get(key)?.size === 0) {
|
|
34
|
+
subscriptions.delete(key);
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
},
|
|
38
|
+
emit(roomType, roomId, event, data, excludeComponentId) {
|
|
39
|
+
const key = getKey(roomType, roomId, event);
|
|
40
|
+
const subs = subscriptions.get(key);
|
|
41
|
+
if (!subs || subs.size === 0) return 0;
|
|
42
|
+
let notified = 0;
|
|
43
|
+
for (const sub of subs) {
|
|
44
|
+
if (excludeComponentId && sub.componentId === excludeComponentId) continue;
|
|
45
|
+
try {
|
|
46
|
+
sub.handler(data);
|
|
47
|
+
notified++;
|
|
48
|
+
} catch (error) {
|
|
49
|
+
console.error(`[RoomEventBus] Error in handler [${key}]:`, error);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return notified;
|
|
53
|
+
},
|
|
54
|
+
unsubscribeAll(componentId) {
|
|
55
|
+
let removed = 0;
|
|
56
|
+
for (const [key, subs] of subscriptions) {
|
|
57
|
+
for (const sub of subs) {
|
|
58
|
+
if (sub.componentId === componentId) {
|
|
59
|
+
subs.delete(sub);
|
|
60
|
+
removed++;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
if (subs.size === 0) {
|
|
64
|
+
subscriptions.delete(key);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return removed;
|
|
68
|
+
},
|
|
69
|
+
clearRoom(roomType, roomId) {
|
|
70
|
+
const prefix = getRoomKey(roomType, roomId);
|
|
71
|
+
let removed = 0;
|
|
72
|
+
for (const key of subscriptions.keys()) {
|
|
73
|
+
if (key.startsWith(prefix)) {
|
|
74
|
+
removed += subscriptions.get(key)?.size ?? 0;
|
|
75
|
+
subscriptions.delete(key);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return removed;
|
|
79
|
+
},
|
|
80
|
+
getStats() {
|
|
81
|
+
const rooms = {};
|
|
82
|
+
let total = 0;
|
|
83
|
+
for (const [key, subs] of subscriptions) {
|
|
84
|
+
const parts = key.split(":");
|
|
85
|
+
const roomKey = `${parts[0]}:${parts[1]}`;
|
|
86
|
+
const event = parts[2] ?? "";
|
|
87
|
+
if (!rooms[roomKey]) {
|
|
88
|
+
rooms[roomKey] = { events: {} };
|
|
89
|
+
}
|
|
90
|
+
rooms[roomKey].events[event] = subs.size;
|
|
91
|
+
total += subs.size;
|
|
92
|
+
}
|
|
93
|
+
return { totalSubscriptions: total, rooms };
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
var RoomEventBus = class {
|
|
98
|
+
subscriptions = /* @__PURE__ */ new Map();
|
|
99
|
+
getKey(roomType, roomId, event) {
|
|
100
|
+
return `${roomType}:${roomId}:${event}`;
|
|
101
|
+
}
|
|
102
|
+
on(roomType, roomId, event, componentId, handler) {
|
|
103
|
+
const key = this.getKey(roomType, roomId, event);
|
|
104
|
+
if (!this.subscriptions.has(key)) {
|
|
105
|
+
this.subscriptions.set(key, /* @__PURE__ */ new Set());
|
|
106
|
+
}
|
|
107
|
+
const subscription = { roomType, roomId, event, handler, componentId };
|
|
108
|
+
this.subscriptions.get(key).add(subscription);
|
|
109
|
+
return () => {
|
|
110
|
+
this.subscriptions.get(key)?.delete(subscription);
|
|
111
|
+
if (this.subscriptions.get(key)?.size === 0) {
|
|
112
|
+
this.subscriptions.delete(key);
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
emit(roomType, roomId, event, data, excludeComponentId) {
|
|
117
|
+
const key = this.getKey(roomType, roomId, event);
|
|
118
|
+
const subs = this.subscriptions.get(key);
|
|
119
|
+
if (!subs || subs.size === 0) return 0;
|
|
120
|
+
let notified = 0;
|
|
121
|
+
for (const sub of subs) {
|
|
122
|
+
if (excludeComponentId && sub.componentId === excludeComponentId) continue;
|
|
123
|
+
try {
|
|
124
|
+
sub.handler(data);
|
|
125
|
+
notified++;
|
|
126
|
+
} catch (error) {
|
|
127
|
+
console.error(`[RoomEventBus] Error in handler [${key}]:`, error);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return notified;
|
|
131
|
+
}
|
|
132
|
+
unsubscribeAll(componentId) {
|
|
133
|
+
let removed = 0;
|
|
134
|
+
for (const [key, subs] of this.subscriptions) {
|
|
135
|
+
for (const sub of subs) {
|
|
136
|
+
if (sub.componentId === componentId) {
|
|
137
|
+
subs.delete(sub);
|
|
138
|
+
removed++;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
if (subs.size === 0) {
|
|
142
|
+
this.subscriptions.delete(key);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return removed;
|
|
146
|
+
}
|
|
147
|
+
clearRoom(roomType, roomId) {
|
|
148
|
+
const prefix = `${roomType}:${roomId}`;
|
|
149
|
+
let removed = 0;
|
|
150
|
+
for (const key of this.subscriptions.keys()) {
|
|
151
|
+
if (key.startsWith(prefix)) {
|
|
152
|
+
removed += this.subscriptions.get(key)?.size ?? 0;
|
|
153
|
+
this.subscriptions.delete(key);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return removed;
|
|
157
|
+
}
|
|
158
|
+
getStats() {
|
|
159
|
+
const rooms = {};
|
|
160
|
+
let total = 0;
|
|
161
|
+
for (const [key, subs] of this.subscriptions) {
|
|
162
|
+
const parts = key.split(":");
|
|
163
|
+
const roomKey = `${parts[0]}:${parts[1]}`;
|
|
164
|
+
const event = parts[2] ?? "";
|
|
165
|
+
if (!rooms[roomKey]) {
|
|
166
|
+
rooms[roomKey] = { events: {} };
|
|
167
|
+
}
|
|
168
|
+
rooms[roomKey].events[event] = subs.size;
|
|
169
|
+
total += subs.size;
|
|
170
|
+
}
|
|
171
|
+
return { totalSubscriptions: total, rooms };
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
// src/debug/LiveLogger.ts
|
|
176
|
+
var componentConfigs = /* @__PURE__ */ new Map();
|
|
177
|
+
var globalConfigParsed = false;
|
|
178
|
+
var globalConfig = false;
|
|
179
|
+
function parseGlobalConfig() {
|
|
180
|
+
if (globalConfigParsed) return globalConfig;
|
|
181
|
+
globalConfigParsed = true;
|
|
182
|
+
const envValue = typeof process !== "undefined" ? process.env?.LIVE_LOGGING : void 0;
|
|
183
|
+
if (!envValue || envValue === "false") {
|
|
184
|
+
globalConfig = false;
|
|
185
|
+
} else if (envValue === "true") {
|
|
186
|
+
globalConfig = true;
|
|
187
|
+
} else {
|
|
188
|
+
globalConfig = envValue.split(",").map((s) => s.trim()).filter(Boolean);
|
|
189
|
+
}
|
|
190
|
+
return globalConfig;
|
|
191
|
+
}
|
|
192
|
+
function registerComponentLogging(componentId, config) {
|
|
193
|
+
if (config !== void 0 && config !== false) {
|
|
194
|
+
componentConfigs.set(componentId, config);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
function unregisterComponentLogging(componentId) {
|
|
198
|
+
componentConfigs.delete(componentId);
|
|
199
|
+
}
|
|
200
|
+
function shouldLog(componentId, category) {
|
|
201
|
+
if (componentId) {
|
|
202
|
+
const config = componentConfigs.get(componentId);
|
|
203
|
+
if (config === void 0 || config === false) return false;
|
|
204
|
+
if (config === true) return true;
|
|
205
|
+
return config.includes(category);
|
|
206
|
+
}
|
|
207
|
+
const cfg = parseGlobalConfig();
|
|
208
|
+
if (cfg === false) return false;
|
|
209
|
+
if (cfg === true) return true;
|
|
210
|
+
return cfg.includes(category);
|
|
211
|
+
}
|
|
212
|
+
var _debugger = null;
|
|
213
|
+
function _setLoggerDebugger(dbg) {
|
|
214
|
+
_debugger = dbg;
|
|
215
|
+
}
|
|
216
|
+
function emitToDebugger(category, level, componentId, message, args) {
|
|
217
|
+
if (!_debugger?.enabled) return;
|
|
218
|
+
const data = { category, level, message };
|
|
219
|
+
if (args.length === 1 && typeof args[0] === "object" && args[0] !== null) {
|
|
220
|
+
data.details = args[0];
|
|
221
|
+
} else if (args.length > 0) {
|
|
222
|
+
data.details = args;
|
|
223
|
+
}
|
|
224
|
+
_debugger.emit("LOG", componentId, null, data);
|
|
225
|
+
}
|
|
226
|
+
function liveLog(category, componentId, message, ...args) {
|
|
227
|
+
emitToDebugger(category, "info", componentId, message, args);
|
|
228
|
+
if (shouldLog(componentId, category)) {
|
|
229
|
+
if (args.length > 0) {
|
|
230
|
+
console.log(message, ...args);
|
|
231
|
+
} else {
|
|
232
|
+
console.log(message);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
function liveWarn(category, componentId, message, ...args) {
|
|
237
|
+
emitToDebugger(category, "warn", componentId, message, args);
|
|
238
|
+
if (shouldLog(componentId, category)) {
|
|
239
|
+
if (args.length > 0) {
|
|
240
|
+
console.warn(message, ...args);
|
|
241
|
+
} else {
|
|
242
|
+
console.warn(message);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// src/protocol/constants.ts
|
|
248
|
+
var PROTOCOL_VERSION = 1;
|
|
249
|
+
var DEFAULT_WS_PATH = "/api/live/ws";
|
|
250
|
+
var DEFAULT_CHUNK_SIZE = 64 * 1024;
|
|
251
|
+
var DEFAULT_RATE_LIMIT_MAX_TOKENS = 100;
|
|
252
|
+
var DEFAULT_RATE_LIMIT_REFILL_RATE = 50;
|
|
253
|
+
var MAX_ROOM_STATE_SIZE = 10 * 1024 * 1024;
|
|
254
|
+
|
|
255
|
+
// src/rooms/LiveRoomManager.ts
|
|
256
|
+
var LiveRoomManager = class {
|
|
257
|
+
// componentId -> roomIds
|
|
258
|
+
constructor(roomEvents) {
|
|
259
|
+
this.roomEvents = roomEvents;
|
|
260
|
+
}
|
|
261
|
+
rooms = /* @__PURE__ */ new Map();
|
|
262
|
+
componentRooms = /* @__PURE__ */ new Map();
|
|
263
|
+
/**
|
|
264
|
+
* Component joins a room
|
|
265
|
+
*/
|
|
266
|
+
joinRoom(componentId, roomId, ws, initialState) {
|
|
267
|
+
if (!roomId || !/^[a-zA-Z0-9_:.-]{1,64}$/.test(roomId)) {
|
|
268
|
+
throw new Error("Invalid room name. Must be 1-64 alphanumeric characters, hyphens, underscores, dots, or colons.");
|
|
269
|
+
}
|
|
270
|
+
if (!this.rooms.has(roomId)) {
|
|
271
|
+
this.rooms.set(roomId, {
|
|
272
|
+
id: roomId,
|
|
273
|
+
state: initialState || {},
|
|
274
|
+
members: /* @__PURE__ */ new Map(),
|
|
275
|
+
createdAt: Date.now(),
|
|
276
|
+
lastActivity: Date.now()
|
|
277
|
+
});
|
|
278
|
+
liveLog("rooms", componentId, `Room '${roomId}' created`);
|
|
279
|
+
}
|
|
280
|
+
const room = this.rooms.get(roomId);
|
|
281
|
+
room.members.set(componentId, {
|
|
282
|
+
componentId,
|
|
283
|
+
ws,
|
|
284
|
+
joinedAt: Date.now()
|
|
285
|
+
});
|
|
286
|
+
room.lastActivity = Date.now();
|
|
287
|
+
if (!this.componentRooms.has(componentId)) {
|
|
288
|
+
this.componentRooms.set(componentId, /* @__PURE__ */ new Set());
|
|
289
|
+
}
|
|
290
|
+
this.componentRooms.get(componentId).add(roomId);
|
|
291
|
+
liveLog("rooms", componentId, `Component '${componentId}' joined room '${roomId}' (${room.members.size} members)`);
|
|
292
|
+
this.broadcastToRoom(roomId, {
|
|
293
|
+
type: "ROOM_SYSTEM",
|
|
294
|
+
componentId,
|
|
295
|
+
roomId,
|
|
296
|
+
event: "$sub:join",
|
|
297
|
+
data: {
|
|
298
|
+
subscriberId: componentId,
|
|
299
|
+
count: room.members.size
|
|
300
|
+
},
|
|
301
|
+
timestamp: Date.now()
|
|
302
|
+
}, componentId);
|
|
303
|
+
return { state: room.state };
|
|
304
|
+
}
|
|
305
|
+
/**
|
|
306
|
+
* Component leaves a room
|
|
307
|
+
*/
|
|
308
|
+
leaveRoom(componentId, roomId) {
|
|
309
|
+
const room = this.rooms.get(roomId);
|
|
310
|
+
if (!room) return;
|
|
311
|
+
room.members.delete(componentId);
|
|
312
|
+
room.lastActivity = Date.now();
|
|
313
|
+
this.componentRooms.get(componentId)?.delete(roomId);
|
|
314
|
+
liveLog("rooms", componentId, `Component '${componentId}' left room '${roomId}' (${room.members.size} members)`);
|
|
315
|
+
this.broadcastToRoom(roomId, {
|
|
316
|
+
type: "ROOM_SYSTEM",
|
|
317
|
+
componentId,
|
|
318
|
+
roomId,
|
|
319
|
+
event: "$sub:leave",
|
|
320
|
+
data: {
|
|
321
|
+
subscriberId: componentId,
|
|
322
|
+
count: room.members.size
|
|
323
|
+
},
|
|
324
|
+
timestamp: Date.now()
|
|
325
|
+
});
|
|
326
|
+
if (room.members.size === 0) {
|
|
327
|
+
setTimeout(() => {
|
|
328
|
+
const currentRoom = this.rooms.get(roomId);
|
|
329
|
+
if (currentRoom && currentRoom.members.size === 0) {
|
|
330
|
+
this.rooms.delete(roomId);
|
|
331
|
+
liveLog("rooms", null, `Room '${roomId}' destroyed (empty)`);
|
|
332
|
+
}
|
|
333
|
+
}, 5 * 60 * 1e3);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
/**
|
|
337
|
+
* Component disconnects - leave all rooms
|
|
338
|
+
*/
|
|
339
|
+
cleanupComponent(componentId) {
|
|
340
|
+
const rooms = this.componentRooms.get(componentId);
|
|
341
|
+
if (!rooms) return;
|
|
342
|
+
for (const roomId of rooms) {
|
|
343
|
+
this.leaveRoom(componentId, roomId);
|
|
344
|
+
}
|
|
345
|
+
this.componentRooms.delete(componentId);
|
|
346
|
+
}
|
|
347
|
+
/**
|
|
348
|
+
* Emit event to all members in a room
|
|
349
|
+
*/
|
|
350
|
+
emitToRoom(roomId, event, data, excludeComponentId) {
|
|
351
|
+
const room = this.rooms.get(roomId);
|
|
352
|
+
if (!room) return 0;
|
|
353
|
+
room.lastActivity = Date.now();
|
|
354
|
+
this.roomEvents.emit("room", roomId, event, data, excludeComponentId);
|
|
355
|
+
return this.broadcastToRoom(roomId, {
|
|
356
|
+
type: "ROOM_EVENT",
|
|
357
|
+
componentId: "",
|
|
358
|
+
roomId,
|
|
359
|
+
event,
|
|
360
|
+
data,
|
|
361
|
+
timestamp: Date.now()
|
|
362
|
+
}, excludeComponentId);
|
|
363
|
+
}
|
|
364
|
+
/**
|
|
365
|
+
* Update room state
|
|
366
|
+
*/
|
|
367
|
+
setRoomState(roomId, updates, excludeComponentId) {
|
|
368
|
+
const room = this.rooms.get(roomId);
|
|
369
|
+
if (!room) return;
|
|
370
|
+
const newState = { ...room.state, ...updates };
|
|
371
|
+
const stateSize = Buffer.byteLength(JSON.stringify(newState), "utf8");
|
|
372
|
+
if (stateSize > MAX_ROOM_STATE_SIZE) {
|
|
373
|
+
throw new Error("Room state exceeds maximum size limit");
|
|
374
|
+
}
|
|
375
|
+
room.state = newState;
|
|
376
|
+
room.lastActivity = Date.now();
|
|
377
|
+
this.broadcastToRoom(roomId, {
|
|
378
|
+
type: "ROOM_STATE",
|
|
379
|
+
componentId: "",
|
|
380
|
+
roomId,
|
|
381
|
+
event: "$state:update",
|
|
382
|
+
data: { state: updates },
|
|
383
|
+
timestamp: Date.now()
|
|
384
|
+
}, excludeComponentId);
|
|
385
|
+
}
|
|
386
|
+
/**
|
|
387
|
+
* Get room state
|
|
388
|
+
*/
|
|
389
|
+
getRoomState(roomId) {
|
|
390
|
+
return this.rooms.get(roomId)?.state || {};
|
|
391
|
+
}
|
|
392
|
+
/**
|
|
393
|
+
* Broadcast to all members in a room
|
|
394
|
+
*/
|
|
395
|
+
broadcastToRoom(roomId, message, excludeComponentId) {
|
|
396
|
+
const room = this.rooms.get(roomId);
|
|
397
|
+
if (!room) return 0;
|
|
398
|
+
let sent = 0;
|
|
399
|
+
for (const [componentId, member] of room.members) {
|
|
400
|
+
if (excludeComponentId && componentId === excludeComponentId) continue;
|
|
401
|
+
try {
|
|
402
|
+
if (member.ws && member.ws.readyState === 1) {
|
|
403
|
+
member.ws.send(JSON.stringify({
|
|
404
|
+
...message,
|
|
405
|
+
componentId
|
|
406
|
+
}));
|
|
407
|
+
sent++;
|
|
408
|
+
}
|
|
409
|
+
} catch (error) {
|
|
410
|
+
console.error(`Failed to send to ${componentId}:`, error);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
return sent;
|
|
414
|
+
}
|
|
415
|
+
/**
|
|
416
|
+
* Check if component is in a room
|
|
417
|
+
*/
|
|
418
|
+
isInRoom(componentId, roomId) {
|
|
419
|
+
return this.rooms.get(roomId)?.members.has(componentId) ?? false;
|
|
420
|
+
}
|
|
421
|
+
/**
|
|
422
|
+
* Get rooms for a component
|
|
423
|
+
*/
|
|
424
|
+
getComponentRooms(componentId) {
|
|
425
|
+
return Array.from(this.componentRooms.get(componentId) || []);
|
|
426
|
+
}
|
|
427
|
+
/**
|
|
428
|
+
* Get statistics
|
|
429
|
+
*/
|
|
430
|
+
getStats() {
|
|
431
|
+
const rooms = {};
|
|
432
|
+
for (const [id, room] of this.rooms) {
|
|
433
|
+
rooms[id] = {
|
|
434
|
+
members: room.members.size,
|
|
435
|
+
createdAt: room.createdAt,
|
|
436
|
+
lastActivity: room.lastActivity
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
return {
|
|
440
|
+
totalRooms: this.rooms.size,
|
|
441
|
+
rooms
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
};
|
|
445
|
+
|
|
446
|
+
// src/debug/LiveDebugger.ts
|
|
447
|
+
var MAX_EVENTS = 500;
|
|
448
|
+
var MAX_STATE_SIZE = 5e4;
|
|
449
|
+
var LiveDebugger = class {
|
|
450
|
+
events = [];
|
|
451
|
+
componentSnapshots = /* @__PURE__ */ new Map();
|
|
452
|
+
debugClients = /* @__PURE__ */ new Set();
|
|
453
|
+
_enabled = false;
|
|
454
|
+
startTime = Date.now();
|
|
455
|
+
eventCounter = 0;
|
|
456
|
+
constructor(enabled = false) {
|
|
457
|
+
this._enabled = enabled;
|
|
458
|
+
}
|
|
459
|
+
get enabled() {
|
|
460
|
+
return this._enabled;
|
|
461
|
+
}
|
|
462
|
+
set enabled(value) {
|
|
463
|
+
this._enabled = value;
|
|
464
|
+
}
|
|
465
|
+
// ===== Event Emission =====
|
|
466
|
+
emit(type, componentId, componentName, data = {}) {
|
|
467
|
+
if (!this._enabled) return;
|
|
468
|
+
const event = {
|
|
469
|
+
id: `dbg-${++this.eventCounter}`,
|
|
470
|
+
timestamp: Date.now(),
|
|
471
|
+
type,
|
|
472
|
+
componentId,
|
|
473
|
+
componentName,
|
|
474
|
+
data: this.sanitizeData(data)
|
|
475
|
+
};
|
|
476
|
+
this.events.push(event);
|
|
477
|
+
if (this.events.length > MAX_EVENTS) {
|
|
478
|
+
this.events.shift();
|
|
479
|
+
}
|
|
480
|
+
if (componentId) {
|
|
481
|
+
this.updateSnapshot(event);
|
|
482
|
+
}
|
|
483
|
+
this.broadcastEvent(event);
|
|
484
|
+
}
|
|
485
|
+
// ===== Component Tracking =====
|
|
486
|
+
trackComponentMount(componentId, componentName, initialState, room, debugLabel) {
|
|
487
|
+
if (!this._enabled) return;
|
|
488
|
+
const snapshot = {
|
|
489
|
+
componentId,
|
|
490
|
+
componentName,
|
|
491
|
+
debugLabel,
|
|
492
|
+
state: this.sanitizeState(initialState),
|
|
493
|
+
rooms: room ? [room] : [],
|
|
494
|
+
mountedAt: Date.now(),
|
|
495
|
+
lastActivity: Date.now(),
|
|
496
|
+
actionCount: 0,
|
|
497
|
+
stateChangeCount: 0,
|
|
498
|
+
errorCount: 0
|
|
499
|
+
};
|
|
500
|
+
this.componentSnapshots.set(componentId, snapshot);
|
|
501
|
+
this.emit("COMPONENT_MOUNT", componentId, componentName, {
|
|
502
|
+
initialState: snapshot.state,
|
|
503
|
+
room: room ?? null,
|
|
504
|
+
debugLabel: debugLabel ?? null
|
|
505
|
+
});
|
|
506
|
+
}
|
|
507
|
+
trackComponentUnmount(componentId) {
|
|
508
|
+
if (!this._enabled) return;
|
|
509
|
+
const snapshot = this.componentSnapshots.get(componentId);
|
|
510
|
+
const componentName = snapshot?.componentName ?? null;
|
|
511
|
+
this.emit("COMPONENT_UNMOUNT", componentId, componentName, {
|
|
512
|
+
lifetime: snapshot ? Date.now() - snapshot.mountedAt : 0,
|
|
513
|
+
totalActions: snapshot?.actionCount ?? 0,
|
|
514
|
+
totalStateChanges: snapshot?.stateChangeCount ?? 0,
|
|
515
|
+
totalErrors: snapshot?.errorCount ?? 0
|
|
516
|
+
});
|
|
517
|
+
this.componentSnapshots.delete(componentId);
|
|
518
|
+
}
|
|
519
|
+
trackStateChange(componentId, delta, fullState, source = "setState") {
|
|
520
|
+
if (!this._enabled) return;
|
|
521
|
+
const snapshot = this.componentSnapshots.get(componentId);
|
|
522
|
+
if (snapshot) {
|
|
523
|
+
snapshot.state = this.sanitizeState(fullState);
|
|
524
|
+
snapshot.stateChangeCount++;
|
|
525
|
+
snapshot.lastActivity = Date.now();
|
|
526
|
+
}
|
|
527
|
+
this.emit("STATE_CHANGE", componentId, snapshot?.componentName ?? null, {
|
|
528
|
+
delta,
|
|
529
|
+
fullState: this.sanitizeState(fullState),
|
|
530
|
+
source
|
|
531
|
+
});
|
|
532
|
+
}
|
|
533
|
+
trackActionCall(componentId, action, payload) {
|
|
534
|
+
if (!this._enabled) return;
|
|
535
|
+
const snapshot = this.componentSnapshots.get(componentId);
|
|
536
|
+
if (snapshot) {
|
|
537
|
+
snapshot.actionCount++;
|
|
538
|
+
snapshot.lastActivity = Date.now();
|
|
539
|
+
}
|
|
540
|
+
this.emit("ACTION_CALL", componentId, snapshot?.componentName ?? null, {
|
|
541
|
+
action,
|
|
542
|
+
payload: this.sanitizeData({ payload }).payload
|
|
543
|
+
});
|
|
544
|
+
}
|
|
545
|
+
trackActionResult(componentId, action, result, duration) {
|
|
546
|
+
if (!this._enabled) return;
|
|
547
|
+
const snapshot = this.componentSnapshots.get(componentId);
|
|
548
|
+
this.emit("ACTION_RESULT", componentId, snapshot?.componentName ?? null, {
|
|
549
|
+
action,
|
|
550
|
+
result: this.sanitizeData({ result }).result,
|
|
551
|
+
duration
|
|
552
|
+
});
|
|
553
|
+
}
|
|
554
|
+
trackActionError(componentId, action, error, duration) {
|
|
555
|
+
if (!this._enabled) return;
|
|
556
|
+
const snapshot = this.componentSnapshots.get(componentId);
|
|
557
|
+
if (snapshot) {
|
|
558
|
+
snapshot.errorCount++;
|
|
559
|
+
}
|
|
560
|
+
this.emit("ACTION_ERROR", componentId, snapshot?.componentName ?? null, {
|
|
561
|
+
action,
|
|
562
|
+
error,
|
|
563
|
+
duration
|
|
564
|
+
});
|
|
565
|
+
}
|
|
566
|
+
trackRoomJoin(componentId, roomId) {
|
|
567
|
+
if (!this._enabled) return;
|
|
568
|
+
const snapshot = this.componentSnapshots.get(componentId);
|
|
569
|
+
if (snapshot && !snapshot.rooms.includes(roomId)) {
|
|
570
|
+
snapshot.rooms.push(roomId);
|
|
571
|
+
}
|
|
572
|
+
this.emit("ROOM_JOIN", componentId, snapshot?.componentName ?? null, { roomId });
|
|
573
|
+
}
|
|
574
|
+
trackRoomLeave(componentId, roomId) {
|
|
575
|
+
if (!this._enabled) return;
|
|
576
|
+
const snapshot = this.componentSnapshots.get(componentId);
|
|
577
|
+
if (snapshot) {
|
|
578
|
+
snapshot.rooms = snapshot.rooms.filter((r) => r !== roomId);
|
|
579
|
+
}
|
|
580
|
+
this.emit("ROOM_LEAVE", componentId, snapshot?.componentName ?? null, { roomId });
|
|
581
|
+
}
|
|
582
|
+
trackRoomEmit(componentId, roomId, event, data) {
|
|
583
|
+
if (!this._enabled) return;
|
|
584
|
+
const snapshot = this.componentSnapshots.get(componentId);
|
|
585
|
+
this.emit("ROOM_EMIT", componentId, snapshot?.componentName ?? null, {
|
|
586
|
+
roomId,
|
|
587
|
+
event,
|
|
588
|
+
data: this.sanitizeData({ data }).data
|
|
589
|
+
});
|
|
590
|
+
}
|
|
591
|
+
trackConnection(connectionId) {
|
|
592
|
+
if (!this._enabled) return;
|
|
593
|
+
this.emit("WS_CONNECT", null, null, { connectionId });
|
|
594
|
+
}
|
|
595
|
+
trackDisconnection(connectionId, componentCount) {
|
|
596
|
+
if (!this._enabled) return;
|
|
597
|
+
this.emit("WS_DISCONNECT", null, null, { connectionId, componentCount });
|
|
598
|
+
}
|
|
599
|
+
trackError(componentId, error, context) {
|
|
600
|
+
if (!this._enabled) return;
|
|
601
|
+
const snapshot = componentId ? this.componentSnapshots.get(componentId) : null;
|
|
602
|
+
if (snapshot) {
|
|
603
|
+
snapshot.errorCount++;
|
|
604
|
+
}
|
|
605
|
+
this.emit("ERROR", componentId, snapshot?.componentName ?? null, {
|
|
606
|
+
error,
|
|
607
|
+
...context
|
|
608
|
+
});
|
|
609
|
+
}
|
|
610
|
+
// ===== Debug Client Management =====
|
|
611
|
+
registerDebugClient(ws) {
|
|
612
|
+
if (!this._enabled) {
|
|
613
|
+
const disabled = {
|
|
614
|
+
type: "DEBUG_DISABLED",
|
|
615
|
+
enabled: false,
|
|
616
|
+
timestamp: Date.now()
|
|
617
|
+
};
|
|
618
|
+
ws.send(JSON.stringify(disabled));
|
|
619
|
+
ws.close();
|
|
620
|
+
return;
|
|
621
|
+
}
|
|
622
|
+
this.debugClients.add(ws);
|
|
623
|
+
const welcome = {
|
|
624
|
+
type: "DEBUG_WELCOME",
|
|
625
|
+
enabled: true,
|
|
626
|
+
snapshot: this.getSnapshot(),
|
|
627
|
+
timestamp: Date.now()
|
|
628
|
+
};
|
|
629
|
+
ws.send(JSON.stringify(welcome));
|
|
630
|
+
for (const event of this.events.slice(-100)) {
|
|
631
|
+
const msg = {
|
|
632
|
+
type: "DEBUG_EVENT",
|
|
633
|
+
event,
|
|
634
|
+
timestamp: Date.now()
|
|
635
|
+
};
|
|
636
|
+
ws.send(JSON.stringify(msg));
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
unregisterDebugClient(ws) {
|
|
640
|
+
this.debugClients.delete(ws);
|
|
641
|
+
}
|
|
642
|
+
// ===== Snapshot =====
|
|
643
|
+
getSnapshot() {
|
|
644
|
+
return {
|
|
645
|
+
components: Array.from(this.componentSnapshots.values()),
|
|
646
|
+
connections: this.debugClients.size,
|
|
647
|
+
uptime: Date.now() - this.startTime,
|
|
648
|
+
totalEvents: this.eventCounter
|
|
649
|
+
};
|
|
650
|
+
}
|
|
651
|
+
getComponentState(componentId) {
|
|
652
|
+
return this.componentSnapshots.get(componentId) ?? null;
|
|
653
|
+
}
|
|
654
|
+
getEvents(filter) {
|
|
655
|
+
let filtered = this.events;
|
|
656
|
+
if (filter?.componentId) {
|
|
657
|
+
filtered = filtered.filter((e) => e.componentId === filter.componentId);
|
|
658
|
+
}
|
|
659
|
+
if (filter?.type) {
|
|
660
|
+
filtered = filtered.filter((e) => e.type === filter.type);
|
|
661
|
+
}
|
|
662
|
+
const limit = filter?.limit ?? 100;
|
|
663
|
+
return filtered.slice(-limit);
|
|
664
|
+
}
|
|
665
|
+
clearEvents() {
|
|
666
|
+
this.events = [];
|
|
667
|
+
}
|
|
668
|
+
// ===== Internal =====
|
|
669
|
+
broadcastEvent(event) {
|
|
670
|
+
if (this.debugClients.size === 0) return;
|
|
671
|
+
const msg = {
|
|
672
|
+
type: "DEBUG_EVENT",
|
|
673
|
+
event,
|
|
674
|
+
timestamp: Date.now()
|
|
675
|
+
};
|
|
676
|
+
const json = JSON.stringify(msg);
|
|
677
|
+
for (const client of this.debugClients) {
|
|
678
|
+
try {
|
|
679
|
+
client.send(json);
|
|
680
|
+
} catch {
|
|
681
|
+
this.debugClients.delete(client);
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
sanitizeData(data) {
|
|
686
|
+
try {
|
|
687
|
+
const json = JSON.stringify(data);
|
|
688
|
+
if (json.length > MAX_STATE_SIZE) {
|
|
689
|
+
return { _truncated: true, _size: json.length, _preview: json.slice(0, 500) + "..." };
|
|
690
|
+
}
|
|
691
|
+
return JSON.parse(json);
|
|
692
|
+
} catch {
|
|
693
|
+
return { _serialization_error: true };
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
sanitizeState(state) {
|
|
697
|
+
try {
|
|
698
|
+
const json = JSON.stringify(state);
|
|
699
|
+
if (json.length > MAX_STATE_SIZE) {
|
|
700
|
+
return { _truncated: true, _size: json.length };
|
|
701
|
+
}
|
|
702
|
+
return JSON.parse(json);
|
|
703
|
+
} catch {
|
|
704
|
+
return { _serialization_error: true };
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
updateSnapshot(event) {
|
|
708
|
+
if (!event.componentId) return;
|
|
709
|
+
const snapshot = this.componentSnapshots.get(event.componentId);
|
|
710
|
+
if (snapshot) {
|
|
711
|
+
snapshot.lastActivity = event.timestamp;
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
};
|
|
715
|
+
|
|
716
|
+
// src/auth/LiveAuthContext.ts
|
|
717
|
+
var AuthenticatedContext = class {
|
|
718
|
+
authenticated = true;
|
|
719
|
+
user;
|
|
720
|
+
token;
|
|
721
|
+
authenticatedAt;
|
|
722
|
+
constructor(user, token) {
|
|
723
|
+
this.user = user;
|
|
724
|
+
this.token = token;
|
|
725
|
+
this.authenticatedAt = Date.now();
|
|
726
|
+
}
|
|
727
|
+
hasRole(role) {
|
|
728
|
+
return this.user.roles?.includes(role) ?? false;
|
|
729
|
+
}
|
|
730
|
+
hasAnyRole(roles) {
|
|
731
|
+
if (!this.user.roles?.length) return false;
|
|
732
|
+
return roles.some((role) => this.user.roles.includes(role));
|
|
733
|
+
}
|
|
734
|
+
hasAllRoles(roles) {
|
|
735
|
+
if (!this.user.roles?.length) return roles.length === 0;
|
|
736
|
+
return roles.every((role) => this.user.roles.includes(role));
|
|
737
|
+
}
|
|
738
|
+
hasPermission(permission) {
|
|
739
|
+
return this.user.permissions?.includes(permission) ?? false;
|
|
740
|
+
}
|
|
741
|
+
hasAllPermissions(permissions) {
|
|
742
|
+
if (!this.user.permissions?.length) return permissions.length === 0;
|
|
743
|
+
return permissions.every((perm) => this.user.permissions.includes(perm));
|
|
744
|
+
}
|
|
745
|
+
hasAnyPermission(permissions) {
|
|
746
|
+
if (!this.user.permissions?.length) return false;
|
|
747
|
+
return permissions.some((perm) => this.user.permissions.includes(perm));
|
|
748
|
+
}
|
|
749
|
+
};
|
|
750
|
+
var AnonymousContext = class {
|
|
751
|
+
authenticated = false;
|
|
752
|
+
user = void 0;
|
|
753
|
+
token = void 0;
|
|
754
|
+
authenticatedAt = void 0;
|
|
755
|
+
hasRole() {
|
|
756
|
+
return false;
|
|
757
|
+
}
|
|
758
|
+
hasAnyRole() {
|
|
759
|
+
return false;
|
|
760
|
+
}
|
|
761
|
+
hasAllRoles() {
|
|
762
|
+
return false;
|
|
763
|
+
}
|
|
764
|
+
hasPermission() {
|
|
765
|
+
return false;
|
|
766
|
+
}
|
|
767
|
+
hasAllPermissions() {
|
|
768
|
+
return false;
|
|
769
|
+
}
|
|
770
|
+
hasAnyPermission() {
|
|
771
|
+
return false;
|
|
772
|
+
}
|
|
773
|
+
};
|
|
774
|
+
var ANONYMOUS_CONTEXT = new AnonymousContext();
|
|
775
|
+
|
|
776
|
+
// src/auth/LiveAuthManager.ts
|
|
777
|
+
var LiveAuthManager = class {
|
|
778
|
+
providers = /* @__PURE__ */ new Map();
|
|
779
|
+
defaultProviderName;
|
|
780
|
+
/**
|
|
781
|
+
* Register an auth provider.
|
|
782
|
+
*/
|
|
783
|
+
register(provider) {
|
|
784
|
+
this.providers.set(provider.name, provider);
|
|
785
|
+
if (!this.defaultProviderName) {
|
|
786
|
+
this.defaultProviderName = provider.name;
|
|
787
|
+
}
|
|
788
|
+
console.log(`[Auth] Provider registered: ${provider.name}`);
|
|
789
|
+
}
|
|
790
|
+
/**
|
|
791
|
+
* Remove an auth provider.
|
|
792
|
+
*/
|
|
793
|
+
unregister(name) {
|
|
794
|
+
this.providers.delete(name);
|
|
795
|
+
if (this.defaultProviderName === name) {
|
|
796
|
+
this.defaultProviderName = this.providers.keys().next().value;
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
/**
|
|
800
|
+
* Set the default auth provider.
|
|
801
|
+
*/
|
|
802
|
+
setDefault(name) {
|
|
803
|
+
if (!this.providers.has(name)) {
|
|
804
|
+
throw new Error(`Auth provider '${name}' not registered`);
|
|
805
|
+
}
|
|
806
|
+
this.defaultProviderName = name;
|
|
807
|
+
}
|
|
808
|
+
/**
|
|
809
|
+
* Returns true if at least one provider is registered.
|
|
810
|
+
*/
|
|
811
|
+
hasProviders() {
|
|
812
|
+
return this.providers.size > 0;
|
|
813
|
+
}
|
|
814
|
+
/**
|
|
815
|
+
* Returns the default provider or undefined.
|
|
816
|
+
*/
|
|
817
|
+
getDefaultProvider() {
|
|
818
|
+
if (!this.defaultProviderName) return void 0;
|
|
819
|
+
return this.providers.get(this.defaultProviderName);
|
|
820
|
+
}
|
|
821
|
+
/**
|
|
822
|
+
* Authenticate credentials using the specified provider, or try all providers.
|
|
823
|
+
* Returns ANONYMOUS_CONTEXT if no credentials or no providers.
|
|
824
|
+
*/
|
|
825
|
+
async authenticate(credentials, providerName) {
|
|
826
|
+
if (!credentials || Object.keys(credentials).every((k) => !credentials[k])) {
|
|
827
|
+
return ANONYMOUS_CONTEXT;
|
|
828
|
+
}
|
|
829
|
+
if (this.providers.size === 0) {
|
|
830
|
+
return ANONYMOUS_CONTEXT;
|
|
831
|
+
}
|
|
832
|
+
if (providerName) {
|
|
833
|
+
const provider = this.providers.get(providerName);
|
|
834
|
+
if (!provider) {
|
|
835
|
+
console.warn(`[Auth] Provider '${providerName}' not found`);
|
|
836
|
+
return ANONYMOUS_CONTEXT;
|
|
837
|
+
}
|
|
838
|
+
try {
|
|
839
|
+
const context = await provider.authenticate(credentials);
|
|
840
|
+
return context || ANONYMOUS_CONTEXT;
|
|
841
|
+
} catch (error) {
|
|
842
|
+
console.error(`[Auth] Failed via '${providerName}':`, error.message);
|
|
843
|
+
return ANONYMOUS_CONTEXT;
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
const providersToTry = [];
|
|
847
|
+
if (this.defaultProviderName) {
|
|
848
|
+
const defaultProvider = this.providers.get(this.defaultProviderName);
|
|
849
|
+
if (defaultProvider) providersToTry.push(defaultProvider);
|
|
850
|
+
}
|
|
851
|
+
for (const [name, provider] of this.providers) {
|
|
852
|
+
if (name !== this.defaultProviderName) {
|
|
853
|
+
providersToTry.push(provider);
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
for (const provider of providersToTry) {
|
|
857
|
+
try {
|
|
858
|
+
const context = await provider.authenticate(credentials);
|
|
859
|
+
if (context && context.authenticated) {
|
|
860
|
+
return context;
|
|
861
|
+
}
|
|
862
|
+
} catch {
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
return ANONYMOUS_CONTEXT;
|
|
866
|
+
}
|
|
867
|
+
/**
|
|
868
|
+
* Verify auth context meets component requirements.
|
|
869
|
+
*/
|
|
870
|
+
authorizeComponent(authContext, authConfig) {
|
|
871
|
+
if (!authConfig) {
|
|
872
|
+
return { allowed: true };
|
|
873
|
+
}
|
|
874
|
+
if (authConfig.required && !authContext.authenticated) {
|
|
875
|
+
return { allowed: false, reason: "Authentication required" };
|
|
876
|
+
}
|
|
877
|
+
if (authConfig.roles?.length) {
|
|
878
|
+
if (!authContext.authenticated) {
|
|
879
|
+
return { allowed: false, reason: `Authentication required. Roles needed: ${authConfig.roles.join(", ")}` };
|
|
880
|
+
}
|
|
881
|
+
if (!authContext.hasAnyRole(authConfig.roles)) {
|
|
882
|
+
return { allowed: false, reason: `Insufficient roles. Required one of: ${authConfig.roles.join(", ")}` };
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
if (authConfig.permissions?.length) {
|
|
886
|
+
if (!authContext.authenticated) {
|
|
887
|
+
return { allowed: false, reason: `Authentication required. Permissions needed: ${authConfig.permissions.join(", ")}` };
|
|
888
|
+
}
|
|
889
|
+
if (!authContext.hasAllPermissions(authConfig.permissions)) {
|
|
890
|
+
return { allowed: false, reason: `Insufficient permissions. Required all: ${authConfig.permissions.join(", ")}` };
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
return { allowed: true };
|
|
894
|
+
}
|
|
895
|
+
/**
|
|
896
|
+
* Verify auth context allows executing a specific action.
|
|
897
|
+
*/
|
|
898
|
+
async authorizeAction(authContext, componentName, action, actionAuth, providerName) {
|
|
899
|
+
if (!actionAuth) {
|
|
900
|
+
return { allowed: true };
|
|
901
|
+
}
|
|
902
|
+
if (actionAuth.roles?.length) {
|
|
903
|
+
if (!authContext.authenticated) {
|
|
904
|
+
return { allowed: false, reason: `Authentication required for action '${action}'` };
|
|
905
|
+
}
|
|
906
|
+
if (!authContext.hasAnyRole(actionAuth.roles)) {
|
|
907
|
+
return { allowed: false, reason: `Insufficient roles for action '${action}'. Required one of: ${actionAuth.roles.join(", ")}` };
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
if (actionAuth.permissions?.length) {
|
|
911
|
+
if (!authContext.authenticated) {
|
|
912
|
+
return { allowed: false, reason: `Authentication required for action '${action}'` };
|
|
913
|
+
}
|
|
914
|
+
if (!authContext.hasAllPermissions(actionAuth.permissions)) {
|
|
915
|
+
return { allowed: false, reason: `Insufficient permissions for action '${action}'. Required all: ${actionAuth.permissions.join(", ")}` };
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
const name = providerName || this.defaultProviderName;
|
|
919
|
+
if (name) {
|
|
920
|
+
const provider = this.providers.get(name);
|
|
921
|
+
if (provider?.authorizeAction) {
|
|
922
|
+
const allowed = await provider.authorizeAction(authContext, componentName, action);
|
|
923
|
+
if (!allowed) {
|
|
924
|
+
return { allowed: false, reason: `Action '${action}' denied by auth provider '${name}'` };
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
return { allowed: true };
|
|
929
|
+
}
|
|
930
|
+
/**
|
|
931
|
+
* Verify auth context allows joining a room.
|
|
932
|
+
*/
|
|
933
|
+
async authorizeRoom(authContext, roomId, providerName) {
|
|
934
|
+
const name = providerName || this.defaultProviderName;
|
|
935
|
+
if (!name) return { allowed: true };
|
|
936
|
+
const provider = this.providers.get(name);
|
|
937
|
+
if (!provider?.authorizeRoom) return { allowed: true };
|
|
938
|
+
try {
|
|
939
|
+
const allowed = await provider.authorizeRoom(authContext, roomId);
|
|
940
|
+
if (!allowed) {
|
|
941
|
+
return { allowed: false, reason: `Access to room '${roomId}' denied by auth provider '${name}'` };
|
|
942
|
+
}
|
|
943
|
+
return { allowed: true };
|
|
944
|
+
} catch (error) {
|
|
945
|
+
return { allowed: false, reason: `Room authorization error: ${error.message}` };
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
/**
|
|
949
|
+
* Get info about registered providers.
|
|
950
|
+
*/
|
|
951
|
+
getInfo() {
|
|
952
|
+
return {
|
|
953
|
+
providers: Array.from(this.providers.keys()),
|
|
954
|
+
defaultProvider: this.defaultProviderName
|
|
955
|
+
};
|
|
956
|
+
}
|
|
957
|
+
};
|
|
958
|
+
var StateSignatureManager = class {
|
|
959
|
+
secret;
|
|
960
|
+
previousSecrets = [];
|
|
961
|
+
rotationTimer;
|
|
962
|
+
usedNonces = /* @__PURE__ */ new Set();
|
|
963
|
+
nonceCleanupTimer;
|
|
964
|
+
stateBackups = /* @__PURE__ */ new Map();
|
|
965
|
+
config;
|
|
966
|
+
constructor(config = {}) {
|
|
967
|
+
const defaultSecret = typeof process !== "undefined" ? process.env?.LIVE_STATE_SECRET : void 0;
|
|
968
|
+
this.config = {
|
|
969
|
+
secret: config.secret ?? defaultSecret ?? "",
|
|
970
|
+
rotationEnabled: config.rotationEnabled ?? false,
|
|
971
|
+
rotationInterval: config.rotationInterval ?? 24 * 60 * 60 * 1e3,
|
|
972
|
+
compressionEnabled: config.compressionEnabled ?? true,
|
|
973
|
+
encryptionEnabled: config.encryptionEnabled ?? false,
|
|
974
|
+
nonceEnabled: config.nonceEnabled ?? false,
|
|
975
|
+
maxStateAge: config.maxStateAge ?? 7 * 24 * 60 * 60 * 1e3,
|
|
976
|
+
backupEnabled: config.backupEnabled ?? true,
|
|
977
|
+
maxBackups: config.maxBackups ?? 3
|
|
978
|
+
};
|
|
979
|
+
if (!this.config.secret) {
|
|
980
|
+
this.config.secret = randomBytes(32).toString("hex");
|
|
981
|
+
liveWarn("state", null, "No LIVE_STATE_SECRET provided. Using random key (state will not persist across restarts).");
|
|
982
|
+
}
|
|
983
|
+
this.secret = Buffer.from(this.config.secret, "utf-8");
|
|
984
|
+
if (this.config.rotationEnabled) {
|
|
985
|
+
this.setupKeyRotation();
|
|
986
|
+
}
|
|
987
|
+
if (this.config.nonceEnabled) {
|
|
988
|
+
this.nonceCleanupTimer = setInterval(() => this.cleanupNonces(), 60 * 60 * 1e3);
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
async signState(componentId, state, version, options) {
|
|
992
|
+
let dataStr = JSON.stringify(state);
|
|
993
|
+
let compressed = false;
|
|
994
|
+
let encrypted = false;
|
|
995
|
+
if ((options?.compress ?? this.config.compressionEnabled) && dataStr.length > 1024) {
|
|
996
|
+
const compressedBuf = gzipSync(Buffer.from(dataStr, "utf-8"));
|
|
997
|
+
const compressedB64 = compressedBuf.toString("base64");
|
|
998
|
+
if (compressedB64.length < dataStr.length * 0.9) {
|
|
999
|
+
dataStr = compressedB64;
|
|
1000
|
+
compressed = true;
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
if (this.config.encryptionEnabled) {
|
|
1004
|
+
const iv = randomBytes(16);
|
|
1005
|
+
const key = this.deriveEncryptionKey();
|
|
1006
|
+
const cipher = createCipheriv("aes-256-cbc", key, iv);
|
|
1007
|
+
let encryptedData = cipher.update(dataStr, "utf-8", "base64");
|
|
1008
|
+
encryptedData += cipher.final("base64");
|
|
1009
|
+
dataStr = iv.toString("base64") + ":" + encryptedData;
|
|
1010
|
+
encrypted = true;
|
|
1011
|
+
}
|
|
1012
|
+
const nonce = this.config.nonceEnabled ? randomBytes(16).toString("hex") : void 0;
|
|
1013
|
+
const signedState = {
|
|
1014
|
+
data: dataStr,
|
|
1015
|
+
signature: "",
|
|
1016
|
+
timestamp: Date.now(),
|
|
1017
|
+
version,
|
|
1018
|
+
componentId,
|
|
1019
|
+
nonce,
|
|
1020
|
+
compressed,
|
|
1021
|
+
encrypted
|
|
1022
|
+
};
|
|
1023
|
+
signedState.signature = this.computeSignature(signedState);
|
|
1024
|
+
if (options?.backup ?? this.config.backupEnabled) {
|
|
1025
|
+
this.backupState(componentId, signedState);
|
|
1026
|
+
}
|
|
1027
|
+
return signedState;
|
|
1028
|
+
}
|
|
1029
|
+
async validateState(signedState) {
|
|
1030
|
+
try {
|
|
1031
|
+
const age = Date.now() - signedState.timestamp;
|
|
1032
|
+
if (age > this.config.maxStateAge) {
|
|
1033
|
+
return { valid: false, error: "State expired" };
|
|
1034
|
+
}
|
|
1035
|
+
if (signedState.nonce && this.config.nonceEnabled) {
|
|
1036
|
+
if (this.usedNonces.has(signedState.nonce)) {
|
|
1037
|
+
return { valid: false, error: "Nonce already used (replay attempt)" };
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
const expectedSig = this.computeSignature(signedState);
|
|
1041
|
+
if (this.timingSafeEqual(signedState.signature, expectedSig)) {
|
|
1042
|
+
if (signedState.nonce) this.usedNonces.add(signedState.nonce);
|
|
1043
|
+
return { valid: true };
|
|
1044
|
+
}
|
|
1045
|
+
for (const prevSecret of this.previousSecrets) {
|
|
1046
|
+
const prevSig = this.computeSignatureWithKey(signedState, prevSecret);
|
|
1047
|
+
if (this.timingSafeEqual(signedState.signature, prevSig)) {
|
|
1048
|
+
if (signedState.nonce) this.usedNonces.add(signedState.nonce);
|
|
1049
|
+
return { valid: true };
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
return { valid: false, error: "Invalid signature" };
|
|
1053
|
+
} catch (error) {
|
|
1054
|
+
return { valid: false, error: error.message };
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
async extractData(signedState) {
|
|
1058
|
+
let dataStr = signedState.data;
|
|
1059
|
+
if (signedState.encrypted) {
|
|
1060
|
+
const [ivB64, encryptedData] = dataStr.split(":");
|
|
1061
|
+
const iv = Buffer.from(ivB64, "base64");
|
|
1062
|
+
const key = this.deriveEncryptionKey();
|
|
1063
|
+
const decipher = createDecipheriv("aes-256-cbc", key, iv);
|
|
1064
|
+
dataStr = decipher.update(encryptedData, "base64", "utf-8");
|
|
1065
|
+
dataStr += decipher.final("utf-8");
|
|
1066
|
+
}
|
|
1067
|
+
if (signedState.compressed) {
|
|
1068
|
+
const decompressed = gunzipSync(Buffer.from(dataStr, "base64"));
|
|
1069
|
+
dataStr = decompressed.toString("utf-8");
|
|
1070
|
+
}
|
|
1071
|
+
return JSON.parse(dataStr);
|
|
1072
|
+
}
|
|
1073
|
+
getBackups(componentId) {
|
|
1074
|
+
return (this.stateBackups.get(componentId) || []).map((b) => b.signedState);
|
|
1075
|
+
}
|
|
1076
|
+
getLatestBackup(componentId) {
|
|
1077
|
+
const backups = this.stateBackups.get(componentId);
|
|
1078
|
+
if (!backups || backups.length === 0) return null;
|
|
1079
|
+
return backups[backups.length - 1].signedState;
|
|
1080
|
+
}
|
|
1081
|
+
backupState(componentId, signedState) {
|
|
1082
|
+
if (!this.stateBackups.has(componentId)) {
|
|
1083
|
+
this.stateBackups.set(componentId, []);
|
|
1084
|
+
}
|
|
1085
|
+
const backups = this.stateBackups.get(componentId);
|
|
1086
|
+
backups.push({ signedState, backedUpAt: Date.now() });
|
|
1087
|
+
while (backups.length > this.config.maxBackups) {
|
|
1088
|
+
backups.shift();
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
computeSignature(signedState) {
|
|
1092
|
+
return this.computeSignatureWithKey(signedState, this.secret);
|
|
1093
|
+
}
|
|
1094
|
+
computeSignatureWithKey(signedState, key) {
|
|
1095
|
+
const payload = `${signedState.componentId}:${signedState.version}:${signedState.timestamp}:${signedState.data}${signedState.nonce ? ":" + signedState.nonce : ""}`;
|
|
1096
|
+
return createHmac("sha256", key).update(payload).digest("hex");
|
|
1097
|
+
}
|
|
1098
|
+
timingSafeEqual(a, b) {
|
|
1099
|
+
if (a.length !== b.length) return false;
|
|
1100
|
+
const bufA = Buffer.from(a);
|
|
1101
|
+
const bufB = Buffer.from(b);
|
|
1102
|
+
try {
|
|
1103
|
+
const { timingSafeEqual: tse } = __require("crypto");
|
|
1104
|
+
return tse(bufA, bufB);
|
|
1105
|
+
} catch {
|
|
1106
|
+
return a === b;
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
deriveEncryptionKey() {
|
|
1110
|
+
return createHmac("sha256", this.secret).update("encryption-key-derivation").digest();
|
|
1111
|
+
}
|
|
1112
|
+
setupKeyRotation() {
|
|
1113
|
+
this.rotationTimer = setInterval(() => {
|
|
1114
|
+
this.previousSecrets.unshift(this.secret);
|
|
1115
|
+
if (this.previousSecrets.length > 3) {
|
|
1116
|
+
this.previousSecrets.pop();
|
|
1117
|
+
}
|
|
1118
|
+
this.secret = randomBytes(32);
|
|
1119
|
+
liveLog("state", null, "Key rotation completed");
|
|
1120
|
+
}, this.config.rotationInterval);
|
|
1121
|
+
}
|
|
1122
|
+
cleanupNonces() {
|
|
1123
|
+
if (this.usedNonces.size > 1e5) {
|
|
1124
|
+
this.usedNonces.clear();
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
shutdown() {
|
|
1128
|
+
if (this.rotationTimer) clearInterval(this.rotationTimer);
|
|
1129
|
+
if (this.nonceCleanupTimer) clearInterval(this.nonceCleanupTimer);
|
|
1130
|
+
}
|
|
1131
|
+
};
|
|
1132
|
+
var PerformanceMonitor = class extends EventEmitter {
|
|
1133
|
+
components = /* @__PURE__ */ new Map();
|
|
1134
|
+
alerts = [];
|
|
1135
|
+
config;
|
|
1136
|
+
constructor(config = {}) {
|
|
1137
|
+
super();
|
|
1138
|
+
this.config = {
|
|
1139
|
+
slowActionThreshold: config.slowActionThreshold ?? 1e3,
|
|
1140
|
+
slowRenderThreshold: config.slowRenderThreshold ?? 500,
|
|
1141
|
+
highErrorRateThreshold: config.highErrorRateThreshold ?? 10,
|
|
1142
|
+
alertsEnabled: config.alertsEnabled ?? true
|
|
1143
|
+
};
|
|
1144
|
+
}
|
|
1145
|
+
initializeComponent(componentId, componentName) {
|
|
1146
|
+
this.components.set(componentId, {
|
|
1147
|
+
componentId,
|
|
1148
|
+
componentName,
|
|
1149
|
+
mountTime: Date.now(),
|
|
1150
|
+
actionTimes: /* @__PURE__ */ new Map(),
|
|
1151
|
+
renderTimes: [],
|
|
1152
|
+
stateChanges: 0,
|
|
1153
|
+
errorCount: 0,
|
|
1154
|
+
lastActivity: Date.now()
|
|
1155
|
+
});
|
|
1156
|
+
}
|
|
1157
|
+
recordRenderTime(componentId, time) {
|
|
1158
|
+
const metrics = this.components.get(componentId);
|
|
1159
|
+
if (!metrics) return;
|
|
1160
|
+
metrics.renderTimes.push(time);
|
|
1161
|
+
metrics.lastActivity = Date.now();
|
|
1162
|
+
if (this.config.alertsEnabled && time > this.config.slowRenderThreshold) {
|
|
1163
|
+
this.addAlert({
|
|
1164
|
+
type: "slow_render",
|
|
1165
|
+
componentId,
|
|
1166
|
+
componentName: metrics.componentName,
|
|
1167
|
+
message: `Slow render: ${time}ms (threshold: ${this.config.slowRenderThreshold}ms)`,
|
|
1168
|
+
value: time,
|
|
1169
|
+
threshold: this.config.slowRenderThreshold,
|
|
1170
|
+
timestamp: Date.now()
|
|
1171
|
+
});
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
recordActionTime(componentId, action, time, error) {
|
|
1175
|
+
const metrics = this.components.get(componentId);
|
|
1176
|
+
if (!metrics) return;
|
|
1177
|
+
if (!metrics.actionTimes.has(action)) {
|
|
1178
|
+
metrics.actionTimes.set(action, []);
|
|
1179
|
+
}
|
|
1180
|
+
metrics.actionTimes.get(action).push(time);
|
|
1181
|
+
metrics.lastActivity = Date.now();
|
|
1182
|
+
if (error) {
|
|
1183
|
+
metrics.errorCount++;
|
|
1184
|
+
if (this.config.alertsEnabled && metrics.errorCount >= this.config.highErrorRateThreshold) {
|
|
1185
|
+
this.addAlert({
|
|
1186
|
+
type: "high_error_rate",
|
|
1187
|
+
componentId,
|
|
1188
|
+
componentName: metrics.componentName,
|
|
1189
|
+
message: `High error rate: ${metrics.errorCount} errors`,
|
|
1190
|
+
value: metrics.errorCount,
|
|
1191
|
+
threshold: this.config.highErrorRateThreshold,
|
|
1192
|
+
timestamp: Date.now()
|
|
1193
|
+
});
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1196
|
+
if (this.config.alertsEnabled && time > this.config.slowActionThreshold) {
|
|
1197
|
+
this.addAlert({
|
|
1198
|
+
type: "slow_action",
|
|
1199
|
+
componentId,
|
|
1200
|
+
componentName: metrics.componentName,
|
|
1201
|
+
message: `Slow action '${action}': ${time}ms (threshold: ${this.config.slowActionThreshold}ms)`,
|
|
1202
|
+
value: time,
|
|
1203
|
+
threshold: this.config.slowActionThreshold,
|
|
1204
|
+
timestamp: Date.now()
|
|
1205
|
+
});
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1208
|
+
recordStateChange(componentId) {
|
|
1209
|
+
const metrics = this.components.get(componentId);
|
|
1210
|
+
if (metrics) {
|
|
1211
|
+
metrics.stateChanges++;
|
|
1212
|
+
metrics.lastActivity = Date.now();
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
1215
|
+
removeComponent(componentId) {
|
|
1216
|
+
this.components.delete(componentId);
|
|
1217
|
+
}
|
|
1218
|
+
getComponentMetrics(componentId) {
|
|
1219
|
+
return this.components.get(componentId) ?? null;
|
|
1220
|
+
}
|
|
1221
|
+
getAllMetrics() {
|
|
1222
|
+
return Array.from(this.components.values());
|
|
1223
|
+
}
|
|
1224
|
+
getAlerts(limit = 50) {
|
|
1225
|
+
return this.alerts.slice(-limit);
|
|
1226
|
+
}
|
|
1227
|
+
clearAlerts() {
|
|
1228
|
+
this.alerts = [];
|
|
1229
|
+
}
|
|
1230
|
+
getStats() {
|
|
1231
|
+
return {
|
|
1232
|
+
totalComponents: this.components.size,
|
|
1233
|
+
totalAlerts: this.alerts.length,
|
|
1234
|
+
components: Array.from(this.components.entries()).map(([id, m]) => ({
|
|
1235
|
+
id,
|
|
1236
|
+
name: m.componentName,
|
|
1237
|
+
renderCount: m.renderTimes.length,
|
|
1238
|
+
avgRenderTime: m.renderTimes.length > 0 ? m.renderTimes.reduce((a, b) => a + b, 0) / m.renderTimes.length : 0,
|
|
1239
|
+
actionCount: Array.from(m.actionTimes.values()).reduce((sum, times) => sum + times.length, 0),
|
|
1240
|
+
stateChanges: m.stateChanges,
|
|
1241
|
+
errorCount: m.errorCount
|
|
1242
|
+
}))
|
|
1243
|
+
};
|
|
1244
|
+
}
|
|
1245
|
+
addAlert(alert) {
|
|
1246
|
+
this.alerts.push(alert);
|
|
1247
|
+
if (this.alerts.length > 1e3) {
|
|
1248
|
+
this.alerts = this.alerts.slice(-500);
|
|
1249
|
+
}
|
|
1250
|
+
liveWarn("performance", alert.componentId, alert.message);
|
|
1251
|
+
this.emit("alert", alert);
|
|
1252
|
+
}
|
|
1253
|
+
};
|
|
1254
|
+
|
|
1255
|
+
// src/upload/FileUploadManager.ts
|
|
1256
|
+
var MAGIC_BYTES = {
|
|
1257
|
+
"image/jpeg": [{ bytes: [255, 216, 255] }],
|
|
1258
|
+
"image/png": [{ bytes: [137, 80, 78, 71, 13, 10, 26, 10] }],
|
|
1259
|
+
"image/gif": [
|
|
1260
|
+
{ bytes: [71, 73, 70, 56, 55, 97] },
|
|
1261
|
+
{ bytes: [71, 73, 70, 56, 57, 97] }
|
|
1262
|
+
],
|
|
1263
|
+
"image/webp": [{ bytes: [82, 73, 70, 70], offset: 0 }],
|
|
1264
|
+
"application/pdf": [{ bytes: [37, 80, 68, 70] }],
|
|
1265
|
+
"application/zip": [
|
|
1266
|
+
{ bytes: [80, 75, 3, 4] },
|
|
1267
|
+
{ bytes: [80, 75, 5, 6] }
|
|
1268
|
+
],
|
|
1269
|
+
"application/gzip": [{ bytes: [31, 139] }]
|
|
1270
|
+
};
|
|
1271
|
+
var FileUploadManager = class {
|
|
1272
|
+
activeUploads = /* @__PURE__ */ new Map();
|
|
1273
|
+
maxUploadSize;
|
|
1274
|
+
chunkTimeout;
|
|
1275
|
+
userUploadBytes = /* @__PURE__ */ new Map();
|
|
1276
|
+
maxBytesPerUser;
|
|
1277
|
+
quotaResetInterval;
|
|
1278
|
+
allowedTypes;
|
|
1279
|
+
blockedExtensions;
|
|
1280
|
+
uploadsDir;
|
|
1281
|
+
customAssembleFile;
|
|
1282
|
+
cleanupTimer;
|
|
1283
|
+
quotaTimer;
|
|
1284
|
+
constructor(config = {}) {
|
|
1285
|
+
this.maxUploadSize = config.maxUploadSize ?? 50 * 1024 * 1024;
|
|
1286
|
+
this.chunkTimeout = config.chunkTimeout ?? 3e4;
|
|
1287
|
+
this.maxBytesPerUser = config.maxBytesPerUser ?? 500 * 1024 * 1024;
|
|
1288
|
+
this.quotaResetInterval = config.quotaResetInterval ?? 24 * 60 * 60 * 1e3;
|
|
1289
|
+
this.allowedTypes = config.allowedTypes ?? [
|
|
1290
|
+
"image/jpeg",
|
|
1291
|
+
"image/png",
|
|
1292
|
+
"image/gif",
|
|
1293
|
+
"image/webp",
|
|
1294
|
+
"image/svg+xml",
|
|
1295
|
+
"application/pdf",
|
|
1296
|
+
"text/plain",
|
|
1297
|
+
"text/csv",
|
|
1298
|
+
"text/markdown",
|
|
1299
|
+
"application/json",
|
|
1300
|
+
"application/zip",
|
|
1301
|
+
"application/gzip"
|
|
1302
|
+
];
|
|
1303
|
+
this.blockedExtensions = new Set(config.blockedExtensions ?? [
|
|
1304
|
+
".exe",
|
|
1305
|
+
".bat",
|
|
1306
|
+
".cmd",
|
|
1307
|
+
".com",
|
|
1308
|
+
".msi",
|
|
1309
|
+
".scr",
|
|
1310
|
+
".pif",
|
|
1311
|
+
".sh",
|
|
1312
|
+
".bash",
|
|
1313
|
+
".zsh",
|
|
1314
|
+
".csh",
|
|
1315
|
+
".ps1",
|
|
1316
|
+
".psm1",
|
|
1317
|
+
".psd1",
|
|
1318
|
+
".vbs",
|
|
1319
|
+
".vbe",
|
|
1320
|
+
".js",
|
|
1321
|
+
".jse",
|
|
1322
|
+
".wsf",
|
|
1323
|
+
".wsh",
|
|
1324
|
+
".dll",
|
|
1325
|
+
".sys",
|
|
1326
|
+
".drv",
|
|
1327
|
+
".so",
|
|
1328
|
+
".dylib"
|
|
1329
|
+
]);
|
|
1330
|
+
this.uploadsDir = config.uploadsDir ?? "./uploads";
|
|
1331
|
+
this.customAssembleFile = config.assembleFile;
|
|
1332
|
+
this.cleanupTimer = setInterval(() => this.cleanupStaleUploads(), 5 * 60 * 1e3);
|
|
1333
|
+
this.quotaTimer = setInterval(() => this.resetUploadQuotas(), this.quotaResetInterval);
|
|
1334
|
+
}
|
|
1335
|
+
async startUpload(message, userId) {
|
|
1336
|
+
try {
|
|
1337
|
+
const { uploadId, componentId, filename, fileType, fileSize, chunkSize = 64 * 1024 } = message;
|
|
1338
|
+
if (fileSize > this.maxUploadSize) {
|
|
1339
|
+
throw new Error(`File too large: ${fileSize} bytes. Max: ${this.maxUploadSize} bytes`);
|
|
1340
|
+
}
|
|
1341
|
+
if (userId) {
|
|
1342
|
+
const currentUsage = this.userUploadBytes.get(userId) || 0;
|
|
1343
|
+
if (currentUsage + fileSize > this.maxBytesPerUser) {
|
|
1344
|
+
throw new Error(`Upload quota exceeded for user.`);
|
|
1345
|
+
}
|
|
1346
|
+
}
|
|
1347
|
+
if (this.allowedTypes.length > 0 && !this.allowedTypes.includes(fileType)) {
|
|
1348
|
+
throw new Error(`File type not allowed: ${fileType}`);
|
|
1349
|
+
}
|
|
1350
|
+
const pathModule = await import('path');
|
|
1351
|
+
const safeBase = pathModule.basename(filename);
|
|
1352
|
+
const ext = pathModule.extname(safeBase).toLowerCase();
|
|
1353
|
+
if (this.blockedExtensions.has(ext)) {
|
|
1354
|
+
throw new Error(`File extension not allowed: ${ext}`);
|
|
1355
|
+
}
|
|
1356
|
+
const parts = safeBase.split(".");
|
|
1357
|
+
if (parts.length > 2) {
|
|
1358
|
+
for (let i = 1; i < parts.length - 1; i++) {
|
|
1359
|
+
const intermediateExt = "." + parts[i].toLowerCase();
|
|
1360
|
+
if (this.blockedExtensions.has(intermediateExt)) {
|
|
1361
|
+
throw new Error(`Suspicious double extension detected: ${intermediateExt} in ${safeBase}`);
|
|
1362
|
+
}
|
|
1363
|
+
}
|
|
1364
|
+
}
|
|
1365
|
+
if (safeBase.length > 255) {
|
|
1366
|
+
throw new Error("Filename too long");
|
|
1367
|
+
}
|
|
1368
|
+
if (this.activeUploads.has(uploadId)) {
|
|
1369
|
+
throw new Error(`Upload ${uploadId} already in progress`);
|
|
1370
|
+
}
|
|
1371
|
+
const totalChunks = Math.ceil(fileSize / chunkSize);
|
|
1372
|
+
const upload = {
|
|
1373
|
+
uploadId,
|
|
1374
|
+
componentId,
|
|
1375
|
+
filename,
|
|
1376
|
+
fileType,
|
|
1377
|
+
fileSize,
|
|
1378
|
+
totalChunks,
|
|
1379
|
+
receivedChunks: /* @__PURE__ */ new Map(),
|
|
1380
|
+
bytesReceived: 0,
|
|
1381
|
+
startTime: Date.now(),
|
|
1382
|
+
lastChunkTime: Date.now()
|
|
1383
|
+
};
|
|
1384
|
+
this.activeUploads.set(uploadId, upload);
|
|
1385
|
+
if (userId) {
|
|
1386
|
+
const currentUsage = this.userUploadBytes.get(userId) || 0;
|
|
1387
|
+
this.userUploadBytes.set(userId, currentUsage + fileSize);
|
|
1388
|
+
}
|
|
1389
|
+
liveLog("messages", componentId, `Upload started: ${uploadId} (${filename}, ${fileSize} bytes)`);
|
|
1390
|
+
return { success: true };
|
|
1391
|
+
} catch (error) {
|
|
1392
|
+
return { success: false, error: error.message };
|
|
1393
|
+
}
|
|
1394
|
+
}
|
|
1395
|
+
async receiveChunk(message, binaryData = null) {
|
|
1396
|
+
const { uploadId, chunkIndex, totalChunks, data } = message;
|
|
1397
|
+
const upload = this.activeUploads.get(uploadId);
|
|
1398
|
+
if (!upload) throw new Error(`Upload ${uploadId} not found`);
|
|
1399
|
+
if (chunkIndex < 0 || chunkIndex >= totalChunks) {
|
|
1400
|
+
throw new Error(`Invalid chunk index: ${chunkIndex}`);
|
|
1401
|
+
}
|
|
1402
|
+
if (!upload.receivedChunks.has(chunkIndex)) {
|
|
1403
|
+
let chunkBytes;
|
|
1404
|
+
if (binaryData) {
|
|
1405
|
+
upload.receivedChunks.set(chunkIndex, binaryData);
|
|
1406
|
+
chunkBytes = binaryData.length;
|
|
1407
|
+
} else {
|
|
1408
|
+
upload.receivedChunks.set(chunkIndex, data);
|
|
1409
|
+
chunkBytes = Buffer.from(data, "base64").length;
|
|
1410
|
+
}
|
|
1411
|
+
upload.lastChunkTime = Date.now();
|
|
1412
|
+
upload.bytesReceived += chunkBytes;
|
|
1413
|
+
}
|
|
1414
|
+
const progress = upload.bytesReceived / upload.fileSize * 100;
|
|
1415
|
+
return {
|
|
1416
|
+
type: "FILE_UPLOAD_PROGRESS",
|
|
1417
|
+
componentId: upload.componentId,
|
|
1418
|
+
uploadId: upload.uploadId,
|
|
1419
|
+
chunkIndex,
|
|
1420
|
+
totalChunks,
|
|
1421
|
+
bytesUploaded: Math.min(upload.bytesReceived, upload.fileSize),
|
|
1422
|
+
totalBytes: upload.fileSize,
|
|
1423
|
+
progress: Math.min(progress, 100),
|
|
1424
|
+
timestamp: Date.now()
|
|
1425
|
+
};
|
|
1426
|
+
}
|
|
1427
|
+
async completeUpload(message) {
|
|
1428
|
+
try {
|
|
1429
|
+
const { uploadId } = message;
|
|
1430
|
+
const upload = this.activeUploads.get(uploadId);
|
|
1431
|
+
if (!upload) throw new Error(`Upload ${uploadId} not found`);
|
|
1432
|
+
if (upload.bytesReceived !== upload.fileSize) {
|
|
1433
|
+
throw new Error(`Incomplete upload: received ${upload.bytesReceived}/${upload.fileSize} bytes`);
|
|
1434
|
+
}
|
|
1435
|
+
this.validateContentMagicBytes(upload);
|
|
1436
|
+
const fileUrl = this.customAssembleFile ? await this.customAssembleFile(upload) : await this.defaultAssembleFile(upload);
|
|
1437
|
+
this.activeUploads.delete(uploadId);
|
|
1438
|
+
return {
|
|
1439
|
+
type: "FILE_UPLOAD_COMPLETE",
|
|
1440
|
+
componentId: upload.componentId,
|
|
1441
|
+
uploadId: upload.uploadId,
|
|
1442
|
+
success: true,
|
|
1443
|
+
filename: upload.filename,
|
|
1444
|
+
fileUrl,
|
|
1445
|
+
timestamp: Date.now()
|
|
1446
|
+
};
|
|
1447
|
+
} catch (error) {
|
|
1448
|
+
return {
|
|
1449
|
+
type: "FILE_UPLOAD_COMPLETE",
|
|
1450
|
+
componentId: "",
|
|
1451
|
+
uploadId: message.uploadId,
|
|
1452
|
+
success: false,
|
|
1453
|
+
error: error.message,
|
|
1454
|
+
timestamp: Date.now()
|
|
1455
|
+
};
|
|
1456
|
+
}
|
|
1457
|
+
}
|
|
1458
|
+
async defaultAssembleFile(upload) {
|
|
1459
|
+
const { writeFile, mkdir } = await import('fs/promises');
|
|
1460
|
+
const { existsSync } = await import('fs');
|
|
1461
|
+
const { join, extname, basename } = await import('path');
|
|
1462
|
+
if (!existsSync(this.uploadsDir)) {
|
|
1463
|
+
await mkdir(this.uploadsDir, { recursive: true });
|
|
1464
|
+
}
|
|
1465
|
+
const extension = extname(basename(upload.filename)).toLowerCase();
|
|
1466
|
+
const safeFilename = `${crypto.randomUUID()}${extension}`;
|
|
1467
|
+
const filePath = join(this.uploadsDir, safeFilename);
|
|
1468
|
+
const chunks = [];
|
|
1469
|
+
for (let i = 0; i < upload.totalChunks; i++) {
|
|
1470
|
+
const chunkData = upload.receivedChunks.get(i);
|
|
1471
|
+
if (chunkData) {
|
|
1472
|
+
if (Buffer.isBuffer(chunkData)) {
|
|
1473
|
+
chunks.push(chunkData);
|
|
1474
|
+
} else {
|
|
1475
|
+
chunks.push(Buffer.from(chunkData, "base64"));
|
|
1476
|
+
}
|
|
1477
|
+
}
|
|
1478
|
+
}
|
|
1479
|
+
await writeFile(filePath, Buffer.concat(chunks));
|
|
1480
|
+
return `/uploads/${safeFilename}`;
|
|
1481
|
+
}
|
|
1482
|
+
validateContentMagicBytes(upload) {
|
|
1483
|
+
const expectedSignatures = MAGIC_BYTES[upload.fileType];
|
|
1484
|
+
if (!expectedSignatures) return;
|
|
1485
|
+
const firstChunk = upload.receivedChunks.get(0);
|
|
1486
|
+
if (!firstChunk) throw new Error("Cannot validate file content: first chunk missing");
|
|
1487
|
+
const headerBuffer = Buffer.isBuffer(firstChunk) ? firstChunk : Buffer.from(firstChunk, "base64");
|
|
1488
|
+
let matched = false;
|
|
1489
|
+
for (const sig of expectedSignatures) {
|
|
1490
|
+
const offset = sig.offset ?? 0;
|
|
1491
|
+
if (headerBuffer.length < offset + sig.bytes.length) continue;
|
|
1492
|
+
let sigMatches = true;
|
|
1493
|
+
for (let i = 0; i < sig.bytes.length; i++) {
|
|
1494
|
+
if (headerBuffer[offset + i] !== sig.bytes[i]) {
|
|
1495
|
+
sigMatches = false;
|
|
1496
|
+
break;
|
|
1497
|
+
}
|
|
1498
|
+
}
|
|
1499
|
+
if (sigMatches) {
|
|
1500
|
+
matched = true;
|
|
1501
|
+
break;
|
|
1502
|
+
}
|
|
1503
|
+
}
|
|
1504
|
+
if (!matched) {
|
|
1505
|
+
throw new Error(
|
|
1506
|
+
`File content does not match claimed type '${upload.fileType}'. The file may be disguised as a different format.`
|
|
1507
|
+
);
|
|
1508
|
+
}
|
|
1509
|
+
}
|
|
1510
|
+
cleanupStaleUploads() {
|
|
1511
|
+
const now = Date.now();
|
|
1512
|
+
for (const [uploadId, upload] of this.activeUploads) {
|
|
1513
|
+
if (now - upload.lastChunkTime > this.chunkTimeout * 2) {
|
|
1514
|
+
this.activeUploads.delete(uploadId);
|
|
1515
|
+
liveLog("messages", null, `Cleaned up stale upload: ${uploadId}`);
|
|
1516
|
+
}
|
|
1517
|
+
}
|
|
1518
|
+
}
|
|
1519
|
+
resetUploadQuotas() {
|
|
1520
|
+
this.userUploadBytes.clear();
|
|
1521
|
+
}
|
|
1522
|
+
getUserUploadUsage(userId) {
|
|
1523
|
+
const used = this.userUploadBytes.get(userId) || 0;
|
|
1524
|
+
return { used, limit: this.maxBytesPerUser, remaining: Math.max(0, this.maxBytesPerUser - used) };
|
|
1525
|
+
}
|
|
1526
|
+
getUploadStatus(uploadId) {
|
|
1527
|
+
return this.activeUploads.get(uploadId) || null;
|
|
1528
|
+
}
|
|
1529
|
+
getStats() {
|
|
1530
|
+
return {
|
|
1531
|
+
activeUploads: this.activeUploads.size,
|
|
1532
|
+
maxUploadSize: this.maxUploadSize,
|
|
1533
|
+
allowedTypes: this.allowedTypes
|
|
1534
|
+
};
|
|
1535
|
+
}
|
|
1536
|
+
shutdown() {
|
|
1537
|
+
if (this.cleanupTimer) clearInterval(this.cleanupTimer);
|
|
1538
|
+
if (this.quotaTimer) clearInterval(this.quotaTimer);
|
|
1539
|
+
}
|
|
1540
|
+
};
|
|
1541
|
+
var WebSocketConnectionManager = class extends EventEmitter {
|
|
1542
|
+
connections = /* @__PURE__ */ new Map();
|
|
1543
|
+
connectionMetrics = /* @__PURE__ */ new Map();
|
|
1544
|
+
connectionPools = /* @__PURE__ */ new Map();
|
|
1545
|
+
messageQueues = /* @__PURE__ */ new Map();
|
|
1546
|
+
healthCheckTimer;
|
|
1547
|
+
heartbeatTimer;
|
|
1548
|
+
config;
|
|
1549
|
+
loadBalancerIndex = 0;
|
|
1550
|
+
constructor(config) {
|
|
1551
|
+
super();
|
|
1552
|
+
this.config = {
|
|
1553
|
+
maxConnections: 1e4,
|
|
1554
|
+
connectionTimeout: 3e4,
|
|
1555
|
+
heartbeatInterval: 3e4,
|
|
1556
|
+
reconnectAttempts: 5,
|
|
1557
|
+
reconnectDelay: 1e3,
|
|
1558
|
+
maxReconnectDelay: 3e4,
|
|
1559
|
+
jitterFactor: 0.1,
|
|
1560
|
+
loadBalancing: "round-robin",
|
|
1561
|
+
healthCheckInterval: 6e4,
|
|
1562
|
+
messageQueueSize: 1e3,
|
|
1563
|
+
offlineQueueEnabled: true,
|
|
1564
|
+
...config
|
|
1565
|
+
};
|
|
1566
|
+
this.setupHealthMonitoring();
|
|
1567
|
+
this.setupHeartbeat();
|
|
1568
|
+
}
|
|
1569
|
+
registerConnection(ws, connectionId, poolId) {
|
|
1570
|
+
if (this.connections.size >= this.config.maxConnections) {
|
|
1571
|
+
throw new Error("Maximum connections exceeded");
|
|
1572
|
+
}
|
|
1573
|
+
const metrics = {
|
|
1574
|
+
id: connectionId,
|
|
1575
|
+
connectedAt: /* @__PURE__ */ new Date(),
|
|
1576
|
+
lastActivity: /* @__PURE__ */ new Date(),
|
|
1577
|
+
messagesSent: 0,
|
|
1578
|
+
messagesReceived: 0,
|
|
1579
|
+
bytesTransferred: 0,
|
|
1580
|
+
latency: 0,
|
|
1581
|
+
status: "connected",
|
|
1582
|
+
errorCount: 0,
|
|
1583
|
+
reconnectCount: 0
|
|
1584
|
+
};
|
|
1585
|
+
this.connections.set(connectionId, ws);
|
|
1586
|
+
this.connectionMetrics.set(connectionId, metrics);
|
|
1587
|
+
if (poolId) {
|
|
1588
|
+
this.addToPool(connectionId, poolId);
|
|
1589
|
+
}
|
|
1590
|
+
this.messageQueues.set(connectionId, []);
|
|
1591
|
+
liveLog("websocket", null, `Connection registered: ${connectionId}`);
|
|
1592
|
+
this.emit("connectionRegistered", { connectionId, poolId });
|
|
1593
|
+
}
|
|
1594
|
+
addToPool(connectionId, poolId) {
|
|
1595
|
+
if (!this.connectionPools.has(poolId)) {
|
|
1596
|
+
this.connectionPools.set(poolId, /* @__PURE__ */ new Set());
|
|
1597
|
+
}
|
|
1598
|
+
this.connectionPools.get(poolId).add(connectionId);
|
|
1599
|
+
}
|
|
1600
|
+
removeFromPool(connectionId, poolId) {
|
|
1601
|
+
const pool = this.connectionPools.get(poolId);
|
|
1602
|
+
if (pool) {
|
|
1603
|
+
pool.delete(connectionId);
|
|
1604
|
+
if (pool.size === 0) this.connectionPools.delete(poolId);
|
|
1605
|
+
}
|
|
1606
|
+
}
|
|
1607
|
+
cleanupConnection(connectionId) {
|
|
1608
|
+
this.connections.delete(connectionId);
|
|
1609
|
+
this.connectionMetrics.delete(connectionId);
|
|
1610
|
+
this.messageQueues.delete(connectionId);
|
|
1611
|
+
for (const [poolId, pool] of this.connectionPools) {
|
|
1612
|
+
if (pool.has(connectionId)) {
|
|
1613
|
+
this.removeFromPool(connectionId, poolId);
|
|
1614
|
+
}
|
|
1615
|
+
}
|
|
1616
|
+
}
|
|
1617
|
+
getConnectionMetrics(connectionId) {
|
|
1618
|
+
return this.connectionMetrics.get(connectionId) || null;
|
|
1619
|
+
}
|
|
1620
|
+
getAllConnectionMetrics() {
|
|
1621
|
+
return Array.from(this.connectionMetrics.values());
|
|
1622
|
+
}
|
|
1623
|
+
getSystemStats() {
|
|
1624
|
+
const totalConnections = this.connections.size;
|
|
1625
|
+
const activeConnections = Array.from(this.connections.values()).filter((ws) => ws.readyState === 1).length;
|
|
1626
|
+
const totalPools = this.connectionPools.size;
|
|
1627
|
+
const totalQueuedMessages = Array.from(this.messageQueues.values()).reduce((sum, queue) => sum + queue.length, 0);
|
|
1628
|
+
return {
|
|
1629
|
+
totalConnections,
|
|
1630
|
+
activeConnections,
|
|
1631
|
+
totalPools,
|
|
1632
|
+
totalQueuedMessages,
|
|
1633
|
+
maxConnections: this.config.maxConnections,
|
|
1634
|
+
connectionUtilization: totalConnections / this.config.maxConnections * 100
|
|
1635
|
+
};
|
|
1636
|
+
}
|
|
1637
|
+
setupHealthMonitoring() {
|
|
1638
|
+
this.healthCheckTimer = setInterval(() => this.performHealthChecks(), this.config.healthCheckInterval);
|
|
1639
|
+
}
|
|
1640
|
+
setupHeartbeat() {
|
|
1641
|
+
this.heartbeatTimer = setInterval(() => {
|
|
1642
|
+
for (const [connectionId, ws] of this.connections) {
|
|
1643
|
+
if (ws.readyState === 1) {
|
|
1644
|
+
try {
|
|
1645
|
+
const wsAny = ws;
|
|
1646
|
+
if (typeof wsAny.ping === "function") {
|
|
1647
|
+
wsAny._pingTime = Date.now();
|
|
1648
|
+
wsAny.ping();
|
|
1649
|
+
}
|
|
1650
|
+
} catch {
|
|
1651
|
+
}
|
|
1652
|
+
}
|
|
1653
|
+
}
|
|
1654
|
+
}, this.config.heartbeatInterval);
|
|
1655
|
+
}
|
|
1656
|
+
async performHealthChecks() {
|
|
1657
|
+
const now = Date.now();
|
|
1658
|
+
const unhealthy = [];
|
|
1659
|
+
for (const [connectionId, metrics] of this.connectionMetrics) {
|
|
1660
|
+
const ws = this.connections.get(connectionId);
|
|
1661
|
+
if (!ws || ws.readyState !== 1) {
|
|
1662
|
+
unhealthy.push(connectionId);
|
|
1663
|
+
continue;
|
|
1664
|
+
}
|
|
1665
|
+
const timeSinceActivity = now - metrics.lastActivity.getTime();
|
|
1666
|
+
if (timeSinceActivity > this.config.heartbeatInterval * 2) {
|
|
1667
|
+
metrics.status = "disconnected";
|
|
1668
|
+
}
|
|
1669
|
+
if (metrics.errorCount > 10) {
|
|
1670
|
+
unhealthy.push(connectionId);
|
|
1671
|
+
}
|
|
1672
|
+
}
|
|
1673
|
+
for (const connectionId of unhealthy) {
|
|
1674
|
+
const ws = this.connections.get(connectionId);
|
|
1675
|
+
if (ws) {
|
|
1676
|
+
try {
|
|
1677
|
+
ws.close();
|
|
1678
|
+
} catch {
|
|
1679
|
+
}
|
|
1680
|
+
}
|
|
1681
|
+
this.cleanupConnection(connectionId);
|
|
1682
|
+
}
|
|
1683
|
+
}
|
|
1684
|
+
shutdown() {
|
|
1685
|
+
if (this.healthCheckTimer) clearInterval(this.healthCheckTimer);
|
|
1686
|
+
if (this.heartbeatTimer) clearInterval(this.heartbeatTimer);
|
|
1687
|
+
for (const [, ws] of this.connections) {
|
|
1688
|
+
try {
|
|
1689
|
+
ws.close();
|
|
1690
|
+
} catch {
|
|
1691
|
+
}
|
|
1692
|
+
}
|
|
1693
|
+
this.connections.clear();
|
|
1694
|
+
this.connectionMetrics.clear();
|
|
1695
|
+
this.connectionPools.clear();
|
|
1696
|
+
this.messageQueues.clear();
|
|
1697
|
+
}
|
|
1698
|
+
};
|
|
1699
|
+
|
|
1700
|
+
// src/component/context.ts
|
|
1701
|
+
var _ctx = null;
|
|
1702
|
+
function setLiveComponentContext(ctx) {
|
|
1703
|
+
_ctx = ctx;
|
|
1704
|
+
}
|
|
1705
|
+
function getLiveComponentContext() {
|
|
1706
|
+
if (!_ctx) throw new Error("@fluxstack/live: LiveServer.start() must be called before using LiveComponents");
|
|
1707
|
+
return _ctx;
|
|
1708
|
+
}
|
|
1709
|
+
|
|
1710
|
+
// src/component/LiveComponent.ts
|
|
1711
|
+
var EMIT_OVERRIDE_KEY = /* @__PURE__ */ Symbol.for("fluxstack:emitOverride");
|
|
1712
|
+
var _liveDebugger = null;
|
|
1713
|
+
function _setLiveDebugger(dbg) {
|
|
1714
|
+
_liveDebugger = dbg;
|
|
1715
|
+
}
|
|
1716
|
+
var LiveComponent = class _LiveComponent {
|
|
1717
|
+
/** Component name for registry lookup - must be defined in subclasses */
|
|
1718
|
+
static componentName;
|
|
1719
|
+
/** Default state - must be defined in subclasses */
|
|
1720
|
+
static defaultState;
|
|
1721
|
+
/**
|
|
1722
|
+
* Per-component logging control. Silent by default.
|
|
1723
|
+
*
|
|
1724
|
+
* @example
|
|
1725
|
+
* static logging = true // all categories
|
|
1726
|
+
* static logging = ['lifecycle', 'messages'] // specific categories
|
|
1727
|
+
*/
|
|
1728
|
+
static logging;
|
|
1729
|
+
/**
|
|
1730
|
+
* Component-level auth configuration.
|
|
1731
|
+
*/
|
|
1732
|
+
static auth;
|
|
1733
|
+
/**
|
|
1734
|
+
* Per-action auth configuration.
|
|
1735
|
+
*/
|
|
1736
|
+
static actionAuth;
|
|
1737
|
+
/**
|
|
1738
|
+
* Data that survives HMR reloads.
|
|
1739
|
+
*/
|
|
1740
|
+
static persistent;
|
|
1741
|
+
/**
|
|
1742
|
+
* When true, only ONE server-side instance exists for this component.
|
|
1743
|
+
* All clients share the same state.
|
|
1744
|
+
*/
|
|
1745
|
+
static singleton;
|
|
1746
|
+
id;
|
|
1747
|
+
_state;
|
|
1748
|
+
state;
|
|
1749
|
+
// Proxy wrapper
|
|
1750
|
+
ws;
|
|
1751
|
+
room;
|
|
1752
|
+
userId;
|
|
1753
|
+
broadcastToRoom = () => {
|
|
1754
|
+
};
|
|
1755
|
+
// Server-only private state (NEVER sent to client)
|
|
1756
|
+
_privateState = {};
|
|
1757
|
+
// Auth context (injected by registry during mount)
|
|
1758
|
+
_authContext = ANONYMOUS_CONTEXT;
|
|
1759
|
+
// Room event subscriptions (cleaned up on destroy)
|
|
1760
|
+
roomEventUnsubscribers = [];
|
|
1761
|
+
joinedRooms = /* @__PURE__ */ new Set();
|
|
1762
|
+
// Room type for typed events (override in subclass)
|
|
1763
|
+
roomType = "default";
|
|
1764
|
+
// Cached room handles
|
|
1765
|
+
roomHandles = /* @__PURE__ */ new Map();
|
|
1766
|
+
// Guard against infinite recursion in onStateChange
|
|
1767
|
+
_inStateChange = false;
|
|
1768
|
+
// Singleton emit override
|
|
1769
|
+
[EMIT_OVERRIDE_KEY] = null;
|
|
1770
|
+
constructor(initialState, ws, options) {
|
|
1771
|
+
this.id = this.generateId();
|
|
1772
|
+
const ctor = this.constructor;
|
|
1773
|
+
this._state = { ...ctor.defaultState, ...initialState };
|
|
1774
|
+
this.state = this.createStateProxy(this._state);
|
|
1775
|
+
this.ws = ws;
|
|
1776
|
+
this.room = options?.room;
|
|
1777
|
+
this.userId = options?.userId;
|
|
1778
|
+
if (this.room) {
|
|
1779
|
+
this.joinedRooms.add(this.room);
|
|
1780
|
+
const ctx = getLiveComponentContext();
|
|
1781
|
+
ctx.roomManager.joinRoom(this.id, this.room, this.ws);
|
|
1782
|
+
}
|
|
1783
|
+
this.createDirectStateAccessors();
|
|
1784
|
+
}
|
|
1785
|
+
// Create getters/setters for each state property directly on `this`
|
|
1786
|
+
createDirectStateAccessors() {
|
|
1787
|
+
const forbidden = /* @__PURE__ */ new Set([
|
|
1788
|
+
...Object.keys(this),
|
|
1789
|
+
...Object.getOwnPropertyNames(Object.getPrototypeOf(this)),
|
|
1790
|
+
"state",
|
|
1791
|
+
"_state",
|
|
1792
|
+
"ws",
|
|
1793
|
+
"id",
|
|
1794
|
+
"room",
|
|
1795
|
+
"userId",
|
|
1796
|
+
"broadcastToRoom",
|
|
1797
|
+
"$private",
|
|
1798
|
+
"_privateState",
|
|
1799
|
+
"$room",
|
|
1800
|
+
"$rooms",
|
|
1801
|
+
"roomType",
|
|
1802
|
+
"roomHandles",
|
|
1803
|
+
"joinedRooms",
|
|
1804
|
+
"roomEventUnsubscribers"
|
|
1805
|
+
]);
|
|
1806
|
+
for (const key of Object.keys(this._state)) {
|
|
1807
|
+
if (!forbidden.has(key)) {
|
|
1808
|
+
Object.defineProperty(this, key, {
|
|
1809
|
+
get: () => this._state[key],
|
|
1810
|
+
set: (value) => {
|
|
1811
|
+
this.state[key] = value;
|
|
1812
|
+
},
|
|
1813
|
+
enumerable: true,
|
|
1814
|
+
configurable: true
|
|
1815
|
+
});
|
|
1816
|
+
}
|
|
1817
|
+
}
|
|
1818
|
+
}
|
|
1819
|
+
// Create a Proxy that auto-emits STATE_DELTA on any mutation
|
|
1820
|
+
createStateProxy(state) {
|
|
1821
|
+
const self = this;
|
|
1822
|
+
return new Proxy(state, {
|
|
1823
|
+
set(target, prop, value) {
|
|
1824
|
+
const oldValue = target[prop];
|
|
1825
|
+
if (oldValue !== value) {
|
|
1826
|
+
target[prop] = value;
|
|
1827
|
+
const changes = { [prop]: value };
|
|
1828
|
+
self.emit("STATE_DELTA", { delta: changes });
|
|
1829
|
+
if (!self._inStateChange) {
|
|
1830
|
+
self._inStateChange = true;
|
|
1831
|
+
try {
|
|
1832
|
+
self.onStateChange(changes);
|
|
1833
|
+
} catch (err) {
|
|
1834
|
+
console.error(`[${self.id}] onStateChange error:`, err?.message || err);
|
|
1835
|
+
} finally {
|
|
1836
|
+
self._inStateChange = false;
|
|
1837
|
+
}
|
|
1838
|
+
}
|
|
1839
|
+
_liveDebugger?.trackStateChange(
|
|
1840
|
+
self.id,
|
|
1841
|
+
changes,
|
|
1842
|
+
target,
|
|
1843
|
+
"proxy"
|
|
1844
|
+
);
|
|
1845
|
+
}
|
|
1846
|
+
return true;
|
|
1847
|
+
},
|
|
1848
|
+
get(target, prop) {
|
|
1849
|
+
return target[prop];
|
|
1850
|
+
}
|
|
1851
|
+
});
|
|
1852
|
+
}
|
|
1853
|
+
// ========================================
|
|
1854
|
+
// $private - Server-Only State
|
|
1855
|
+
// ========================================
|
|
1856
|
+
get $private() {
|
|
1857
|
+
return this._privateState;
|
|
1858
|
+
}
|
|
1859
|
+
// ========================================
|
|
1860
|
+
// $room - Unified Room System
|
|
1861
|
+
// ========================================
|
|
1862
|
+
get $room() {
|
|
1863
|
+
const self = this;
|
|
1864
|
+
const ctx = getLiveComponentContext();
|
|
1865
|
+
const createHandle = (roomId) => {
|
|
1866
|
+
if (this.roomHandles.has(roomId)) {
|
|
1867
|
+
return this.roomHandles.get(roomId);
|
|
1868
|
+
}
|
|
1869
|
+
const handle = {
|
|
1870
|
+
get id() {
|
|
1871
|
+
return roomId;
|
|
1872
|
+
},
|
|
1873
|
+
get state() {
|
|
1874
|
+
return ctx.roomManager.getRoomState(roomId);
|
|
1875
|
+
},
|
|
1876
|
+
join: (initialState) => {
|
|
1877
|
+
if (self.joinedRooms.has(roomId)) return;
|
|
1878
|
+
self.joinedRooms.add(roomId);
|
|
1879
|
+
ctx.roomManager.joinRoom(self.id, roomId, self.ws, initialState);
|
|
1880
|
+
try {
|
|
1881
|
+
self.onRoomJoin(roomId);
|
|
1882
|
+
} catch (err) {
|
|
1883
|
+
console.error(`[${self.id}] onRoomJoin error:`, err?.message || err);
|
|
1884
|
+
}
|
|
1885
|
+
},
|
|
1886
|
+
leave: () => {
|
|
1887
|
+
if (!self.joinedRooms.has(roomId)) return;
|
|
1888
|
+
self.joinedRooms.delete(roomId);
|
|
1889
|
+
ctx.roomManager.leaveRoom(self.id, roomId);
|
|
1890
|
+
try {
|
|
1891
|
+
self.onRoomLeave(roomId);
|
|
1892
|
+
} catch (err) {
|
|
1893
|
+
console.error(`[${self.id}] onRoomLeave error:`, err?.message || err);
|
|
1894
|
+
}
|
|
1895
|
+
},
|
|
1896
|
+
emit: (event, data) => {
|
|
1897
|
+
return ctx.roomManager.emitToRoom(roomId, event, data, self.id);
|
|
1898
|
+
},
|
|
1899
|
+
on: (event, handler) => {
|
|
1900
|
+
const unsubscribe = ctx.roomEvents.on(
|
|
1901
|
+
"room",
|
|
1902
|
+
roomId,
|
|
1903
|
+
event,
|
|
1904
|
+
self.id,
|
|
1905
|
+
handler
|
|
1906
|
+
);
|
|
1907
|
+
self.roomEventUnsubscribers.push(unsubscribe);
|
|
1908
|
+
return unsubscribe;
|
|
1909
|
+
},
|
|
1910
|
+
setState: (updates) => {
|
|
1911
|
+
ctx.roomManager.setRoomState(roomId, updates, self.id);
|
|
1912
|
+
}
|
|
1913
|
+
};
|
|
1914
|
+
this.roomHandles.set(roomId, handle);
|
|
1915
|
+
return handle;
|
|
1916
|
+
};
|
|
1917
|
+
const proxyFn = ((roomId) => createHandle(roomId));
|
|
1918
|
+
const defaultHandle = this.room ? createHandle(this.room) : null;
|
|
1919
|
+
Object.defineProperties(proxyFn, {
|
|
1920
|
+
id: { get: () => self.room },
|
|
1921
|
+
state: { get: () => defaultHandle?.state ?? {} },
|
|
1922
|
+
join: {
|
|
1923
|
+
value: (initialState) => {
|
|
1924
|
+
if (!defaultHandle) throw new Error("No default room set");
|
|
1925
|
+
defaultHandle.join(initialState);
|
|
1926
|
+
}
|
|
1927
|
+
},
|
|
1928
|
+
leave: {
|
|
1929
|
+
value: () => {
|
|
1930
|
+
if (!defaultHandle) throw new Error("No default room set");
|
|
1931
|
+
defaultHandle.leave();
|
|
1932
|
+
}
|
|
1933
|
+
},
|
|
1934
|
+
emit: {
|
|
1935
|
+
value: (event, data) => {
|
|
1936
|
+
if (!defaultHandle) throw new Error("No default room set");
|
|
1937
|
+
return defaultHandle.emit(event, data);
|
|
1938
|
+
}
|
|
1939
|
+
},
|
|
1940
|
+
on: {
|
|
1941
|
+
value: (event, handler) => {
|
|
1942
|
+
if (!defaultHandle) throw new Error("No default room set");
|
|
1943
|
+
return defaultHandle.on(event, handler);
|
|
1944
|
+
}
|
|
1945
|
+
},
|
|
1946
|
+
setState: {
|
|
1947
|
+
value: (updates) => {
|
|
1948
|
+
if (!defaultHandle) throw new Error("No default room set");
|
|
1949
|
+
defaultHandle.setState(updates);
|
|
1950
|
+
}
|
|
1951
|
+
}
|
|
1952
|
+
});
|
|
1953
|
+
return proxyFn;
|
|
1954
|
+
}
|
|
1955
|
+
/**
|
|
1956
|
+
* List of room IDs this component is participating in
|
|
1957
|
+
*/
|
|
1958
|
+
get $rooms() {
|
|
1959
|
+
return Array.from(this.joinedRooms);
|
|
1960
|
+
}
|
|
1961
|
+
// ========================================
|
|
1962
|
+
// $auth - Authentication Context
|
|
1963
|
+
// ========================================
|
|
1964
|
+
get $auth() {
|
|
1965
|
+
return this._authContext;
|
|
1966
|
+
}
|
|
1967
|
+
/** @internal */
|
|
1968
|
+
setAuthContext(context) {
|
|
1969
|
+
this._authContext = context;
|
|
1970
|
+
if (context.authenticated && context.user?.id && !this.userId) {
|
|
1971
|
+
this.userId = context.user.id;
|
|
1972
|
+
}
|
|
1973
|
+
}
|
|
1974
|
+
// ========================================
|
|
1975
|
+
// $persistent - HMR-Safe State
|
|
1976
|
+
// ========================================
|
|
1977
|
+
get $persistent() {
|
|
1978
|
+
const ctor = this.constructor;
|
|
1979
|
+
const name = ctor.componentName || ctor.name;
|
|
1980
|
+
const key = `__fluxstack_persistent_${name}`;
|
|
1981
|
+
if (!globalThis[key]) {
|
|
1982
|
+
globalThis[key] = { ...ctor.persistent || {} };
|
|
1983
|
+
}
|
|
1984
|
+
return globalThis[key];
|
|
1985
|
+
}
|
|
1986
|
+
// ========================================
|
|
1987
|
+
// Lifecycle Hooks
|
|
1988
|
+
// ========================================
|
|
1989
|
+
onConnect() {
|
|
1990
|
+
}
|
|
1991
|
+
onMount() {
|
|
1992
|
+
}
|
|
1993
|
+
onDisconnect() {
|
|
1994
|
+
}
|
|
1995
|
+
onDestroy() {
|
|
1996
|
+
}
|
|
1997
|
+
onStateChange(changes) {
|
|
1998
|
+
}
|
|
1999
|
+
onRoomJoin(roomId) {
|
|
2000
|
+
}
|
|
2001
|
+
onRoomLeave(roomId) {
|
|
2002
|
+
}
|
|
2003
|
+
onRehydrate(previousState) {
|
|
2004
|
+
}
|
|
2005
|
+
onAction(action, payload) {
|
|
2006
|
+
}
|
|
2007
|
+
onClientJoin(connectionId, connectionCount) {
|
|
2008
|
+
}
|
|
2009
|
+
onClientLeave(connectionId, connectionCount) {
|
|
2010
|
+
}
|
|
2011
|
+
// ========================================
|
|
2012
|
+
// State Management
|
|
2013
|
+
// ========================================
|
|
2014
|
+
setState(updates) {
|
|
2015
|
+
const newUpdates = typeof updates === "function" ? updates(this._state) : updates;
|
|
2016
|
+
const actualChanges = {};
|
|
2017
|
+
let hasChanges = false;
|
|
2018
|
+
for (const key of Object.keys(newUpdates)) {
|
|
2019
|
+
if (this._state[key] !== newUpdates[key]) {
|
|
2020
|
+
actualChanges[key] = newUpdates[key];
|
|
2021
|
+
hasChanges = true;
|
|
2022
|
+
}
|
|
2023
|
+
}
|
|
2024
|
+
if (!hasChanges) return;
|
|
2025
|
+
Object.assign(this._state, actualChanges);
|
|
2026
|
+
this.emit("STATE_DELTA", { delta: actualChanges });
|
|
2027
|
+
if (!this._inStateChange) {
|
|
2028
|
+
this._inStateChange = true;
|
|
2029
|
+
try {
|
|
2030
|
+
this.onStateChange(actualChanges);
|
|
2031
|
+
} catch (err) {
|
|
2032
|
+
console.error(`[${this.id}] onStateChange error:`, err?.message || err);
|
|
2033
|
+
} finally {
|
|
2034
|
+
this._inStateChange = false;
|
|
2035
|
+
}
|
|
2036
|
+
}
|
|
2037
|
+
_liveDebugger?.trackStateChange(
|
|
2038
|
+
this.id,
|
|
2039
|
+
actualChanges,
|
|
2040
|
+
this._state,
|
|
2041
|
+
"setState"
|
|
2042
|
+
);
|
|
2043
|
+
}
|
|
2044
|
+
async setValue(payload) {
|
|
2045
|
+
const { key, value } = payload;
|
|
2046
|
+
const update = { [key]: value };
|
|
2047
|
+
this.setState(update);
|
|
2048
|
+
return { success: true, key, value };
|
|
2049
|
+
}
|
|
2050
|
+
// ========================================
|
|
2051
|
+
// Action Security
|
|
2052
|
+
// ========================================
|
|
2053
|
+
static publicActions;
|
|
2054
|
+
static BLOCKED_ACTIONS = /* @__PURE__ */ new Set([
|
|
2055
|
+
"constructor",
|
|
2056
|
+
"destroy",
|
|
2057
|
+
"executeAction",
|
|
2058
|
+
"getSerializableState",
|
|
2059
|
+
"onMount",
|
|
2060
|
+
"onDestroy",
|
|
2061
|
+
"onConnect",
|
|
2062
|
+
"onDisconnect",
|
|
2063
|
+
"onStateChange",
|
|
2064
|
+
"onRoomJoin",
|
|
2065
|
+
"onRoomLeave",
|
|
2066
|
+
"onRehydrate",
|
|
2067
|
+
"onAction",
|
|
2068
|
+
"onClientJoin",
|
|
2069
|
+
"onClientLeave",
|
|
2070
|
+
"setState",
|
|
2071
|
+
"emit",
|
|
2072
|
+
"broadcast",
|
|
2073
|
+
"broadcastToRoom",
|
|
2074
|
+
"createStateProxy",
|
|
2075
|
+
"createDirectStateAccessors",
|
|
2076
|
+
"generateId",
|
|
2077
|
+
"setAuthContext",
|
|
2078
|
+
"$auth",
|
|
2079
|
+
"$private",
|
|
2080
|
+
"_privateState",
|
|
2081
|
+
"$persistent",
|
|
2082
|
+
"_inStateChange",
|
|
2083
|
+
"$room",
|
|
2084
|
+
"$rooms",
|
|
2085
|
+
"subscribeToRoom",
|
|
2086
|
+
"unsubscribeFromRoom",
|
|
2087
|
+
"emitRoomEvent",
|
|
2088
|
+
"onRoomEvent",
|
|
2089
|
+
"emitRoomEventWithState"
|
|
2090
|
+
]);
|
|
2091
|
+
async executeAction(action, payload) {
|
|
2092
|
+
const actionStart = Date.now();
|
|
2093
|
+
try {
|
|
2094
|
+
if (_LiveComponent.BLOCKED_ACTIONS.has(action)) {
|
|
2095
|
+
throw new Error(`Action '${action}' is not callable`);
|
|
2096
|
+
}
|
|
2097
|
+
if (action.startsWith("_") || action.startsWith("#")) {
|
|
2098
|
+
throw new Error(`Action '${action}' is not callable`);
|
|
2099
|
+
}
|
|
2100
|
+
const componentClass = this.constructor;
|
|
2101
|
+
const publicActions = componentClass.publicActions;
|
|
2102
|
+
if (!publicActions) {
|
|
2103
|
+
console.warn(`[SECURITY] Component '${componentClass.componentName || componentClass.name}' has no publicActions defined. All remote actions are blocked.`);
|
|
2104
|
+
throw new Error(`Action '${action}' is not callable - component has no publicActions defined`);
|
|
2105
|
+
}
|
|
2106
|
+
if (!publicActions.includes(action)) {
|
|
2107
|
+
const methodExists = typeof this[action] === "function";
|
|
2108
|
+
if (methodExists) {
|
|
2109
|
+
const name = componentClass.componentName || componentClass.name;
|
|
2110
|
+
throw new Error(
|
|
2111
|
+
`Action '${action}' exists on '${name}' but is not listed in publicActions. Add it to: static publicActions = [..., '${action}']`
|
|
2112
|
+
);
|
|
2113
|
+
}
|
|
2114
|
+
throw new Error(`Action '${action}' is not callable`);
|
|
2115
|
+
}
|
|
2116
|
+
const method = this[action];
|
|
2117
|
+
if (typeof method !== "function") {
|
|
2118
|
+
throw new Error(`Action '${action}' not found on component`);
|
|
2119
|
+
}
|
|
2120
|
+
if (Object.prototype.hasOwnProperty.call(Object.prototype, action)) {
|
|
2121
|
+
throw new Error(`Action '${action}' is not callable`);
|
|
2122
|
+
}
|
|
2123
|
+
_liveDebugger?.trackActionCall(this.id, action, payload);
|
|
2124
|
+
let hookResult;
|
|
2125
|
+
try {
|
|
2126
|
+
hookResult = await this.onAction(action, payload);
|
|
2127
|
+
} catch (hookError) {
|
|
2128
|
+
_liveDebugger?.trackActionError(this.id, action, hookError.message, Date.now() - actionStart);
|
|
2129
|
+
this.emit("ERROR", {
|
|
2130
|
+
action,
|
|
2131
|
+
error: `Action '${action}' failed pre-validation`
|
|
2132
|
+
});
|
|
2133
|
+
throw hookError;
|
|
2134
|
+
}
|
|
2135
|
+
if (hookResult === false) {
|
|
2136
|
+
_liveDebugger?.trackActionError(this.id, action, "Action cancelled", Date.now() - actionStart);
|
|
2137
|
+
throw new Error(`Action '${action}' was cancelled`);
|
|
2138
|
+
}
|
|
2139
|
+
const result = await method.call(this, payload);
|
|
2140
|
+
_liveDebugger?.trackActionResult(this.id, action, result, Date.now() - actionStart);
|
|
2141
|
+
return result;
|
|
2142
|
+
} catch (error) {
|
|
2143
|
+
if (!error.message?.includes("was cancelled") && !error.message?.includes("pre-validation")) {
|
|
2144
|
+
_liveDebugger?.trackActionError(this.id, action, error.message, Date.now() - actionStart);
|
|
2145
|
+
this.emit("ERROR", {
|
|
2146
|
+
action,
|
|
2147
|
+
error: error.message
|
|
2148
|
+
});
|
|
2149
|
+
}
|
|
2150
|
+
throw error;
|
|
2151
|
+
}
|
|
2152
|
+
}
|
|
2153
|
+
// ========================================
|
|
2154
|
+
// Messaging
|
|
2155
|
+
// ========================================
|
|
2156
|
+
emit(type, payload) {
|
|
2157
|
+
const override = this[EMIT_OVERRIDE_KEY];
|
|
2158
|
+
if (override) {
|
|
2159
|
+
override(type, payload);
|
|
2160
|
+
return;
|
|
2161
|
+
}
|
|
2162
|
+
const message = {
|
|
2163
|
+
type,
|
|
2164
|
+
componentId: this.id,
|
|
2165
|
+
payload,
|
|
2166
|
+
timestamp: Date.now(),
|
|
2167
|
+
userId: this.userId,
|
|
2168
|
+
room: this.room
|
|
2169
|
+
};
|
|
2170
|
+
if (this.ws && this.ws.send) {
|
|
2171
|
+
this.ws.send(JSON.stringify(message));
|
|
2172
|
+
}
|
|
2173
|
+
}
|
|
2174
|
+
broadcast(type, payload, excludeCurrentUser = false) {
|
|
2175
|
+
if (!this.room) {
|
|
2176
|
+
liveWarn("rooms", this.id, `[${this.id}] Cannot broadcast '${type}' - no room set`);
|
|
2177
|
+
return;
|
|
2178
|
+
}
|
|
2179
|
+
const message = {
|
|
2180
|
+
type,
|
|
2181
|
+
payload,
|
|
2182
|
+
room: this.room,
|
|
2183
|
+
excludeUser: excludeCurrentUser ? this.userId : void 0
|
|
2184
|
+
};
|
|
2185
|
+
liveLog("rooms", this.id, `[${this.id}] Broadcasting '${type}' to room '${this.room}'`);
|
|
2186
|
+
this.broadcastToRoom(message);
|
|
2187
|
+
}
|
|
2188
|
+
// ========================================
|
|
2189
|
+
// Room Events - Internal Server Events
|
|
2190
|
+
// ========================================
|
|
2191
|
+
emitRoomEvent(event, data, notifySelf = false) {
|
|
2192
|
+
if (!this.room) {
|
|
2193
|
+
liveWarn("rooms", this.id, `[${this.id}] Cannot emit room event '${event}' - no room set`);
|
|
2194
|
+
return 0;
|
|
2195
|
+
}
|
|
2196
|
+
const ctx = getLiveComponentContext();
|
|
2197
|
+
const excludeId = notifySelf ? void 0 : this.id;
|
|
2198
|
+
const notified = ctx.roomEvents.emit(this.roomType, this.room, event, data, excludeId);
|
|
2199
|
+
liveLog("rooms", this.id, `[${this.id}] Room event '${event}' -> ${notified} components`);
|
|
2200
|
+
_liveDebugger?.trackRoomEmit(this.id, this.room, event, data);
|
|
2201
|
+
return notified;
|
|
2202
|
+
}
|
|
2203
|
+
onRoomEvent(event, handler) {
|
|
2204
|
+
if (!this.room) {
|
|
2205
|
+
liveWarn("rooms", this.id, `[${this.id}] Cannot subscribe to room event '${event}' - no room set`);
|
|
2206
|
+
return;
|
|
2207
|
+
}
|
|
2208
|
+
const ctx = getLiveComponentContext();
|
|
2209
|
+
const unsubscribe = ctx.roomEvents.on(
|
|
2210
|
+
this.roomType,
|
|
2211
|
+
this.room,
|
|
2212
|
+
event,
|
|
2213
|
+
this.id,
|
|
2214
|
+
handler
|
|
2215
|
+
);
|
|
2216
|
+
this.roomEventUnsubscribers.push(unsubscribe);
|
|
2217
|
+
liveLog("rooms", this.id, `[${this.id}] Subscribed to room event '${event}'`);
|
|
2218
|
+
}
|
|
2219
|
+
emitRoomEventWithState(event, data, stateUpdates) {
|
|
2220
|
+
this.setState(stateUpdates);
|
|
2221
|
+
return this.emitRoomEvent(event, data, false);
|
|
2222
|
+
}
|
|
2223
|
+
async subscribeToRoom(roomId) {
|
|
2224
|
+
this.room = roomId;
|
|
2225
|
+
}
|
|
2226
|
+
async unsubscribeFromRoom() {
|
|
2227
|
+
this.room = void 0;
|
|
2228
|
+
}
|
|
2229
|
+
// ========================================
|
|
2230
|
+
// Internal
|
|
2231
|
+
// ========================================
|
|
2232
|
+
generateId() {
|
|
2233
|
+
return `live-${crypto.randomUUID()}`;
|
|
2234
|
+
}
|
|
2235
|
+
destroy() {
|
|
2236
|
+
try {
|
|
2237
|
+
this.onDestroy();
|
|
2238
|
+
} catch (err) {
|
|
2239
|
+
console.error(`[${this.id}] onDestroy error:`, err?.message || err);
|
|
2240
|
+
}
|
|
2241
|
+
for (const unsubscribe of this.roomEventUnsubscribers) {
|
|
2242
|
+
unsubscribe();
|
|
2243
|
+
}
|
|
2244
|
+
this.roomEventUnsubscribers = [];
|
|
2245
|
+
const ctx = getLiveComponentContext();
|
|
2246
|
+
for (const roomId of this.joinedRooms) {
|
|
2247
|
+
ctx.roomManager.leaveRoom(this.id, roomId);
|
|
2248
|
+
}
|
|
2249
|
+
this.joinedRooms.clear();
|
|
2250
|
+
this.roomHandles.clear();
|
|
2251
|
+
this._privateState = {};
|
|
2252
|
+
this.unsubscribeFromRoom();
|
|
2253
|
+
}
|
|
2254
|
+
getSerializableState() {
|
|
2255
|
+
return this.state;
|
|
2256
|
+
}
|
|
2257
|
+
};
|
|
2258
|
+
|
|
2259
|
+
// src/component/ComponentRegistry.ts
|
|
2260
|
+
var ComponentRegistry = class {
|
|
2261
|
+
components = /* @__PURE__ */ new Map();
|
|
2262
|
+
definitions = /* @__PURE__ */ new Map();
|
|
2263
|
+
metadata = /* @__PURE__ */ new Map();
|
|
2264
|
+
rooms = /* @__PURE__ */ new Map();
|
|
2265
|
+
wsConnections = /* @__PURE__ */ new Map();
|
|
2266
|
+
autoDiscoveredComponents = /* @__PURE__ */ new Map();
|
|
2267
|
+
healthCheckInterval;
|
|
2268
|
+
singletons = /* @__PURE__ */ new Map();
|
|
2269
|
+
authManager;
|
|
2270
|
+
debugger;
|
|
2271
|
+
stateSignature;
|
|
2272
|
+
performanceMonitor;
|
|
2273
|
+
constructor(deps) {
|
|
2274
|
+
this.authManager = deps.authManager;
|
|
2275
|
+
this.debugger = deps.debugger;
|
|
2276
|
+
this.stateSignature = deps.stateSignature;
|
|
2277
|
+
this.performanceMonitor = deps.performanceMonitor;
|
|
2278
|
+
_setLiveDebugger(deps.debugger);
|
|
2279
|
+
this.setupHealthMonitoring();
|
|
2280
|
+
}
|
|
2281
|
+
setupHealthMonitoring() {
|
|
2282
|
+
this.healthCheckInterval = setInterval(() => this.performHealthChecks(), 3e4);
|
|
2283
|
+
}
|
|
2284
|
+
registerComponent(definition) {
|
|
2285
|
+
this.definitions.set(definition.name, definition);
|
|
2286
|
+
liveLog("lifecycle", null, `Registered component: ${definition.name}`);
|
|
2287
|
+
}
|
|
2288
|
+
registerComponentClass(name, componentClass) {
|
|
2289
|
+
this.autoDiscoveredComponents.set(name, componentClass);
|
|
2290
|
+
}
|
|
2291
|
+
async autoDiscoverComponents(componentsPath) {
|
|
2292
|
+
try {
|
|
2293
|
+
const fs = await import('fs');
|
|
2294
|
+
const path = await import('path');
|
|
2295
|
+
if (!fs.existsSync(componentsPath)) return;
|
|
2296
|
+
const files = fs.readdirSync(componentsPath);
|
|
2297
|
+
for (const file of files) {
|
|
2298
|
+
if (file.endsWith(".ts") || file.endsWith(".js")) {
|
|
2299
|
+
try {
|
|
2300
|
+
const fullPath = path.join(componentsPath, file);
|
|
2301
|
+
const module = await import(fullPath);
|
|
2302
|
+
Object.keys(module).forEach((exportName) => {
|
|
2303
|
+
const exportedItem = module[exportName];
|
|
2304
|
+
if (typeof exportedItem === "function" && exportedItem.prototype && this.isLiveComponentClass(exportedItem)) {
|
|
2305
|
+
const componentName = exportedItem.componentName || exportName.replace(/Component$/, "");
|
|
2306
|
+
this.registerComponentClass(componentName, exportedItem);
|
|
2307
|
+
liveLog("lifecycle", null, `Auto-discovered component: ${componentName} (from ${file})`);
|
|
2308
|
+
}
|
|
2309
|
+
});
|
|
2310
|
+
} catch {
|
|
2311
|
+
}
|
|
2312
|
+
}
|
|
2313
|
+
}
|
|
2314
|
+
} catch (error) {
|
|
2315
|
+
console.error("Auto-discovery failed:", error);
|
|
2316
|
+
}
|
|
2317
|
+
}
|
|
2318
|
+
isLiveComponentClass(cls) {
|
|
2319
|
+
try {
|
|
2320
|
+
if (typeof cls.componentName === "string") return true;
|
|
2321
|
+
if (cls.prototype && typeof cls.prototype.executeAction === "function" && typeof cls.prototype.setState === "function" && typeof cls.prototype.getSerializableState === "function") return true;
|
|
2322
|
+
let prototype = cls.prototype;
|
|
2323
|
+
while (prototype) {
|
|
2324
|
+
const name = prototype.constructor.name;
|
|
2325
|
+
if (name === "LiveComponent" || name === "_LiveComponent") return true;
|
|
2326
|
+
prototype = Object.getPrototypeOf(prototype);
|
|
2327
|
+
}
|
|
2328
|
+
return false;
|
|
2329
|
+
} catch {
|
|
2330
|
+
return false;
|
|
2331
|
+
}
|
|
2332
|
+
}
|
|
2333
|
+
async mountComponent(ws, componentName, props = {}, options) {
|
|
2334
|
+
const startTime = Date.now();
|
|
2335
|
+
try {
|
|
2336
|
+
const definition = this.definitions.get(componentName);
|
|
2337
|
+
let ComponentClass = null;
|
|
2338
|
+
let initialState = {};
|
|
2339
|
+
if (definition) {
|
|
2340
|
+
ComponentClass = definition.component;
|
|
2341
|
+
initialState = definition.initialState;
|
|
2342
|
+
} else {
|
|
2343
|
+
ComponentClass = this.autoDiscoveredComponents.get(componentName) ?? null;
|
|
2344
|
+
if (!ComponentClass) {
|
|
2345
|
+
const variations = [
|
|
2346
|
+
componentName + "Component",
|
|
2347
|
+
componentName.charAt(0).toUpperCase() + componentName.slice(1) + "Component",
|
|
2348
|
+
componentName.charAt(0).toUpperCase() + componentName.slice(1)
|
|
2349
|
+
];
|
|
2350
|
+
for (const variation of variations) {
|
|
2351
|
+
ComponentClass = this.autoDiscoveredComponents.get(variation) ?? null;
|
|
2352
|
+
if (ComponentClass) break;
|
|
2353
|
+
}
|
|
2354
|
+
}
|
|
2355
|
+
if (!ComponentClass) throw new Error(`Component '${componentName}' not found`);
|
|
2356
|
+
initialState = {};
|
|
2357
|
+
}
|
|
2358
|
+
const authContext = ws.data?.authContext || ANONYMOUS_CONTEXT;
|
|
2359
|
+
const componentAuth = ComponentClass.auth;
|
|
2360
|
+
const authResult = this.authManager.authorizeComponent(authContext, componentAuth);
|
|
2361
|
+
if (!authResult.allowed) throw new Error(`AUTH_DENIED: ${authResult.reason}`);
|
|
2362
|
+
const isSingleton = ComponentClass.singleton === true;
|
|
2363
|
+
if (isSingleton) {
|
|
2364
|
+
const existing = this.singletons.get(componentName);
|
|
2365
|
+
if (existing) {
|
|
2366
|
+
const connId = ws.data?.connectionId || `ws-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
2367
|
+
existing.connections.set(connId, ws);
|
|
2368
|
+
this.ensureWsData(ws, options?.userId);
|
|
2369
|
+
ws.data.components.set(existing.instance.id, existing.instance);
|
|
2370
|
+
const signedState2 = await this.stateSignature.signState(existing.instance.id, {
|
|
2371
|
+
...existing.instance.getSerializableState(),
|
|
2372
|
+
__componentName: componentName
|
|
2373
|
+
}, 1, { compress: true, backup: true });
|
|
2374
|
+
ws.send(JSON.stringify({
|
|
2375
|
+
type: "STATE_UPDATE",
|
|
2376
|
+
componentId: existing.instance.id,
|
|
2377
|
+
payload: { state: existing.instance.getSerializableState(), signedState: signedState2 },
|
|
2378
|
+
timestamp: Date.now()
|
|
2379
|
+
}));
|
|
2380
|
+
try {
|
|
2381
|
+
existing.instance.onClientJoin(connId, existing.connections.size);
|
|
2382
|
+
} catch {
|
|
2383
|
+
}
|
|
2384
|
+
return { componentId: existing.instance.id, initialState: existing.instance.getSerializableState(), signedState: signedState2 };
|
|
2385
|
+
}
|
|
2386
|
+
}
|
|
2387
|
+
const component = new ComponentClass({ ...initialState, ...props }, ws, options);
|
|
2388
|
+
component.setAuthContext(authContext);
|
|
2389
|
+
component.broadcastToRoom = (message) => {
|
|
2390
|
+
this.broadcastToRoom(message, component.id);
|
|
2391
|
+
};
|
|
2392
|
+
const metadata = this.createComponentMetadata(component.id, componentName, options?.version);
|
|
2393
|
+
this.metadata.set(component.id, metadata);
|
|
2394
|
+
this.components.set(component.id, component);
|
|
2395
|
+
this.wsConnections.set(component.id, ws);
|
|
2396
|
+
if (options?.room) this.subscribeToRoom(component.id, options.room);
|
|
2397
|
+
this.ensureWsData(ws, options?.userId);
|
|
2398
|
+
ws.data.components.set(component.id, component);
|
|
2399
|
+
if (isSingleton) {
|
|
2400
|
+
const connId = ws.data.connectionId || `ws-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
2401
|
+
const connections = /* @__PURE__ */ new Map();
|
|
2402
|
+
connections.set(connId, ws);
|
|
2403
|
+
this.singletons.set(componentName, { instance: component, connections });
|
|
2404
|
+
component[EMIT_OVERRIDE_KEY] = (type, payload) => {
|
|
2405
|
+
const message = {
|
|
2406
|
+
type,
|
|
2407
|
+
componentId: component.id,
|
|
2408
|
+
payload,
|
|
2409
|
+
timestamp: Date.now(),
|
|
2410
|
+
userId: component.userId,
|
|
2411
|
+
room: component.room
|
|
2412
|
+
};
|
|
2413
|
+
const serialized = JSON.stringify(message);
|
|
2414
|
+
const singleton = this.singletons.get(componentName);
|
|
2415
|
+
if (singleton) {
|
|
2416
|
+
const dead = [];
|
|
2417
|
+
for (const [cId, cWs] of singleton.connections) {
|
|
2418
|
+
try {
|
|
2419
|
+
cWs.send(serialized);
|
|
2420
|
+
} catch {
|
|
2421
|
+
dead.push(cId);
|
|
2422
|
+
}
|
|
2423
|
+
}
|
|
2424
|
+
for (const cId of dead) singleton.connections.delete(cId);
|
|
2425
|
+
}
|
|
2426
|
+
};
|
|
2427
|
+
try {
|
|
2428
|
+
component.onClientJoin(connId, 1);
|
|
2429
|
+
} catch {
|
|
2430
|
+
}
|
|
2431
|
+
}
|
|
2432
|
+
metadata.state = "active";
|
|
2433
|
+
const renderTime = Date.now() - startTime;
|
|
2434
|
+
this.recordComponentMetrics(component.id, renderTime);
|
|
2435
|
+
registerComponentLogging(component.id, ComponentClass.logging);
|
|
2436
|
+
this.performanceMonitor.initializeComponent(component.id, componentName);
|
|
2437
|
+
this.performanceMonitor.recordRenderTime(component.id, renderTime);
|
|
2438
|
+
const signedState = await this.stateSignature.signState(component.id, {
|
|
2439
|
+
...component.getSerializableState(),
|
|
2440
|
+
__componentName: componentName
|
|
2441
|
+
}, 1, { compress: true, backup: true });
|
|
2442
|
+
component.emit("STATE_UPDATE", {
|
|
2443
|
+
state: component.getSerializableState(),
|
|
2444
|
+
signedState
|
|
2445
|
+
});
|
|
2446
|
+
try {
|
|
2447
|
+
component.onConnect();
|
|
2448
|
+
} catch {
|
|
2449
|
+
}
|
|
2450
|
+
try {
|
|
2451
|
+
await component.onMount();
|
|
2452
|
+
} catch (err) {
|
|
2453
|
+
;
|
|
2454
|
+
component.emit("ERROR", { action: "onMount", error: `Mount initialization failed: ${err?.message || err}` });
|
|
2455
|
+
}
|
|
2456
|
+
this.debugger.trackComponentMount(
|
|
2457
|
+
component.id,
|
|
2458
|
+
componentName,
|
|
2459
|
+
component.getSerializableState(),
|
|
2460
|
+
options?.room,
|
|
2461
|
+
options?.debugLabel
|
|
2462
|
+
);
|
|
2463
|
+
return { componentId: component.id, initialState: component.getSerializableState(), signedState };
|
|
2464
|
+
} catch (error) {
|
|
2465
|
+
console.error(`Failed to mount component ${componentName}:`, error);
|
|
2466
|
+
throw error;
|
|
2467
|
+
}
|
|
2468
|
+
}
|
|
2469
|
+
async rehydrateComponent(componentId, componentName, signedState, ws, options) {
|
|
2470
|
+
try {
|
|
2471
|
+
const validation = await this.stateSignature.validateState(signedState);
|
|
2472
|
+
if (!validation.valid) return { success: false, error: validation.error || "Invalid state signature" };
|
|
2473
|
+
const definition = this.definitions.get(componentName);
|
|
2474
|
+
let ComponentClass = null;
|
|
2475
|
+
let initialState = {};
|
|
2476
|
+
if (definition) {
|
|
2477
|
+
ComponentClass = definition.component;
|
|
2478
|
+
initialState = definition.initialState;
|
|
2479
|
+
} else {
|
|
2480
|
+
ComponentClass = this.autoDiscoveredComponents.get(componentName) ?? null;
|
|
2481
|
+
if (!ComponentClass) {
|
|
2482
|
+
const variations = [componentName + "Component", componentName.charAt(0).toUpperCase() + componentName.slice(1) + "Component", componentName.charAt(0).toUpperCase() + componentName.slice(1)];
|
|
2483
|
+
for (const variation of variations) {
|
|
2484
|
+
ComponentClass = this.autoDiscoveredComponents.get(variation) ?? null;
|
|
2485
|
+
if (ComponentClass) break;
|
|
2486
|
+
}
|
|
2487
|
+
}
|
|
2488
|
+
if (!ComponentClass) return { success: false, error: `Component '${componentName}' not found` };
|
|
2489
|
+
}
|
|
2490
|
+
const authContext = ws.data?.authContext || ANONYMOUS_CONTEXT;
|
|
2491
|
+
const componentAuth = ComponentClass.auth;
|
|
2492
|
+
const authResult = this.authManager.authorizeComponent(authContext, componentAuth);
|
|
2493
|
+
if (!authResult.allowed) return { success: false, error: `AUTH_DENIED: ${authResult.reason}` };
|
|
2494
|
+
const clientState = await this.stateSignature.extractData(signedState);
|
|
2495
|
+
if (clientState.__componentName && clientState.__componentName !== componentName) {
|
|
2496
|
+
return { success: false, error: "Component class mismatch - state tampering detected" };
|
|
2497
|
+
}
|
|
2498
|
+
const { __componentName, ...cleanState } = clientState;
|
|
2499
|
+
const finalState = definition ? { ...initialState, ...cleanState } : cleanState;
|
|
2500
|
+
const component = new ComponentClass(finalState, ws, options);
|
|
2501
|
+
component.setAuthContext(authContext);
|
|
2502
|
+
this.components.set(component.id, component);
|
|
2503
|
+
this.wsConnections.set(component.id, ws);
|
|
2504
|
+
if (options?.room) this.subscribeToRoom(component.id, options.room);
|
|
2505
|
+
this.ensureWsData(ws, options?.userId);
|
|
2506
|
+
ws.data.components.set(component.id, component);
|
|
2507
|
+
registerComponentLogging(component.id, ComponentClass.logging);
|
|
2508
|
+
const newSignedState = await this.stateSignature.signState(
|
|
2509
|
+
component.id,
|
|
2510
|
+
{ ...component.getSerializableState(), __componentName: componentName },
|
|
2511
|
+
signedState.version + 1
|
|
2512
|
+
);
|
|
2513
|
+
component.emit("STATE_REHYDRATED", {
|
|
2514
|
+
state: component.getSerializableState(),
|
|
2515
|
+
signedState: newSignedState,
|
|
2516
|
+
oldComponentId: componentId,
|
|
2517
|
+
newComponentId: component.id
|
|
2518
|
+
});
|
|
2519
|
+
try {
|
|
2520
|
+
component.onConnect();
|
|
2521
|
+
} catch {
|
|
2522
|
+
}
|
|
2523
|
+
try {
|
|
2524
|
+
component.onRehydrate(clientState);
|
|
2525
|
+
} catch {
|
|
2526
|
+
}
|
|
2527
|
+
try {
|
|
2528
|
+
await component.onMount();
|
|
2529
|
+
} catch {
|
|
2530
|
+
}
|
|
2531
|
+
return { success: true, newComponentId: component.id };
|
|
2532
|
+
} catch (error) {
|
|
2533
|
+
return { success: false, error: error.message };
|
|
2534
|
+
}
|
|
2535
|
+
}
|
|
2536
|
+
ensureWsData(ws, userId) {
|
|
2537
|
+
if (!ws.data) {
|
|
2538
|
+
ws.data = {
|
|
2539
|
+
connectionId: `ws-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
|
2540
|
+
components: /* @__PURE__ */ new Map(),
|
|
2541
|
+
subscriptions: /* @__PURE__ */ new Set(),
|
|
2542
|
+
connectedAt: /* @__PURE__ */ new Date(),
|
|
2543
|
+
userId
|
|
2544
|
+
};
|
|
2545
|
+
}
|
|
2546
|
+
if (!ws.data.components) ws.data.components = /* @__PURE__ */ new Map();
|
|
2547
|
+
}
|
|
2548
|
+
isSingletonComponent(componentId) {
|
|
2549
|
+
for (const [, s] of this.singletons) if (s.instance.id === componentId) return true;
|
|
2550
|
+
return false;
|
|
2551
|
+
}
|
|
2552
|
+
removeSingletonConnection(componentId, connId, context = "unmount") {
|
|
2553
|
+
for (const [name, singleton] of this.singletons) {
|
|
2554
|
+
if (singleton.instance.id !== componentId) continue;
|
|
2555
|
+
if (connId) singleton.connections.delete(connId);
|
|
2556
|
+
if (singleton.connections.size === 0) {
|
|
2557
|
+
try {
|
|
2558
|
+
singleton.instance.onDisconnect();
|
|
2559
|
+
} catch {
|
|
2560
|
+
}
|
|
2561
|
+
this.cleanupComponent(componentId);
|
|
2562
|
+
this.singletons.delete(name);
|
|
2563
|
+
}
|
|
2564
|
+
return true;
|
|
2565
|
+
}
|
|
2566
|
+
return false;
|
|
2567
|
+
}
|
|
2568
|
+
async unmountComponent(componentId, ws) {
|
|
2569
|
+
const component = this.components.get(componentId);
|
|
2570
|
+
if (!component) return;
|
|
2571
|
+
if (ws) {
|
|
2572
|
+
const connId = ws.data?.connectionId;
|
|
2573
|
+
ws.data?.components?.delete(componentId);
|
|
2574
|
+
if (this.isSingletonComponent(componentId)) {
|
|
2575
|
+
const singleton = this.singletons.get(this.getSingletonName(componentId) || "");
|
|
2576
|
+
const remaining = singleton ? singleton.connections.size - 1 : 0;
|
|
2577
|
+
try {
|
|
2578
|
+
component.onClientLeave(connId || "unknown", Math.max(0, remaining));
|
|
2579
|
+
} catch {
|
|
2580
|
+
}
|
|
2581
|
+
}
|
|
2582
|
+
if (this.removeSingletonConnection(componentId, connId, "unmount")) return;
|
|
2583
|
+
} else {
|
|
2584
|
+
if (this.removeSingletonConnection(componentId, void 0, "unmount")) return;
|
|
2585
|
+
}
|
|
2586
|
+
this.debugger.trackComponentUnmount(componentId);
|
|
2587
|
+
component.destroy?.();
|
|
2588
|
+
this.unsubscribeFromAllRooms(componentId);
|
|
2589
|
+
this.components.delete(componentId);
|
|
2590
|
+
this.wsConnections.delete(componentId);
|
|
2591
|
+
unregisterComponentLogging(componentId);
|
|
2592
|
+
}
|
|
2593
|
+
getSingletonName(componentId) {
|
|
2594
|
+
for (const [name, s] of this.singletons) {
|
|
2595
|
+
if (s.instance.id === componentId) return name;
|
|
2596
|
+
}
|
|
2597
|
+
return null;
|
|
2598
|
+
}
|
|
2599
|
+
async executeAction(componentId, action, payload) {
|
|
2600
|
+
const component = this.components.get(componentId);
|
|
2601
|
+
if (!component) throw new Error(`COMPONENT_REHYDRATION_REQUIRED:${componentId}`);
|
|
2602
|
+
const componentClass = component.constructor;
|
|
2603
|
+
const actionAuthMap = componentClass.actionAuth;
|
|
2604
|
+
const actionAuth = actionAuthMap?.[action];
|
|
2605
|
+
if (actionAuth) {
|
|
2606
|
+
const authContext = component.$auth || ANONYMOUS_CONTEXT;
|
|
2607
|
+
const componentName = componentClass.componentName || componentClass.name;
|
|
2608
|
+
const authResult = await this.authManager.authorizeAction(authContext, componentName, action, actionAuth);
|
|
2609
|
+
if (!authResult.allowed) throw new Error(`AUTH_DENIED: ${authResult.reason}`);
|
|
2610
|
+
}
|
|
2611
|
+
return await component.executeAction?.(action, payload);
|
|
2612
|
+
}
|
|
2613
|
+
updateProperty(componentId, property, value) {
|
|
2614
|
+
const component = this.components.get(componentId);
|
|
2615
|
+
if (!component) throw new Error(`Component '${componentId}' not found`);
|
|
2616
|
+
component.setState?.({ [property]: value });
|
|
2617
|
+
}
|
|
2618
|
+
subscribeToRoom(componentId, roomId) {
|
|
2619
|
+
if (!this.rooms.has(roomId)) this.rooms.set(roomId, /* @__PURE__ */ new Set());
|
|
2620
|
+
this.rooms.get(roomId).add(componentId);
|
|
2621
|
+
}
|
|
2622
|
+
unsubscribeFromRoom(componentId, roomId) {
|
|
2623
|
+
const room = this.rooms.get(roomId);
|
|
2624
|
+
if (room) {
|
|
2625
|
+
room.delete(componentId);
|
|
2626
|
+
if (room.size === 0) this.rooms.delete(roomId);
|
|
2627
|
+
}
|
|
2628
|
+
}
|
|
2629
|
+
unsubscribeFromAllRooms(componentId) {
|
|
2630
|
+
for (const [roomId, components] of Array.from(this.rooms.entries())) {
|
|
2631
|
+
if (components.has(componentId)) this.unsubscribeFromRoom(componentId, roomId);
|
|
2632
|
+
}
|
|
2633
|
+
}
|
|
2634
|
+
broadcastToRoom(message, senderComponentId) {
|
|
2635
|
+
if (!message.room) return;
|
|
2636
|
+
const roomComponents = this.rooms.get(message.room);
|
|
2637
|
+
if (!roomComponents) return;
|
|
2638
|
+
const broadcastMessage = {
|
|
2639
|
+
type: "BROADCAST",
|
|
2640
|
+
componentId: senderComponentId || "system",
|
|
2641
|
+
payload: { type: message.type, data: message.payload },
|
|
2642
|
+
timestamp: Date.now(),
|
|
2643
|
+
room: message.room
|
|
2644
|
+
};
|
|
2645
|
+
for (const componentId of Array.from(roomComponents)) {
|
|
2646
|
+
const component = this.components.get(componentId);
|
|
2647
|
+
if (message.excludeUser && component?.userId === message.excludeUser) continue;
|
|
2648
|
+
const ws = this.wsConnections.get(componentId);
|
|
2649
|
+
if (ws && ws.send) ws.send(JSON.stringify(broadcastMessage));
|
|
2650
|
+
}
|
|
2651
|
+
}
|
|
2652
|
+
async handleMessage(ws, message) {
|
|
2653
|
+
try {
|
|
2654
|
+
if (message.componentId) this.updateComponentActivity(message.componentId);
|
|
2655
|
+
switch (message.type) {
|
|
2656
|
+
case "COMPONENT_MOUNT":
|
|
2657
|
+
const mountResult = await this.mountComponent(ws, message.payload.component, message.payload.props, {
|
|
2658
|
+
room: message.payload.room,
|
|
2659
|
+
userId: message.userId,
|
|
2660
|
+
debugLabel: message.payload.debugLabel
|
|
2661
|
+
});
|
|
2662
|
+
return { success: true, result: mountResult };
|
|
2663
|
+
case "COMPONENT_UNMOUNT":
|
|
2664
|
+
await this.unmountComponent(message.componentId, ws);
|
|
2665
|
+
return { success: true };
|
|
2666
|
+
case "CALL_ACTION":
|
|
2667
|
+
this.recordComponentMetrics(message.componentId, void 0, message.action);
|
|
2668
|
+
const actionStart = Date.now();
|
|
2669
|
+
try {
|
|
2670
|
+
const actionResult = await this.executeAction(message.componentId, message.action, message.payload);
|
|
2671
|
+
this.performanceMonitor.recordActionTime(message.componentId, message.action, Date.now() - actionStart);
|
|
2672
|
+
if (message.expectResponse) return { success: true, result: actionResult };
|
|
2673
|
+
return null;
|
|
2674
|
+
} catch (error) {
|
|
2675
|
+
this.performanceMonitor.recordActionTime(message.componentId, message.action, Date.now() - actionStart, error);
|
|
2676
|
+
throw error;
|
|
2677
|
+
}
|
|
2678
|
+
case "PROPERTY_UPDATE":
|
|
2679
|
+
this.updateProperty(message.componentId, message.property, message.payload.value);
|
|
2680
|
+
return { success: true };
|
|
2681
|
+
default:
|
|
2682
|
+
return { success: false, error: "Unknown message type" };
|
|
2683
|
+
}
|
|
2684
|
+
} catch (error) {
|
|
2685
|
+
if (message.componentId) this.recordComponentError(message.componentId, error);
|
|
2686
|
+
return { success: false, error: error.message };
|
|
2687
|
+
}
|
|
2688
|
+
}
|
|
2689
|
+
cleanupConnection(ws) {
|
|
2690
|
+
if (!ws.data?.components) return;
|
|
2691
|
+
const componentsToCleanup = Array.from(ws.data.components.keys());
|
|
2692
|
+
const connId = ws.data.connectionId;
|
|
2693
|
+
for (const componentId of componentsToCleanup) {
|
|
2694
|
+
const component = this.components.get(componentId);
|
|
2695
|
+
if (component && !this.isSingletonComponent(componentId)) {
|
|
2696
|
+
try {
|
|
2697
|
+
component.onDisconnect();
|
|
2698
|
+
} catch {
|
|
2699
|
+
}
|
|
2700
|
+
}
|
|
2701
|
+
if (!this.removeSingletonConnection(componentId, connId || void 0, "disconnect")) {
|
|
2702
|
+
this.cleanupComponent(componentId);
|
|
2703
|
+
}
|
|
2704
|
+
}
|
|
2705
|
+
ws.data.components.clear();
|
|
2706
|
+
}
|
|
2707
|
+
getStats() {
|
|
2708
|
+
return {
|
|
2709
|
+
components: this.components.size,
|
|
2710
|
+
definitions: this.definitions.size,
|
|
2711
|
+
rooms: this.rooms.size,
|
|
2712
|
+
connections: this.wsConnections.size,
|
|
2713
|
+
singletons: Object.fromEntries(
|
|
2714
|
+
Array.from(this.singletons.entries()).map(([name, s]) => [name, { componentId: s.instance.id, connections: s.connections.size }])
|
|
2715
|
+
),
|
|
2716
|
+
roomDetails: Object.fromEntries(
|
|
2717
|
+
Array.from(this.rooms.entries()).map(([roomId, components]) => [roomId, components.size])
|
|
2718
|
+
)
|
|
2719
|
+
};
|
|
2720
|
+
}
|
|
2721
|
+
getRegisteredComponentNames() {
|
|
2722
|
+
return [.../* @__PURE__ */ new Set([...this.definitions.keys(), ...this.autoDiscoveredComponents.keys()])];
|
|
2723
|
+
}
|
|
2724
|
+
getComponent(componentId) {
|
|
2725
|
+
return this.components.get(componentId);
|
|
2726
|
+
}
|
|
2727
|
+
getRoomComponents(roomId) {
|
|
2728
|
+
const componentIds = this.rooms.get(roomId) || /* @__PURE__ */ new Set();
|
|
2729
|
+
return Array.from(componentIds).map((id) => this.components.get(id)).filter(Boolean);
|
|
2730
|
+
}
|
|
2731
|
+
createComponentMetadata(componentId, componentName, version = "1.0.0") {
|
|
2732
|
+
return {
|
|
2733
|
+
id: componentId,
|
|
2734
|
+
name: componentName,
|
|
2735
|
+
version,
|
|
2736
|
+
mountedAt: /* @__PURE__ */ new Date(),
|
|
2737
|
+
lastActivity: /* @__PURE__ */ new Date(),
|
|
2738
|
+
state: "mounting",
|
|
2739
|
+
healthStatus: "healthy",
|
|
2740
|
+
dependencies: [],
|
|
2741
|
+
services: /* @__PURE__ */ new Map(),
|
|
2742
|
+
metrics: { renderCount: 0, actionCount: 0, errorCount: 0, averageRenderTime: 0, memoryUsage: 0 },
|
|
2743
|
+
migrationHistory: []
|
|
2744
|
+
};
|
|
2745
|
+
}
|
|
2746
|
+
updateComponentActivity(componentId) {
|
|
2747
|
+
const metadata = this.metadata.get(componentId);
|
|
2748
|
+
if (metadata) {
|
|
2749
|
+
metadata.lastActivity = /* @__PURE__ */ new Date();
|
|
2750
|
+
metadata.state = "active";
|
|
2751
|
+
return true;
|
|
2752
|
+
}
|
|
2753
|
+
return false;
|
|
2754
|
+
}
|
|
2755
|
+
recordComponentMetrics(componentId, renderTime, action) {
|
|
2756
|
+
const metadata = this.metadata.get(componentId);
|
|
2757
|
+
if (!metadata) return;
|
|
2758
|
+
if (renderTime) {
|
|
2759
|
+
metadata.metrics.renderCount++;
|
|
2760
|
+
metadata.metrics.averageRenderTime = (metadata.metrics.averageRenderTime * (metadata.metrics.renderCount - 1) + renderTime) / metadata.metrics.renderCount;
|
|
2761
|
+
metadata.metrics.lastRenderTime = renderTime;
|
|
2762
|
+
}
|
|
2763
|
+
if (action) metadata.metrics.actionCount++;
|
|
2764
|
+
this.updateComponentActivity(componentId);
|
|
2765
|
+
}
|
|
2766
|
+
recordComponentError(componentId, error) {
|
|
2767
|
+
const metadata = this.metadata.get(componentId);
|
|
2768
|
+
if (metadata) {
|
|
2769
|
+
metadata.metrics.errorCount++;
|
|
2770
|
+
metadata.healthStatus = metadata.metrics.errorCount > 5 ? "unhealthy" : "degraded";
|
|
2771
|
+
}
|
|
2772
|
+
}
|
|
2773
|
+
async performHealthChecks() {
|
|
2774
|
+
for (const [componentId, metadata] of this.metadata) {
|
|
2775
|
+
if (!this.components.get(componentId)) continue;
|
|
2776
|
+
if (metadata.metrics.errorCount > 10) metadata.healthStatus = "unhealthy";
|
|
2777
|
+
else if (Date.now() - metadata.lastActivity.getTime() > 3e5) metadata.healthStatus = "degraded";
|
|
2778
|
+
}
|
|
2779
|
+
}
|
|
2780
|
+
cleanupComponent(componentId) {
|
|
2781
|
+
const component = this.components.get(componentId);
|
|
2782
|
+
if (component) try {
|
|
2783
|
+
component.destroy?.();
|
|
2784
|
+
} catch {
|
|
2785
|
+
}
|
|
2786
|
+
this.performanceMonitor.removeComponent(componentId);
|
|
2787
|
+
unregisterComponentLogging(componentId);
|
|
2788
|
+
this.components.delete(componentId);
|
|
2789
|
+
this.metadata.delete(componentId);
|
|
2790
|
+
this.wsConnections.delete(componentId);
|
|
2791
|
+
for (const [roomId, componentIds] of this.rooms) {
|
|
2792
|
+
componentIds.delete(componentId);
|
|
2793
|
+
if (componentIds.size === 0) this.rooms.delete(roomId);
|
|
2794
|
+
}
|
|
2795
|
+
}
|
|
2796
|
+
cleanup() {
|
|
2797
|
+
if (this.healthCheckInterval) clearInterval(this.healthCheckInterval);
|
|
2798
|
+
this.singletons.clear();
|
|
2799
|
+
for (const [componentId] of this.components) this.cleanupComponent(componentId);
|
|
2800
|
+
}
|
|
2801
|
+
};
|
|
2802
|
+
|
|
2803
|
+
// src/connection/RateLimiter.ts
|
|
2804
|
+
var ConnectionRateLimiter = class {
|
|
2805
|
+
tokens;
|
|
2806
|
+
lastRefill;
|
|
2807
|
+
maxTokens;
|
|
2808
|
+
refillRate;
|
|
2809
|
+
// tokens per second
|
|
2810
|
+
constructor(maxTokens = DEFAULT_RATE_LIMIT_MAX_TOKENS, refillRate = DEFAULT_RATE_LIMIT_REFILL_RATE) {
|
|
2811
|
+
this.maxTokens = maxTokens;
|
|
2812
|
+
this.tokens = maxTokens;
|
|
2813
|
+
this.refillRate = refillRate;
|
|
2814
|
+
this.lastRefill = Date.now();
|
|
2815
|
+
}
|
|
2816
|
+
tryConsume(count = 1) {
|
|
2817
|
+
this.refill();
|
|
2818
|
+
if (this.tokens >= count) {
|
|
2819
|
+
this.tokens -= count;
|
|
2820
|
+
return true;
|
|
2821
|
+
}
|
|
2822
|
+
return false;
|
|
2823
|
+
}
|
|
2824
|
+
refill() {
|
|
2825
|
+
const now = Date.now();
|
|
2826
|
+
const elapsed = (now - this.lastRefill) / 1e3;
|
|
2827
|
+
this.tokens = Math.min(this.maxTokens, this.tokens + elapsed * this.refillRate);
|
|
2828
|
+
this.lastRefill = now;
|
|
2829
|
+
}
|
|
2830
|
+
};
|
|
2831
|
+
var RateLimiterRegistry = class {
|
|
2832
|
+
limiters = /* @__PURE__ */ new Map();
|
|
2833
|
+
maxTokens;
|
|
2834
|
+
refillRate;
|
|
2835
|
+
constructor(maxTokens = DEFAULT_RATE_LIMIT_MAX_TOKENS, refillRate = DEFAULT_RATE_LIMIT_REFILL_RATE) {
|
|
2836
|
+
this.maxTokens = maxTokens;
|
|
2837
|
+
this.refillRate = refillRate;
|
|
2838
|
+
}
|
|
2839
|
+
/**
|
|
2840
|
+
* Get or create a rate limiter for a connection.
|
|
2841
|
+
*/
|
|
2842
|
+
get(connectionId) {
|
|
2843
|
+
let limiter = this.limiters.get(connectionId);
|
|
2844
|
+
if (!limiter) {
|
|
2845
|
+
limiter = new ConnectionRateLimiter(this.maxTokens, this.refillRate);
|
|
2846
|
+
this.limiters.set(connectionId, limiter);
|
|
2847
|
+
}
|
|
2848
|
+
return limiter;
|
|
2849
|
+
}
|
|
2850
|
+
/**
|
|
2851
|
+
* Remove a rate limiter for a disconnected connection.
|
|
2852
|
+
*/
|
|
2853
|
+
remove(connectionId) {
|
|
2854
|
+
this.limiters.delete(connectionId);
|
|
2855
|
+
}
|
|
2856
|
+
};
|
|
2857
|
+
|
|
2858
|
+
// src/protocol/binary.ts
|
|
2859
|
+
function encodeBinaryChunk(header, data) {
|
|
2860
|
+
const headerJson = JSON.stringify(header);
|
|
2861
|
+
const headerBuffer = Buffer.from(headerJson, "utf-8");
|
|
2862
|
+
const result = Buffer.alloc(4 + headerBuffer.length + data.length);
|
|
2863
|
+
result.writeUInt32LE(headerBuffer.length, 0);
|
|
2864
|
+
headerBuffer.copy(result, 4);
|
|
2865
|
+
Buffer.from(data).copy(result, 4 + headerBuffer.length);
|
|
2866
|
+
return result;
|
|
2867
|
+
}
|
|
2868
|
+
function decodeBinaryChunk(raw) {
|
|
2869
|
+
const buffer = raw instanceof ArrayBuffer ? Buffer.from(raw) : Buffer.from(raw.buffer, raw.byteOffset, raw.byteLength);
|
|
2870
|
+
const headerLength = buffer.readUInt32LE(0);
|
|
2871
|
+
const headerJson = buffer.slice(4, 4 + headerLength).toString("utf-8");
|
|
2872
|
+
const header = JSON.parse(headerJson);
|
|
2873
|
+
const data = buffer.slice(4 + headerLength);
|
|
2874
|
+
return { header, data };
|
|
2875
|
+
}
|
|
2876
|
+
|
|
2877
|
+
// src/server/LiveServer.ts
|
|
2878
|
+
var LiveServer = class {
|
|
2879
|
+
// Public singletons (accessible for transport adapters & advanced usage)
|
|
2880
|
+
roomEvents;
|
|
2881
|
+
roomManager;
|
|
2882
|
+
debugger;
|
|
2883
|
+
authManager;
|
|
2884
|
+
stateSignature;
|
|
2885
|
+
performanceMonitor;
|
|
2886
|
+
fileUploadManager;
|
|
2887
|
+
connectionManager;
|
|
2888
|
+
registry;
|
|
2889
|
+
rateLimiter;
|
|
2890
|
+
transport;
|
|
2891
|
+
options;
|
|
2892
|
+
constructor(options) {
|
|
2893
|
+
this.options = options;
|
|
2894
|
+
this.transport = options.transport;
|
|
2895
|
+
this.roomEvents = new RoomEventBus();
|
|
2896
|
+
this.roomManager = new LiveRoomManager(this.roomEvents);
|
|
2897
|
+
this.debugger = new LiveDebugger(options.debug ?? false);
|
|
2898
|
+
this.authManager = new LiveAuthManager();
|
|
2899
|
+
this.stateSignature = new StateSignatureManager(options.stateSignature);
|
|
2900
|
+
this.performanceMonitor = new PerformanceMonitor(options.performance);
|
|
2901
|
+
this.fileUploadManager = new FileUploadManager(options.fileUpload);
|
|
2902
|
+
this.connectionManager = new WebSocketConnectionManager(options.connection);
|
|
2903
|
+
this.rateLimiter = new RateLimiterRegistry(options.rateLimitMaxTokens, options.rateLimitRefillRate);
|
|
2904
|
+
this.registry = new ComponentRegistry({
|
|
2905
|
+
authManager: this.authManager,
|
|
2906
|
+
debugger: this.debugger,
|
|
2907
|
+
stateSignature: this.stateSignature,
|
|
2908
|
+
performanceMonitor: this.performanceMonitor
|
|
2909
|
+
});
|
|
2910
|
+
_setLoggerDebugger(this.debugger);
|
|
2911
|
+
setLiveComponentContext({
|
|
2912
|
+
roomEvents: this.roomEvents,
|
|
2913
|
+
roomManager: this.roomManager,
|
|
2914
|
+
debugger: this.debugger
|
|
2915
|
+
});
|
|
2916
|
+
}
|
|
2917
|
+
/**
|
|
2918
|
+
* Register an auth provider.
|
|
2919
|
+
*/
|
|
2920
|
+
useAuth(provider) {
|
|
2921
|
+
this.authManager.register(provider);
|
|
2922
|
+
return this;
|
|
2923
|
+
}
|
|
2924
|
+
/**
|
|
2925
|
+
* Start the LiveServer: register WS + HTTP handlers on the transport.
|
|
2926
|
+
*/
|
|
2927
|
+
async start() {
|
|
2928
|
+
if (this.options.componentsPath) {
|
|
2929
|
+
await this.registry.autoDiscoverComponents(this.options.componentsPath);
|
|
2930
|
+
}
|
|
2931
|
+
const wsConfig = {
|
|
2932
|
+
path: this.options.wsPath ?? DEFAULT_WS_PATH,
|
|
2933
|
+
onOpen: (ws) => this.handleOpen(ws),
|
|
2934
|
+
onMessage: (ws, message, isBinary) => this.handleMessage(ws, message, isBinary),
|
|
2935
|
+
onClose: (ws, code, reason) => this.handleClose(ws, code, reason),
|
|
2936
|
+
onError: (ws, error) => this.handleError(ws, error)
|
|
2937
|
+
};
|
|
2938
|
+
await this.transport.registerWebSocket(wsConfig);
|
|
2939
|
+
if (this.options.httpPrefix !== false) {
|
|
2940
|
+
const prefix = this.options.httpPrefix ?? "/api/live";
|
|
2941
|
+
await this.transport.registerHttpRoutes(this.buildHttpRoutes(prefix));
|
|
2942
|
+
}
|
|
2943
|
+
if (this.transport.start) {
|
|
2944
|
+
await this.transport.start();
|
|
2945
|
+
}
|
|
2946
|
+
liveLog("lifecycle", null, `LiveServer started (ws: ${wsConfig.path})`);
|
|
2947
|
+
}
|
|
2948
|
+
/**
|
|
2949
|
+
* Graceful shutdown.
|
|
2950
|
+
*/
|
|
2951
|
+
async shutdown() {
|
|
2952
|
+
this.registry.cleanup();
|
|
2953
|
+
this.connectionManager.shutdown();
|
|
2954
|
+
this.fileUploadManager.shutdown();
|
|
2955
|
+
this.stateSignature.shutdown();
|
|
2956
|
+
if (this.transport.shutdown) await this.transport.shutdown();
|
|
2957
|
+
liveLog("lifecycle", null, "LiveServer shut down");
|
|
2958
|
+
}
|
|
2959
|
+
// ===== WebSocket Handlers =====
|
|
2960
|
+
handleOpen(ws) {
|
|
2961
|
+
const connectionId = `ws-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
2962
|
+
ws.data = {
|
|
2963
|
+
connectionId,
|
|
2964
|
+
components: /* @__PURE__ */ new Map(),
|
|
2965
|
+
subscriptions: /* @__PURE__ */ new Set(),
|
|
2966
|
+
connectedAt: /* @__PURE__ */ new Date()
|
|
2967
|
+
};
|
|
2968
|
+
this.connectionManager.registerConnection(ws, connectionId);
|
|
2969
|
+
this.debugger.trackConnection(connectionId);
|
|
2970
|
+
ws.send(JSON.stringify({
|
|
2971
|
+
type: "CONNECTION_ESTABLISHED",
|
|
2972
|
+
connectionId,
|
|
2973
|
+
timestamp: Date.now()
|
|
2974
|
+
}));
|
|
2975
|
+
liveLog("websocket", null, `Connection opened: ${connectionId}`);
|
|
2976
|
+
}
|
|
2977
|
+
async handleMessage(ws, rawMessage, isBinary) {
|
|
2978
|
+
const connectionId = ws.data?.connectionId;
|
|
2979
|
+
if (connectionId) {
|
|
2980
|
+
const limiter = this.rateLimiter.get(connectionId);
|
|
2981
|
+
if (!limiter.tryConsume()) {
|
|
2982
|
+
ws.send(JSON.stringify({ type: "ERROR", error: "Rate limit exceeded", timestamp: Date.now() }));
|
|
2983
|
+
return;
|
|
2984
|
+
}
|
|
2985
|
+
}
|
|
2986
|
+
if (isBinary && rawMessage instanceof ArrayBuffer) {
|
|
2987
|
+
try {
|
|
2988
|
+
const { header, data } = decodeBinaryChunk(rawMessage);
|
|
2989
|
+
if (header.type === "FILE_UPLOAD_CHUNK") {
|
|
2990
|
+
const chunkMessage = { ...header, data: "" };
|
|
2991
|
+
const progress = await this.fileUploadManager.receiveChunk(chunkMessage, data);
|
|
2992
|
+
if (progress) ws.send(JSON.stringify(progress));
|
|
2993
|
+
}
|
|
2994
|
+
} catch (error) {
|
|
2995
|
+
ws.send(JSON.stringify({ type: "ERROR", error: error.message, timestamp: Date.now() }));
|
|
2996
|
+
}
|
|
2997
|
+
return;
|
|
2998
|
+
}
|
|
2999
|
+
let message;
|
|
3000
|
+
try {
|
|
3001
|
+
const str = typeof rawMessage === "string" ? rawMessage : new TextDecoder().decode(rawMessage);
|
|
3002
|
+
message = JSON.parse(str);
|
|
3003
|
+
} catch {
|
|
3004
|
+
ws.send(JSON.stringify({ type: "ERROR", error: "Invalid JSON", timestamp: Date.now() }));
|
|
3005
|
+
return;
|
|
3006
|
+
}
|
|
3007
|
+
try {
|
|
3008
|
+
if (message.type === "AUTH") {
|
|
3009
|
+
const authContext = await this.authManager.authenticate(message.payload || {});
|
|
3010
|
+
if (ws.data) ws.data.authContext = authContext;
|
|
3011
|
+
ws.send(JSON.stringify({
|
|
3012
|
+
type: "AUTH_RESPONSE",
|
|
3013
|
+
success: authContext.authenticated,
|
|
3014
|
+
payload: authContext.authenticated ? { userId: authContext.user?.id } : { error: "Authentication failed" },
|
|
3015
|
+
timestamp: Date.now()
|
|
3016
|
+
}));
|
|
3017
|
+
return;
|
|
3018
|
+
}
|
|
3019
|
+
if (message.type === "ROOM_JOIN" || message.type === "ROOM_LEAVE" || message.type === "ROOM_EMIT" || message.type === "ROOM_STATE_SET" || message.type === "ROOM_STATE_GET") {
|
|
3020
|
+
await this.handleRoomMessage(ws, message);
|
|
3021
|
+
return;
|
|
3022
|
+
}
|
|
3023
|
+
if (message.type === "FILE_UPLOAD_START") {
|
|
3024
|
+
const result2 = await this.fileUploadManager.startUpload(message, ws.data?.userId);
|
|
3025
|
+
ws.send(JSON.stringify({
|
|
3026
|
+
type: "FILE_UPLOAD_START_RESPONSE",
|
|
3027
|
+
componentId: message.componentId,
|
|
3028
|
+
uploadId: message.payload?.uploadId,
|
|
3029
|
+
success: result2.success,
|
|
3030
|
+
error: result2.error,
|
|
3031
|
+
requestId: message.requestId,
|
|
3032
|
+
timestamp: Date.now()
|
|
3033
|
+
}));
|
|
3034
|
+
return;
|
|
3035
|
+
}
|
|
3036
|
+
if (message.type === "FILE_UPLOAD_CHUNK") {
|
|
3037
|
+
const progress = await this.fileUploadManager.receiveChunk(message);
|
|
3038
|
+
if (progress) ws.send(JSON.stringify(progress));
|
|
3039
|
+
return;
|
|
3040
|
+
}
|
|
3041
|
+
if (message.type === "FILE_UPLOAD_COMPLETE") {
|
|
3042
|
+
const result2 = await this.fileUploadManager.completeUpload(message);
|
|
3043
|
+
ws.send(JSON.stringify(result2));
|
|
3044
|
+
return;
|
|
3045
|
+
}
|
|
3046
|
+
if (message.type === "COMPONENT_REHYDRATE") {
|
|
3047
|
+
const result2 = await this.registry.rehydrateComponent(
|
|
3048
|
+
message.componentId,
|
|
3049
|
+
message.payload.component,
|
|
3050
|
+
message.payload.signedState,
|
|
3051
|
+
ws,
|
|
3052
|
+
{ room: message.payload.room, userId: message.userId }
|
|
3053
|
+
);
|
|
3054
|
+
ws.send(JSON.stringify({
|
|
3055
|
+
type: "COMPONENT_REHYDRATED",
|
|
3056
|
+
componentId: message.componentId,
|
|
3057
|
+
success: result2.success,
|
|
3058
|
+
result: result2.success ? { newComponentId: result2.newComponentId } : void 0,
|
|
3059
|
+
error: result2.error,
|
|
3060
|
+
requestId: message.requestId,
|
|
3061
|
+
timestamp: Date.now()
|
|
3062
|
+
}));
|
|
3063
|
+
return;
|
|
3064
|
+
}
|
|
3065
|
+
const result = await this.registry.handleMessage(ws, message);
|
|
3066
|
+
if (result !== null) {
|
|
3067
|
+
const response = {
|
|
3068
|
+
type: message.type === "CALL_ACTION" ? "ACTION_RESPONSE" : "MESSAGE_RESPONSE",
|
|
3069
|
+
originalType: message.type,
|
|
3070
|
+
componentId: message.componentId,
|
|
3071
|
+
success: result.success,
|
|
3072
|
+
result: result.result,
|
|
3073
|
+
error: result.error,
|
|
3074
|
+
requestId: message.requestId,
|
|
3075
|
+
responseId: message.responseId,
|
|
3076
|
+
timestamp: Date.now()
|
|
3077
|
+
};
|
|
3078
|
+
ws.send(JSON.stringify(response));
|
|
3079
|
+
}
|
|
3080
|
+
} catch (error) {
|
|
3081
|
+
ws.send(JSON.stringify({
|
|
3082
|
+
type: "ERROR",
|
|
3083
|
+
componentId: message.componentId,
|
|
3084
|
+
error: error.message,
|
|
3085
|
+
requestId: message.requestId,
|
|
3086
|
+
timestamp: Date.now()
|
|
3087
|
+
}));
|
|
3088
|
+
}
|
|
3089
|
+
}
|
|
3090
|
+
handleClose(ws, code, reason) {
|
|
3091
|
+
const connectionId = ws.data?.connectionId;
|
|
3092
|
+
const componentCount = ws.data?.components?.size || 0;
|
|
3093
|
+
this.registry.cleanupConnection(ws);
|
|
3094
|
+
this.roomManager.cleanupComponent(connectionId || "");
|
|
3095
|
+
if (connectionId) {
|
|
3096
|
+
this.connectionManager.cleanupConnection(connectionId);
|
|
3097
|
+
this.rateLimiter.remove(connectionId);
|
|
3098
|
+
}
|
|
3099
|
+
this.debugger.trackDisconnection(connectionId || "", componentCount);
|
|
3100
|
+
liveLog("websocket", null, `Connection closed: ${connectionId} (${componentCount} components)`);
|
|
3101
|
+
}
|
|
3102
|
+
handleError(ws, error) {
|
|
3103
|
+
console.error(`[LiveServer] WebSocket error:`, error.message);
|
|
3104
|
+
}
|
|
3105
|
+
// ===== Room Message Router =====
|
|
3106
|
+
async handleRoomMessage(ws, message) {
|
|
3107
|
+
const { componentId } = message;
|
|
3108
|
+
const roomId = message.roomId || message.payload?.roomId;
|
|
3109
|
+
switch (message.type) {
|
|
3110
|
+
case "ROOM_JOIN": {
|
|
3111
|
+
const result = this.roomManager.joinRoom(componentId, roomId, ws, message.payload?.initialState);
|
|
3112
|
+
ws.send(JSON.stringify({
|
|
3113
|
+
type: "ROOM_JOINED",
|
|
3114
|
+
componentId,
|
|
3115
|
+
payload: { roomId, state: result.state },
|
|
3116
|
+
requestId: message.requestId,
|
|
3117
|
+
timestamp: Date.now()
|
|
3118
|
+
}));
|
|
3119
|
+
break;
|
|
3120
|
+
}
|
|
3121
|
+
case "ROOM_LEAVE":
|
|
3122
|
+
this.roomManager.leaveRoom(componentId, roomId);
|
|
3123
|
+
ws.send(JSON.stringify({
|
|
3124
|
+
type: "ROOM_LEFT",
|
|
3125
|
+
componentId,
|
|
3126
|
+
payload: { roomId },
|
|
3127
|
+
requestId: message.requestId,
|
|
3128
|
+
timestamp: Date.now()
|
|
3129
|
+
}));
|
|
3130
|
+
break;
|
|
3131
|
+
case "ROOM_EMIT":
|
|
3132
|
+
this.roomManager.emitToRoom(roomId, message.payload?.event, message.payload?.data, componentId);
|
|
3133
|
+
break;
|
|
3134
|
+
case "ROOM_STATE_SET":
|
|
3135
|
+
this.roomManager.setRoomState(roomId, message.payload?.state, componentId);
|
|
3136
|
+
break;
|
|
3137
|
+
case "ROOM_STATE_GET": {
|
|
3138
|
+
const state = this.roomManager.getRoomState(roomId);
|
|
3139
|
+
ws.send(JSON.stringify({
|
|
3140
|
+
type: "ROOM_STATE",
|
|
3141
|
+
componentId,
|
|
3142
|
+
payload: { roomId, state },
|
|
3143
|
+
requestId: message.requestId,
|
|
3144
|
+
timestamp: Date.now()
|
|
3145
|
+
}));
|
|
3146
|
+
break;
|
|
3147
|
+
}
|
|
3148
|
+
}
|
|
3149
|
+
}
|
|
3150
|
+
// ===== HTTP Monitoring Routes =====
|
|
3151
|
+
buildHttpRoutes(prefix) {
|
|
3152
|
+
return [
|
|
3153
|
+
{
|
|
3154
|
+
method: "GET",
|
|
3155
|
+
path: `${prefix}/stats`,
|
|
3156
|
+
handler: async () => ({
|
|
3157
|
+
body: {
|
|
3158
|
+
components: this.registry.getStats(),
|
|
3159
|
+
rooms: this.roomManager.getStats(),
|
|
3160
|
+
connections: this.connectionManager.getSystemStats(),
|
|
3161
|
+
uploads: this.fileUploadManager.getStats(),
|
|
3162
|
+
performance: this.performanceMonitor.getStats()
|
|
3163
|
+
}
|
|
3164
|
+
}),
|
|
3165
|
+
metadata: { summary: "Live Components system statistics", tags: ["live"] }
|
|
3166
|
+
},
|
|
3167
|
+
{
|
|
3168
|
+
method: "GET",
|
|
3169
|
+
path: `${prefix}/components`,
|
|
3170
|
+
handler: async () => ({
|
|
3171
|
+
body: { names: this.registry.getRegisteredComponentNames() }
|
|
3172
|
+
}),
|
|
3173
|
+
metadata: { summary: "List registered component names", tags: ["live"] }
|
|
3174
|
+
},
|
|
3175
|
+
{
|
|
3176
|
+
method: "POST",
|
|
3177
|
+
path: `${prefix}/rooms/:roomId/messages`,
|
|
3178
|
+
handler: async (req) => {
|
|
3179
|
+
const roomId = req.params.roomId;
|
|
3180
|
+
this.roomManager.emitToRoom(roomId, "message:new", req.body);
|
|
3181
|
+
return { body: { success: true, roomId } };
|
|
3182
|
+
},
|
|
3183
|
+
metadata: { summary: "Send message to room via HTTP", tags: ["live", "rooms"] }
|
|
3184
|
+
},
|
|
3185
|
+
{
|
|
3186
|
+
method: "POST",
|
|
3187
|
+
path: `${prefix}/rooms/:roomId/emit`,
|
|
3188
|
+
handler: async (req) => {
|
|
3189
|
+
const roomId = req.params.roomId;
|
|
3190
|
+
const { event, data } = req.body;
|
|
3191
|
+
this.roomManager.emitToRoom(roomId, event, data);
|
|
3192
|
+
return { body: { success: true, roomId, event } };
|
|
3193
|
+
},
|
|
3194
|
+
metadata: { summary: "Emit custom event to room via HTTP", tags: ["live", "rooms"] }
|
|
3195
|
+
}
|
|
3196
|
+
];
|
|
3197
|
+
}
|
|
3198
|
+
};
|
|
3199
|
+
|
|
3200
|
+
// src/rooms/RoomStateManager.ts
|
|
3201
|
+
function createTypedRoomState() {
|
|
3202
|
+
const rooms = /* @__PURE__ */ new Map();
|
|
3203
|
+
const getKey = (type, roomId) => `${type}:${roomId}`;
|
|
3204
|
+
return {
|
|
3205
|
+
get(type, roomId, defaultState) {
|
|
3206
|
+
const key = getKey(type, roomId);
|
|
3207
|
+
const room = rooms.get(key);
|
|
3208
|
+
if (room) return room.state;
|
|
3209
|
+
rooms.set(key, { state: defaultState, componentCount: 0, createdAt: Date.now(), lastUpdate: Date.now() });
|
|
3210
|
+
return defaultState;
|
|
3211
|
+
},
|
|
3212
|
+
update(type, roomId, updates) {
|
|
3213
|
+
const key = getKey(type, roomId);
|
|
3214
|
+
const room = rooms.get(key);
|
|
3215
|
+
if (room) {
|
|
3216
|
+
room.state = { ...room.state, ...updates };
|
|
3217
|
+
room.lastUpdate = Date.now();
|
|
3218
|
+
return room.state;
|
|
3219
|
+
}
|
|
3220
|
+
const newState = updates;
|
|
3221
|
+
rooms.set(key, { state: newState, componentCount: 0, createdAt: Date.now(), lastUpdate: Date.now() });
|
|
3222
|
+
return newState;
|
|
3223
|
+
},
|
|
3224
|
+
set(type, roomId, state) {
|
|
3225
|
+
const key = getKey(type, roomId);
|
|
3226
|
+
const room = rooms.get(key);
|
|
3227
|
+
if (room) {
|
|
3228
|
+
room.state = state;
|
|
3229
|
+
room.lastUpdate = Date.now();
|
|
3230
|
+
} else {
|
|
3231
|
+
rooms.set(key, { state, componentCount: 0, createdAt: Date.now(), lastUpdate: Date.now() });
|
|
3232
|
+
}
|
|
3233
|
+
},
|
|
3234
|
+
join(type, roomId) {
|
|
3235
|
+
const room = rooms.get(getKey(type, roomId));
|
|
3236
|
+
if (room) room.componentCount++;
|
|
3237
|
+
},
|
|
3238
|
+
leave(type, roomId) {
|
|
3239
|
+
const key = getKey(type, roomId);
|
|
3240
|
+
const room = rooms.get(key);
|
|
3241
|
+
if (room) {
|
|
3242
|
+
room.componentCount--;
|
|
3243
|
+
if (room.componentCount <= 0) {
|
|
3244
|
+
setTimeout(() => {
|
|
3245
|
+
const current = rooms.get(key);
|
|
3246
|
+
if (current && current.componentCount <= 0) {
|
|
3247
|
+
rooms.delete(key);
|
|
3248
|
+
}
|
|
3249
|
+
}, 5 * 60 * 1e3);
|
|
3250
|
+
}
|
|
3251
|
+
}
|
|
3252
|
+
},
|
|
3253
|
+
has(type, roomId) {
|
|
3254
|
+
return rooms.has(getKey(type, roomId));
|
|
3255
|
+
},
|
|
3256
|
+
delete(type, roomId) {
|
|
3257
|
+
return rooms.delete(getKey(type, roomId));
|
|
3258
|
+
},
|
|
3259
|
+
getStats() {
|
|
3260
|
+
const roomStats = {};
|
|
3261
|
+
for (const [key, info] of rooms) {
|
|
3262
|
+
roomStats[key] = { componentCount: info.componentCount, stateKeys: Object.keys(info.state) };
|
|
3263
|
+
}
|
|
3264
|
+
return { totalRooms: rooms.size, rooms: roomStats };
|
|
3265
|
+
}
|
|
3266
|
+
};
|
|
3267
|
+
}
|
|
3268
|
+
var RoomStateManager = class {
|
|
3269
|
+
rooms = /* @__PURE__ */ new Map();
|
|
3270
|
+
get(roomId, defaultState) {
|
|
3271
|
+
const room = this.rooms.get(roomId);
|
|
3272
|
+
if (room) return room.state;
|
|
3273
|
+
if (defaultState) {
|
|
3274
|
+
this.rooms.set(roomId, { state: defaultState, componentCount: 0, createdAt: Date.now(), lastUpdate: Date.now() });
|
|
3275
|
+
return defaultState;
|
|
3276
|
+
}
|
|
3277
|
+
return {};
|
|
3278
|
+
}
|
|
3279
|
+
update(roomId, updates) {
|
|
3280
|
+
const room = this.rooms.get(roomId);
|
|
3281
|
+
if (room) {
|
|
3282
|
+
room.state = { ...room.state, ...updates };
|
|
3283
|
+
room.lastUpdate = Date.now();
|
|
3284
|
+
return room.state;
|
|
3285
|
+
}
|
|
3286
|
+
const newState = updates;
|
|
3287
|
+
this.rooms.set(roomId, { state: newState, componentCount: 0, createdAt: Date.now(), lastUpdate: Date.now() });
|
|
3288
|
+
return newState;
|
|
3289
|
+
}
|
|
3290
|
+
set(roomId, state) {
|
|
3291
|
+
const room = this.rooms.get(roomId);
|
|
3292
|
+
if (room) {
|
|
3293
|
+
room.state = state;
|
|
3294
|
+
room.lastUpdate = Date.now();
|
|
3295
|
+
} else {
|
|
3296
|
+
this.rooms.set(roomId, { state, componentCount: 0, createdAt: Date.now(), lastUpdate: Date.now() });
|
|
3297
|
+
}
|
|
3298
|
+
}
|
|
3299
|
+
join(roomId) {
|
|
3300
|
+
const room = this.rooms.get(roomId);
|
|
3301
|
+
if (room) room.componentCount++;
|
|
3302
|
+
}
|
|
3303
|
+
leave(roomId) {
|
|
3304
|
+
const room = this.rooms.get(roomId);
|
|
3305
|
+
if (room) {
|
|
3306
|
+
room.componentCount--;
|
|
3307
|
+
if (room.componentCount <= 0) {
|
|
3308
|
+
setTimeout(() => {
|
|
3309
|
+
const current = this.rooms.get(roomId);
|
|
3310
|
+
if (current && current.componentCount <= 0) {
|
|
3311
|
+
this.rooms.delete(roomId);
|
|
3312
|
+
}
|
|
3313
|
+
}, 5 * 60 * 1e3);
|
|
3314
|
+
}
|
|
3315
|
+
}
|
|
3316
|
+
}
|
|
3317
|
+
has(roomId) {
|
|
3318
|
+
return this.rooms.has(roomId);
|
|
3319
|
+
}
|
|
3320
|
+
delete(roomId) {
|
|
3321
|
+
return this.rooms.delete(roomId);
|
|
3322
|
+
}
|
|
3323
|
+
getStats() {
|
|
3324
|
+
const rooms = {};
|
|
3325
|
+
for (const [roomId, info] of this.rooms) {
|
|
3326
|
+
rooms[roomId] = { componentCount: info.componentCount, stateKeys: Object.keys(info.state) };
|
|
3327
|
+
}
|
|
3328
|
+
return { totalRooms: this.rooms.size, rooms };
|
|
3329
|
+
}
|
|
3330
|
+
};
|
|
3331
|
+
|
|
3332
|
+
export { ANONYMOUS_CONTEXT, AnonymousContext, AuthenticatedContext, ComponentRegistry, ConnectionRateLimiter, DEFAULT_CHUNK_SIZE, DEFAULT_WS_PATH, FileUploadManager, LiveAuthManager, LiveComponent, LiveDebugger, LiveRoomManager, LiveServer, PROTOCOL_VERSION, PerformanceMonitor, RateLimiterRegistry, RoomEventBus, RoomStateManager, StateSignatureManager, WebSocketConnectionManager, createTypedRoomEventBus, createTypedRoomState, decodeBinaryChunk, encodeBinaryChunk, getLiveComponentContext, liveLog, liveWarn, registerComponentLogging, setLiveComponentContext, unregisterComponentLogging };
|
|
3333
|
+
//# sourceMappingURL=index.js.map
|
|
3334
|
+
//# sourceMappingURL=index.js.map
|