Skip to content

atmosphere.js

TypeScript client for the Atmosphere Framework (currently 5.0.22). Supports WebTransport/HTTP3, WebSocket, SSE, HTTP Streaming, and Long-Polling transports with first-class React, Vue, and Svelte hooks.

Terminal window
npm install atmosphere.js
import { atmosphere } from 'atmosphere.js';
const subscription = await atmosphere.subscribe({
url: 'http://localhost:8080/chat',
transport: 'websocket',
}, {
message: (response) => console.log('Received:', response.responseBody),
open: (response) => console.log('Connected via:', response.transport),
close: (response) => console.log('Connection closed'),
error: (error) => console.error('Error:', error),
});
subscription.push({ user: 'John', message: 'Hello World' });

The package ships ESM, CommonJS, and TypeScript declarations. Framework integrations are tree-shakeable subpath exports:

import { Atmosphere } from 'atmosphere.js';
import { useAtmosphere, useStreaming, useRoom, usePresence } from 'atmosphere.js/react';
import { useAtmosphere, useStreaming, useRoom } from 'atmosphere.js/vue';
import { createAtmosphereStore, createStreamingStore, createRoomStore } from 'atmosphere.js/svelte';
import { useAtmosphereRN, useStreamingRN, setupReactNative } from 'atmosphere.js/react-native';

Manages subscriptions with automatic transport selection and fallback. Typically you use the shared atmosphere singleton, but you can also construct your own instance with per-client configuration or client-side interceptors:

import { Atmosphere } from 'atmosphere.js';
const atm = new Atmosphere({
logLevel: 'info', // 'debug' | 'info' | 'warn' | 'error' | 'silent'
defaultTransport: 'websocket',
fallbackTransport: 'long-polling',
});
atm.version; // '5.0.22'
atm.closeAll(); // Close every active subscription
atm.getSubscriptions(); // Map<string, Subscription>

The subscribe() callback object accepts more than the four happy-path handlers shown in Quick Start. The extra handlers are essential for production reconnection UX:

const subscription = await atmosphere.subscribe<ChatMessage>(
{
url: 'http://localhost:8080/atmosphere/chat',
transport: 'websocket',
fallbackTransport: 'sse',
reconnect: true,
reconnectInterval: 2000,
maxReconnectOnClose: 5,
contentType: 'application/json',
},
{
open: (response) => console.log('Connected via', response.transport),
message: (response) => console.log('Received:', response.responseBody),
close: (response) => console.log('Disconnected'),
error: (error) => console.error('Error:', error),
transportFailure: (reason, request) => {
console.warn(`${request.transport} failed: ${reason}, trying fallback`);
},
reconnect: (request, response) => {
console.log('Reconnecting...');
},
failureToReconnect: (request, response) => {
console.error('All reconnection attempts exhausted');
},
},
);
subscription.push('Hello, world!'); // string
subscription.push({ author: 'Alice', text: 'Hi' }); // JSON object
subscription.push(new ArrayBuffer(16)); // binary
subscription.suspend(); // pause receiving
await subscription.resume(); // resume
await subscription.close(); // disconnect

subscription.state reflects the lifecycle and is what framework hooks expose as state:

StateDescription
disconnectedNot connected
connectingConnection in progress
connectedConnection established
reconnectingAttempting to reconnect
suspendedPaused via suspend()
closedClosed via close()
errorConnection error

AtmosphereRooms is the vanilla-TypeScript counterpart of the useRoom / createRoomStore hooks. Use it when you don’t want a framework integration or when you need a single rooms instance shared across parts of your app. It pairs with the server-side RoomManager and RoomInterceptor.

import { Atmosphere, AtmosphereRooms } from 'atmosphere.js';
const atm = new Atmosphere();
const rooms = new AtmosphereRooms(atm, {
url: '/atmosphere/chat',
transport: 'websocket',
});
const lobby = await rooms.join('lobby', { id: 'alice' }, {
message: (data, member) => console.log(`${member.id}: ${data}`),
join: (event) => console.log(`${event.member.id} joined`),
leave: (event) => console.log(`${event.member.id} left`),
});
lobby.broadcast('Hello!');
lobby.sendTo('bob', 'Direct message');
lobby.members; // ReadonlyMap<string, RoomMember>
lobby.leave();
await rooms.leaveAll(); // Leave every room and close the underlying subscription
AtmosphereRooms methodDescription
join(roomName, member, handlers)Join a room; returns Promise<RoomHandle>
leave(roomName)Leave a specific room
leaveAll()Leave all rooms and close the connection
room(name)Get a RoomHandle by name (or undefined)
joinedRooms()List all joined room names

All hooks require an <AtmosphereProvider> ancestor.

import { AtmosphereProvider } from 'atmosphere.js/react';
function App() {
return (
<AtmosphereProvider>
<Chat />
</AtmosphereProvider>
);
}

Subscribe to an endpoint:

import { useAtmosphere } from 'atmosphere.js/react';
function Chat() {
const { data, state, push } = useAtmosphere<Message>({
request: { url: '/chat', transport: 'websocket' },
});
return state === 'connected'
? <button onClick={() => push({ text: 'Hello' })}>Send</button>
: <p>Connecting...</p>;
}

Join a room with presence:

import { useRoom } from 'atmosphere.js/react';
function ChatRoom() {
const { joined, members, messages, broadcast } = useRoom<ChatMessage>({
request: { url: '/atmosphere/room', transport: 'websocket' },
room: 'lobby',
member: { id: 'user-1' },
});
return (
<div>
<p>{members.length} online</p>
{messages.map((m, i) => <div key={i}>{m.member.id}: {m.data.text}</div>)}
<button onClick={() => broadcast({ text: 'Hi' })}>Send</button>
</div>
);
}

Lightweight presence tracking:

import { usePresence } from 'atmosphere.js/react';
function OnlineUsers() {
const { members, count, isOnline } = usePresence({
request: { url: '/atmosphere/room', transport: 'websocket' },
room: 'lobby',
member: { id: currentUser.id },
});
return <p>{count} users online. Alice is {isOnline('alice') ? 'here' : 'away'}.</p>;
}

AI/LLM text streaming:

import { useStreaming } from 'atmosphere.js/react';
function AiChat() {
const { fullText, isStreaming, stats, routing, send } = useStreaming({
request: { url: '/ai/chat', transport: 'websocket' },
});
return (
<div>
<button onClick={() => send('Explain WebSockets')} disabled={isStreaming}>Ask</button>
<p>{fullText}</p>
{stats && <small>{stats.totalStreamingTexts} streaming texts</small>}
</div>
);
}

Vue composables do not require a provider — they create or accept an Atmosphere instance directly.

<script setup lang="ts">
import { useAtmosphere } from 'atmosphere.js/vue';
const { data, state, push } = useAtmosphere<ChatMessage>({
url: '/chat',
transport: 'websocket',
});
</script>
<template>
<p>Status: {{ state }}</p>
<button @click="push({ text: 'Hello!' })">Send</button>
</template>
<script setup lang="ts">
import { useRoom } from 'atmosphere.js/vue';
const { members, messages, broadcast } = useRoom<ChatMessage>(
{ url: '/atmosphere/room', transport: 'websocket' },
'lobby',
{ id: 'user-1' },
);
</script>
<script setup lang="ts">
import { usePresence } from 'atmosphere.js/vue';
const { members, count, isOnline } = usePresence(
{ url: '/atmosphere/room', transport: 'websocket' },
'lobby',
{ id: currentUser.id },
);
</script>
<script setup lang="ts">
import { useStreaming } from 'atmosphere.js/vue';
const { fullText, isStreaming, send, reset } = useStreaming({
url: '/ai/chat',
transport: 'websocket',
});
</script>
<template>
<button @click="send('What is Atmosphere?')">Ask</button>
<p>{{ fullText }}</p>
<span v-if="isStreaming">Generating...</span>
</template>

Svelte integrations use the store pattern — each factory returns a Svelte-compatible readable store plus action functions.

<script>
import { createAtmosphereStore } from 'atmosphere.js/svelte';
const { store: chat, push } = createAtmosphereStore({ url: '/chat', transport: 'websocket' });
</script>
<p>Status: {$chat.state}</p>
<button on:click={() => push({ text: 'Hello!' })}>Send</button>
<script>
import { createRoomStore } from 'atmosphere.js/svelte';
const { store: lobby, broadcast } = createRoomStore(
{ url: '/atmosphere/room', transport: 'websocket' },
'lobby',
{ id: 'user-1' },
);
</script>
<p>Members: {$lobby.members.map(m => m.id).join(', ')}</p>
<button on:click={() => broadcast({ text: 'Hello!' })}>Broadcast</button>
<script>
import { createPresenceStore } from 'atmosphere.js/svelte';
const presence = createPresenceStore(
{ url: '/atmosphere/room', transport: 'websocket' },
'lobby',
{ id: 'user-1' },
);
</script>
<p>{$presence.count} users online</p>
<script>
import { createStreamingStore } from 'atmosphere.js/svelte';
const { store, send, reset } = createStreamingStore({
url: '/ai/chat',
transport: 'websocket',
});
</script>
<button on:click={() => send('What is Atmosphere?')}>Ask</button>
<p>{$store.fullText}</p>
{#if $store.isStreaming}<span>Generating...</span>{/if}

The server sends JSON messages using the Atmosphere AI streaming protocol:

{"type": "streaming-text", "data": "Hello", "sessionId": "abc-123", "seq": 1}
{"type": "progress", "data": "Thinking...", "sessionId": "abc-123", "seq": 2}
{"type": "metadata", "key": "model", "value": "gpt-4", "sessionId": "abc-123", "seq": 3}
{"type": "complete", "data": "Done", "sessionId": "abc-123", "seq": 10}
{"type": "error", "data": "Rate limited","sessionId": "abc-123", "seq": 11}

Use subscribeStreaming for framework-agnostic streaming, or the hooks above for React/Vue/Svelte.

OptionTypeDefaultDescription
urlstring(required)Endpoint URL
transportTransportType'websocket''webtransport' · 'websocket' · 'sse' · 'long-polling' · 'streaming'
fallbackTransportTransportTypeTransport to use if primary fails
contentTypestring'text/plain'Content type for messages
trackMessageLengthbooleanEnable message length prefixing (recommended: true)
messageDelimiterstring'|'Delimiter used with length prefixing
enableProtocolbooleanEnable Atmosphere protocol handshake
reconnectbooleantrueAuto-reconnect on disconnect
reconnectIntervalnumberBase delay between reconnection attempts (ms)
maxReconnectOnClosenumber5Max reconnection attempts
maxRequestnumberMax long-polling request cycles
timeoutnumberInactivity timeout (ms)
connectTimeoutnumberConnection timeout (ms)
headersRecord<string, string>Custom HTTP headers
withCredentialsbooleanInclude cookies (CORS)
sessionTokenstringDurable session token, sent as X-Atmosphere-Session-Token
heartbeat{ client?: number; server?: number }Heartbeat intervals (ms)

atmosphere.js supports durable sessions (see Durable Sessions tutorial) via the sessionToken request property. The server assigns a token on first connect and sends it in the response headers; the client stores it (e.g., in localStorage) and replays it on reconnect to resume the same logical session across page refreshes and network outages:

const sub = await atmosphere.subscribe({
url: '/atmosphere/chat',
transport: 'websocket',
sessionToken: localStorage.getItem('atmosphere-session') ?? undefined,
}, {
open: (response) => {
const token = response.headers['X-Atmosphere-Session-Token'];
if (token) localStorage.setItem('atmosphere-session', token);
},
});

Interceptors transform messages before sending and after receiving, like a middleware stack. Outgoing interceptors run in declaration order; incoming interceptors run in reverse order so the outermost wrapper unwraps last.

const atm = new Atmosphere({
interceptors: [
{
name: 'json',
onOutgoing: (data) => typeof data === 'string' ? data : JSON.stringify(data),
onIncoming: (body) => JSON.parse(body),
},
{
name: 'envelope',
onOutgoing: (body) => JSON.stringify({ payload: body, ts: Date.now() }),
onIncoming: (body) => JSON.parse(body).payload,
},
],
});

The client connects to server endpoints defined with @ManagedService. A minimal server-client pair:

Server (Java):

@ManagedService(path = "/atmosphere/chat")
public class Chat {
@Inject private AtmosphereResource r;
@Ready
public void onReady() {
// Client connected
}
@Message
public String onMessage(String message) {
return message; // Echo to all subscribers
}
}

Client (TypeScript):

const sub = await atmosphere.subscribe(
{ url: '/atmosphere/chat', transport: 'websocket' },
{ message: (res) => console.log(res.responseBody) },
);
sub.push('Hello from atmosphere.js');

See @ManagedService tutorial for the full server-side API.

  • Chrome/Edge: last 2 versions
  • Firefox: last 2 versions + ESR
  • Safari: last 2 versions
  • Mobile Safari (iOS): last 2 versions
  • Chrome Android: last 2 versions

Complete TypeScript type reference for the public surface.

class Atmosphere {
readonly version: string; // '5.0.22'
constructor(config?: {
logLevel?: 'debug' | 'info' | 'warn' | 'error' | 'silent';
defaultTransport?: TransportType;
fallbackTransport?: TransportType;
interceptors?: AtmosphereInterceptor[];
});
subscribe<T = unknown>(
request: AtmosphereRequest,
handlers: SubscriptionHandlers<T>,
): Promise<Subscription<T>>;
closeAll(): void;
getSubscriptions(): Map<string, Subscription>;
}
interface AtmosphereRequest {
url: string;
transport?: TransportType;
fallbackTransport?: TransportType;
contentType?: string;
trackMessageLength?: boolean;
messageDelimiter?: string;
enableProtocol?: boolean;
timeout?: number;
connectTimeout?: number;
reconnect?: boolean;
reconnectInterval?: number;
maxReconnectOnClose?: number;
maxRequest?: number;
headers?: Record<string, string>;
withCredentials?: boolean;
sessionToken?: string;
heartbeat?: { client?: number; server?: number };
}
type TransportType =
| 'webtransport'
| 'websocket'
| 'sse'
| 'long-polling'
| 'streaming';
interface SubscriptionHandlers<T = unknown> {
open?: (response: AtmosphereResponse<T>) => void;
message?: (response: AtmosphereResponse<T>) => void;
close?: (response: AtmosphereResponse<T>) => void;
error?: (error: Error) => void;
reopen?: (response: AtmosphereResponse<T>) => void;
reconnect?: (request: AtmosphereRequest, response: AtmosphereResponse<T>) => void;
transportFailure?: (reason: string, request: AtmosphereRequest) => void;
clientTimeout?: (request: AtmosphereRequest) => void;
failureToReconnect?: (request: AtmosphereRequest, response: AtmosphereResponse<T>) => void;
}
interface Subscription<T = unknown> {
readonly id: string;
readonly state: ConnectionState;
push(message: string | object | ArrayBuffer): void;
close(): Promise<void>;
suspend(): void;
resume(): Promise<void>;
on(event: string, handler: (...args: unknown[]) => void): void;
off(event: string, handler: (...args: unknown[]) => void): void;
}
type ConnectionState =
| 'disconnected'
| 'connecting'
| 'connected'
| 'reconnecting'
| 'suspended'
| 'closed'
| 'error';

RoomHandle, RoomMember, PresenceEvent, RoomMessage

Section titled “RoomHandle, RoomMember, PresenceEvent, RoomMessage”
interface RoomHandle<T = unknown> {
readonly name: string;
readonly members: ReadonlyMap<string, RoomMember>;
broadcast(data: T): void;
sendTo(memberId: string, data: T): void;
leave(): Promise<void>;
}
interface RoomMember {
id: string;
metadata?: Record<string, unknown>;
}
interface PresenceEvent {
type: 'join' | 'leave';
room: string;
member: RoomMember;
timestamp: number;
}
interface RoomMessage<T = unknown> {
type: 'join' | 'leave' | 'broadcast' | 'direct' | 'presence';
room: string;
data?: T;
member?: RoomMember;
target?: string; // for direct messages
}
interface AtmosphereInterceptor {
name?: string;
onOutgoing?: (data: string | ArrayBuffer) => string | ArrayBuffer;
onIncoming?: (body: string) => string;
}

Applied in order for outgoing messages and in reverse order for incoming — the middleware-stack pattern.