diff --git a/app/src/store/session.ts b/app/src/store/session.ts index 6b28363..8853faa 100644 --- a/app/src/store/session.ts +++ b/app/src/store/session.ts @@ -1,6 +1,7 @@ import { defineStore } from 'pinia' -import { FileGraph, FileObject } from '@linux-odyssey/file-graph' +import { FileGraph, FileGraphUpdateEvent } from '@linux-odyssey/file-graph' import { IResponse, ITask } from '@linux-odyssey/game' +import type { SessionDetail } from 'server/src/routers/sessionRouter' import Socket from '../utils/socket' import SocketTerminal from '../utils/terminal' import { Session } from '../types' @@ -21,6 +22,7 @@ interface SessionUpdate { status: string responses: IResponse[] tasks: ITask[] + graphUpdate: FileGraphUpdateEvent } interface Store { @@ -40,12 +42,14 @@ const useSession = defineStore('session', { this.quest = await trpc.quests.getQuestDetail.query(questId) this.questId = questId }, - async setSession(session: Session) { + async setSession(session: SessionDetail) { console.log('set session', session) - this.session = session - // this.session.graph = new FileGraph(session.graph) + this.session = { + ...session, + graph: new FileGraph(session.graph), + } term.reset() - await socket.connect(session) + await socket.connect(this.session) term.focus() }, async createSession() { @@ -63,24 +67,14 @@ const useSession = defineStore('session', { this.setSession(session) } }, - // updateGraph(event: { discover?: FileObject[]; pwd?: string }) { - // if (!this.session) return - // if (event.discover) { - // this.session.graph.discover(event.discover) - // } - // if (event.pwd) { - // this.session.pwd = event.pwd - // } - // }, newUpdate(update: SessionUpdate) { if (!this.session) return this.session.responses = update.responses this.session.tasks = update.tasks this.session.status = update.status - // this.session.hints.push(response.hints) - // if (response.tasks) { - // this.session.tasks = response.tasks - // } + if (update.graphUpdate) { + this.session.graph.handleEvent(update.graphUpdate) + } // if ( // response.status === 'finished' && // this.session.status !== 'finished' diff --git a/app/src/types.ts b/app/src/types.ts index e59e4c7..6e0da1c 100644 --- a/app/src/types.ts +++ b/app/src/types.ts @@ -17,7 +17,7 @@ export interface StageResponse { export interface Session { _id: string status: string - // graph: FileGraph + graph: FileGraph // pwd: string // hints: string[][] tasks: ITask[] diff --git a/packages/file-graph/package.json b/packages/file-graph/package.json index 79cc492..1c615a7 100644 --- a/packages/file-graph/package.json +++ b/packages/file-graph/package.json @@ -1,17 +1,19 @@ { "name": "@linux-odyssey/file-graph", "version": "1.0.0", + "type": "module", "main": "./dist/cjs/index.js", "types": "./dist/types/index.d.ts", "exports": { ".": { "require": "./dist/cjs/index.js", + "import": "./dist/esm/index.js", "types": "./dist/types/index.d.ts" } }, "license": "MIT", "scripts": { - "build": "tsc -p tsconfig.cjs.json", + "build": "tsc -p tsconfig.json && tsc -p tsconfig.cjs.json", "dev": "tsc -w", "test": "yarn build && jest" } diff --git a/packages/file-graph/src/fileGraph.ts b/packages/file-graph/src/fileGraph.ts index b974967..4f77de2 100644 --- a/packages/file-graph/src/fileGraph.ts +++ b/packages/file-graph/src/fileGraph.ts @@ -1,7 +1,7 @@ import { FileObject } from './file.js' import FileNode from './fileNode.js' -export interface Event { +export interface FileGraphUpdateEvent { add?: FileObject[] remove?: FileObject[] discover?: FileObject[] @@ -42,7 +42,7 @@ export default class FileGraph extends FileNode { this.merge(newNode) } - handleEvent(event: Event) { + handleEvent(event: FileGraphUpdateEvent) { // eslint-disable-next-line default-case switch (true) { case !!event.add: diff --git a/packages/file-graph/src/fileNode.ts b/packages/file-graph/src/fileNode.ts index 6b45456..c149220 100644 --- a/packages/file-graph/src/fileNode.ts +++ b/packages/file-graph/src/fileNode.ts @@ -6,10 +6,17 @@ import { } from './errors.js' import { dirname } from './utils.js' -export default class FileNode extends File { +export interface IFileNode { + path: string + type: 'file' | 'directory' + discovered: boolean + children?: IFileNode[] +} + +export default class FileNode extends File implements IFileNode { children: FileNode[] - constructor(file: FileObject | FileNode) { + constructor(file: FileObject | IFileNode) { super(file) this.children = [] if ((file).children) diff --git a/packages/file-graph/src/index.ts b/packages/file-graph/src/index.ts index d3f4a64..6f0cb49 100644 --- a/packages/file-graph/src/index.ts +++ b/packages/file-graph/src/index.ts @@ -1,10 +1,3 @@ export { default as File, FileObject } from './file.js' -export { default as FileNode } from './fileNode.js' -export { default as FileGraph, Event } from './fileGraph.js' - -export interface IFileNode { - path: string - type: string - discovered: boolean - children?: IFileNode[] -} +export { default as FileNode, IFileNode } from './fileNode.js' +export { default as FileGraph, FileGraphUpdateEvent } from './fileGraph.js' diff --git a/packages/file-graph/tsconfig.json b/packages/file-graph/tsconfig.json index 83e051b..8eb4125 100644 --- a/packages/file-graph/tsconfig.json +++ b/packages/file-graph/tsconfig.json @@ -4,8 +4,11 @@ "rootDir": "src", "composite": true, "module": "NodeNext", + "moduleResolution": "NodeNext", "outDir": "./dist/esm", - "declarationDir": "./dist/types" + "declarationDir": "./dist/types", + "target": "ES2020", + "useDefineForClassFields": true }, "include": ["src"], "exclude": ["node_modules", "dist"] diff --git a/packages/game/src/Session.ts b/packages/game/src/Session.ts index 011e80e..d44fc98 100644 --- a/packages/game/src/Session.ts +++ b/packages/game/src/Session.ts @@ -7,6 +7,7 @@ import { IFileExistenceChecker, IResponse, ITask, + ICommandParams, } from './schema' export class GameSession { @@ -22,7 +23,7 @@ export class GameSession { this.completed = completedEvents || [] this.quest = new Quest(quest, checker) this.graph = new FileGraph( - graph ?? new FileNode({ path: '/', type: 'root', discovered: false }) + graph ?? { path: '/', type: 'directory', discovered: false } ) } @@ -39,6 +40,9 @@ export class GameSession { } async runCommand(command: ICommand) { + if (command.params) { + this.graph.handleEvent(command.params) + } const stageId = await this.quest.findSatisfiedEvent(command, this.completed) if (stageId) { this.complete(stageId) diff --git a/packages/game/test/session.test.ts b/packages/game/test/session.test.ts index b1d633f..2fcb805 100644 --- a/packages/game/test/session.test.ts +++ b/packages/game/test/session.test.ts @@ -168,4 +168,47 @@ describe('Session', () => { { id: 'stage4', name: 'Stage 4', completed: false }, ]) }) + + it('should update graph', () => { + const session = new GameSession({}, quest, new MockFileChecker()) + + expect(session.getGraph()).toMatchObject({ + path: '/', + type: 'directory', + discovered: false, + }) + + session.runCommand({ + command: 'ls', + params: { + discover: [ + { + path: '/home/user', + type: 'directory', + discovered: true, + }, + ], + }, + }) + + expect(session.getGraph()).toMatchObject({ + path: '/', + type: 'directory', + discovered: false, + children: [ + { + path: '/home', + type: 'directory', + discovered: false, + children: [ + { + path: '/home/user', + type: 'directory', + discovered: true, + }, + ], + }, + ], + }) + }) }) diff --git a/packages/models/src/session.ts b/packages/models/src/session.ts index 1190f99..cae81e8 100644 --- a/packages/models/src/session.ts +++ b/packages/models/src/session.ts @@ -2,14 +2,17 @@ import { model, Schema, Types } from 'mongoose' export interface INode { path: string - type: string + type: 'file' | 'directory' discovered: boolean children?: INode[] } const nodeSchema = new Schema({ path: String, - type: String, + type: { + type: String, + enum: ['file', 'directory'], + }, discovered: { type: Boolean, default: false, diff --git a/server/src/api/controllers/commandController.ts b/server/src/api/controllers/commandController.ts index 2784af4..daa985a 100644 --- a/server/src/api/controllers/commandController.ts +++ b/server/src/api/controllers/commandController.ts @@ -54,6 +54,7 @@ export const newCommand = asyncHandler(async (req: Request, res: Response) => { await c.save() if (event) { session.completedEvents = gameSession.completedEvents + session.graph = gameSession.getGraph() if (gameSession.isFinished()) { await finishSession(session) @@ -64,6 +65,7 @@ export const newCommand = asyncHandler(async (req: Request, res: Response) => { responses: gameSession.getResponses(), tasks: gameSession.getTasks(), status: session.status, + graphUpdate: command.params, }) } diff --git a/server/src/routers/sessionRouter.ts b/server/src/routers/sessionRouter.ts index 0c7ba7d..cf82e83 100644 --- a/server/src/routers/sessionRouter.ts +++ b/server/src/routers/sessionRouter.ts @@ -7,6 +7,8 @@ import config from '../config.js' import { protectedProcedure, router } from '../trpc.js' import { questManager } from '../models/quest.js' +export type SessionDetail = Awaited> + function sessionDetail(session: ISession) { const quest = questManager.get(session.quest) if (!quest) { @@ -22,14 +24,15 @@ function sessionDetail(session: ISession) { ) return { _id: session._id.toString(), - user: session.user, + user: session.user.toString(), quest: session.quest, status: session.status, - createdAt: session.createdAt, - lastActivityAt: session.lastActivityAt, + createdAt: session.createdAt.toISOString(), + lastActivityAt: session.lastActivityAt.toISOString(), completedEvents: session.completedEvents, responses: gameSession.getResponses(), tasks: gameSession.getTasks(), + graph: gameSession.getGraph(), } } diff --git a/tsconfig.json b/tsconfig.json index 862aad0..e4f25ca 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,7 +1,7 @@ { "compilerOptions": { "target": "ESNext", // 要編譯成的 JS 目標版本 - "module": "NodeNext", // 要編譯成的 module + "module": "ESNext", // 要編譯成的 module "moduleResolution": "NodeNext", // module 解析方式, default "classic" "esModuleInterop": true, // module 兼容, 會影響 build 方式 "forceConsistentCasingInFileNames": true, @@ -9,6 +9,15 @@ "skipLibCheck": true, "sourceMap": true, "declaration": true, - "declarationMap": true + "declarationMap": true, + "baseUrl": ".", + "paths": { + "@/*": ["*"], + "@app/*": ["app/*"], + "@server/*": ["server/*"], + "@models/*": ["packages/models/src/*"], + "@game/*": ["packages/game/src/*"], + "@file-graph/*": ["packages/file-graph/src/*"] + } } }