From 743c27685accc38a830fe1eee3833070842442e9 Mon Sep 17 00:00:00 2001 From: Neko Ayaka Date: Thu, 6 Nov 2025 03:44:58 +0800 Subject: [PATCH] refactor(stage-tamagotchi): better structure & lifecycle, set max listener for IpcRenderer --- apps/stage-tamagotchi/src/main/index.ts | 53 +++---------------- .../services/airi/channel-server/index.ts | 47 ++++++++++++++++ .../src/main/windows/caption/index.ts | 2 +- .../src/main/windows/main/index.ts | 5 ++ .../main/windows/main/rpc/index.electron.ts | 2 +- apps/stage-tamagotchi/src/preload/shared.ts | 7 ++- apps/stage-tamagotchi/src/renderer/App.vue | 10 ++-- .../src/renderer/pages/caption.vue | 8 +-- .../pages/settings/system/developer.vue | 6 +-- 9 files changed, 77 insertions(+), 63 deletions(-) create mode 100644 apps/stage-tamagotchi/src/main/services/airi/channel-server/index.ts diff --git a/apps/stage-tamagotchi/src/main/index.ts b/apps/stage-tamagotchi/src/main/index.ts index c7533ee98..73ec0dc43 100644 --- a/apps/stage-tamagotchi/src/main/index.ts +++ b/apps/stage-tamagotchi/src/main/index.ts @@ -1,6 +1,6 @@ import type { BrowserWindow } from 'electron' -import { env, platform } from 'node:process' +import { platform } from 'node:process' import { electronApp, optimizer } from '@electron-toolkit/utils' import { Format, LogLevel, setGlobalFormat, setGlobalLogLevel, useLogg } from '@guiiai/logg' @@ -15,6 +15,7 @@ import macOSTrayIcon from '../../resources/tray-icon-macos.png?asset' import { openDebugger, setupDebugger } from './app/debugger' import { emitAppBeforeQuit, emitAppReady, emitAppWindowAllClosed, onAppBeforeQuit } from './libs/bootkit/lifecycle' import { setElectronMainDirname } from './libs/electron/location' +import { setupChannelServer } from './services/airi/channel-server' import { setupCaptionWindowManager } from './windows/caption' import { setupChatWindowReusableFunc } from './windows/chat' import { setupInlayWindow } from './windows/inlay' @@ -89,53 +90,12 @@ function setupTray(params: { })() } -async function setupProjectAIRIServerRuntime() { - // Start the server-runtime server with WebSocket support - try { - // Dynamically import the server-runtime and listhen - const serverRuntime = await import('@proj-airi/server-runtime') - const { serve } = await import('h3') - const { plugin: ws } = await import('crossws/server') - - const serverInstance = serve(serverRuntime.app, { - // TODO: fix types - // @ts-expect-error - the .crossws property wasn't extended in types - plugins: [ws({ resolve: async req => (await serverRuntime.app.fetch(req)).crossws })], - port: env.PORT ? Number(env.PORT) : 6121, - hostname: env.SERVER_RUNTIME_HOSTNAME || 'localhost', - reusePort: true, - }) - - log.log('@proj-airi/server-runtime started on ws://localhost:6121') - - onAppBeforeQuit(async () => { - if (serverInstance && typeof serverInstance.close === 'function') { - try { - await serverInstance.close() - log.log('WebSocket server closed') - } - catch (error) { - log.withError(error).error('Error closing WebSocket server') - } - } - }) - } - catch (error) { - log.withError(error).error('failed to start WebSocket server') - } -} - app.whenReady().then(async () => { - await setupProjectAIRIServerRuntime() - injecta.setLogger(createLoggLogger(useLogg('injecta').useGlobalConfig())) - const settingsWindow = injecta.provide('windows:settings', { - build: () => setupSettingsWindowReusableFunc(), - }) - const chatWindow = injecta.provide('windows:chat', { - build: () => setupChatWindowReusableFunc(), - }) + const channelServerModule = injecta.provide('modules:channel-server', async () => setupChannelServer()) + const settingsWindow = injecta.provide('windows:settings', () => setupSettingsWindowReusableFunc()) + const chatWindow = injecta.provide('windows:chat', { build: () => setupChatWindowReusableFunc() }) const mainWindow = injecta.provide('windows:main', { dependsOn: { settingsWindow, chatWindow }, build: async ({ dependsOn }) => setupMainWindow(dependsOn), @@ -149,7 +109,7 @@ app.whenReady().then(async () => { build: async ({ dependsOn }) => setupTray(dependsOn), }) injecta.invoke({ - dependsOn: { mainWindow, tray }, + dependsOn: { mainWindow, tray, channelServerModule }, callback: noop, }) @@ -183,4 +143,5 @@ app.on('window-all-closed', () => { // Clean up server and intervals when app quits app.on('before-quit', async () => { emitAppBeforeQuit() + injecta.stop() }) diff --git a/apps/stage-tamagotchi/src/main/services/airi/channel-server/index.ts b/apps/stage-tamagotchi/src/main/services/airi/channel-server/index.ts new file mode 100644 index 000000000..16e6dbb21 --- /dev/null +++ b/apps/stage-tamagotchi/src/main/services/airi/channel-server/index.ts @@ -0,0 +1,47 @@ +import { env } from 'node:process' + +import { useLogg } from '@guiiai/logg' + +import { onAppBeforeQuit } from '../../../libs/bootkit/lifecycle' + +export async function setupChannelServer() { + const log = useLogg('main/server-runtime').useGlobalConfig() + + // Start the server-runtime server with WebSocket support + try { + // Dynamically import the server-runtime and listhen + const serverRuntime = await import('@proj-airi/server-runtime') + const { serve } = await import('h3') + const { plugin: ws } = await import('crossws/server') + + const serverInstance = serve(serverRuntime.app, { + // TODO: fix types + // @ts-expect-error - the .crossws property wasn't extended in types + plugins: [ws({ resolve: async req => (await serverRuntime.app.fetch(req)).crossws })], + port: env.PORT ? Number(env.PORT) : 6121, + hostname: env.SERVER_RUNTIME_HOSTNAME || 'localhost', + reusePort: true, + gracefulShutdown: { + forceTimeout: 500, + gracefulTimeout: 500, + }, + }) + + log.log('@proj-airi/server-runtime started on ws://localhost:6121') + + onAppBeforeQuit(async () => { + if (serverInstance && typeof serverInstance.close === 'function') { + try { + await serverInstance.close() + log.log('WebSocket server closed') + } + catch (error) { + log.withError(error).error('Error closing WebSocket server') + } + } + }) + } + catch (error) { + log.withError(error).error('failed to start WebSocket server') + } +} diff --git a/apps/stage-tamagotchi/src/main/windows/caption/index.ts b/apps/stage-tamagotchi/src/main/windows/caption/index.ts index 552c0727a..a9024ee5f 100644 --- a/apps/stage-tamagotchi/src/main/windows/caption/index.ts +++ b/apps/stage-tamagotchi/src/main/windows/caption/index.ts @@ -249,7 +249,7 @@ export function setupCaptionWindowManager(params: { mainWindow: BrowserWindow }) // TODO: once we refactored eventa to support window-namespaced contexts, // we can remove the setMaxListeners call below since eventa will be able to dispatch and // manage events within eventa's context system. - ipcMain.setMaxListeners(100) + ipcMain.setMaxListeners(0) const window = createCaptionWindow() const { context } = createContext(ipcMain, window) diff --git a/apps/stage-tamagotchi/src/main/windows/main/index.ts b/apps/stage-tamagotchi/src/main/windows/main/index.ts index a9bf3d9c8..6615698c2 100644 --- a/apps/stage-tamagotchi/src/main/windows/main/index.ts +++ b/apps/stage-tamagotchi/src/main/windows/main/index.ts @@ -143,6 +143,11 @@ export async function setupMainWindow(params: { } } + // TODO: once we refactored eventa to support window-namespaced contexts, + // we can remove the setMaxListeners call below since eventa will be able to dispatch and + // manage events within eventa's context system. + ipcMain.setMaxListeners(0) + const { context } = createContext(ipcMain, window) const cleanUpWindowDraggingInvokeHandler = defineInvokeHandler(context, electronStartDraggingWindow, handleStartDraggingWindow) diff --git a/apps/stage-tamagotchi/src/main/windows/main/rpc/index.electron.ts b/apps/stage-tamagotchi/src/main/windows/main/rpc/index.electron.ts index 7c8bd4ed9..206949cff 100644 --- a/apps/stage-tamagotchi/src/main/windows/main/rpc/index.electron.ts +++ b/apps/stage-tamagotchi/src/main/windows/main/rpc/index.electron.ts @@ -16,7 +16,7 @@ export function setupMainWindowElectronInvokes(params: { // TODO: once we refactored eventa to support window-namespaced contexts, // we can remove the setMaxListeners call below since eventa will be able to dispatch and // manage events within eventa's context system. - ipcMain.setMaxListeners(100) + ipcMain.setMaxListeners(0) const { context } = createContext(ipcMain, params.window) diff --git a/apps/stage-tamagotchi/src/preload/shared.ts b/apps/stage-tamagotchi/src/preload/shared.ts index a6fa91646..10353bcba 100644 --- a/apps/stage-tamagotchi/src/preload/shared.ts +++ b/apps/stage-tamagotchi/src/preload/shared.ts @@ -3,9 +3,14 @@ import type { ElectronWindow } from '@proj-airi/stage-shared' import { contextIsolated, platform } from 'node:process' import { electronAPI } from '@electron-toolkit/preload' -import { contextBridge } from 'electron' +import { contextBridge, ipcRenderer } from 'electron' export function expose(customApi: CustomApi = undefined as CustomApi) { + // TODO: once we refactored eventa to support window-namespaced contexts, + // we can remove the setMaxListeners call below since eventa will be able to dispatch and + // manage events within eventa's context system. + ipcRenderer.setMaxListeners(0) + // Use `contextBridge` APIs to expose Electron APIs to // renderer only if context isolation is enabled, otherwise // just add to the DOM global. diff --git a/apps/stage-tamagotchi/src/renderer/App.vue b/apps/stage-tamagotchi/src/renderer/App.vue index b08ec8d47..de7b91ca0 100644 --- a/apps/stage-tamagotchi/src/renderer/App.vue +++ b/apps/stage-tamagotchi/src/renderer/App.vue @@ -3,13 +3,13 @@ import { useDisplayModelsStore } from '@proj-airi/stage-ui/stores/display-models import { useOnboardingStore } from '@proj-airi/stage-ui/stores/onboarding' import { useSettings } from '@proj-airi/stage-ui/stores/settings' import { defineInvoke, defineInvokeHandler } from '@unbird/eventa' -import { createContext } from '@unbird/eventa/adapters/electron/renderer' import { storeToRefs } from 'pinia' import { onMounted, watch } from 'vue' import { useI18n } from 'vue-i18n' import { RouterView, useRouter } from 'vue-router' import { electronOpenSettings, electronStartTrackMousePosition } from '../shared/eventa' +import { useElectronEventaContext } from './composables/electron-vueuse' const i18n = useI18n() const displayModelsStore = useDisplayModelsStore() @@ -29,14 +29,12 @@ onMounted(async () => { await displayModelsStore.loadDisplayModelsFromIndexedDB() await settingsStore.initializeStageModel() - const { context } = createContext(window.electron.ipcRenderer) - const startTrackingCursorPoint = defineInvoke(context, electronStartTrackMousePosition) + const context = useElectronEventaContext() + const startTrackingCursorPoint = defineInvoke(context.value, electronStartTrackMousePosition) await startTrackingCursorPoint() // Listen for open-settings IPC message from main process - defineInvokeHandler(context, electronOpenSettings, () => { - router.push('/settings') - }) + defineInvokeHandler(context.value, electronOpenSettings, () => router.push('/settings')) }) watch(themeColorsHue, () => { diff --git a/apps/stage-tamagotchi/src/renderer/pages/caption.vue b/apps/stage-tamagotchi/src/renderer/pages/caption.vue index ce33ee7c0..6ee2bc7c6 100644 --- a/apps/stage-tamagotchi/src/renderer/pages/caption.vue +++ b/apps/stage-tamagotchi/src/renderer/pages/caption.vue @@ -1,10 +1,10 @@