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.

Previous Websocket Api
Next Rest Api