fix(stage-*): not respond if no websocket works
This commit is contained in:
@@ -1,6 +1,12 @@
|
||||
import { initializeApp } from '@proj-airi/stage-ui/services'
|
||||
import type { Plugin } from 'vue'
|
||||
|
||||
import Tres from '@tresjs/core'
|
||||
|
||||
import { autoAnimatePlugin } from '@formkit/auto-animate/vue'
|
||||
import { MotionPlugin } from '@vueuse/motion'
|
||||
import { createPinia } from 'pinia'
|
||||
import { setupLayouts } from 'virtual:generated-layouts'
|
||||
import { createApp } from 'vue'
|
||||
import { createRouter, createWebHashHistory } from 'vue-router'
|
||||
import { routes } from 'vue-router/auto-routes'
|
||||
|
||||
@@ -32,11 +38,12 @@ const router = createRouter({
|
||||
routes: setupLayouts(routes),
|
||||
})
|
||||
|
||||
// Initialize and mount the app using the shared initialization logic
|
||||
initializeApp(App, {
|
||||
router,
|
||||
pinia,
|
||||
i18n,
|
||||
}).then((app) => {
|
||||
app.mount('#app')
|
||||
})
|
||||
createApp(App)
|
||||
.use(MotionPlugin)
|
||||
// TODO: Fix autoAnimatePlugin type error
|
||||
.use(autoAnimatePlugin as unknown as Plugin)
|
||||
.use(router)
|
||||
.use(pinia)
|
||||
.use(i18n)
|
||||
.use(Tres)
|
||||
.mount('#app')
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
import { initializeApp } from '@proj-airi/stage-ui/services'
|
||||
import type { Plugin } from 'vue'
|
||||
|
||||
import Tres from '@tresjs/core'
|
||||
|
||||
import { autoAnimatePlugin } from '@formkit/auto-animate/vue'
|
||||
import { MotionPlugin } from '@vueuse/motion'
|
||||
import { createPinia } from 'pinia'
|
||||
import { setupLayouts } from 'virtual:generated-layouts'
|
||||
import { createApp } from 'vue'
|
||||
import { createRouter, createWebHashHistory } from 'vue-router'
|
||||
import { routes } from 'vue-router/auto-routes'
|
||||
|
||||
@@ -32,11 +38,12 @@ const router = createRouter({
|
||||
routes: setupLayouts(routes),
|
||||
})
|
||||
|
||||
// Initialize and mount the app using the shared initialization logic
|
||||
initializeApp(App, {
|
||||
router,
|
||||
pinia,
|
||||
i18n,
|
||||
}).then((app) => {
|
||||
app.mount('#app')
|
||||
})
|
||||
createApp(App)
|
||||
.use(MotionPlugin)
|
||||
// TODO: Fix autoAnimatePlugin type error
|
||||
.use(autoAnimatePlugin as unknown as Plugin)
|
||||
.use(router)
|
||||
.use(pinia)
|
||||
.use(i18n)
|
||||
.use(Tres)
|
||||
.mount('#app')
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
<script setup lang="ts">
|
||||
import { OnboardingDialog, ToasterRoot } from '@proj-airi/stage-ui/components'
|
||||
import { useConfiguratorForAiriSdk } from '@proj-airi/stage-ui/stores/configurator'
|
||||
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 { StageTransitionGroup } from '@proj-airi/ui-transitions'
|
||||
import { useDark } from '@vueuse/core'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { computed, onMounted, watch } from 'vue'
|
||||
import { computed, onMounted, onUnmounted, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { RouterView } from 'vue-router'
|
||||
import { toast, Toaster } from 'vue-sonner'
|
||||
@@ -23,6 +24,7 @@ const settings = storeToRefs(settingsStore)
|
||||
const onboardingStore = useOnboardingStore()
|
||||
const { shouldShowSetup } = storeToRefs(onboardingStore)
|
||||
const isDark = useDark()
|
||||
const { dispose } = useConfiguratorForAiriSdk()
|
||||
|
||||
const primaryColor = computed(() => {
|
||||
return isDark.value
|
||||
@@ -66,6 +68,10 @@ onMounted(async () => {
|
||||
await settingsStore.initializeStageModel()
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
dispose()
|
||||
})
|
||||
|
||||
// Handle first-time setup events
|
||||
function handleSetupConfigured() {
|
||||
onboardingStore.markSetupCompleted()
|
||||
|
||||
+14
-29
@@ -1,11 +1,14 @@
|
||||
import type { App as VueApp } from 'vue'
|
||||
import type { Plugin } from 'vue'
|
||||
import type { Router } from 'vue-router'
|
||||
|
||||
import Tres from '@tresjs/core'
|
||||
import NProgress from 'nprogress'
|
||||
|
||||
import { initializeApp } from '@proj-airi/stage-ui/services'
|
||||
import { autoAnimatePlugin } from '@formkit/auto-animate/vue'
|
||||
import { MotionPlugin } from '@vueuse/motion'
|
||||
import { createPinia } from 'pinia'
|
||||
import { setupLayouts } from 'virtual:generated-layouts'
|
||||
import { createApp } from 'vue'
|
||||
import { createRouter, createWebHashHistory, createWebHistory } from 'vue-router'
|
||||
import { routes } from 'vue-router/auto-routes'
|
||||
|
||||
@@ -37,30 +40,12 @@ router.afterEach(() => {
|
||||
NProgress.done()
|
||||
})
|
||||
|
||||
// Custom initialization callback for web-specific post-initialization
|
||||
async function onInitialized(_app: VueApp, router: Router) {
|
||||
// Handle PWA registration after router is ready
|
||||
router.isReady()
|
||||
.then(async () => {
|
||||
if (import.meta.env.SSR) {
|
||||
return
|
||||
}
|
||||
if (import.meta.env.VITE_APP_TARGET_HUGGINGFACE_SPACE) {
|
||||
return
|
||||
}
|
||||
|
||||
const { registerSW } = await import('./modules/pwa')
|
||||
registerSW({ immediate: true })
|
||||
})
|
||||
.catch(error => console.error('Failed during post-initialization:', error))
|
||||
}
|
||||
|
||||
// Initialize and mount the app using the shared initialization logic with web-specific options
|
||||
initializeApp(App, {
|
||||
router,
|
||||
pinia,
|
||||
i18n,
|
||||
onInitialized,
|
||||
}).then((app) => {
|
||||
app.mount('#app')
|
||||
})
|
||||
createApp(App)
|
||||
.use(MotionPlugin)
|
||||
// TODO: Fix autoAnimatePlugin type error
|
||||
.use(autoAnimatePlugin as unknown as Plugin)
|
||||
.use(router)
|
||||
.use(pinia)
|
||||
.use(i18n)
|
||||
.use(Tres)
|
||||
.mount('#app')
|
||||
|
||||
@@ -49,6 +49,7 @@ export default defineConfig({
|
||||
|
||||
resolve: {
|
||||
alias: {
|
||||
'@proj-airi/server-sdk': resolve(join(import.meta.dirname, '..', '..', 'packages', 'server-sdk', 'src')),
|
||||
'@proj-airi/stage-ui': resolve(join(import.meta.dirname, '..', '..', 'packages', 'stage-ui', 'src')),
|
||||
'@proj-airi/i18n': resolve(join(import.meta.dirname, '..', '..', 'packages', 'i18n', 'src')),
|
||||
},
|
||||
|
||||
@@ -4,6 +4,13 @@ import WebSocket from 'crossws/websocket'
|
||||
|
||||
import { sleep } from '@moeru/std'
|
||||
|
||||
class ReconnectingError extends Error {
|
||||
constructor(message: string) {
|
||||
super(message)
|
||||
this.name = 'ReconnectingError'
|
||||
}
|
||||
}
|
||||
|
||||
export interface ClientOptions<C = undefined> {
|
||||
url?: string
|
||||
name: string
|
||||
@@ -18,6 +25,7 @@ export interface ClientOptions<C = undefined> {
|
||||
|
||||
export class Client<C = undefined> {
|
||||
private connected = false
|
||||
private connecting = false
|
||||
private websocket?: WebSocket
|
||||
private shouldClose = false
|
||||
|
||||
@@ -70,6 +78,10 @@ export class Client<C = undefined> {
|
||||
return
|
||||
}
|
||||
catch (err) {
|
||||
if (err instanceof ReconnectingError) {
|
||||
return
|
||||
}
|
||||
|
||||
this.opts.onError?.(err)
|
||||
const delay = Math.min(2 ** attempts * 1000, 30_000) // capped exponential backoff
|
||||
await sleep(delay)
|
||||
@@ -82,6 +94,7 @@ export class Client<C = undefined> {
|
||||
if (this.shouldClose) {
|
||||
return
|
||||
}
|
||||
|
||||
await this.retryWithExponentialBackoff(() => this._connect())
|
||||
}
|
||||
|
||||
@@ -89,18 +102,27 @@ export class Client<C = undefined> {
|
||||
if (this.shouldClose || this.connected) {
|
||||
return Promise.resolve()
|
||||
}
|
||||
if (this.connecting) {
|
||||
return Promise.reject(new ReconnectingError('Already connecting'))
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
this.connecting = true
|
||||
|
||||
const ws = new WebSocket(this.opts.url)
|
||||
this.websocket = ws
|
||||
|
||||
ws.onmessage = this.handleMessageBound
|
||||
ws.onerror = (event: any) => {
|
||||
this.connecting = false
|
||||
this.connected = false
|
||||
|
||||
this.opts.onError?.(event)
|
||||
reject(event?.error ?? new Error('WebSocket error'))
|
||||
}
|
||||
|
||||
ws.onclose = () => {
|
||||
this.connecting = false
|
||||
|
||||
if (this.connected) {
|
||||
this.connected = false
|
||||
this.opts.onClose?.()
|
||||
@@ -109,11 +131,10 @@ export class Client<C = undefined> {
|
||||
void this.tryReconnectWithExponentialBackoff()
|
||||
}
|
||||
}
|
||||
|
||||
ws.onmessage = this.handleMessageBound
|
||||
|
||||
ws.onopen = () => {
|
||||
this.connecting = false
|
||||
this.connected = true
|
||||
|
||||
this.opts.token ? this.tryAuthenticate() : this.tryAnnounce()
|
||||
resolve()
|
||||
}
|
||||
|
||||
@@ -26,8 +26,6 @@
|
||||
"./constants": "./src/constants/index.ts",
|
||||
"./libs/*": "./src/libs/*.ts",
|
||||
"./libs": "./src/libs/index.ts",
|
||||
"./services/*": "./src/services/*.ts",
|
||||
"./services": "./src/services/index.ts",
|
||||
"./stores/*": "./src/stores/*.ts",
|
||||
"./stores": "./src/stores/index.ts",
|
||||
"./utils": "./src/utils/index.ts"
|
||||
|
||||
@@ -1,66 +0,0 @@
|
||||
import type { Pinia } from 'pinia'
|
||||
import type { App, Component, Plugin } from 'vue'
|
||||
import type { I18n } from 'vue-i18n'
|
||||
import type { Router } from 'vue-router'
|
||||
|
||||
import Tres from '@tresjs/core'
|
||||
|
||||
import { autoAnimatePlugin } from '@formkit/auto-animate/vue'
|
||||
import { MotionPlugin } from '@vueuse/motion'
|
||||
import { createApp } from 'vue'
|
||||
|
||||
import { settingsBroadcaster } from './settings-broadcaster'
|
||||
|
||||
interface AppInitializationOptions {
|
||||
/** Router for the app - must be provided */
|
||||
router: Router
|
||||
/** Pinia instance for state management */
|
||||
pinia: Pinia
|
||||
/** I18n instance for internationalization - using any to maintain compatibility with various locale configurations */
|
||||
i18n: I18n<any, any, any, any, any>
|
||||
/** Optional callback to run before mounting the app */
|
||||
beforeMountCallback?: () => void | Promise<void>
|
||||
/** Callback to run after the app is initialized but before mounting */
|
||||
onInitialized?: (app: App, router: Router) => void | Promise<void>
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes a Vue application with common plugins and services.
|
||||
* This function creates a Vue app instance with all the standard plugins
|
||||
* and services used across AIRI applications.
|
||||
*
|
||||
* Note: The router, pinia, and i18n instances must be provided by the caller
|
||||
* as they are application-specific and cannot be created in the shared module.
|
||||
*/
|
||||
export async function initializeApp(
|
||||
AppComponent: Component,
|
||||
options: AppInitializationOptions,
|
||||
): Promise<App> {
|
||||
const app = createApp(AppComponent)
|
||||
.use(MotionPlugin)
|
||||
.use(autoAnimatePlugin as unknown as Plugin) // TODO: Fix autoAnimatePlugin type error
|
||||
.use(options.router)
|
||||
.use(options.pinia)
|
||||
.use(options.i18n)
|
||||
.use(Tres)
|
||||
|
||||
// Run any custom initialization logic
|
||||
if (options.onInitialized) {
|
||||
await options.onInitialized(app, options.router)
|
||||
}
|
||||
|
||||
// Initialize settings broadcaster connection
|
||||
try {
|
||||
await settingsBroadcaster.connect()
|
||||
}
|
||||
catch (error) {
|
||||
console.error('Failed to connect settings broadcaster:', error)
|
||||
}
|
||||
|
||||
// Run any custom before-mount logic
|
||||
if (options.beforeMountCallback) {
|
||||
await options.beforeMountCallback()
|
||||
}
|
||||
|
||||
return app
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
export { initializeApp } from './app-initializer'
|
||||
export { settingsBroadcaster } from './settings-broadcaster'
|
||||
@@ -1,92 +0,0 @@
|
||||
import { Client } from '@proj-airi/server-sdk'
|
||||
|
||||
class SettingsBroadcaster {
|
||||
private client: Client | null = null
|
||||
private connected = false
|
||||
private pendingConfigurations: Array<{ moduleName: string, config: Record<string, unknown> }> = []
|
||||
|
||||
constructor() {
|
||||
this.initClient()
|
||||
}
|
||||
|
||||
private initClient() {
|
||||
try {
|
||||
this.client = new Client({
|
||||
name: 'settings-broadcaster',
|
||||
url: import.meta.env.VITE_AIRI_WS_URL || 'ws://localhost:6121/ws',
|
||||
possibleEvents: [
|
||||
'ui:configure',
|
||||
'module:authenticated',
|
||||
],
|
||||
})
|
||||
|
||||
this.client.onEvent('module:authenticated', (event) => {
|
||||
if (event.data.authenticated) {
|
||||
this.connected = true
|
||||
// Send any pending configurations
|
||||
this.sendPendingConfigurations()
|
||||
}
|
||||
})
|
||||
}
|
||||
catch (error) {
|
||||
// In a real implementation, you might want to use a proper error handling mechanism
|
||||
// For now, we'll just log the error to comply with linting rules
|
||||
console.error('Failed to initialize SettingsBroadcaster client:', error)
|
||||
}
|
||||
}
|
||||
|
||||
private sendPendingConfigurations() {
|
||||
if (!this.client) {
|
||||
console.warn('SettingsBroadcaster client is not initialized. Pending configurations were not sent.')
|
||||
return
|
||||
}
|
||||
for (const { moduleName, config } of this.pendingConfigurations) {
|
||||
this.client.send({
|
||||
type: 'ui:configure' as const,
|
||||
data: {
|
||||
moduleName,
|
||||
config,
|
||||
},
|
||||
})
|
||||
}
|
||||
this.pendingConfigurations = []
|
||||
}
|
||||
|
||||
public sendConfiguration(moduleName: string, config: Record<string, unknown>): void {
|
||||
if (!this.client) {
|
||||
console.warn('SettingsBroadcaster client is not initialized. Configuration was not sent.', { moduleName, config })
|
||||
return
|
||||
}
|
||||
|
||||
const configData = {
|
||||
type: 'ui:configure' as const,
|
||||
data: {
|
||||
moduleName,
|
||||
config,
|
||||
},
|
||||
}
|
||||
|
||||
if (this.connected) {
|
||||
this.client.send(configData)
|
||||
}
|
||||
else {
|
||||
// Queue the configuration to send when connected
|
||||
this.pendingConfigurations.push({ moduleName, config })
|
||||
}
|
||||
}
|
||||
|
||||
public async connect(): Promise<void> {
|
||||
if (this.client) {
|
||||
await this.client.connect()
|
||||
}
|
||||
}
|
||||
|
||||
public disconnect(): void {
|
||||
if (this.client) {
|
||||
this.client.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create a singleton instance
|
||||
export const settingsBroadcaster = new SettingsBroadcaster()
|
||||
@@ -0,0 +1,97 @@
|
||||
import { Client } from '@proj-airi/server-sdk'
|
||||
import { defineStore } from 'pinia'
|
||||
import { onMounted, ref } from 'vue'
|
||||
|
||||
export const useConfiguratorForAiriSdk = defineStore('configurator:adapter:proj-airi:server-sdk', () => {
|
||||
const connected = ref(false)
|
||||
const client = ref<Client>()
|
||||
const pendingUpdates = ref<Array<{
|
||||
moduleName: string
|
||||
config: Record<string, unknown>
|
||||
}>>([])
|
||||
|
||||
function flushPending() {
|
||||
if (client.value && connected.value) {
|
||||
for (const update of pendingUpdates.value) {
|
||||
client.value.send({
|
||||
type: 'ui:configure' as const,
|
||||
data: {
|
||||
moduleName: update.moduleName,
|
||||
config: update.config,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
pendingUpdates.value = []
|
||||
}
|
||||
}
|
||||
|
||||
function updateFor(moduleName: string, config: Record<string, unknown>) {
|
||||
if (client.value && connected.value) {
|
||||
client.value.send({
|
||||
type: 'ui:configure' as const,
|
||||
data: {
|
||||
moduleName,
|
||||
config,
|
||||
},
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
pendingUpdates.value.push({ moduleName, config })
|
||||
}
|
||||
|
||||
function init(options?: { token?: string }) {
|
||||
return new Promise((resolve, reject) => {
|
||||
client.value = new Client({
|
||||
name: 'proj-airi:ui:stage',
|
||||
url: import.meta.env.VITE_AIRI_WS_URL || 'ws://localhost:6121/ws',
|
||||
token: options?.token,
|
||||
possibleEvents: [
|
||||
'ui:configure',
|
||||
'module:authenticated',
|
||||
],
|
||||
onError: (error) => {
|
||||
reject(error)
|
||||
},
|
||||
})
|
||||
|
||||
client.value.onEvent('module:authenticated', (event) => {
|
||||
if (event.data.authenticated) {
|
||||
connected.value = true
|
||||
flushPending()
|
||||
resolve(true)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function clearAllPending() {
|
||||
pendingUpdates.value = []
|
||||
}
|
||||
|
||||
function dispose() {
|
||||
flushPending()
|
||||
|
||||
client.value?.close()
|
||||
connected.value = false
|
||||
client.value = undefined
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
init()
|
||||
})
|
||||
|
||||
return {
|
||||
connected,
|
||||
client,
|
||||
|
||||
flushPending,
|
||||
clearAllPending,
|
||||
updateFor,
|
||||
|
||||
dispose,
|
||||
init,
|
||||
}
|
||||
})
|
||||
@@ -2,16 +2,17 @@ import { useLocalStorage } from '@vueuse/core'
|
||||
import { defineStore } from 'pinia'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import { settingsBroadcaster } from '../../services/settings-broadcaster'
|
||||
import { useConfiguratorForAiriSdk } from '../configurator'
|
||||
|
||||
export const useDiscordStore = defineStore('discord', () => {
|
||||
const configurator = useConfiguratorForAiriSdk()
|
||||
const enabled = useLocalStorage('settings/discord/enabled', false)
|
||||
const token = useLocalStorage('settings/discord/token', '')
|
||||
|
||||
function saveSettings() {
|
||||
// Data is automatically saved to localStorage via useLocalStorage
|
||||
// Also broadcast configuration to backend
|
||||
settingsBroadcaster.sendConfiguration('discord', {
|
||||
configurator.updateFor('discord', {
|
||||
token: token.value,
|
||||
enabled: enabled.value,
|
||||
})
|
||||
|
||||
@@ -2,17 +2,19 @@ import { useLocalStorage } from '@vueuse/core'
|
||||
import { defineStore } from 'pinia'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import { settingsBroadcaster } from '../../services/settings-broadcaster'
|
||||
import { useConfiguratorForAiriSdk } from '../configurator'
|
||||
|
||||
export function createGamingModuleStore(moduleName: string, defaultPort: number) {
|
||||
return defineStore(moduleName, () => {
|
||||
const configurator = useConfiguratorForAiriSdk()
|
||||
|
||||
const enabled = useLocalStorage(`settings/${moduleName}/enabled`, false)
|
||||
const serverAddress = useLocalStorage(`settings/${moduleName}/server-address`, '')
|
||||
const serverPort = useLocalStorage<number | null>(`settings/${moduleName}/server-port`, defaultPort)
|
||||
const username = useLocalStorage(`settings/${moduleName}/username`, '')
|
||||
|
||||
function saveSettings() {
|
||||
settingsBroadcaster.sendConfiguration(moduleName, {
|
||||
configurator.updateFor(moduleName, {
|
||||
enabled: enabled.value,
|
||||
serverAddress: serverAddress.value,
|
||||
serverPort: serverPort.value,
|
||||
|
||||
@@ -2,9 +2,11 @@ import { useLocalStorage } from '@vueuse/core'
|
||||
import { defineStore } from 'pinia'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import { settingsBroadcaster } from '../../services/settings-broadcaster'
|
||||
import { useConfiguratorForAiriSdk } from '../configurator'
|
||||
|
||||
export const useTwitterStore = defineStore('twitter', () => {
|
||||
const configurator = useConfiguratorForAiriSdk()
|
||||
|
||||
const enabled = useLocalStorage('settings/twitter/enabled', false)
|
||||
const apiKey = useLocalStorage('settings/twitter/api-key', '')
|
||||
const apiSecret = useLocalStorage('settings/twitter/api-secret', '')
|
||||
@@ -14,7 +16,7 @@ export const useTwitterStore = defineStore('twitter', () => {
|
||||
function saveSettings() {
|
||||
// Data is automatically saved to localStorage via useLocalStorage
|
||||
// Also broadcast configuration to backend
|
||||
settingsBroadcaster.sendConfiguration('twitter', {
|
||||
configurator.updateFor('twitter', {
|
||||
enabled: enabled.value,
|
||||
apiKey: apiKey.value,
|
||||
apiSecret: apiSecret.value,
|
||||
|
||||
Reference in New Issue
Block a user