From 46ce034ababc1341e023419c855b98a473d2c9fd Mon Sep 17 00:00:00 2001 From: "Fabian @ Blax Software" Date: Thu, 16 Apr 2026 08:17:56 +0200 Subject: [PATCH] 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 --- src/nuxt.ts | 37 +++++++++++++++++++++++++++++++++---- src/types.ts | 5 ++++- src/ws.ts | 33 +++++++++++++++++++++++++++++++++ 3 files changed, 70 insertions(+), 5 deletions(-) diff --git a/src/nuxt.ts b/src/nuxt.ts index 30954ba..af1be0e 100644 --- a/src/nuxt.ts +++ b/src/nuxt.ts @@ -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 /** Additional ApiClientConfig overrides */ apiConfig?: Partial @@ -45,20 +54,24 @@ export function createFromNuxtConfig(options: NuxtNetworkingOptions = {}): { api: ApiClient ws: WsClient } { - // Nuxt auto-imports — these are available at plugin/composable scope - // @ts-expect-error Nuxt auto-import - const config = useRuntimeConfig() + // 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 + ?? (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 = (initial: T): ReactiveRef => ref(initial) as ReactiveRef + // 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, }, diff --git a/src/types.ts b/src/types.ts index 9445774..1e9c0dc 100644 --- a/src/types.ts +++ b/src/types.ts @@ -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 diff --git a/src/ws.ts b/src/ws.ts index 9a00705..3c33cbb 100644 --- a/src/ws.ts +++ b/src/ws.ts @@ -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) }