parse-dashboard-analytics 7.4.1 → 7.4.4
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/Parse-Dashboard/Authentication.js +9 -4
- package/Parse-Dashboard/CLI/mfa.js +11 -5
- package/Parse-Dashboard/app.js +89 -20
- package/Parse-Dashboard/browser-control/BrowserControlAPI.js +468 -0
- package/Parse-Dashboard/browser-control/BrowserEventStream.js +294 -0
- package/Parse-Dashboard/browser-control/BrowserSessionManager.js +304 -0
- package/Parse-Dashboard/browser-control/README.md +852 -0
- package/Parse-Dashboard/browser-control/ServerOrchestrator.js +334 -0
- package/Parse-Dashboard/browser-control/index.js +27 -0
- package/Parse-Dashboard/browser-control/setup.js +405 -0
- package/Parse-Dashboard/public/bundles/120.bundle.js +1 -1
- package/Parse-Dashboard/public/bundles/183.bundle.js +1 -1
- package/Parse-Dashboard/public/bundles/19.bundle.js +1 -1
- package/Parse-Dashboard/public/bundles/221.bundle.js +1 -1
- package/Parse-Dashboard/public/bundles/372.bundle.js +1 -1
- package/Parse-Dashboard/public/bundles/448.bundle.js +1 -1
- package/Parse-Dashboard/public/bundles/573.bundle.js +1 -1
- package/Parse-Dashboard/public/bundles/578.bundle.js +1 -1
- package/Parse-Dashboard/public/bundles/584.bundle.js +1 -1
- package/Parse-Dashboard/public/bundles/629.bundle.js +1 -1
- package/Parse-Dashboard/public/bundles/643.bundle.js +1 -1
- package/Parse-Dashboard/public/bundles/647.bundle.js +1 -1
- package/Parse-Dashboard/public/bundles/701.bundle.js +1 -1
- package/Parse-Dashboard/public/bundles/729.bundle.js +1 -1
- package/Parse-Dashboard/public/bundles/75.bundle.js +1 -1
- package/Parse-Dashboard/public/bundles/753.bundle.js +1 -1
- package/Parse-Dashboard/public/bundles/817.bundle.js +1 -1
- package/Parse-Dashboard/public/bundles/844.bundle.js +1 -1
- package/Parse-Dashboard/public/bundles/862.bundle.js +1 -1
- package/Parse-Dashboard/public/bundles/866.bundle.js +1 -1
- package/Parse-Dashboard/public/bundles/874.bundle.js +1 -1
- package/Parse-Dashboard/public/bundles/881.bundle.js +1 -1
- package/Parse-Dashboard/public/bundles/929.bundle.js +1 -1
- package/Parse-Dashboard/public/bundles/950.bundle.js +1 -1
- package/Parse-Dashboard/public/bundles/97.bundle.js +1 -1
- package/Parse-Dashboard/public/bundles/976.bundle.js +1 -1
- package/Parse-Dashboard/public/bundles/dashboard.bundle.js +1 -1
- package/Parse-Dashboard/public/bundles/dashboard.bundle.js.LICENSE.txt +27 -33
- package/Parse-Dashboard/public/bundles/login.bundle.js +1 -1
- package/Parse-Dashboard/public/bundles/sprites.svg +294 -154
- package/Parse-Dashboard/server.js +33 -2
- package/README.md +475 -117
- package/package.json +68 -53
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
const WebSocket = require('ws');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* BrowserEventStream
|
|
5
|
+
*
|
|
6
|
+
* WebSocket server for streaming real-time browser events to AI agents.
|
|
7
|
+
* Broadcasts console logs, network activity, navigation events, and errors.
|
|
8
|
+
*
|
|
9
|
+
* Features:
|
|
10
|
+
* - Real-time event streaming via WebSocket
|
|
11
|
+
* - Per-session event filtering
|
|
12
|
+
* - Automatic cleanup on disconnect
|
|
13
|
+
* - Support for multiple clients per session
|
|
14
|
+
*/
|
|
15
|
+
class BrowserEventStream {
|
|
16
|
+
/**
|
|
17
|
+
* @param {http.Server} httpServer - HTTP server to attach WebSocket to
|
|
18
|
+
* @param {BrowserSessionManager} sessionManager - Session manager instance
|
|
19
|
+
*/
|
|
20
|
+
constructor(httpServer, sessionManager) {
|
|
21
|
+
this.sessionManager = sessionManager;
|
|
22
|
+
this.clients = new Map(); // sessionId -> Set of WebSocket clients
|
|
23
|
+
|
|
24
|
+
// Create WebSocket server without path (we'll handle routing manually)
|
|
25
|
+
this.wss = new WebSocket.Server({
|
|
26
|
+
noServer: true,
|
|
27
|
+
clientTracking: true
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
// Handle WebSocket upgrade requests manually to support dynamic paths
|
|
31
|
+
// Path pattern: /browser-control/stream/:sessionId
|
|
32
|
+
httpServer.on('upgrade', (request, socket, head) => {
|
|
33
|
+
const pathname = new URL(request.url, 'http://localhost').pathname;
|
|
34
|
+
|
|
35
|
+
// Check if this is a browser-control stream request
|
|
36
|
+
if (pathname.startsWith('/browser-control/stream/')) {
|
|
37
|
+
this.wss.handleUpgrade(request, socket, head, (ws) => {
|
|
38
|
+
this.wss.emit('connection', ws, request);
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
// Let other upgrade requests pass through (don't destroy socket)
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
// Handle new WebSocket connections
|
|
45
|
+
this.wss.on('connection', (ws, req) => {
|
|
46
|
+
this.handleConnection(ws, req);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
// Listen to session manager events
|
|
50
|
+
this.attachSessionListeners();
|
|
51
|
+
|
|
52
|
+
console.log('Browser Event Stream WebSocket server initialized');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Handle new WebSocket connection
|
|
57
|
+
* @private
|
|
58
|
+
*/
|
|
59
|
+
handleConnection(ws, req) {
|
|
60
|
+
// Extract session ID from URL path
|
|
61
|
+
// Expected format: /browser-control/stream/sess_abc123
|
|
62
|
+
const sessionId = this.extractSessionId(req.url);
|
|
63
|
+
|
|
64
|
+
if (!sessionId) {
|
|
65
|
+
ws.close(1008, 'Session ID required in URL path: /browser-control/stream/:sessionId');
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Validate session exists
|
|
70
|
+
if (!this.sessionManager.hasSession(sessionId)) {
|
|
71
|
+
ws.close(1008, `Invalid session ID: ${sessionId}`);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Track client for this session
|
|
76
|
+
if (!this.clients.has(sessionId)) {
|
|
77
|
+
this.clients.set(sessionId, new Set());
|
|
78
|
+
}
|
|
79
|
+
this.clients.get(sessionId).add(ws);
|
|
80
|
+
|
|
81
|
+
console.log(`Client connected to session ${sessionId} stream`);
|
|
82
|
+
|
|
83
|
+
// Send welcome message
|
|
84
|
+
this.sendToClient(ws, {
|
|
85
|
+
type: 'connected',
|
|
86
|
+
sessionId,
|
|
87
|
+
timestamp: Date.now(),
|
|
88
|
+
message: 'Connected to browser event stream'
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
// Handle client messages (optional commands)
|
|
92
|
+
ws.on('message', (data) => {
|
|
93
|
+
this.handleClientMessage(ws, sessionId, data);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
// Handle disconnect
|
|
97
|
+
ws.on('close', () => {
|
|
98
|
+
const clients = this.clients.get(sessionId);
|
|
99
|
+
if (clients) {
|
|
100
|
+
clients.delete(ws);
|
|
101
|
+
if (clients.size === 0) {
|
|
102
|
+
this.clients.delete(sessionId);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
console.log(`Client disconnected from session ${sessionId} stream`);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
// Handle errors
|
|
109
|
+
ws.on('error', (error) => {
|
|
110
|
+
console.error(`WebSocket error for session ${sessionId}:`, error.message);
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Extract session ID from WebSocket URL
|
|
116
|
+
* @private
|
|
117
|
+
*/
|
|
118
|
+
extractSessionId(url) {
|
|
119
|
+
// URL format: /browser-control/stream/sess_abc123?query=params
|
|
120
|
+
const match = url.match(/\/browser-control\/stream\/([^?]+)/);
|
|
121
|
+
return match ? match[1] : null;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Handle messages from clients
|
|
126
|
+
* @private
|
|
127
|
+
*/
|
|
128
|
+
handleClientMessage(ws, sessionId, data) {
|
|
129
|
+
try {
|
|
130
|
+
const message = JSON.parse(data.toString());
|
|
131
|
+
|
|
132
|
+
// Optional: Support simple commands via WebSocket
|
|
133
|
+
if (message.action === 'ping') {
|
|
134
|
+
this.sendToClient(ws, {
|
|
135
|
+
type: 'pong',
|
|
136
|
+
timestamp: Date.now()
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
} catch (error) {
|
|
140
|
+
console.error('Failed to parse client message:', error.message);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Attach listeners to session manager events
|
|
146
|
+
* @private
|
|
147
|
+
*/
|
|
148
|
+
attachSessionListeners() {
|
|
149
|
+
// Console messages
|
|
150
|
+
this.sessionManager.on('console', (sessionId, data) => {
|
|
151
|
+
this.broadcast(sessionId, {
|
|
152
|
+
type: 'console',
|
|
153
|
+
level: data.level,
|
|
154
|
+
text: data.text,
|
|
155
|
+
timestamp: data.timestamp
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
// Page errors
|
|
160
|
+
this.sessionManager.on('error', (sessionId, data) => {
|
|
161
|
+
this.broadcast(sessionId, {
|
|
162
|
+
type: 'error',
|
|
163
|
+
message: data.message,
|
|
164
|
+
stack: data.stack,
|
|
165
|
+
timestamp: data.timestamp
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
// Network requests
|
|
170
|
+
this.sessionManager.on('network', (sessionId, data) => {
|
|
171
|
+
this.broadcast(sessionId, {
|
|
172
|
+
type: 'network',
|
|
173
|
+
method: data.method,
|
|
174
|
+
url: data.url,
|
|
175
|
+
status: data.status,
|
|
176
|
+
timestamp: data.timestamp
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
// Page navigation
|
|
181
|
+
this.sessionManager.on('navigation', (sessionId, data) => {
|
|
182
|
+
this.broadcast(sessionId, {
|
|
183
|
+
type: 'navigation',
|
|
184
|
+
url: data.url,
|
|
185
|
+
timestamp: data.timestamp
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
// Session events
|
|
190
|
+
this.sessionManager.on('session-created', ({ sessionId }) => {
|
|
191
|
+
this.broadcast(sessionId, {
|
|
192
|
+
type: 'session-event',
|
|
193
|
+
event: 'created',
|
|
194
|
+
sessionId,
|
|
195
|
+
timestamp: Date.now()
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
this.sessionManager.on('session-cleaned', ({ sessionId }) => {
|
|
200
|
+
this.broadcast(sessionId, {
|
|
201
|
+
type: 'session-event',
|
|
202
|
+
event: 'cleaned',
|
|
203
|
+
sessionId,
|
|
204
|
+
timestamp: Date.now()
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
// Close all client connections for this session
|
|
208
|
+
const clients = this.clients.get(sessionId);
|
|
209
|
+
if (clients) {
|
|
210
|
+
clients.forEach(ws => {
|
|
211
|
+
ws.close(1000, 'Session closed');
|
|
212
|
+
});
|
|
213
|
+
this.clients.delete(sessionId);
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
this.sessionManager.on('session-crashed', ({ sessionId }) => {
|
|
218
|
+
this.broadcast(sessionId, {
|
|
219
|
+
type: 'session-event',
|
|
220
|
+
event: 'crashed',
|
|
221
|
+
sessionId,
|
|
222
|
+
timestamp: Date.now()
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Broadcast a message to all clients watching a session
|
|
229
|
+
* @param {string} sessionId - Session ID
|
|
230
|
+
* @param {Object} message - Message to broadcast
|
|
231
|
+
*/
|
|
232
|
+
broadcast(sessionId, message) {
|
|
233
|
+
const clients = this.clients.get(sessionId);
|
|
234
|
+
if (!clients || clients.size === 0) {
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const payload = JSON.stringify(message);
|
|
239
|
+
|
|
240
|
+
clients.forEach(client => {
|
|
241
|
+
if (client.readyState === WebSocket.OPEN) {
|
|
242
|
+
client.send(payload);
|
|
243
|
+
}
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Send a message to a specific client
|
|
249
|
+
* @private
|
|
250
|
+
*/
|
|
251
|
+
sendToClient(ws, message) {
|
|
252
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
253
|
+
ws.send(JSON.stringify(message));
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Get count of active connections per session
|
|
259
|
+
* @returns {Map} - sessionId -> client count
|
|
260
|
+
*/
|
|
261
|
+
getConnectionCounts() {
|
|
262
|
+
const counts = new Map();
|
|
263
|
+
for (const [sessionId, clients] of this.clients.entries()) {
|
|
264
|
+
counts.set(sessionId, clients.size);
|
|
265
|
+
}
|
|
266
|
+
return counts;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Close all WebSocket connections
|
|
271
|
+
*/
|
|
272
|
+
closeAll() {
|
|
273
|
+
this.wss.clients.forEach(client => {
|
|
274
|
+
client.close(1000, 'Server shutting down');
|
|
275
|
+
});
|
|
276
|
+
this.clients.clear();
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Shutdown the WebSocket server
|
|
281
|
+
* @returns {Promise<void>}
|
|
282
|
+
*/
|
|
283
|
+
async shutdown() {
|
|
284
|
+
return new Promise((resolve) => {
|
|
285
|
+
this.closeAll();
|
|
286
|
+
this.wss.close(() => {
|
|
287
|
+
console.log('Browser Event Stream shut down');
|
|
288
|
+
resolve();
|
|
289
|
+
});
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
module.exports = BrowserEventStream;
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
const EventEmitter = require('events');
|
|
2
|
+
const puppeteer = require('puppeteer');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* BrowserSessionManager
|
|
6
|
+
*
|
|
7
|
+
* Manages persistent Puppeteer browser instances for AI agent verification.
|
|
8
|
+
* Each session represents an isolated browser with its own page and event listeners.
|
|
9
|
+
*
|
|
10
|
+
* Features:
|
|
11
|
+
* - Create/manage multiple browser sessions
|
|
12
|
+
* - Track session activity with automatic timeout
|
|
13
|
+
* - Capture console logs, errors, and network events
|
|
14
|
+
* - Handle browser crashes with recovery logic
|
|
15
|
+
*/
|
|
16
|
+
class BrowserSessionManager extends EventEmitter {
|
|
17
|
+
constructor(options = {}) {
|
|
18
|
+
super();
|
|
19
|
+
this.sessions = new Map(); // sessionId -> { browser, page, metadata }
|
|
20
|
+
this.sessionTimeout = options.sessionTimeout || 30 * 60 * 1000; // 30 minutes default
|
|
21
|
+
this.maxSessions = options.maxSessions || 5;
|
|
22
|
+
|
|
23
|
+
// Start cleanup interval to remove inactive sessions
|
|
24
|
+
this.cleanupInterval = setInterval(() => {
|
|
25
|
+
this.cleanupInactiveSessions();
|
|
26
|
+
}, 60000); // Check every minute
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Create a new browser session
|
|
31
|
+
* @param {Object} options - Configuration options
|
|
32
|
+
* @param {boolean} options.headless - Run in headless mode (default: true)
|
|
33
|
+
* @param {number} options.width - Browser width (default: 1280)
|
|
34
|
+
* @param {number} options.height - Browser height (default: 720)
|
|
35
|
+
* @param {number} options.slowMo - Slow down operations by N milliseconds
|
|
36
|
+
* @returns {Promise<Object>} - { sessionId, page, browser }
|
|
37
|
+
*/
|
|
38
|
+
async createSession(options = {}) {
|
|
39
|
+
// Check session limit
|
|
40
|
+
if (this.sessions.size >= this.maxSessions) {
|
|
41
|
+
throw new Error(`Maximum number of sessions (${this.maxSessions}) reached`);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const headless = options.headless ?? (process.env.BROWSER_HEADLESS !== 'false');
|
|
45
|
+
const slowMo = options.slowMo || parseInt(process.env.BROWSER_SLOW_MO) || 0;
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
// Launch Puppeteer browser
|
|
49
|
+
const browser = await puppeteer.launch({
|
|
50
|
+
headless: headless ? 'new' : false,
|
|
51
|
+
args: [
|
|
52
|
+
'--no-sandbox',
|
|
53
|
+
'--disable-setuid-sandbox',
|
|
54
|
+
'--disable-dev-shm-usage'
|
|
55
|
+
],
|
|
56
|
+
defaultViewport: {
|
|
57
|
+
width: options.width || 1280,
|
|
58
|
+
height: options.height || 720
|
|
59
|
+
},
|
|
60
|
+
slowMo
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
// Create new page
|
|
64
|
+
const page = await browser.newPage();
|
|
65
|
+
|
|
66
|
+
// Generate unique session ID
|
|
67
|
+
const sessionId = `sess_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
68
|
+
|
|
69
|
+
// Set up event listeners
|
|
70
|
+
this.attachPageListeners(sessionId, page);
|
|
71
|
+
|
|
72
|
+
// Handle browser disconnect/crash
|
|
73
|
+
browser.on('disconnected', () => {
|
|
74
|
+
this.handleBrowserCrash(sessionId);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
// Store session
|
|
78
|
+
const sessionData = {
|
|
79
|
+
browser,
|
|
80
|
+
page,
|
|
81
|
+
createdAt: Date.now(),
|
|
82
|
+
lastActivity: Date.now(),
|
|
83
|
+
options,
|
|
84
|
+
crashed: false
|
|
85
|
+
};
|
|
86
|
+
this.sessions.set(sessionId, sessionData);
|
|
87
|
+
|
|
88
|
+
this.emit('session-created', { sessionId, options });
|
|
89
|
+
|
|
90
|
+
return { sessionId, page, browser };
|
|
91
|
+
} catch (error) {
|
|
92
|
+
throw new Error(`Failed to create browser session: ${error.message}`);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Attach event listeners to page for logging and debugging
|
|
98
|
+
* @private
|
|
99
|
+
*/
|
|
100
|
+
attachPageListeners(sessionId, page) {
|
|
101
|
+
// Console messages
|
|
102
|
+
page.on('console', msg => {
|
|
103
|
+
const data = {
|
|
104
|
+
level: msg.type(),
|
|
105
|
+
text: msg.text(),
|
|
106
|
+
timestamp: Date.now()
|
|
107
|
+
};
|
|
108
|
+
this.emit('console', sessionId, data);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
// Page errors
|
|
112
|
+
page.on('pageerror', error => {
|
|
113
|
+
const data = {
|
|
114
|
+
message: error.message,
|
|
115
|
+
stack: error.stack,
|
|
116
|
+
timestamp: Date.now()
|
|
117
|
+
};
|
|
118
|
+
this.emit('error', sessionId, data);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
// Network requests
|
|
122
|
+
page.on('requestfinished', request => {
|
|
123
|
+
const response = request.response();
|
|
124
|
+
if (response) {
|
|
125
|
+
const data = {
|
|
126
|
+
method: request.method(),
|
|
127
|
+
url: request.url(),
|
|
128
|
+
status: response.status(),
|
|
129
|
+
timestamp: Date.now()
|
|
130
|
+
};
|
|
131
|
+
this.emit('network', sessionId, data);
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
// Page navigation
|
|
136
|
+
page.on('framenavigated', frame => {
|
|
137
|
+
if (frame === page.mainFrame()) {
|
|
138
|
+
const data = {
|
|
139
|
+
url: frame.url(),
|
|
140
|
+
timestamp: Date.now()
|
|
141
|
+
};
|
|
142
|
+
this.emit('navigation', sessionId, data);
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Get an existing session and update its last activity time
|
|
149
|
+
* @param {string} sessionId - The session ID
|
|
150
|
+
* @returns {Object} - Session data
|
|
151
|
+
* @throws {Error} - If session not found
|
|
152
|
+
*/
|
|
153
|
+
getSession(sessionId) {
|
|
154
|
+
const session = this.sessions.get(sessionId);
|
|
155
|
+
if (!session) {
|
|
156
|
+
throw new Error(`Session ${sessionId} not found or expired`);
|
|
157
|
+
}
|
|
158
|
+
if (session.crashed) {
|
|
159
|
+
throw new Error(`Session ${sessionId} has crashed`);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Update last activity
|
|
163
|
+
session.lastActivity = Date.now();
|
|
164
|
+
return session;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Get all active sessions
|
|
169
|
+
* @returns {Array} - Array of session info objects
|
|
170
|
+
*/
|
|
171
|
+
getAllSessions() {
|
|
172
|
+
const sessions = [];
|
|
173
|
+
for (const [sessionId, data] of this.sessions.entries()) {
|
|
174
|
+
sessions.push({
|
|
175
|
+
sessionId,
|
|
176
|
+
createdAt: data.createdAt,
|
|
177
|
+
lastActivity: data.lastActivity,
|
|
178
|
+
uptime: Date.now() - data.createdAt,
|
|
179
|
+
crashed: data.crashed,
|
|
180
|
+
pageUrl: data.page?.url?.() || 'unknown'
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
return sessions;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Check if a session exists
|
|
188
|
+
* @param {string} sessionId - The session ID
|
|
189
|
+
* @returns {boolean}
|
|
190
|
+
*/
|
|
191
|
+
hasSession(sessionId) {
|
|
192
|
+
return this.sessions.has(sessionId);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Clean up a specific session
|
|
197
|
+
* @param {string} sessionId - The session ID
|
|
198
|
+
* @returns {Promise<boolean>} - True if session was cleaned up
|
|
199
|
+
*/
|
|
200
|
+
async cleanup(sessionId) {
|
|
201
|
+
const session = this.sessions.get(sessionId);
|
|
202
|
+
if (!session) {
|
|
203
|
+
return false;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
try {
|
|
207
|
+
// Close page first
|
|
208
|
+
if (session.page && !session.page.isClosed()) {
|
|
209
|
+
await session.page.close();
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Close browser
|
|
213
|
+
if (session.browser && session.browser.isConnected()) {
|
|
214
|
+
await session.browser.close();
|
|
215
|
+
}
|
|
216
|
+
} catch (error) {
|
|
217
|
+
console.error(`Error cleaning up session ${sessionId}:`, error.message);
|
|
218
|
+
} finally {
|
|
219
|
+
// Remove from sessions map
|
|
220
|
+
this.sessions.delete(sessionId);
|
|
221
|
+
this.emit('session-cleaned', { sessionId });
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return true;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Clean up all sessions
|
|
229
|
+
* @returns {Promise<void>}
|
|
230
|
+
*/
|
|
231
|
+
async cleanupAll() {
|
|
232
|
+
const sessionIds = Array.from(this.sessions.keys());
|
|
233
|
+
await Promise.all(sessionIds.map(id => this.cleanup(id)));
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Clean up inactive sessions based on timeout
|
|
238
|
+
* @private
|
|
239
|
+
*/
|
|
240
|
+
cleanupInactiveSessions() {
|
|
241
|
+
const now = Date.now();
|
|
242
|
+
|
|
243
|
+
// Collect inactive session IDs first to avoid mutating this.sessions during iteration
|
|
244
|
+
const inactiveSessionIds = [];
|
|
245
|
+
for (const [sessionId, session] of this.sessions.entries()) {
|
|
246
|
+
if (now - session.lastActivity > this.sessionTimeout) {
|
|
247
|
+
inactiveSessionIds.push(sessionId);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Clean up collected sessions outside the iteration
|
|
252
|
+
if (inactiveSessionIds.length > 0) {
|
|
253
|
+
console.log(`Cleaning up ${inactiveSessionIds.length} inactive session(s)`);
|
|
254
|
+
|
|
255
|
+
Promise.allSettled(
|
|
256
|
+
inactiveSessionIds.map(sessionId => this.cleanup(sessionId))
|
|
257
|
+
).then(results => {
|
|
258
|
+
results.forEach((result, index) => {
|
|
259
|
+
if (result.status === 'rejected') {
|
|
260
|
+
console.error(`Failed to cleanup session ${inactiveSessionIds[index]}:`, result.reason);
|
|
261
|
+
}
|
|
262
|
+
});
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Handle browser crash
|
|
269
|
+
* @private
|
|
270
|
+
*/
|
|
271
|
+
handleBrowserCrash(sessionId) {
|
|
272
|
+
const session = this.sessions.get(sessionId);
|
|
273
|
+
if (session) {
|
|
274
|
+
session.crashed = true;
|
|
275
|
+
this.emit('session-crashed', { sessionId });
|
|
276
|
+
console.error(`Browser session ${sessionId} crashed`);
|
|
277
|
+
|
|
278
|
+
// Clean up after a short delay
|
|
279
|
+
setTimeout(() => {
|
|
280
|
+
this.cleanup(sessionId).catch(err => {
|
|
281
|
+
console.error(`Failed to cleanup crashed session ${sessionId}:`, err);
|
|
282
|
+
});
|
|
283
|
+
}, 5000);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Shutdown the session manager and clean up all sessions
|
|
289
|
+
* @returns {Promise<void>}
|
|
290
|
+
*/
|
|
291
|
+
async shutdown() {
|
|
292
|
+
// Stop cleanup interval
|
|
293
|
+
if (this.cleanupInterval) {
|
|
294
|
+
clearInterval(this.cleanupInterval);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// Clean up all sessions
|
|
298
|
+
await this.cleanupAll();
|
|
299
|
+
|
|
300
|
+
this.emit('shutdown');
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
module.exports = BrowserSessionManager;
|