JavaScript Client
The official Adama JavaScript client wraps the WebSocket protocol so you don't have to think about connection management, reconnection, state synchronization, or message framing. You just connect, subscribe to data, and send messages.
Installation
Browser (CDN)
Include the library directly:
<script src="https://aws-us-east-2.adama-platform.com/libadama.js"></script>
This exposes the global Adama object.
npm/yarn
For Node.js or bundled applications:
npm install @anthropic/adama
import { Adama } from '@anthropic/adama';
// or
const { Adama } = require('@anthropic/adama');
Creating a Connection
The Connection class manages the WebSocket connection to Adama:
// Create a connection to the production Adama service
const connection = new Adama.Connection(Adama.Production);
// Start the connection
connection.start();
// Wait for the connection to be ready
connection.wait_connected().then(() => {
console.log('Connected to Adama!');
});
Connection Options
// Connect to a specific endpoint
const connection = new Adama.Connection('wss://us-east-1.adama-platform.com/~s');
// With configuration
const connection = new Adama.Connection(Adama.Production, {
// Reconnection settings
reconnectDelay: 1000,
maxReconnectDelay: 30000,
// Logging
verbose: true
});
Connection Lifecycle
const connection = new Adama.Connection(Adama.Production);
// Called when connection is established
connection.onconnected = () => {
console.log('Connected');
};
// Called when connection is lost
connection.ondisconnected = () => {
console.log('Disconnected - will auto-reconnect');
};
connection.start();
Authentication
You need an identity token before creating document connections. There are a few ways to get one.
Developer Account Login
// Step 1: Request a verification code via email
connection.InitSetupAccount('developer@example.com', {
success: function() {
console.log('Check your email for the code');
},
failure: function(reason) {
console.error('Setup failed:', reason);
}
});
// Step 2: Complete setup with the code from email
connection.InitCompleteAccount('developer@example.com', false, '123456', {
success: function(response) {
const identity = response.identity;
console.log('Got identity token');
// Store this securely for future use
localStorage.setItem('adama_identity', identity);
},
failure: function(reason) {
console.error('Completion failed:', reason);
}
});
Password Login
connection.AccountLogin('developer@example.com', 'password', {
success: function(response) {
const identity = response.identity;
// Use this identity for subsequent operations
},
failure: function(reason) {
console.error('Login failed:', reason);
}
});
Document Authorization
Documents can handle their own authentication:
// Username/password auth defined by the document
connection.DocumentAuthorize('myspace', 'doc-key', 'username', 'password', {
success: function(response) {
const identity = response.identity;
// This identity is scoped to this specific document
},
failure: function(reason) {
console.error('Auth failed:', reason);
}
});
// Custom auth message (for OAuth, magic links, etc.)
connection.DocumentAuthorization('myspace', 'doc-key', {
type: 'magic_link',
token: 'abc123xyz'
}, {
success: function(response) {
const identity = response.identity;
},
failure: function(reason) {
console.error('Auth failed:', reason);
}
});
Connecting to Documents
Creating a Document Connection
ConnectionCreate establishes a real-time connection to a document:
const identity = localStorage.getItem('adama_identity');
const docConnection = connection.ConnectionCreate(
identity, // Your identity token
'myspace', // Space name
'document-key', // Document key
{}, // Initial viewer state
{
next: function(payload) {
// Called for each state update
if (payload.delta) {
console.log('State changed:', payload.delta);
}
},
complete: function() {
// Called when connection closes normally
console.log('Document connection closed');
},
failure: function(reason) {
// Called on error
console.error('Connection failed:', reason);
}
}
);
Connection via Domain
If you have a domain mapped to a document:
const docConnection = connection.ConnectionCreateViaDomain(
identity,
'app.yourdomain.com',
{},
{
next: function(payload) { /* ... */ },
complete: function() { /* ... */ },
failure: function(reason) { /* ... */ }
}
);
Managing State with AdamaTree
AdamaTree is where things get nice. It takes the raw deltas, applies them, and notifies subscribers when things change:
// Create a tree to hold document state
const tree = new AdamaTree();
// Subscribe to specific paths in the state
tree.subscribe({
counter: function(value) {
document.getElementById('counter').textContent = value;
},
users: function(users) {
// users is automatically converted to an array from the table format
renderUserList(users);
}
});
// Connect the tree to document updates
const docConnection = connection.ConnectionCreate(
identity, 'myspace', 'doc-key', {},
{
next: function(payload) {
if (payload.delta && payload.delta.data) {
tree.update(payload.delta.data);
}
},
complete: function() {
console.log('Connection closed');
},
failure: function(reason) {
console.error('Error:', reason);
}
}
);
Subscribing to Nested Data
tree.subscribe({
// Simple value
'title': function(title) {
document.querySelector('h1').textContent = title;
},
// Nested path using dot notation
'settings.theme': function(theme) {
document.body.className = theme;
},
// Table/array data
'messages': function(messages) {
// Messages is an ordered array
messages.forEach(msg => {
console.log(`${msg.author}: ${msg.text}`);
});
}
});
Full State Access
tree.subscribe({
// Use '__all' to get notified of any change
'__all': function(state) {
console.log('Full state:', state);
}
});
Sending Messages
Channel Messages
Send messages to document channels:
docConnection.send(
'say', // Channel name
{ text: 'Hello!' }, // Message payload
{
success: function() {
console.log('Message sent');
},
failure: function(reason) {
console.error('Send failed:', reason);
}
}
);
Handling Channel Responses
If your channel returns a value:
channel bid(int amount) -> bool {
// ... bidding logic
return true; // bid accepted
}
docConnection.send('bid', { amount: 100 }, {
success: function(response) {
if (response.result) {
console.log('Bid accepted!');
} else {
console.log('Bid rejected');
}
},
failure: function(reason) {
console.error('Bid failed:', reason);
}
});
Send Once (Idempotent)
For operations that should only execute once, even if retried:
docConnection.sendOnce(
'process-payment',
'unique-tx-id-12345', // Dedupe key
{ amount: 99.99 },
{
success: function() { /* ... */ },
failure: function(reason) { /* ... */ }
}
);
Updating Viewer State
Viewer state lets you customize what data the document sends to each client:
// Initial viewer state when connecting
const docConnection = connection.ConnectionCreate(
identity, 'myspace', 'doc-key',
{
page: 1,
itemsPerPage: 20,
searchQuery: ''
},
{ /* handlers */ }
);
// Update viewer state later
docConnection.update({
page: 2
});
// Or replace entirely
docConnection.update({
page: 1,
itemsPerPage: 50,
searchQuery: 'hello'
});
In your Adama document, access viewer state via bubbles:
view string searchQuery;
view int page;
view int itemsPerPage;
record Item {
public int id;
public string title;
}
table<Item> items;
bubble visible_items = iterate items
where @viewer.searchQuery == "" || title.contains(@viewer.searchQuery)
offset (@viewer.page - 1) * @viewer.itemsPerPage
limit @viewer.itemsPerPage;
The beauty of this is that filtering and pagination happen server-side, and each client only receives the data they asked for.
Handling Disconnections
The client library handles reconnection automatically. You can respond to connection state changes:
connection.onconnected = () => {
console.log('Connected to Adama');
// Re-authenticate or restore state
};
connection.ondisconnected = () => {
console.log('Lost connection, reconnecting...');
// Show offline indicator
};
Manual Reconnection Control
// Stop auto-reconnect
connection.stop();
// Manually restart
connection.start();
Document Connection Survival
Document connections are automatically re-established after a reconnect. The document sends fresh state, so your tree gets rebuilt correctly.
Direct Message Sending
Send messages without maintaining a connection:
connection.MessageDirectSend(
identity,
'myspace',
'doc-key',
'process', // channel
{ action: 'trigger' }, // message
{
success: function(response) {
console.log('Processed at seq:', response.seq);
},
failure: function(reason) {
console.error('Failed:', reason);
}
}
);
// Idempotent version
connection.MessageDirectSendOnce(
identity,
'myspace',
'doc-key',
'unique-dedupe-key',
'process',
{ action: 'trigger' },
{ /* handlers */ }
);
Document Management
Creating Documents
connection.DocumentCreate(
identity,
'myspace',
'new-doc-key',
null, // entropy (optional)
{ title: 'New Document', owner: 'user@example.com' }, // @construct arg
{
success: function() {
console.log('Document created');
},
failure: function(reason) {
console.error('Create failed:', reason);
}
}
);
Listing Documents
connection.DocumentList(
identity,
'myspace',
null, // marker for pagination
100, // limit
{
next: function(doc) {
console.log('Document:', doc.key, 'created:', doc.created);
},
complete: function() {
console.log('List complete');
},
failure: function(reason) {
console.error('List failed:', reason);
}
}
);
TypeScript Support
The library includes TypeScript definitions. Define types for your document state and messages:
interface ChatState {
title: string;
messages: Array<{
id: number;
author: string;
text: string;
timestamp: number;
}>;
}
interface SayMessage {
text: string;
}
// Typed connection handling
const tree = new AdamaTree<ChatState>();
tree.subscribe({
messages: (messages: ChatState['messages']) => {
messages.forEach(msg => {
console.log(`${msg.author}: ${msg.text}`);
});
}
});
// Typed message sending
function sendMessage(text: string): void {
const message: SayMessage = { text };
docConnection.send('say', message, {
success: () => console.log('Sent'),
failure: (reason: number) => console.error('Failed:', reason)
});
}
Type Generation
You can generate TypeScript types from your Adama schema using space/reflect:
connection.SpaceReflect(identity, 'myspace', 'doc-key', {
success: function(response) {
console.log('Schema:', response.reflection);
// Use this to generate TypeScript interfaces
},
failure: function(reason) {
console.error('Reflect failed:', reason);
}
});
Complete Example
Here's a complete chat application — everything in one file:
<!DOCTYPE html>
<html>
<head>
<title>Adama Chat</title>
<script src="https://aws-us-east-2.adama-platform.com/libadama.js"></script>
</head>
<body>
<div id="status">Connecting...</div>
<div id="messages"></div>
<input type="text" id="input" placeholder="Type a message...">
<button id="send">Send</button>
<script>
// Configuration
const SPACE = 'chat-demo';
const KEY = 'room-1';
const identity = localStorage.getItem('adama_identity');
// State management
const tree = new AdamaTree();
let docConnection = null;
// Connect to Adama
const connection = new Adama.Connection(Adama.Production);
connection.onconnected = () => {
document.getElementById('status').textContent = 'Connected';
connectToDocument();
};
connection.ondisconnected = () => {
document.getElementById('status').textContent = 'Reconnecting...';
};
connection.start();
// Connect to the chat document
function connectToDocument() {
docConnection = connection.ConnectionCreate(
identity,
SPACE,
KEY,
{},
{
next: function(payload) {
if (payload.delta && payload.delta.data) {
tree.update(payload.delta.data);
}
},
complete: function() {
console.log('Connection closed');
},
failure: function(reason) {
console.error('Connection failed:', reason);
}
}
);
}
// Subscribe to messages
tree.subscribe({
messages: function(messages) {
const container = document.getElementById('messages');
container.innerHTML = '';
messages.forEach(msg => {
const div = document.createElement('div');
div.textContent = `${msg.author}: ${msg.text}`;
container.appendChild(div);
});
// Scroll to bottom
container.scrollTop = container.scrollHeight;
}
});
// Send message
document.getElementById('send').onclick = function() {
const input = document.getElementById('input');
const text = input.value.trim();
if (text && docConnection) {
docConnection.send('say', { text: text }, {
success: function() {
input.value = '';
},
failure: function(reason) {
alert('Failed to send: ' + reason);
}
});
}
};
// Send on Enter key
document.getElementById('input').onkeypress = function(e) {
if (e.key === 'Enter') {
document.getElementById('send').click();
}
};
</script>
</body>
</html>
Framework Integration
React
import { useEffect, useState, useRef } from 'react';
function useAdamaDocument(space, key, identity) {
const [state, setState] = useState({});
const [connected, setConnected] = useState(false);
const connectionRef = useRef(null);
const docConnectionRef = useRef(null);
useEffect(() => {
const conn = new Adama.Connection(Adama.Production);
connectionRef.current = conn;
conn.onconnected = () => {
setConnected(true);
const tree = new AdamaTree();
tree.subscribe({
'__all': (data) => setState(data)
});
docConnectionRef.current = conn.ConnectionCreate(
identity, space, key, {},
{
next: (payload) => {
if (payload.delta?.data) {
tree.update(payload.delta.data);
}
},
complete: () => setConnected(false),
failure: (reason) => console.error(reason)
}
);
};
conn.ondisconnected = () => setConnected(false);
conn.start();
return () => {
if (docConnectionRef.current) {
docConnectionRef.current.end();
}
conn.stop();
};
}, [space, key, identity]);
const send = (channel, message) => {
return new Promise((resolve, reject) => {
if (docConnectionRef.current) {
docConnectionRef.current.send(channel, message, {
success: resolve,
failure: reject
});
} else {
reject(new Error('Not connected'));
}
});
};
return { state, connected, send };
}
// Usage
function ChatRoom() {
const { state, connected, send } = useAdamaDocument(
'chat', 'room-1', identity
);
const [input, setInput] = useState('');
const handleSend = async () => {
if (input.trim()) {
await send('say', { text: input });
setInput('');
}
};
return (
<div>
<div>{connected ? 'Connected' : 'Disconnected'}</div>
<div>
{(state.messages || []).map(msg => (
<div key={msg.id}>{msg.author}: {msg.text}</div>
))}
</div>
<input value={input} onChange={e => setInput(e.target.value)} />
<button onClick={handleSend}>Send</button>
</div>
);
}
Practical Advice
Store identity tokens securely.
// Browser - use localStorage or secure cookie
localStorage.setItem('adama_identity', identity);
// Node.js - use environment variables or secure config
process.env.ADAMA_IDENTITY
Handle all callback cases. Always implement success, failure, and (for streaming) complete. Swallowing errors silently is a recipe for debugging nightmares later:
connection.SomeMethod(args, {
success: (response) => { /* handle success */ },
failure: (reason) => { /* handle and report error */ },
complete: () => { /* cleanup for streaming responses */ }
});
Clean up connections. When your component unmounts or the page unloads:
window.addEventListener('beforeunload', () => {
if (docConnection) {
docConnection.end();
}
connection.stop();
});
Use viewer state for filtering. Don't fetch everything and filter client-side. Let the server do it:
// Less efficient - fetching everything
// state.allItems.filter(item => item.category === 'books')
// More efficient - server-side filtering
docConnection.update({ category: 'books' });
// Document uses @viewer.category to filter
Debounce UI updates. AdamaTree may fire rapidly. Consider batching renders:
let renderScheduled = false;
tree.subscribe({
data: (data) => {
if (!renderScheduled) {
renderScheduled = true;
requestAnimationFrame(() => {
render(data);
renderScheduled = false;
});
}
}
});
For lower-level control, see the WebSocket API. For HTTP-based integration, see the REST API.