2024-06-01 08:56:16 +05:30
|
|
|
import { RemoteBrowser } from "./RemoteBrowser";
|
|
|
|
|
import logger from "../../logger";
|
|
|
|
|
|
2024-06-01 08:56:52 +05:30
|
|
|
/**
|
|
|
|
|
* @category Types
|
|
|
|
|
*/
|
2024-06-01 08:56:16 +05:30
|
|
|
interface BrowserPoolInfo {
|
2024-06-01 08:56:52 +05:30
|
|
|
/**
|
|
|
|
|
* The instance of remote browser.
|
|
|
|
|
*/
|
2024-06-01 08:56:16 +05:30
|
|
|
browser: RemoteBrowser,
|
2024-06-01 08:56:52 +05:30
|
|
|
/**
|
|
|
|
|
* States if the browser's instance is being actively used.
|
|
|
|
|
* Helps to persist the progress on the frontend when the application has been reloaded.
|
|
|
|
|
* @default false
|
|
|
|
|
*/
|
2024-06-01 08:56:16 +05:30
|
|
|
active: boolean,
|
2024-06-01 10:24:20 +05:30
|
|
|
/**
|
2025-03-07 22:40:07 +05:30
|
|
|
* The user ID that owns this browser instance.
|
2024-06-01 10:24:20 +05:30
|
|
|
*/
|
2025-03-07 22:40:07 +05:30
|
|
|
userId: string,
|
2024-06-01 10:24:20 +05:30
|
|
|
}
|
2025-03-07 22:40:07 +05:30
|
|
|
|
2025-03-07 22:40:34 +05:30
|
|
|
/**
|
|
|
|
|
* Dictionary of all the active remote browser's instances indexed by their id.
|
|
|
|
|
* The value in this dictionary is of type BrowserPoolInfo,
|
|
|
|
|
* which provides additional information about the browser's usage.
|
|
|
|
|
* @category Types
|
|
|
|
|
*/
|
|
|
|
|
interface PoolDictionary {
|
|
|
|
|
[key: string]: BrowserPoolInfo,
|
|
|
|
|
}
|
|
|
|
|
|
2025-03-07 22:41:03 +05:30
|
|
|
/**
|
|
|
|
|
* A browser pool is a collection of remote browsers that are initialized and ready to be used.
|
|
|
|
|
* Enforces a "1 User - 1 Browser" policy, while allowing multiple users to have their own browser instances.
|
|
|
|
|
* Adds the possibility to add, remove and retrieve remote browsers from the pool.
|
|
|
|
|
* @category BrowserManagement
|
|
|
|
|
*/
|
|
|
|
|
export class BrowserPool {
|
|
|
|
|
/**
|
|
|
|
|
* Holds all the instances of remote browsers.
|
|
|
|
|
*/
|
|
|
|
|
private pool: PoolDictionary = {};
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Maps user IDs to their browser IDs.
|
|
|
|
|
*/
|
|
|
|
|
private userToBrowserMap: Map<string, string> = new Map();
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Adds a remote browser instance to the pool for a specific user.
|
|
|
|
|
* If the user already has a browser, the existing browser will be closed and replaced.
|
|
|
|
|
*
|
|
|
|
|
* @param id remote browser instance's id
|
|
|
|
|
* @param browser remote browser instance
|
|
|
|
|
* @param userId the user ID that owns this browser instance
|
|
|
|
|
* @param active states if the browser's instance is being actively used
|
|
|
|
|
* @returns true if a new browser was added, false if an existing browser was replaced
|
|
|
|
|
*/
|
2025-03-07 22:41:25 +05:30
|
|
|
public addRemoteBrowser = (
|
|
|
|
|
id: string,
|
|
|
|
|
browser: RemoteBrowser,
|
|
|
|
|
userId: string,
|
|
|
|
|
active: boolean = false
|
|
|
|
|
): boolean => {
|
|
|
|
|
// Check if user already has a browser
|
|
|
|
|
const existingBrowserId = this.userToBrowserMap.get(userId);
|
|
|
|
|
let replaced = false;
|
|
|
|
|
|
|
|
|
|
if (existingBrowserId) {
|
|
|
|
|
// Close and remove the existing browser
|
|
|
|
|
if (existingBrowserId !== id) {
|
|
|
|
|
this.closeAndDeleteBrowser(existingBrowserId);
|
|
|
|
|
replaced = true;
|
|
|
|
|
} else {
|
|
|
|
|
// If it's the same browser ID, just update the info
|
|
|
|
|
this.pool[id] = {
|
|
|
|
|
browser,
|
|
|
|
|
active,
|
|
|
|
|
userId,
|
|
|
|
|
};
|
|
|
|
|
logger.log('debug', `Updated existing browser with id: ${id} for user: ${userId}`);
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Add the new browser to the pool
|
|
|
|
|
this.pool[id] = {
|
|
|
|
|
browser,
|
|
|
|
|
active,
|
|
|
|
|
userId,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Update the user-to-browser mapping
|
|
|
|
|
this.userToBrowserMap.set(userId, id);
|
|
|
|
|
|
|
|
|
|
logger.log('debug', `Remote browser with id: ${id} added to the pool for user: ${userId}`);
|
|
|
|
|
return !replaced;
|
|
|
|
|
};
|
|
|
|
|
|
2025-03-07 22:41:47 +05:30
|
|
|
/**
|
|
|
|
|
* Removes the remote browser instance from the pool.
|
|
|
|
|
* Note: This doesn't handle browser closing as RemoteBrowser doesn't expose a close method.
|
|
|
|
|
* The caller should ensure the browser is properly closed before calling this method.
|
|
|
|
|
*
|
|
|
|
|
* @param id remote browser instance's id
|
|
|
|
|
* @returns true if the browser was removed successfully, false otherwise
|
|
|
|
|
*/
|
|
|
|
|
public closeAndDeleteBrowser = (id: string): boolean => {
|
|
|
|
|
if (!this.pool[id]) {
|
|
|
|
|
logger.log('warn', `Remote browser with id: ${id} does not exist in the pool`);
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Remove the user-to-browser mapping
|
|
|
|
|
const userId = this.pool[id].userId;
|
|
|
|
|
if (this.userToBrowserMap.get(userId) === id) {
|
|
|
|
|
this.userToBrowserMap.delete(userId);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Remove from pool
|
|
|
|
|
delete this.pool[id];
|
|
|
|
|
logger.log('debug', `Remote browser with id: ${id} removed from the pool`);
|
|
|
|
|
return true;
|
|
|
|
|
};
|
|
|
|
|
|
2025-03-07 22:42:07 +05:30
|
|
|
/**
|
|
|
|
|
* Removes the remote browser instance from the pool without attempting to close it.
|
|
|
|
|
*
|
|
|
|
|
* @param id remote browser instance's id
|
|
|
|
|
* @returns true if the browser was removed successfully, false otherwise
|
|
|
|
|
*/
|
|
|
|
|
public deleteRemoteBrowser = (id: string): boolean => {
|
|
|
|
|
if (!this.pool[id]) {
|
|
|
|
|
logger.log('warn', `Remote browser with id: ${id} does not exist in the pool`);
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Remove the user-to-browser mapping
|
|
|
|
|
const userId = this.pool[id].userId;
|
|
|
|
|
if (this.userToBrowserMap.get(userId) === id) {
|
|
|
|
|
this.userToBrowserMap.delete(userId);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Remove from pool
|
|
|
|
|
delete this.pool[id];
|
|
|
|
|
logger.log('debug', `Remote browser with id: ${id} deleted from the pool`);
|
|
|
|
|
return true;
|
|
|
|
|
};
|
|
|
|
|
|
2025-03-07 22:42:23 +05:30
|
|
|
/**
|
|
|
|
|
* Returns the remote browser instance from the pool.
|
|
|
|
|
*
|
|
|
|
|
* @param id remote browser instance's id
|
|
|
|
|
* @returns remote browser instance or undefined if it does not exist in the pool
|
|
|
|
|
*/
|
|
|
|
|
public getRemoteBrowser = (id: string): RemoteBrowser | undefined => {
|
|
|
|
|
logger.log('debug', `Remote browser with id: ${id} retrieved from the pool`);
|
|
|
|
|
return this.pool[id]?.browser;
|
|
|
|
|
};
|
|
|
|
|
|
2025-03-07 22:42:49 +05:30
|
|
|
/**
|
|
|
|
|
* Returns the active browser's instance id for a specific user.
|
|
|
|
|
*
|
|
|
|
|
* @param userId the user ID to find the browser for
|
|
|
|
|
* @returns the browser ID for the user, or null if no browser exists
|
|
|
|
|
*/
|
|
|
|
|
public getActiveBrowserId = (userId: string): string | null => {
|
|
|
|
|
const browserId = this.userToBrowserMap.get(userId);
|
|
|
|
|
if (!browserId) {
|
|
|
|
|
logger.log('debug', `No browser found for user: ${userId}`);
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Verify the browser still exists in the pool
|
|
|
|
|
if (!this.pool[browserId]) {
|
|
|
|
|
this.userToBrowserMap.delete(userId);
|
|
|
|
|
logger.log('warn', `Browser mapping found for user: ${userId}, but browser doesn't exist in pool`);
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
console.log(`Browser Id ${browserId} found for user: ${userId}`);
|
|
|
|
|
return browserId;
|
|
|
|
|
};
|
|
|
|
|
|
2025-03-07 22:43:06 +05:30
|
|
|
/**
|
|
|
|
|
* Returns the user ID associated with a browser ID.
|
|
|
|
|
*
|
|
|
|
|
* @param browserId the browser ID to find the user for
|
|
|
|
|
* @returns the user ID for the browser, or null if the browser doesn't exist
|
|
|
|
|
*/
|
|
|
|
|
public getUserForBrowser = (browserId: string): string | null => {
|
|
|
|
|
if (!this.pool[browserId]) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
return this.pool[browserId].userId;
|
|
|
|
|
};
|
|
|
|
|
|
2025-03-07 22:43:45 +05:30
|
|
|
/**
|
|
|
|
|
* Sets the active state of a browser.
|
|
|
|
|
*
|
|
|
|
|
* @param id the browser ID
|
|
|
|
|
* @param active the new active state
|
|
|
|
|
* @returns true if successful, false if the browser wasn't found
|
|
|
|
|
*/
|
|
|
|
|
public setActiveBrowser = (id: string, active: boolean): boolean => {
|
|
|
|
|
if (!this.pool[id]) {
|
|
|
|
|
logger.log('warn', `Remote browser with id: ${id} does not exist in the pool`);
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.pool[id].active = active;
|
|
|
|
|
logger.log('debug', `Remote browser with id: ${id} set to ${active ? 'active' : 'inactive'}`);
|
|
|
|
|
return true;
|
|
|
|
|
};
|
|
|
|
|
|
2025-03-07 22:44:05 +05:30
|
|
|
/**
|
|
|
|
|
* Returns all browser instances for a specific user.
|
|
|
|
|
* Should only be one per the "1 User - 1 Browser" policy, but included for flexibility.
|
|
|
|
|
*
|
|
|
|
|
* @param userId the user ID to find browsers for
|
|
|
|
|
* @returns an array of browser IDs belonging to the user
|
|
|
|
|
*/
|
|
|
|
|
public getAllBrowserIdsForUser = (userId: string): string[] => {
|
|
|
|
|
const browserIds: string[] = [];
|
|
|
|
|
|
|
|
|
|
// Normally this would just return the one browser from the map
|
|
|
|
|
const mappedBrowserId = this.userToBrowserMap.get(userId);
|
|
|
|
|
if (mappedBrowserId && this.pool[mappedBrowserId]) {
|
|
|
|
|
browserIds.push(mappedBrowserId);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// But as a safeguard, also check the entire pool for any browsers assigned to this user
|
|
|
|
|
// This helps detect and fix any inconsistencies in the maps
|
|
|
|
|
for (const [id, info] of Object.entries(this.pool)) {
|
|
|
|
|
if (info.userId === userId && !browserIds.includes(id)) {
|
|
|
|
|
browserIds.push(id);
|
|
|
|
|
// Fix the map if it's inconsistent
|
|
|
|
|
if (!mappedBrowserId) {
|
|
|
|
|
this.userToBrowserMap.set(userId, id);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return browserIds;
|
|
|
|
|
};
|
|
|
|
|
|
2025-03-07 22:44:34 +05:30
|
|
|
/**
|
|
|
|
|
* Returns the total number of browsers in the pool.
|
|
|
|
|
*/
|
|
|
|
|
public getPoolSize = (): number => {
|
|
|
|
|
return Object.keys(this.pool).length;
|
|
|
|
|
};
|
|
|
|
|
|
2025-03-07 22:44:50 +05:30
|
|
|
/**
|
|
|
|
|
* Returns the total number of active users (users with browsers).
|
|
|
|
|
*/
|
|
|
|
|
public getActiveUserCount = (): number => {
|
|
|
|
|
return this.userToBrowserMap.size;
|
|
|
|
|
};
|
|
|
|
|
|
2025-03-07 22:45:20 +05:30
|
|
|
/**
|
|
|
|
|
* Gets the current active browser for the system if there's only one active user.
|
|
|
|
|
* This is a migration helper to support code that hasn't been updated to the user-browser model yet.
|
|
|
|
|
*
|
|
|
|
|
* @param currentUserId The ID of the current user, which will be prioritized if multiple browsers exist
|
|
|
|
|
* @returns A browser ID if one can be determined, or null
|
|
|
|
|
*/
|
|
|
|
|
public getActiveBrowserForMigration = (currentUserId?: string): string | null => {
|
|
|
|
|
// If a current user ID is provided and they have a browser, return that
|
|
|
|
|
if (currentUserId) {
|
|
|
|
|
const browserForUser = this.getActiveBrowserId(currentUserId);
|
|
|
|
|
if (browserForUser) {
|
|
|
|
|
return browserForUser;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// If only one user has a browser, return that
|
|
|
|
|
if (this.userToBrowserMap.size === 1) {
|
|
|
|
|
const userId = Array.from(this.userToBrowserMap.keys())[0];
|
|
|
|
|
return this.userToBrowserMap.get(userId) || null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Fall back to the first active browser if any
|
|
|
|
|
for (const id of Object.keys(this.pool)) {
|
|
|
|
|
if (this.pool[id].active) {
|
|
|
|
|
return id;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// If all else fails, return the first browser in the pool
|
|
|
|
|
const browserIds = Object.keys(this.pool);
|
|
|
|
|
return browserIds.length > 0 ? browserIds[0] : null;
|
|
|
|
|
};
|
|
|
|
|
|
2025-03-07 22:46:00 +05:30
|
|
|
/**
|
|
|
|
|
* Returns the first active browser's instance id from the pool.
|
|
|
|
|
* If there is no active browser, it returns null.
|
|
|
|
|
* If there are multiple active browsers, it returns the first one.
|
|
|
|
|
*
|
|
|
|
|
* @returns the first remote active browser instance's id from the pool
|
|
|
|
|
* @deprecated Use getBrowserIdForUser instead to enforce the 1 User - 1 Browser policy
|
|
|
|
|
*/
|
2025-03-07 22:46:47 +05:30
|
|
|
public getActiveBrowserIdLegacy = (): string | null => {
|
2025-03-07 22:46:00 +05:30
|
|
|
for (const id of Object.keys(this.pool)) {
|
|
|
|
|
if (this.pool[id].active) {
|
|
|
|
|
return id;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// Don't log a warning since this behavior is expected in the user-browser model
|
|
|
|
|
// logger.log('warn', `No active browser in the pool`);
|
|
|
|
|
return null;
|
|
|
|
|
};
|
2025-03-07 22:41:03 +05:30
|
|
|
}
|