Go to file
Fabian @ Blax Software 737954ba2d fix(ws): don't strand sends when a connection attempt fails (0.3.2)
A page's send() queued behind a failing first connect hung forever — the
close handler dropped the ensureConnected() waiter without settling it, and
a throttled connect() was silently skipped, so nothing ever reconnected.
Symptom in apps: sidebar renders, page body stays blank until a reload.

- ensureConnected(): one waiter per outage, resolved by whichever attempt
  succeeds next (no longer orphaned by a failed attempt)
- connect(): a call that lands on the reconnect throttle is retried when the
  window opens instead of being dropped
- a replaced (connect(true)) or destroy()ed socket's late error/close no
  longer wipes the live socket's state or schedules a reconnect
- destroy() cancels a pending auto-reconnect
- new opt-in `rejectOnDisconnect`: a send already written to a socket that
  closes rejects with { message, code: 'connection_lost' } instead of
  hanging (default off — unchanged behaviour for existing consumers)
- node:test suite (`npm test`) with a scripted fake socket covering each case

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-24 09:46:52 +02:00
.github feat: expose ws globally for console debugging 2026-04-16 08:48:19 +02:00
src fix(ws): don't strand sends when a connection attempt fails (0.3.2) 2026-09-24 09:46:52 +02:00
test fix(ws): don't strand sends when a connection attempt fails (0.3.2) 2026-09-24 09:46:52 +02:00
.gitignore chore: revert dist/ commit, add prepare script for git installs 2026-04-16 08:38:03 +02:00
AGENTS.md fix(ws): don't strand sends when a connection attempt fails (0.3.2) 2026-09-24 09:46:52 +02:00
README.md fix(ws): don't strand sends when a connection attempt fails (0.3.2) 2026-09-24 09:46:52 +02:00
package-lock.json fix(ws): don't strand sends when a connection attempt fails (0.3.2) 2026-09-24 09:46:52 +02:00
package.json fix(ws): don't strand sends when a connection attempt fails (0.3.2) 2026-09-24 09:46:52 +02:00
tsconfig.json Init 2026-04-07 08:44:00 +02:00
tsup.config.ts Init 2026-04-07 08:44:00 +02:00

README.md

@blax-software/networking

Framework-agnostic API and WebSocket client library with optional Vue, Nuxt, and axios adapters.

Features

  • Zero required dependencies — core uses native fetch and WebSocket
  • Tree-shakeable — import only what you need
  • SSR-safe — server detection with automatic no-op stubs
  • Framework adapters — Vue ref() reactivity, Nuxt auto-config, axios compatibility
  • TypeScript-first — Full type coverage with .d.ts exports

Install

npm install @blax-software/networking

# Optional peer dependencies:
npm install vue          # for /vue and /nuxt entry points
npm install axios        # for /axios adapter

Quick Start

Vanilla / React / Any framework

import { createApiClient, createWsClient } from '@blax-software/networking'

const api = createApiClient({
  serverUrl: 'https://api.example.com',
})

const ws = createWsClient({
  url: 'wss://api.example.com/app/ws',
  getAuthToken: () => api.bearerToken,
})

// REST
const users = await api.get('users')

// WebSocket
await ws.connect()
ws.setAppReady()
const result = await ws.send('user.profile', { id: 123 })

// Listen for events — returns an unsubscribe function
const off = ws.listen('chat.message', null, (data) => {
  console.log('New message:', data)
})
// Call off() to stop listening (works with React useEffect cleanup, etc.)

Vue 3

import { useApiClient, useWsClient, useWsListener } from '@blax-software/networking/vue'

const api = useApiClient({
  serverUrl: 'https://api.example.com',
})

const ws = useWsClient({
  url: 'wss://api.example.com/app/ws',
  getAuthToken: () => api.bearerToken,
})

// ws.is_setup, ws.is_opened etc. are Vue refs
watch(ws.is_setup, (ready) => {
  if (ready) console.log('WebSocket ready')
})

// Auto-cleanup on component unmount
useWsListener(ws, 'notifications.new', null, (data) => {
  console.log('Notification:', data)
})

Nuxt 3

// plugins/networking.client.ts
import { createFromNuxtConfig } from '@blax-software/networking/nuxt'

export default defineNuxtPlugin(() => {
  const { api, ws } = createFromNuxtConfig()

  // Optional: store bearer, connect WS, etc.
  api.setBearer(localStorage.getItem('bearerToken') ?? '')
  ws.connect()
  ws.setAppReady()

  return { provide: { api, ws } }
})

Reads these keys from useRuntimeConfig().public:

Key Description
SERVER_URL REST API base URL
SERVER_URL_INTERNAL Internal URL for SSR requests
WEBS_URL WebSocket hostname
WS_PROTOCOL 'wss' or 'ws'

With axios

import { createApiClient } from '@blax-software/networking'
import { createAxiosAdapter } from '@blax-software/networking/axios'
import axios from 'axios'

const axiosInstance = axios.create({
  baseURL: 'https://api.example.com',
  withCredentials: true,
})

const api = createApiClient({
  serverUrl: 'https://api.example.com',
  http: createAxiosAdapter(axiosInstance),
})

API Reference

createApiClient(config: ApiClientConfig): ApiClient

Config Type Default Description
serverUrl string | () => string required Base URL for HTTP requests
ssrServerUrl string | () => string — Alternate URL for server-side requests
http HttpAdapter fetchAdapter HTTP adapter (fetch, axios, custom)
notify NotifyFn — Notification callback for errors
storage StorageAdapter browserStorage Token persistence adapter
storageKey string 'bearerToken' Storage key for the bearer token
timeout number 10000 Request timeout in ms
withCredentials boolean true Include cookies in requests
retryOn503 boolean true Auto-retry on HTTP 503
apiPrefix string 'api/' Prefix prepended to relative paths
isServer boolean | () => boolean false Whether running on the server (SSR)

ApiClient methods:

Method Returns Description
get(path, params?) Promise<HttpResponse> GET request
post(path, data?, headers?) Promise<HttpResponse> POST request
put(path, data?) Promise<HttpResponse> PUT request
delete(path, headers?) Promise<HttpResponse> DELETE request
patch(path, data?) Promise<HttpResponse> PATCH request
csrf(path?) Promise<HttpResponse> Fetch CSRF cookie
setBearer(token | null) void Set the auth token (pass null to clear)
bearerToken string (getter) Current auth token
loadBearerFromStorage() string | null Load the token from the storage adapter
getBackendUrl() string Resolve current server base URL
clientAsset(path) string Build a backend asset URL
cleanseUrl(url) string Normalize a URL/path
parseError(error) never Extract error, notify, and re-throw
parseThen(response, msg?) any Show success notification, return data
configure(partial) void Update config at runtime

createWsClient(config: WsClientConfig, createRef?): WsClient

Config Type Default Description
url string | () => string required Full WebSocket URL
getAuthToken () => string | null — Auth token getter for channel subscription
notify NotifyFn — Connection state notifications
translate TranslateFn — Translation function for notification text
defaultChannel string 'websocket' Default channel name
heartbeatInterval number 20000 Ping interval (ms)
reconnectDelay number 3000 Delay before reconnect attempt (ms)
autoReconnect boolean true Auto-reconnect on disconnect
rejectOnDisconnect boolean false Reject in-flight sends with { code: 'connection_lost' } when their socket closes (≥ 0.3.2)
isServer boolean | () => boolean false Returns safe no-op stub when true
isNativePlatform boolean | () => boolean false Suppresses browser-only notifications

WsClient methods:

Method Returns Description
connect(force?) Promise<WebSocket | void> Open socket connection
send(event, data?, channel?, progress?) Promise<T> Send event, await response
listen(event, channel, callback) () => void Listen for event, returns unsubscribe fn
listenOnce(event, channel?) Promise<any> Resolve on next occurrence
setAppReady() void Unblock gated send() calls
resetConnection() void Force channels to re-subscribe
destroy() void Close and clean up everything

Reactive state (Vue refs when using vue/nuxt adapters, plain objects otherwise):

Property Type Description
is_opened ReactiveRef<boolean> Socket is open
is_setup ReactiveRef<boolean> Default channel established
is_connecting_socket ReactiveRef<boolean> Connection in progress
is_after_lost_connection ReactiveRef<boolean> Had a connection loss

Storage Adapters

import { browserStorage, memoryStorage } from '@blax-software/networking'

// Default — uses localStorage (safe for SSR, returns null on errors)
const api1 = createApiClient({ serverUrl: '...', storage: browserStorage })

// In-memory — for SSR, tests, or environments without localStorage
const api2 = createApiClient({ serverUrl: '...', storage: memoryStorage() })

License

MIT

Star History

Star History Chart