feat: configurable appKey, URL validation, connection diagnostics

- types: add appKey to WsClientConfig
- nuxt: read PUSHER_APP_KEY from runtimeConfig, validate wsUrl/appKey
- ws: URL validation, debug logging, 5s connection_established timeout
This commit is contained in:
Fabian @ Blax Software 2026-04-16 08:17:56 +02:00
parent e80b3d09b1
commit 46ce034aba
3 changed files with 70 additions and 5 deletions

View File

@ -18,6 +18,15 @@ export interface NuxtNetworkingOptions {
wsUrlKey?: string
/** runtimeConfig key for WS protocol — `'wss'` or `'ws'` (default: `'WS_PROTOCOL'`) */
wsProtocolKey?: string
/** runtimeConfig key for the WebSocket app key (default: `'PUSHER_APP_KEY'`) */
appKeyConfigKey?: string
/**
* Pass in `useRuntimeConfig()` from the calling plugin/composable.
* If omitted the function falls back to the Nuxt auto-import (works only
* when Nuxt treats this file as part of the app's auto-import context).
*/
runtimeConfig?: Record<string, any>
/** Additional ApiClientConfig overrides */
apiConfig?: Partial<ApiClientConfig>
@ -45,20 +54,24 @@ export function createFromNuxtConfig(options: NuxtNetworkingOptions = {}): {
api: ApiClient
ws: WsClient
} {
// Nuxt auto-imports — these are available at plugin/composable scope
// Use provided runtimeConfig or fall back to Nuxt auto-import
const config = options.runtimeConfig
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error Nuxt auto-import
const config = useRuntimeConfig()
?? (typeof useRuntimeConfig === 'function' ? useRuntimeConfig() : {})
const serverUrlKey = options.serverUrlKey ?? 'SERVER_URL'
const serverUrlInternalKey = options.serverUrlInternalKey ?? 'SERVER_URL_INTERNAL'
const wsUrlKey = options.wsUrlKey ?? 'WEBS_URL'
const wsProtocolKey = options.wsProtocolKey ?? 'WS_PROTOCOL'
const appKeyConfigKey = options.appKeyConfigKey ?? 'PUSHER_APP_KEY'
const pub = config.public ?? config
const serverUrl: string = pub[serverUrlKey] ?? ''
const serverUrlInternal: string = pub[serverUrlInternalKey] ?? ''
const wsUrl: string = pub[wsUrlKey] ?? ''
const wsProtocol: string = pub[wsProtocolKey] ?? 'wss'
const appKey: string = pub[appKeyConfigKey] ?? ''
// @ts-expect-error Nuxt/Vite global
const isServer: boolean = import.meta.server ?? false
@ -84,9 +97,25 @@ export function createFromNuxtConfig(options: NuxtNetworkingOptions = {}): {
// Nuxt always has Vue — use ref directly for reactive state
const vueRef = <T>(initial: T): ReactiveRef<T> => ref(initial) as ReactiveRef<T>
// Validate WebSocket configuration
if (!wsUrl) {
console.error(
`[blax-networking] Missing WebSocket URL. Set runtimeConfig.public.${wsUrlKey} ` +
`or the NUXT_PUBLIC_${wsUrlKey} environment variable.`,
)
}
if (!appKey) {
console.error(
`[blax-networking] Missing WebSocket app key. Set runtimeConfig.public.${appKeyConfigKey} ` +
`or the NUXT_PUBLIC_${appKeyConfigKey} environment variable. ` +
`This must match PUSHER_APP_KEY on the backend.`,
)
}
const ws = createWsClient(
{
url: `${wsProtocol === 'wss' ? 'wss' : 'ws'}://${wsUrl}/app/ws`,
url: `${wsProtocol === 'wss' ? 'wss' : 'ws'}://${wsUrl}/app/${appKey}`,
appKey,
isServer: () => isServer,
...options.wsConfig,
},

View File

@ -131,9 +131,12 @@ export interface ApiClientConfig {
// ---------------------------------------------------------------------------
export interface WsClientConfig {
/** Full WebSocket URL (e.g. `'wss://example.com/app/ws'`). String or getter. */
/** Full WebSocket URL (e.g. `'wss://example.com/app/mykey'`). String or getter. */
url: string | (() => string)
/** Application key used in the WebSocket path (e.g. `/app/{appKey}`). */
appKey?: string
/** Called on each channel establishment to get the current auth token. */
getAuthToken?: () => string | null | undefined

View File

@ -327,6 +327,7 @@ class WsClientImpl extends EventTarget implements WsClient {
if (force_reset) this.channels = []
const url = this._getUrl()
console.debug('[blax-networking] Connecting to', url)
this.socket = new WebSocket(url) as WsSocket
this.is_connecting_socket.value = true
this._config.onConnectionStateChange?.('connecting')
@ -377,6 +378,21 @@ class WsClientImpl extends EventTarget implements WsClient {
this._config.onConnectionStateChange?.('connected')
// Warmup ping
socket.send('{"event":"websocket.ping","data":{}}')
// Detect missing connection_established response (usually app key mismatch)
setTimeout(() => {
if (this.socket === socket && !socket.socket_id) {
const wsUrl = this._getUrl()
console.error(
'[blax-networking] WebSocket opened but server did not send ' +
'"websocket.connection_established" within 5s.\n' +
'Connected to: ' + wsUrl + '\n' +
'This usually means the app key in the URL does not match any app ' +
'configured on the backend (PUSHER_APP_KEY). Check that the path ' +
'segment after /app/ matches the server\'s PUSHER_APP_KEY.',
)
}
}, 5000)
})
socket.addEventListener('message', (raw) => {
@ -625,5 +641,22 @@ export function createWsClient(
? config.isServer()
: (config.isServer ?? false)
if (isServer) return createSsrStub(createRef)
// Validate configuration and warn developers about common issues
const url = typeof config.url === 'function' ? config.url() : config.url
if (!url || url === 'wss:///app/' || url === 'ws:///app/') {
console.error(
'[blax-networking] WebSocket URL is empty or malformed: ' + JSON.stringify(url) + '\n' +
'Ensure WEBS_URL and PUSHER_APP_KEY are configured. ' +
'Expected format: wss://your-ws-host/app/{appKey}',
)
} else if (url.endsWith('/app/') || url.endsWith('/app')) {
console.error(
'[blax-networking] WebSocket URL is missing the app key: ' + url + '\n' +
'Ensure PUSHER_APP_KEY is set. The URL must end with /app/{appKey} ' +
'where {appKey} matches the PUSHER_APP_KEY on the backend.',
)
}
return new WsClientImpl(config, createRef)
}