Files
order_site/apps/backend/src/services/session/session-service-store.ts
T
2026-05-21 15:28:32 +08:00

205 lines
5.2 KiB
TypeScript

import crypto from 'node:crypto'
import fs from 'node:fs/promises'
import path from 'node:path'
import {
DATA_ROOT,
REDEEM_SUCCESS_AUTO_CLOSE_MS,
} from './session-service-config.js'
import { SessionServiceError } from './session-service-runtime.js'
import {
resolveInitialQrSessionState,
SESSION_TTL_MS,
} from './session-state.js'
type BrowserContextLike = {
close?: () => Promise<void>
}
export type TencentBrowserSession = Record<string, any> & {
sessionId: string
loginType: string
status: string
notice: string
createdAt: string
updatedAt: string
expiresAt: number
autoCloseAt: number
autoCloseTimer: ReturnType<typeof setTimeout> | null
sessionDir: string
browserContext?: BrowserContextLike | null
page?: unknown
browserContextClosed?: boolean
}
type RegisterSessionInput = {
loginType: string
browserContext: BrowserContextLike
page: unknown
}
type CloseSessionDependency = {
closeSession: (session: TencentBrowserSession, options: { markClosed: boolean }) => Promise<void>
}
type CloseSessionOptions = {
markClosed: boolean
persistSessionState: (session: TencentBrowserSession) => Promise<unknown>
closeBrowserIfIdle: () => Promise<unknown>
}
type PersistSessionStateOptions = {
buildSessionPayload: (session: TencentBrowserSession) => unknown
}
const sessions = new Map<string, TencentBrowserSession>()
export async function registerTencentBrowserSession({
loginType,
browserContext,
page,
}: RegisterSessionInput): Promise<TencentBrowserSession> {
const sessionId = createSessionId()
const sessionDir = path.join(DATA_ROOT, sessionId)
await fs.mkdir(sessionDir, { recursive: true })
const now = new Date().toISOString()
const session = {
sessionId,
loginType,
status: 'created',
notice: '浏览器会话已创建',
createdAt: now,
updatedAt: now,
expiresAt: Date.now() + SESSION_TTL_MS,
autoCloseAt: 0,
autoCloseTimer: null,
sessionDir,
browserContext,
page,
qrImageBase64: '',
qrImagePath: '',
qrUpdatedAt: '',
lastState: null,
lastRedeem: null,
lastReview: null,
lastError: '',
presentationSyncAttempts: 0,
}
sessions.set(sessionId, session)
return session
}
export function listActiveTencentBrowserSessions(): TencentBrowserSession[] {
return [...sessions.values()]
}
export async function getRequiredSession(sessionId: unknown): Promise<TencentBrowserSession> {
const key = String(sessionId || '').trim()
if (!key) {
throw new SessionServiceError('缺少 sessionId', {
statusCode: 400,
errorCode: 'missing_session_id',
})
}
const session = sessions.get(key)
if (!session) {
throw new SessionServiceError('浏览器会话不存在或已过期', {
statusCode: 404,
errorCode: 'session_not_found',
})
}
return session
}
export function applyInitialQrSessionState(session: TencentBrowserSession): void {
resetTencentSessionAutoClose(session)
Object.assign(session, resolveInitialQrSessionState(session.loginType))
}
export function armRedeemedSessionAutoClose(session: TencentBrowserSession, { closeSession }: CloseSessionDependency): void {
resetTencentSessionAutoClose(session)
const autoCloseAt = Date.now() + REDEEM_SUCCESS_AUTO_CLOSE_MS
session.autoCloseAt = autoCloseAt
session.expiresAt = autoCloseAt
session.autoCloseTimer = setTimeout(() => {
void closeSession(session, { markClosed: true }).catch(() => {
// ignore delayed auto-close failures
})
}, REDEEM_SUCCESS_AUTO_CLOSE_MS)
}
export function resetTencentSessionAutoClose(session?: TencentBrowserSession | null): void {
if (session?.autoCloseTimer) {
clearTimeout(session.autoCloseTimer)
session.autoCloseTimer = null
}
if (session) {
session.autoCloseAt = 0
}
}
export async function cleanupExpiredTencentBrowserSessions({ closeSession }: CloseSessionDependency): Promise<void> {
const expired = listActiveTencentBrowserSessions().filter((session) => session.expiresAt <= Date.now())
await Promise.all(expired.map((session) => closeSession(session, { markClosed: false })))
}
export async function closeSession(
session: TencentBrowserSession,
{
markClosed,
persistSessionState,
closeBrowserIfIdle,
}: CloseSessionOptions,
): Promise<void> {
resetTencentSessionAutoClose(session)
sessions.delete(session.sessionId)
try {
await session.browserContext?.close()
} catch {
// ignore browser context close failures
}
session.browserContextClosed = true
session.updatedAt = new Date().toISOString()
if (markClosed) {
session.status = 'closed'
session.notice = '浏览器会话已关闭'
await persistSessionState(session)
}
await closeBrowserIfIdle()
}
export async function persistSessionState(
session: TencentBrowserSession,
{ buildSessionPayload }: PersistSessionStateOptions,
): Promise<void> {
await fs.mkdir(session.sessionDir, { recursive: true })
await fs.writeFile(
path.join(session.sessionDir, 'session.json'),
JSON.stringify(buildSessionPayload(session), null, 2),
'utf8',
)
}
function createSessionId(): string {
let sessionId = ''
do {
sessionId = `txbs-${crypto.randomBytes(6).toString('hex')}`
} while (sessions.has(sessionId))
return sessionId
}