From 8dac3188dffa676f4d07043a926a8e2f0ef9924e Mon Sep 17 00:00:00 2001 From: Litrix Date: Tue, 17 Dec 2024 20:33:47 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=A6=84=20refactor:=20=E6=A3=8B=E7=B1=BBso?= =?UTF-8?q?cket=E9=80=9A=E7=94=A8=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/utils/{2d-array.ts => array.ts} | 6 ++ src/utils/game-socket.ts | 127 +++++++++++++++++++++++- src/utils/index.ts | 4 +- src/views/GobangListPage.vue | 147 ++++------------------------ src/views/GobangPlayPage.vue | 37 ++++++- 5 files changed, 185 insertions(+), 136 deletions(-) rename src/utils/{2d-array.ts => array.ts} (70%) diff --git a/src/utils/2d-array.ts b/src/utils/array.ts similarity index 70% rename from src/utils/2d-array.ts rename to src/utils/array.ts index 478c2ae..c1900c9 100644 --- a/src/utils/2d-array.ts +++ b/src/utils/array.ts @@ -1,3 +1,5 @@ +import type { MaybeArray } from '.'; + export function create2DArray( height: number, width: number, @@ -16,3 +18,7 @@ export function get2DArrayItem( ): T { return colFirst ? grid[first][second] : grid[second][first]; } +export function ensureArray(obj: MaybeArray): T[]; +export function ensureArray(obj: unknown) { + return Array.isArray(obj) ? obj : [obj]; +} diff --git a/src/utils/game-socket.ts b/src/utils/game-socket.ts index a1b1d79..2d4110e 100644 --- a/src/utils/game-socket.ts +++ b/src/utils/game-socket.ts @@ -1,4 +1,9 @@ -import type { MaybeArray } from '.'; +import router from '@/router'; +import { useUserStore } from '@/stores/user'; +import { useWebSocket } from '@vueuse/core'; +import { ElMessage } from 'element-plus'; +import type { ValueOf } from 'element-plus/es/components/table/src/table-column/defaults.mjs'; +import { ensureArray, type MaybeArray } from '.'; export interface SimplePart { name: N; @@ -7,5 +12,121 @@ type Payload = Record; export interface PayloadPart extends SimplePart { payload: P; } -type Relations = Record>; -export function useGameSocket, const TRelations extends Relations,>{} +type BaseResp = PayloadPart>; +type Relations = Partial< + Record> +>; +export interface UseGameSocketOptions< + TRequest extends SimplePart, + TResp extends BaseResp, + TRelations extends Relations, +> { + succeed: { + [P in TResp['name']]?: (payload: Extract['payload']) => void; + }; + error?: { + [P in TRequest['name']]?: (reason: string) => boolean | void; + }; + finally?: { + // 只有拥有依赖关系的请求才可以有finally + [P in keyof TRelations]?: (error: boolean) => void; + }; +} +export function useGameSocket, TResp extends BaseResp>() { + return >( + relations: TRelations, + options: UseGameSocketOptions, + ) => { + interface ErrorResp extends PayloadPart<'Error', { reason: string }> { + originalPacketName: TRequest['name']; + } + const isErrorResp = (obj: BaseResp): obj is ErrorResp => obj.name === 'Error'; + /** resp->request */ + const reversedRelations: Record | undefined> = {}; + for (const [k, values] of Object.entries>(relations as any)) { + if (!values) continue; + for (const v of ensureArray(values)) { + (reversedRelations[v] ??= new Set()).add(k); + } + } + const userStore = useUserStore(); + const relationMap = new Map]>(); + const ws = useWebSocket(`wss://wzpmc.cn:18080/chess/${userStore.token}`, { + // autoReconnect: { + // delay: 500, + // }, + // onConnected() { + // // 连接无故断开的临时解决方案 + // if (firstConnected) { + // console.log('connected'); + // firstConnected = false; + // return; + // } + // console.log('reconnected'); + // for (const [req] of relationMap.values()) { + // send(req); + // } + // }, + onMessage(_, { data }) { + const resp: TResp | ErrorResp = JSON.parse(data); + console.log(resp); + const name = resp.name as TResp['name']; + let reqName: keyof TRelations | undefined; + let error = false; + try { + if (isErrorResp(resp)) { + const { originalPacketName, payload } = resp; + error = true; + if (relations[originalPacketName]) { + reqName = originalPacketName; + relationMap.delete(originalPacketName); + } + const errorHandler = options.error?.[originalPacketName]; + if (errorHandler) { + const res = errorHandler(payload.reason); + if (res) return; + } + ElMessage.error(payload.reason); + return; + } + const set = reversedRelations[name]; + if (set) { + for (const k of set) { + const v = relationMap.get(k); + if (!v) continue; + const [, set] = v; + set.delete(name); + if (set.size) continue; + relationMap.delete(k); + reqName = k; + break; + } + } + options.succeed[name]?.(resp.payload as any); + } finally { + if (reqName && relations[reqName]) { + options.finally?.[reqName]?.(error); + } + } + }, + onDisconnected(_, ev) { + console.log('disconnected'); + // 正常断开 + if (ev.code === 1000) return; + ElMessage.error(`连接已断开:${ev.reason}`); + router.push('/'); + }, + }); + const send = (data: TRequest) => { + const name = data.name as TRequest['name']; + if (relations[name]) { + relationMap.set(name, [data, new Set(ensureArray(relations[name]))]); + } + console.log('send', data); + ws.send(JSON.stringify(data)); + }; + return { + send, + }; + }; +} diff --git a/src/utils/index.ts b/src/utils/index.ts index 8d1b880..ea2f0f9 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1,6 +1,6 @@ import { reactive, readonly, type Ref, ref, watch } from 'vue'; -export * from './2d-array'; +export * from './array'; export * from './api'; export const timeout = (delay: number = 0) => @@ -139,7 +139,7 @@ export function waitRef(ref: Ref, ...expect: R[]) { }); } export type MaybePromise = T | Promise; -export type MaybeArray = T | T[]; +export type MaybeArray = T | (R extends false ? T[] : readonly T[]); export type ValueOf = T[keyof T]; /** * 检测key是否为obj的键,并收紧key的类型. diff --git a/src/views/GobangListPage.vue b/src/views/GobangListPage.vue index 4674095..d78bd33 100644 --- a/src/views/GobangListPage.vue +++ b/src/views/GobangListPage.vue @@ -68,7 +68,7 @@ export enum RoomState { } /** 房间UUID */ export type RoomId = string; -export interface RoomInfo { +export interface Room { id: RoomId; full: boolean; state: RoomState; @@ -76,146 +76,41 @@ export interface RoomInfo { isBlackAcceptRestart: boolean; canWhiteDown: boolean; } -export interface RoomDetail {} -export interface SimplePart { - name: N; -} -type Payload = Record; -export interface PayloadPart extends SimplePart { - payload: P; -} +export type RoomDetail = { + roomId: RoomId; + state: RoomState; + pieces: (-1 | 0 | 1)[][]; + whiteUser?: UserInfo; + blackUser?: UserInfo; +}; export type Request = | SimplePart<'RoomList'> | SimplePart<'CreateRoom'> | PayloadPart<'PlayerJoin', { roomId: RoomId }> | SimplePart<'ResetRoom'>; export type Resp = - | PayloadPart<'UserInfo', SucceedUserInfoResponse> - | PayloadPart<'RoomList', { rooms: RoomInfo[] }> + | PayloadPart<'UserInfo', UserInfo> + | PayloadPart<'RoomList', { rooms: Room[] }> | PayloadPart<'RoomCreated', { roomId: RoomId }> - | PayloadPart<'PlayerSideAllocation', { isWhite: boolean }>; -const relations = { + | PayloadPart<'PlayerSideAllocation', { isWhite: boolean }> + | PayloadPart<'RoomInfo', RoomDetail>; +export const relations = { RoomList: 'RoomList', - RoomCreated: 'CreateRoom', - PlayerSideAllocation: 'PlayerJoin', -} as const satisfies Partial>; -type RelatedRequestNames = ValueOf; -const reversedRelations = {} as Record; -for (const [k, v] of Object.entries(relations)) { - (reversedRelations[v] ??= []).push(k); -} -export interface ErrorResp extends PayloadPart<'Error', { reason: string }> { - originalPacketName: Request['name']; -} -export type RequestOf = Extract; -export type RespOf = Extract; -export interface UseGobangSocketOptions { - succeed: { - [P in Resp['name']]?: (payload: RespOf

['payload']) => void; - }; - error?: { - [P in Request['name']]?: (reason: string) => boolean | void; - }; - finally?: { - // 只有拥有依赖关系的请求才可以有finally - [P in RelatedRequestNames]?: (error: boolean) => void; - }; -} -export function useGobangSocket(options: UseGobangSocketOptions) { - const userStore = useUserStore(); - const relationMap = new Map]>(); - // let firstConnected = true; - const ws = useWebSocket(`ws://172.16.127.101:58080/chess/${userStore.token}`, { - // autoReconnect: { - // delay: 500, - // }, - // onConnected() { - // // 连接无故断开的临时解决方案 - // if (firstConnected) { - // console.log('connected'); - // firstConnected = false; - // return; - // } - // console.log('reconnected'); - // for (const [req] of relationMap.values()) { - // send(req); - // } - // }, - onMessage(_, { data }) { - const resp: Resp | ErrorResp = JSON.parse(data); - console.log(resp); - const { name } = resp; - let reqName: Request['name'] | undefined; - let error = false; - try { - if (name === 'Error') { - const { originalPacketName } = resp; - reqName = originalPacketName; - error = true; - if (isKeyOf(relations, originalPacketName)) { - relationMap.delete(originalPacketName); - } - const errorHandler = options.error?.[originalPacketName]; - if (errorHandler) { - const res = errorHandler(resp.payload.reason); - if (res) return; - } - ElMessage.error(resp.payload.reason); - return; - } - if (isKeyOf(relations, name)) { - const v = relationMap.get(relations[name]); - if (v) { - const [, set] = v; - set.delete(name); - if (!set.size) { - relationMap.delete(relations[name]); - reqName = relations[name]; - } - } - } - options.succeed[name]?.(resp.payload as any); - } finally { - if (reqName && isKeyOf(reversedRelations, reqName)) { - options.finally?.[reqName]?.(error); - } - } - }, - onDisconnected(_, ev) { - console.log('disconnected'); - // 正常断开 - if (ev.code === 1000) return; - ElMessage.error(`连接已断开:${ev.reason}`); - router.push('/'); - }, - }); - const send = (data: Request) => { - const { name } = data; - if (isKeyOf(reversedRelations, name)) { - relationMap.set(name, [data, new Set(reversedRelations[name])]); - } - console.log('send', data); - ws.send(JSON.stringify(data)); - }; - return { - send, - }; -} + CreateRoom: 'RoomCreated', + PlayerJoin: ['PlayerSideAllocation', 'RoomInfo'], +} as const;