From 7cdc542c191152c72ba291e53f181a086c0a94a1 Mon Sep 17 00:00:00 2001
From: wancat
Date: Thu, 27 Jun 2024 11:23:21 +0800
Subject: [PATCH] refactor: rewrite session store using pinia and fix start
button
---
app/src/components/game/ControlPalette.vue | 9 +-
app/src/components/game/HintPart.vue | 3 +-
app/src/components/game/QuestPart.vue | 3 +-
app/src/components/game/StartButton.vue | 38 +---
app/src/components/game/TreeChart.vue | 4 +-
.../header/GameHeaderComponents.vue | 13 +-
app/src/store/session.ts | 211 +++++++-----------
app/src/utils/api.ts | 20 ++
app/src/views/GamePage.vue | 28 +--
packages/constants/index.ts | 13 +-
packages/constants/package.json | 3 +
server/swagger.yaml | 13 ++
yarn.lock | 5 +
13 files changed, 171 insertions(+), 192 deletions(-)
diff --git a/app/src/components/game/ControlPalette.vue b/app/src/components/game/ControlPalette.vue
index bd2bbf7..b423112 100644
--- a/app/src/components/game/ControlPalette.vue
+++ b/app/src/components/game/ControlPalette.vue
@@ -1,13 +1,14 @@
diff --git a/app/src/components/header/GameHeaderComponents.vue b/app/src/components/header/GameHeaderComponents.vue
index c497807..ef9a685 100644
--- a/app/src/components/header/GameHeaderComponents.vue
+++ b/app/src/components/header/GameHeaderComponents.vue
@@ -4,10 +4,11 @@ import { useRouter } from 'vue-router'
import { useToast } from 'vue-toastification'
import { logout } from '../../utils/auth'
import { bugReportUrl, surveyUrl } from '../../config'
-import { reset } from '../../store/session'
+import useSession from '../../store/session'
import useUserProfile from '../../store/userProfile'
-const store = useUserProfile()
+const userStore = useUserProfile()
+const sessionStore = useSession()
const router = useRouter()
const toast = useToast()
@@ -15,8 +16,8 @@ const toast = useToast()
const handleLogout = async () => {
try {
await logout()
- store.resetUserProfile()
- reset()
+ userStore.resetUserProfile()
+ sessionStore.reset()
router.push({ name: 'login' })
} catch (err) {
toast.error('登出失敗')
@@ -26,7 +27,7 @@ const handleLogout = async () => {
onMounted(async () => {
try {
- await store.loadUserProfile()
+ await userStore.loadUserProfile()
} catch (err) {
toast.error('無法讀取使用者資料')
console.error(err)
@@ -57,7 +58,7 @@ onMounted(async () => {
class="text-text inline-block whitespace-nowrap px-1.5"
style="font-size: 2vh"
>
- {{ store.username }}
+ {{ userStore.username }}
({
- session: newSession(),
- questId: '',
- quest: null,
+const useSession = defineStore('session', {
+ state: (): Store => ({
+ session: {
+ _id: '',
+ status: 'inactive',
+ graph: new FileGraph({
+ path: '/',
+ type: 'folder',
+ discovered: false,
+ }),
+ pwd: '',
+ hints: [],
+ tasks: [],
+ responses: [],
+ },
+ questId: '',
+ quest: null,
+ }),
+ actions: {
+ async setQuest(questId: string) {
+ this.quest = await getQuest(questId)
+ this.questId = questId
+ },
+ async setSession(session: Session) {
+ this.session = session
+ this.session.graph = new FileGraph(session.graph)
+ term.reset()
+ await socket.connect(session)
+ term.focus()
+ },
+ async createSession() {
+ const session = await createSession(this.questId)
+ this.setSession(session)
+ },
+ async getActiveSession() {
+ const sessions = await getSessions({
+ questId: this.questId,
+ limit: 1,
+ order: 'desc',
+ status: 'active',
+ })
+ if (sessions.length > 0) {
+ this.setSession(sessions[0])
+ return true
+ }
+ return false
+ },
+ updateGraph(event: { discover?: FileObject[]; pwd?: string }) {
+ if (event.discover) {
+ this.session.graph.discover(event.discover)
+ }
+ if (event.pwd) {
+ this.session.pwd = event.pwd
+ }
+ },
+ newResponse(response: StageResponse) {
+ this.session.responses.push(response.responses)
+ this.session.hints.push(response.hints)
+ this.session.tasks = response.tasks
+ this.session.status = response.status
+ },
+ reset() {
+ socket.reset()
+ term.reset()
+ this.$reset()
+ },
+ setup() {
+ socket.on('terminal', (data: string) => {
+ term.write(data)
+ })
+ term.onData((data: string) => {
+ socket.emit('terminal', data)
+ })
+ socket.on('graph', (event: { discover: FileObject[]; pwd: string }) => {
+ this.updateGraph(event)
+ })
+ socket.on('response', (response: StageResponse) => {
+ this.newResponse(response)
+ })
+ },
+ },
})
-const socket = new Socket()
-const term = new SocketTerminal(40, 80)
-
-export function setQuest(questId: string) {
- return api.get(`/quests/${questId}`).then((res) => {
- store.questId = res.data._id
- store.quest = res.data
- })
-}
-
-async function setSession(session: Session) {
- try {
- console.log('Setting session...', session)
- store.session = session
- store.session.graph = new FileGraph(session.graph)
- term.reset()
- await socket.connect(session)
- term.focus()
- } catch (err: any) {
- console.error(err)
- if (err instanceof Error) {
- toast.error(err.message)
- } else {
- toast.error('Failed to connect to the session. Please try again later.')
- }
- throw err
- }
-}
-
-export async function createSession() {
- try {
- console.log('Creating a new session...')
- const res = await api.post('/sessions', { questId: store.questId })
- const { data } = res
- await setSession(data)
- socket.emit('terminal', 'echo start')
- } catch (err) {
- console.error(err)
- toast.error('Failed to create a new session. Please try again later.')
- }
-}
-
-export async function getActiveSession(questId: string) {
- try {
- // const res = await api.post('/sessions/active', { questId })
- const res = await api.get(`/sessions/${questId}`, {
- params: { status: 'active' },
- })
- await setSession(res.data)
- } catch (err) {
- console.error(err)
- throw new LoadSessionError('Failed to load session', questId)
- // toast.warning('Failed to connect previous session. Creating a new one...')
- // await createSession()
- }
-}
-
-function updateGraph(event: { discover: FileObject[]; pwd: string }) {
- if (event.discover) {
- store.session.graph.discover(event.discover)
- }
- if (event.pwd) {
- store.session.pwd = event.pwd
- }
-}
-
-function newResponse(response: StageResponse) {
- console.log('newResponse', response, 'session', store.session)
- store.session.responses.push(response.responses)
- store.session.hints.push(response.hints)
- store.session.tasks = response.tasks
- store.session.status = response.status
-}
-
export function useTerminal() {
return term
}
-export function reset() {
- socket.reset()
- term.reset()
- store.session = newSession()
- store.quest = null
-}
-
// export async function init(questId: string) {
// if (!questId) throw new Error('No quest ID provided')
// reset()
@@ -141,21 +119,4 @@ export function reset() {
// }
// }
-function setup() {
- socket.on('terminal', (data: string) => {
- term.write(data)
- })
- term.onData((data: string) => {
- socket.emit('terminal', data)
- })
- socket.on('graph', (event: { discover: FileObject[]; pwd: string }) => {
- updateGraph(event)
- })
- socket.on('response', (response: StageResponse) => {
- newResponse(response)
- })
-}
-
-setup()
-
-export default store
+export default useSession
diff --git a/app/src/utils/api.ts b/app/src/utils/api.ts
index f298863..a5808ac 100644
--- a/app/src/utils/api.ts
+++ b/app/src/utils/api.ts
@@ -1,4 +1,7 @@
import axios from 'axios'
+import { IQuest } from '@linux-odyssey/models'
+import { GetSessionsRequest } from '@linux-odyssey/constants'
+import { Session } from '../types'
import { UserProfile } from '../store/userProfile'
const api = axios.create({
@@ -14,3 +17,20 @@ export async function getUserProfile(): Promise {
const res = await api.get('/users/me')
return res.data
}
+
+export async function getQuest(id: string): Promise {
+ const res = await api.get(`/quests/${id}`)
+ return res.data
+}
+
+export async function createSession(questId: string): Promise {
+ const res = await api.post('/sessions', { questId })
+ return res.data
+}
+
+export async function getSessions(
+ request: GetSessionsRequest
+): Promise {
+ const res = await api.get('/sessions', { params: request })
+ return res.data
+}
diff --git a/app/src/views/GamePage.vue b/app/src/views/GamePage.vue
index fcea8e4..c18eee0 100644
--- a/app/src/views/GamePage.vue
+++ b/app/src/views/GamePage.vue
@@ -2,7 +2,7 @@
import { computed } from 'vue'
// import { onMounted } from 'vue'
// import { useToast } from 'vue-toastification'
-import sessionStore from '../store/session'
+import useSession from '../store/session'
// import { init } from '../store/session'
import HeaderPart from '../components/header/HeaderPart.vue'
import GameHeaderComponents from '../components/header/GameHeaderComponents.vue'
@@ -13,19 +13,14 @@ import TerminalPart from '../components/game/TerminalPart.vue'
import VisualizationPart from '../components/game/VisualizationPart.vue'
import ControlPalette from '../components/game/ControlPalette.vue'
import CompleteModal from '../components/game/CompleteModal.vue'
-// import { LoadQuestError, LoadSessionError } from '../utils/errors'
import StartButton from '../components/game/StartButton.vue'
+const sessionStore = useSession()
+
const completed = computed(() => {
return sessionStore.session.status === 'finished'
})
-// const props = defineProps({
-// questId: {
-// type: String,
-// required: true,
-// },
-// })
defineProps({
questId: {
type: String,
@@ -33,23 +28,6 @@ defineProps({
},
})
// const toast = useToast()
-
-// onMounted(async () => {
-// try {
-// await init(props.questId)
-// } catch (err: any) {
-// if (err instanceof LoadQuestError) {
-// toast.error(`無法讀取關卡: ${err.questId},請確認網頁連結。`)
-// return
-// }
-// if (err instanceof LoadSessionError) {
-// toast.error(`無法建立工作階段: ${err.questId},請重新登入再試一次。`)
-// return
-// }
-// console.error(err)
-// toast.error(err.message)
-// }
-// })
diff --git a/packages/constants/index.ts b/packages/constants/index.ts
index 0ff060b..c555e8e 100644
--- a/packages/constants/index.ts
+++ b/packages/constants/index.ts
@@ -1,3 +1,5 @@
+import { z } from 'zod'
+
export const passwordPolicy = {
minLength: 8,
minNumbers: 1,
@@ -6,6 +8,11 @@ export const passwordPolicy = {
minSymbols: 0,
}
-export default {
- passwordPolicy,
-}
+export const getSessionsRequestSchema = z.object({
+ questId: z.string().optional(),
+ status: z.enum(['active', 'inactive', 'finished']).optional(),
+ limit: z.coerce.number().optional(),
+ order: z.enum(['asc', 'desc']).optional(),
+})
+
+export type GetSessionsRequest = z.infer
diff --git a/packages/constants/package.json b/packages/constants/package.json
index 39f0201..ec494fd 100644
--- a/packages/constants/package.json
+++ b/packages/constants/package.json
@@ -10,5 +10,8 @@
"scripts": {
"build": "tsc",
"dev": "tsc -w"
+ },
+ "dependencies": {
+ "zod": "^3.23.8"
}
}
diff --git a/server/swagger.yaml b/server/swagger.yaml
index b2a0605..c5e854b 100644
--- a/server/swagger.yaml
+++ b/server/swagger.yaml
@@ -37,6 +37,19 @@ paths:
schema:
type: string
enum: [active, finished, inactive]
+ - in: query
+ name: limit
+ description: limit the number of sessions to return
+ schema:
+ type: number
+ example: 10
+ - in: query
+ name: order
+ description: order the sessions by createdAt
+ schema:
+ type: string
+ enum: [asc, desc]
+ example: desc
responses:
'200':
description: list of existing session
diff --git a/yarn.lock b/yarn.lock
index ab01e36..643f242 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -8475,6 +8475,11 @@ yocto-queue@^0.1.0:
resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b"
integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==
+zod@^3.23.8:
+ version "3.23.8"
+ resolved "https://registry.yarnpkg.com/zod/-/zod-3.23.8.tgz#e37b957b5d52079769fb8097099b592f0ef4067d"
+ integrity sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==
+
zrender@5.5.0:
version "5.5.0"
resolved "https://registry.yarnpkg.com/zrender/-/zrender-5.5.0.tgz#54d0d6c4eda81a96d9f60a9cd74dc48ea026bc1e"