Files
OJ2/apps/api/src/events.ts
T
xuyue d25b5dbe6d
Deploy / deploy (push) Canceled after 0s
update
2026-09-27 19:36:28 +08:00

102 lines
3.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import type { FlowchartUpdate } from "@oj2/contract"
import { redis } from "./redis"
export const userEventChannel = "user:events"
/**
* 站点配置变更广播。旧后端的 `utils/websocket.push_config_update`:
* 超管改了配置,所有开着页面的人立刻生效,不必刷新。
* 这是全站广播,不分用户,所以是一个固定 topic 而不是 per-user。
*/
export const configUpdateChannel = "config:updates"
export const configTopic = "events:config"
export async function publishConfigUpdate(key: string, value: unknown) {
await redis.publish(configUpdateChannel, JSON.stringify({ type: "config_update", key, value }))
}
/**
* 会话吊销广播。让还挂着的 WebSocket 立刻知道自己该下线了。
*
* 两种作用域,**不能混**:
* - `{ token }` 用户登出。只该断这一张会话 —— 同一个人在别的设备上是另一张会话,
* 不该被牵连。
* - `{ userId }` 账号被禁用。所有设备都得断。禁用只改数据库的 isDisabled 列、
* 不动 Redis 里的会话,WebSocket 那边的会话巡检永远发现不了,只能靠这条主动通知。
*/
export const sessionRevokedChannel = "session:revoked"
export type SessionRevokedReason = "session-ended" | "account-disabled"
export interface SessionRevoked {
token?: string
userId?: number
reason: SessionRevokedReason
}
export async function publishSessionRevoked(
target: { token: string } | { userId: number },
reason: SessionRevokedReason,
) {
await redis.publish(sessionRevokedChannel, JSON.stringify({ ...target, reason }))
}
export function parseSessionRevoked(raw: string): SessionRevoked | null {
try {
const value = JSON.parse(raw) as SessionRevoked
if (typeof value.token !== "string" && !Number.isInteger(value.userId)) return null
if (value.reason !== "session-ended" && value.reason !== "account-disabled") return null
return value
} catch {
return null
}
}
interface UserEvent {
userId: number
data: FlowchartUpdate | Record<string, unknown>
}
interface AchievementNotification {
id: number
name: string
description: string
icon: string
rarity: string
kind: "achievement" | "badge"
}
export function userEventTopic(userId: number) {
return `events:user:${userId}`
}
export async function publishFlowchartUpdate(userId: number, data: FlowchartUpdate) {
await redis.publish(userEventChannel, JSON.stringify({ userId, data }))
}
export async function publishAchievementNotification(
userId: number,
achievements: AchievementNotification[],
) {
if (!achievements.length) return
await redis.publish(
userEventChannel,
JSON.stringify({
userId,
data: { type: "achievement_unlocked", achievements },
}),
)
}
export function parseUserEvent(raw: string): UserEvent | null {
try {
const value = JSON.parse(raw) as UserEvent
if (!Number.isInteger(value.userId) || !value.data || typeof value.data !== "object")
return null
return value
} catch {
return null
}
}