Skip to content

Latest commit

 

History

History
785 lines (604 loc) · 19.4 KB

File metadata and controls

785 lines (604 loc) · 19.4 KB

atmosphere.js

TypeScript client for the Atmosphere Framework. Supports WebSocket, SSE, and Long-Polling transports.

npm version TypeScript License

Features

  • TypeScript with full type safety and IntelliSense support
  • Multiple transports: WebSocket, SSE, Long-polling, Streaming
  • Automatic reconnection with exponential backoff
  • Tree-shakeable — import only what you need
  • No runtime dependencies (React, Vue, and Svelte are optional peer dependencies)
  • Promise-based API with async/await
  • Comprehensive test coverage

Installation

npm install atmosphere.js

Quick Start

import { atmosphere } from 'atmosphere.js';

// Subscribe to an endpoint
const subscription = await atmosphere.subscribe({
  url: 'http://localhost:8080/chat',
  transport: 'websocket',
}, {
  message: (response) => {
    console.log('Received:', response.responseBody);
  },
  open: (response) => {
    console.log('Connected with transport:', response.transport);
  },
  close: (response) => {
    console.log('Connection closed');
  },
  error: (error) => {
    console.error('Error:', error);
  }
});

// Send a message
subscription.push({ 
  user: 'John', 
  message: 'Hello World' 
});

// Close the connection
await subscription.close();

API Reference

Creating an Atmosphere Instance

import { Atmosphere } from 'atmosphere.js';

const atmosphere = new Atmosphere({
  logLevel: 'info',
  defaultTransport: 'websocket',
  fallbackTransport: 'long-polling'
});

Subscribe to an Endpoint

const subscription = await atmosphere.subscribe(
  {
    url: 'http://localhost:8080/chat',
    transport: 'websocket',
    reconnect: true,
    reconnectInterval: 5000,
    maxReconnectOnClose: 10,
    trackMessageLength: false,
    headers: {
      'Authorization': 'Bearer token123'
    }
  },
  {
    message: (response) => { /* handle message */ },
    open: (response) => { /* handle open */ },
    close: (response) => { /* handle close */ },
    error: (error) => { /* handle error */ },
    reconnect: (request, response) => { /* handle reconnect */ }
  }
);

Request Options

interface AtmosphereRequest {
  url: string;                      // Endpoint URL
  transport: TransportType;         // 'websocket' | 'sse' | 'long-polling' | 'streaming' | 'jsonp'
  fallbackTransport?: TransportType;// Transport to use if primary fails
  contentType?: string;             // Content-Type header
  timeout?: number;                 // Request timeout in milliseconds
  reconnect?: boolean;              // Enable auto-reconnection
  reconnectInterval?: number;       // Time between reconnections (ms)
  maxReconnectOnClose?: number;     // Maximum reconnection attempts
  trackMessageLength?: boolean;     // Enable message length tracking
  messageDelimiter?: string;        // Delimiter for split messages
  enableProtocol?: boolean;         // Enable Atmosphere protocol
  headers?: Record<string, string>; // Custom headers
  withCredentials?: boolean;        // Include credentials
}

Subscription Methods

// Send a message
subscription.push('Hello');                    // String
subscription.push({ message: 'Hello' });       // Object (auto-stringified)
subscription.push(new ArrayBuffer(8));         // Binary data

// Get current state
const state = subscription.state; // 'disconnected' | 'connecting' | 'connected' | 'reconnecting' | 'suspended' | 'closed' | 'error'

// Close the subscription
await subscription.close();

// Event emitter style
subscription.on('custom-event', (data) => {
  console.log(data);
});

subscription.off('custom-event', handler);

Examples

Basic WebSocket Connection

import { atmosphere } from 'atmosphere.js';

const subscription = await atmosphere.subscribe({
  url: 'ws://localhost:8080/chat',
  transport: 'websocket'
}, {
  message: (response) => {
    console.log(response.responseBody);
  }
});

subscription.push('Hello server!');

With Reconnection

const subscription = await atmosphere.subscribe({
  url: 'http://localhost:8080/chat',
  transport: 'websocket',
  reconnect: true,
  reconnectInterval: 3000,
  maxReconnectOnClose: 10
}, {
  message: (response) => {
    console.log('Message:', response.responseBody);
  },
  reconnect: (request, response) => {
    console.log('Reconnecting... Attempt:', request);
  },
  open: (response) => {
    console.log('Connection established');
  }
});

Custom Headers and Authentication

const subscription = await atmosphere.subscribe({
  url: 'http://localhost:8080/secure-chat',
  transport: 'websocket',
  headers: {
    'Authorization': `Bearer ${authToken}`,
    'X-Custom-Header': 'value'
  },
  withCredentials: true
}, {
  message: (response) => {
    console.log(response.responseBody);
  }
});

Type-Safe Messages

interface ChatMessage {
  user: string;
  message: string;
  timestamp: number;
}

const subscription = await atmosphere.subscribe<ChatMessage>({
  url: 'http://localhost:8080/chat',
  transport: 'websocket'
}, {
  message: (response) => {
    // response.responseBody is typed as ChatMessage
    const msg = response.responseBody;
    console.log(`${msg.user}: ${msg.message}`);
  }
});

Multiple Subscriptions

const chat = await atmosphere.subscribe({
  url: 'http://localhost:8080/chat',
  transport: 'websocket'
}, {
  message: (response) => console.log('Chat:', response.responseBody)
});

const notifications = await atmosphere.subscribe({
  url: 'http://localhost:8080/notifications',
  transport: 'websocket'
}, {
  message: (response) => console.log('Notification:', response.responseBody)
});

// Close all subscriptions
await atmosphere.closeAll();

Error Handling

try {
  const subscription = await atmosphere.subscribe({
    url: 'http://localhost:8080/chat',
    transport: 'websocket'
  }, {
    error: (error) => {
      console.error('Connection error:', error);
    },
    close: (response) => {
      console.log('Connection closed:', response.reasonPhrase);
    }
  });
} catch (error) {
  console.error('Failed to connect:', error);
}

Framework Hooks

atmosphere.js ships with first-class integrations for React, Vue, and Svelte. Each framework integration is a separate entry point that can be imported independently and is fully tree-shakeable.

React

Import from atmosphere.js/react. All hooks require an <AtmosphereProvider> ancestor.

Setup

import { AtmosphereProvider } from 'atmosphere.js/react';

function App() {
  return (
    <AtmosphereProvider config={{ logLevel: 'info' }}>
      <Chat />
    </AtmosphereProvider>
  );
}

useAtmosphere<T> -- subscribe to an endpoint

import { useAtmosphere } from 'atmosphere.js/react';

function Chat() {
  const { data, state, push } = useAtmosphere<ChatMessage>({
    request: { url: '/chat', transport: 'websocket' },
  });

  return (
    <div>
      <p>Status: {state}</p>
      <p>Last message: {JSON.stringify(data)}</p>
      <button onClick={() => push({ text: 'Hello!' })}>Send</button>
    </div>
  );
}

Returns { subscription, state, data, error, push }.

useRoom<T> -- join a room with presence

import { useRoom } from 'atmosphere.js/react';

function Lobby() {
  const { joined, members, messages, broadcast, sendTo } = useRoom<ChatMessage>({
    request: { url: '/atmosphere/room', transport: 'websocket' },
    room: 'lobby',
    member: { id: 'user-1' },
  });

  return (
    <div>
      <p>Members: {members.map(m => m.id).join(', ')}</p>
      <button onClick={() => broadcast({ text: 'Hello room!' })}>Broadcast</button>
      <button onClick={() => sendTo('user-2', { text: 'Hey' })}>DM user-2</button>
    </div>
  );
}

Returns { joined, members, messages, broadcast, sendTo, error }.

usePresence -- 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>;
}

Returns { joined, members, count, isOnline }.

useStreaming -- AI/LLM text streaming

import { useStreaming } from 'atmosphere.js/react';

function AiChat() {
  const { fullText, isStreaming, send, reset, progress, metadata, error } = useStreaming({
    request: { url: '/ai/chat', transport: 'websocket' },
  });

  return (
    <div>
      <button onClick={() => send('What is Atmosphere?')}>Ask</button>
      <button onClick={reset}>Clear</button>
      <p>{fullText}</p>
      {isStreaming && <span>{progress ?? 'Generating...'}</span>}
    </div>
  );
}

Returns { fullText, streamingTexts, isStreaming, progress, metadata, error, send, reset, close }.

Vue

Import from atmosphere.js/vue. Vue composables do not require a provider -- they create or accept an Atmosphere instance directly.

useAtmosphere<T>

<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>
  <p>{{ data }}</p>
  <button @click="push({ text: 'Hello!' })">Send</button>
</template>

useRoom<T>

<script setup lang="ts">
import { useRoom } from 'atmosphere.js/vue';

const { members, messages, broadcast, sendTo } = useRoom<ChatMessage>(
  { url: '/atmosphere/room', transport: 'websocket' },
  'lobby',
  { id: 'user-1' },
);
</script>

usePresence

<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>

useStreaming

<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

Import from atmosphere.js/svelte. Svelte integrations use the store pattern -- each factory returns a Svelte-compatible readable store plus action functions.

createAtmosphereStore<T>

<script>
  import { createAtmosphereStore } from 'atmosphere.js/svelte';

  const { store: chat, push } = createAtmosphereStore({ url: '/chat', transport: 'websocket' });
  // $chat.state, $chat.data, $chat.error
</script>

<p>Status: {$chat.state}</p>
<p>{JSON.stringify($chat.data)}</p>
<button on:click={() => push({ text: 'Hello!' })}>Send</button>

createRoomStore<T>

<script>
  import { createRoomStore } from 'atmosphere.js/svelte';

  const { store: lobby, broadcast, sendTo } = createRoomStore(
    { url: '/atmosphere/room', transport: 'websocket' },
    'lobby',
    { id: 'user-1' },
  );
  // $lobby.joined, $lobby.members, $lobby.messages
</script>

<p>Members: {$lobby.members.map(m => m.id).join(', ')}</p>
<button on:click={() => broadcast({ text: 'Hello!' })}>Broadcast</button>

createPresenceStore

<script>
  import { createPresenceStore } from 'atmosphere.js/svelte';

  const presence = createPresenceStore(
    { url: '/atmosphere/room', transport: 'websocket' },
    'lobby',
    { id: 'user-1' },
  );
  // $presence.joined, $presence.members, $presence.count
</script>

<p>{$presence.count} users online</p>

createStreamingStore

<script>
  import { createStreamingStore } from 'atmosphere.js/svelte';

  const { store, send, reset } = createStreamingStore({
    url: '/ai/chat',
    transport: 'websocket',
  });
  // $store.fullText, $store.isStreaming, $store.streamingTexts, $store.progress
</script>

<button on:click={() => send('What is Atmosphere?')}>Ask</button>
<p>{$store.fullText}</p>
{#if $store.isStreaming}<span>Generating...</span>{/if}

React Native / Expo

Import from atmosphere.js/react-native. Call setupReactNative() once at app startup. All hooks require an <AtmosphereProvider> ancestor.

Full guide: docs/react-native.md

Setup

import { setupReactNative, AtmosphereProvider } from 'atmosphere.js/react-native';

setupReactNative(); // installs polyfills, detects capabilities

export default function App() {
  return (
    <AtmosphereProvider config={{ logLevel: 'info' }}>
      <Chat />
    </AtmosphereProvider>
  );
}

useAtmosphereRN<T> -- subscribe with AppState + NetInfo

import { useAtmosphereRN } from 'atmosphere.js/react-native';

function Chat() {
  const { data, state, push, isConnected } = useAtmosphereRN<ChatMessage>({
    request: { url: 'https://example.com/chat', transport: 'websocket' },
    backgroundBehavior: 'suspend', // 'suspend' | 'disconnect' | 'keep-alive'
  });
  // ...
}

Returns { subscription, state, data, error, push, isConnected, isInternetReachable }.

useStreamingRN -- AI streaming with AppState + NetInfo

import { useStreamingRN } from 'atmosphere.js/react-native';

function AiChat() {
  const { fullText, isStreaming, isConnected, send, reset } = useStreamingRN({
    request: { url: 'https://example.com/ai/chat', transport: 'websocket' },
  });
  // ...
}

Returns the same fields as useStreaming plus isConnected.

Installation

bun add atmosphere.js
bun add @react-native-community/netinfo  # optional, for network-aware reconnection

Rooms and Presence

The room system provides a high-level API for joining named rooms, broadcasting messages, sending direct messages, and tracking who is online. It works with the server-side RoomManager and RoomInterceptor.

Framework-agnostic usage

import { Atmosphere } from 'atmosphere.js';
import { AtmosphereRooms } from 'atmosphere.js'; // or from the internal module

const atmosphere = new Atmosphere();
const rooms = new AtmosphereRooms(atmosphere, {
  url: 'ws://localhost:8080/atmosphere/room',
  transport: 'websocket',
});

// Join a room
const lobby = await rooms.join('lobby', { id: 'user-1' }, {
  joined: (roomName, memberList) => {
    console.log(`Joined ${roomName}, members:`, memberList);
  },
  message: (data, sender) => {
    console.log(`${sender.id}: ${data}`);
  },
  join: (event) => {
    console.log(`${event.member.id} joined at ${event.timestamp}`);
  },
  leave: (event) => {
    console.log(`${event.member.id} left`);
  },
  error: (err) => {
    console.error('Room error:', err);
  },
});

// Broadcast to all members
lobby.broadcast({ text: 'Hello everyone!' });

// Direct message to a specific member
lobby.sendTo('user-2', { text: 'Private message' });

// Check current members
console.log('Members:', [...lobby.members.values()]);

// Leave the room
lobby.leave();

// Or leave all rooms and close the connection
await rooms.leaveAll();

RoomMember

Each member has a required id field and an optional info record for metadata:

interface RoomMember {
  readonly id: string;
  readonly info?: Record<string, unknown>;
}

Presence events

Presence events are delivered as PresenceEvent objects:

interface PresenceEvent {
  readonly type: 'join' | 'leave';
  readonly room: string;
  readonly member: RoomMember;
  readonly timestamp: number;
}

For framework-specific usage, see useRoom / usePresence (React), useRoom / usePresence (Vue), and createRoomStore / createPresenceStore (Svelte) in the Framework Hooks section above.


AI Streaming

atmosphere.js includes a streaming decoder and subscription helper for AI/LLM endpoints that use the Atmosphere AI streaming wire protocol (server-side @AiEndpoint and DefaultStreamingSession).

Wire protocol

Each message from the server is a JSON object with type, sessionId, and seq fields:

{"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}

Message types: streaming-text, progress, complete, error, metadata.

parseStreamingMessage(raw)

Low-level decoder that parses a raw string into a StreamingMessage, or returns null if it is not a valid streaming protocol message. This is an internal utility used by subscribeStreaming:

import { parseStreamingMessage } from 'atmosphere.js/streaming/decoder';

const msg = parseStreamingMessage('{"type":"streaming-text","data":"Hi","sessionId":"s1","seq":1}');
if (msg) {
  console.log(msg.type, msg.data); // "streaming-text" "Hi"
}

Note: Most applications should use subscribeStreaming or framework hooks (useStreaming) instead of calling this directly.

subscribeStreaming(atmosphere, request, handlers)

Framework-agnostic helper that creates a subscription, parses streaming messages automatically (with dedup via sequence numbers), and dispatches to handler callbacks:

import { Atmosphere } from 'atmosphere.js';
import { subscribeStreaming } from 'atmosphere.js';

const atmosphere = new Atmosphere();
const handle = await subscribeStreaming(atmosphere, {
  url: '/ai/chat',
  transport: 'websocket',
}, {
  onStreamingText: (streamingText, seq) => process.stdout.write(streamingText),
  onProgress: (message) => console.log('Progress:', message),
  onComplete: (summary) => console.log('\nDone!', summary),
  onError: (error) => console.error('Error:', error),
  onMetadata: (key, value) => console.log(`${key}: ${value}`),
});

// Send a prompt to start streaming
handle.send('Explain virtual threads in Java 21');

// Session ID assigned by the server
console.log('Session:', handle.sessionId);

// Close when done
await handle.close();

For framework-specific wrappers, see useStreaming (React/Vue) and createStreamingStore (Svelte) in the Framework Hooks section above.


Browser Compatibility

  • 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

Development

# Install dependencies
npm install

# Run tests
npm test

# Run tests with UI
npm run test:ui

# Run tests with coverage
npm run test:ci

# Build
npm run build

# Development mode (watch)
npm run dev

# Type checking
npm run typecheck

# Linting
npm run lint

# Format code
npm run format

License

Apache License 2.0 - see LICENSE file for details

Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

Links


Maintained by the Atmosphere team.