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
This commit is contained in:
@@ -0,0 +1,132 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user