Files
liqa/server/src/session/session-context.service.ts
T
ars9 5cc16725fb chore(repo): restructure as monorepo with server and client workspaces
- move NestJS app into server/ subdirectory
- add client/ React+TypeScript (Vite) app with Hello World
- update docker-compose to build and run both services
- add root package.json declaring npm workspaces
- update .gitignore to cover node_modules and dist at all depths
2026-04-08 21:28:01 +03:00

133 lines
4.1 KiB
TypeScript

import {
Injectable,
OnModuleDestroy,
BadRequestException,
NotFoundException,
} from "@nestjs/common";
import type { Browser, BrowserContext, Page } from "playwright";
import { TraceLogger } from "../common/trace-logger";
import { SessionService } from "./session.service";
export interface SessionHandle {
browser: Browser;
context: BrowserContext;
page: Page;
}
/**
* Keeps live Playwright browser contexts in memory, keyed by sessionName.
* A session must be explicitly registered (after login) to be usable.
* Sessions marked as closed in the DB cannot be used via getHandle().
*/
@Injectable()
export class SessionContextService implements OnModuleDestroy {
private readonly logger = new TraceLogger(SessionContextService.name);
private readonly handles = new Map<string, SessionHandle>();
constructor(private readonly sessionService: SessionService) {}
/**
* Store a live browser context after a successful login.
* If a handle already exists for this session it is closed first.
*/
register(
sessionName: string,
browser: Browser,
context: BrowserContext,
page: Page,
): void {
const existing = this.handles.get(sessionName);
if (existing) {
existing.browser.close().catch((err: unknown) => {
this.logger.warn(
`Error closing stale browser for "${sessionName}": ${(err as Error).message}`,
);
});
}
this.handles.set(sessionName, { browser, context, page });
this.logger.log(`Session "${sessionName}" registered in context pool`);
}
/**
* Return the live handle for a named session and bump lastUsedAt.
* Throws 404 if session does not exist in DB.
* Throws 400 if session is closed or context is not in memory.
*/
async getHandle(sessionName: string): Promise<SessionHandle> {
const handle = this.handles.get(sessionName);
if (handle) {
await this.sessionService.touchLastUsed(sessionName);
return handle;
}
const session = await this.sessionService.findBySessionName(sessionName);
if (!session) {
throw new NotFoundException(`Session not found: ${sessionName}`);
}
if (session.status === "closed") {
throw new BadRequestException(
`Session "${sessionName}" is closed — please login again`,
);
}
// Session is open in DB but context is not in memory (e.g. after unexpected restart).
throw new BadRequestException(
`Session "${sessionName}" context is not available — please login again`,
);
}
/**
* Close the Playwright browser for a session and mark it as closed in DB.
* Safe to call even if the session is not currently in memory.
*/
async close(sessionName: string): Promise<void> {
const handle = this.handles.get(sessionName);
if (handle) {
try {
await handle.browser.close();
} catch (err) {
this.logger.warn(
`Error closing browser for session "${sessionName}": ${(err as Error).message}`,
);
}
this.handles.delete(sessionName);
}
await this.sessionService.markClosed(sessionName);
this.logger.log(`Session "${sessionName}" closed`);
}
/**
* Close all in-memory handles and mark every open session as closed in DB.
*/
async closeAll(): Promise<void> {
const names = Array.from(this.handles.keys());
await Promise.allSettled(names.map((name) => this.close(name)));
if (names.length > 0) {
this.logger.log(`Closed ${names.length} session context(s)`);
}
}
/**
* Close the context (if open) and delete the session record from DB.
* Throws 404 if the session ID does not exist.
*/
async delete(id: number): Promise<void> {
const session = await this.sessionService.findById(id);
if (!session) {
throw new NotFoundException(`Session ${id} not found`);
}
if (this.handles.has(session.sessionName)) {
await this.close(session.sessionName);
}
await this.sessionService.remove(id);
}
/** Returns true if a live Playwright context exists for this session. */
isOpen(sessionName: string): boolean {
return this.handles.has(sessionName);
}
async onModuleDestroy(): Promise<void> {
await this.closeAll();
}
}