refactor(stage-tamagotchi): better structure & lifecycle, set max listener for IpcRenderer

This commit is contained in:
Neko Ayaka
2025-11-06 03:44:58 +08:00
parent 36ccf4b246
commit 743c27685a
9 changed files with 77 additions and 63 deletions
+7 -46
View File
@@ -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()
})
@@ -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')
}
}
@@ -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)
@@ -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)
@@ -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)
+6 -1
View File
@@ -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 = unknown>(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.
+4 -6
View File
@@ -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, () => {
@@ -1,10 +1,10 @@
<script setup lang="ts">
import { defineInvoke } from '@unbird/eventa'
import { createContext } from '@unbird/eventa/adapters/electron/renderer'
import { useBroadcastChannel } from '@vueuse/core'
import { onMounted, ref, watch } from 'vue'
import { captionGetIsFollowingWindow, captionIsFollowingWindowChanged } from '../../shared/eventa'
import { useElectronEventaContext } from '../composables/electron-vueuse'
const attached = ref(true)
const speakerText = ref('')
@@ -16,8 +16,8 @@ type CaptionChannelEvent
| { type: 'caption-assistant', text: string }
const { data } = useBroadcastChannel<CaptionChannelEvent, CaptionChannelEvent>({ name: 'airi-caption-overlay' })
const { context } = createContext(window.electron.ipcRenderer)
const getAttached = defineInvoke(context, captionGetIsFollowingWindow)
const context = useElectronEventaContext()
const getAttached = defineInvoke(context.value, captionGetIsFollowingWindow)
onMounted(async () => {
try {
@@ -27,7 +27,7 @@ onMounted(async () => {
catch {}
try {
context.on(captionIsFollowingWindowChanged, (event) => {
context.value.on(captionIsFollowingWindowChanged, (event) => {
attached.value = Boolean(event?.body)
})
}
@@ -1,12 +1,11 @@
<script setup lang="ts">
import { ButtonBar, CheckBar, IconItem } from '@proj-airi/stage-ui/components'
import { useSettings } from '@proj-airi/stage-ui/stores/settings'
import { defineInvoke } from '@unbird/eventa'
import { createContext } from '@unbird/eventa/adapters/electron/renderer'
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
import { electronOpenMainDevtools } from '../../../../shared/eventa'
import { useElectronEventaInvoke } from '../../../composables/electron-vueuse'
const { t } = useI18n()
const settings = useSettings()
@@ -44,8 +43,7 @@ const menu = computed(() => [
},
])
const { context } = createContext(window.electron.ipcRenderer)
const openDevTools = defineInvoke(context, electronOpenMainDevtools)
const openDevTools = useElectronEventaInvoke(electronOpenMainDevtools)
</script>
<template>