Go to file
Fabian @ Blax Software d0a8f46d80 feat(ws): opt-in send timeout, per call or client-wide (0.3.3)
send() waited forever when the server never answered. In apps that awaited
a read during page setup, a dropped response left the old layout on screen
with an empty body until a reload.

- sendTimeout (ms, default 0 = off): reject unanswered sends with
  { message, code: 'timeout' }. The clock starts at the call, so a send
  stuck behind the connect or channel subscribe times out too.
- send(event, data, channel, { timeout, progress }): per-call override;
  a bare function as 4th argument still means progress. timeout 0 opts out.
- A send that timed out before it was written is dropped from the queue
  instead of going out late after a reconnect.
- Protocol events (websocket.*) never time out.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-28 14:42:03 +02:00
.github feat: expose ws globally for console debugging 2026-04-16 08:48:19 +02:00
src feat(ws): opt-in send timeout, per call or client-wide (0.3.3) 2026-09-28 14:42:03 +02:00
test feat(ws): opt-in send timeout, per call or client-wide (0.3.3) 2026-09-28 14:42:03 +02:00
.gitignore chore: revert dist/ commit, add prepare script for git installs 2026-04-16 08:38:03 +02:00
AGENTS.md feat(ws): opt-in send timeout, per call or client-wide (0.3.3) 2026-09-28 14:42:03 +02:00
README.md feat(ws): opt-in send timeout, per call or client-wide (0.3.3) 2026-09-28 14:42:03 +02:00
package-lock.json feat(ws): opt-in send timeout, per call or client-wide (0.3.3) 2026-09-28 14:42:03 +02:00
package.json feat(ws): opt-in send timeout, per call or client-wide (0.3.3) 2026-09-28 14:42:03 +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)
sendTimeout number (ms) 0 (off) Reject sends the server has not answered in time with { code: 'timeout' }; per call: send(e, d, ch, { timeout }) (≥ 0.3.3)
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. progress is a callback or { progress, timeout }
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