diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..0abe79f --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,35 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + check: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: pnpm/action-setup@v4 + with: + version: 10 + + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: pnpm + + - run: pnpm install --frozen-lockfile + + - name: Tests + run: pnpm test -- --run + + - name: Type check + run: | + pnpm --filter server exec tsc --noEmit + pnpm --filter frontend exec vue-tsc --noEmit + + - name: Lint + run: pnpm lint diff --git a/.gitignore b/.gitignore index 8d9520a..63ec5f0 100644 --- a/.gitignore +++ b/.gitignore @@ -5,10 +5,14 @@ target/ __pycache__/ *.pyc .env -.env.local +.env.* +!.env.example .DS_Store loop/loop.log server/data/ loop/ .pnpm-store/ .npmrc +*.pem +*.key +*.crt diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000..ea6dd12 --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,31 @@ +import tseslint from '@typescript-eslint/eslint-plugin' +import tsparser from '@typescript-eslint/parser' + +export default [ + { + ignores: ['**/dist/**', '**/node_modules/**', '**/*.js', '**/*.mjs', '**/*.cjs', '**/*.vue'], + }, + { + files: ['**/*.ts'], + languageOptions: { + parser: tsparser, + parserOptions: { + projectService: true, + }, + }, + plugins: { + '@typescript-eslint': tseslint, + }, + rules: { + '@typescript-eslint/no-floating-promises': 'error', + 'no-console': ['warn', { allow: ['warn', 'error'] }], + }, + }, + // Frontend game engine: fire-and-forget async (audio, animations) is intentional + { + files: ['frontend/src/game/**/*.ts'], + rules: { + '@typescript-eslint/no-floating-promises': 'warn', + }, + }, +] diff --git a/frontend/src/composables/useHumanChallenge.ts b/frontend/src/composables/useHumanChallenge.ts index ca10fb3..f9092f2 100644 --- a/frontend/src/composables/useHumanChallenge.ts +++ b/frontend/src/composables/useHumanChallenge.ts @@ -51,7 +51,7 @@ export function useHumanChallenge( if (humanChoices.value.length > 0 && !humanAnswer.value.trim()) { humanAnswer.value = humanChoices.value[Math.floor(Math.random() * humanChoices.value.length)] } - submitHumanAnswer() + void submitHumanAnswer() } } }, 1000) @@ -77,7 +77,7 @@ export function useHumanChallenge( function submitChoice(choice: string) { humanAnswer.value = choice - submitHumanAnswer() + void submitHumanAnswer() } async function pollForChallenge() { @@ -110,8 +110,8 @@ export function useHumanChallenge( function startHumanPolling() { if (!myBotId.value) return - pollForChallenge() - humanPollHandle = setInterval(pollForChallenge, 400) + void pollForChallenge() + humanPollHandle = setInterval(() => { void pollForChallenge() }, 400) } function stopHumanPolling() { diff --git a/frontend/src/game/tts.ts b/frontend/src/game/tts.ts index 5c307be..337d6a3 100644 --- a/frontend/src/game/tts.ts +++ b/frontend/src/game/tts.ts @@ -120,7 +120,7 @@ const _pending = new Map() +const audioCache = new Map() const MAX_CACHE = 50 const activeSources: Set = new Set() @@ -265,7 +265,7 @@ async function _generateAndCache(text: string, profile: string): Promise= MAX_CACHE) { - const oldest = audioCache.keys().next().value - if (oldest) audioCache.delete(oldest) + let lruKey: string | undefined + let lruTime = Infinity + for (const [k, v] of audioCache) { + if (v.lastAccess < lruTime) { lruTime = v.lastAccess; lruKey = k } + } + if (lruKey) audioCache.delete(lruKey) } - audioCache.set(key, buf) + audioCache.set(key, { buf, lastAccess: Date.now() }) return buf } @@ -355,7 +359,8 @@ export function kokoroSpeak( const key = _cacheKey(text, profileName) const cached = audioCache.get(key) if (cached) { - _playBuffer(cached, dest, volume) + cached.lastAccess = Date.now() + _playBuffer(cached.buf, dest, volume) return true } // Generate in worker — will play when ready diff --git a/frontend/src/pages/ArenaPage.vue b/frontend/src/pages/ArenaPage.vue index 55e337b..f73587b 100644 --- a/frontend/src/pages/ArenaPage.vue +++ b/frontend/src/pages/ArenaPage.vue @@ -34,7 +34,7 @@ const tierClass = (t: number) => `tier-${t}`