anear-js-api
v2.3.1
Published
Javascript Developer API for Anear Apps
Downloads
2,145
Readme
Anear JavaScript API (JSAPI)
The Anear JavaScript API is a runtime SDK that enables app developers to create real-time interactive events without needing to understand the underlying complexity of Ably.io interactions, event lifecycle management, and participant/spectator coordination.
Overview
The anear-js-api is the server-side Node.js runtime for Anear applications. It manages real-time communication via Ably.io, coordinates participant presence, handles event lifecycle, and renders dynamic UI templates. App developers write XState state machines that define their application logic, and the JSAPI handles all the infrastructure concerns.
Key Philosophy
The Anear platform is designed for hyperlocal, face-to-face interaction. Anear Apps are experiences designed for in-person participants who share the same physical space. The core principle is to leverage technology to enhance, not replace, real-world social dynamics. Participants are typically in the same room, fostering an environment of direct, face-to-face interaction.
System Purpose
The anear-js-api abstracts away:
- Ably.io channel management and messaging
- Event lifecycle state transitions (
created→announce→live→closed) - Participant presence tracking and coordination
- Display rendering and template compilation
- Asset management (CSS, images) and CDN uploads
- Timeout orchestration for
allParticipantsaction windows - Reconnection handling and error recovery
Architecture
State Machine Hierarchy
AnearCoreServiceMachine (Root)
├── AnearEventMachine (Per Event)
│ └── AppEventMachine (Developer's App Logic)Core Components
AnearCoreServiceMachine
- Purpose: Highest-level parent state machine managing realtime messaging and event lifecycle
- Responsibilities:
- Initialize Ably.io realtime messaging
- Manage multiple concurrent events via
anearEventMachinesobject - Handle app data fetching and asset uploads (CSS, images)
- Load and compile PUG templates
- Listen for
CREATE_EVENTmessages from backend via Ably REST API
- Key States:
waitForContextUpdate→fetchAppDataWithRetry→initRealtimeMessaging→waitAnearEventLifecycleCommand
AnearEventMachine (AEM)
- Purpose: Manages individual event lifecycle and participant coordination
- Responsibilities:
- Handle event lifecycle states:
created→announce→live→closed - Route messages between participants and app logic
- Manage participant presence (enter/leave/exit)
- Coordinate display rendering for all channels
- Handle allParticipants timeout orchestration
- Handle event lifecycle states:
- Key States:
registerCreator→eventCreated→announce→live→closeEvent
Breaking vNext: APM Removed
- Participant-machine orchestration is removed from JSAPI.
- AEM now manages participant private channel lifecycle and private display publishing directly.
- JSAPI no longer emits
PARTICIPANT_TIMEOUT; participant-local timeout UX is owned by browser runtime (<anear-timeout>).
Separation of Concerns: JSAPI vs. AppM
A key architectural principle is the clear separation of concerns between the Anear JSAPI (the "engine") and the App Event Machine (the "application").
JSAPI (AEM): The Engine Room
The JSAPI is responsible for all the underlying infrastructure and communication mechanics. Its concerns are purely technical:
- Event Lifecycle & State: Manages the low-level state transitions of an event (
created→announce→live→closed) and the lifecycle of participants. - Real-time Communication: Handles all Ably channel setup, messaging, presence events, and connection state.
- Orderly Operations: Ensures smooth, reliable startup and shutdown of all services and participant connections.
- Notifier: The JSAPI acts as a notifier. It informs the AppM of important events (
PARTICIPANT_ENTER,PARTICIPANT_DISCONNECT,ACTION,ACTIONS_TIMEOUT) but does not decide what these events mean for the application.
AppM: The Application & User Experience
The AppM developer is responsible for everything related to the specific event's logic and what the user sees:
- Game/Event Logic: Controls the flow, rules, and state of the interactive event.
- User Experience (UX/UI): Defines what the user sees at every stage via
metadisplay properties in its state nodes. All display content is the AppM's responsibility. - Decision Maker: The AppM is the decision-maker. When the JSAPI notifies it of an event (e.g., an
ACTION/ACTIONS_TIMEOUT), the AppM decides what to do. Should the game end? Should the participant be removed? Should the state change? That is application-level logic.
This strict separation allows AppM developers to focus entirely on their application logic without needing to manage the complexities of real-time infrastructure.
Getting Started
Installation
npm install anear-js-apiBasic Setup
// index.js
const { AnearService } = require('anear-js-api')
const MachineFactory = require('./StateMachine')
// Starts and instantiates the service
AnearService(MachineFactory)Required Environment Variables
ANEARAPP_API_KEY=your_api_key
ANEARAPP_API_SECRET=your_api_secret
ANEARAPP_API_VERSION=v1
ANEARAPP_API_URL=http://api.lvh.me:3001/developer # Optional, for local developmentLocal Dev Quickstart (no Docker)
To keep development behavior close to today:
- Keep runtime asset publishing enabled (default):
unset ANEARAPP_SKIP_RUNTIME_ASSET_SYNC- Start your app process normally:
npm install
node index.js- If your ANAPI environment is not Rails
development, set:
export ANEARAPP_SINGLE_LATEST_VERSION=trueThis preserves a single mutable latest app version for iterative development.
Developer API Authentication
anear-js-api communicates with the ANAPI Developer API (/developer/v1/* endpoints) using JWT authentication:
- Challenge (credentials → JWT): On startup, exchanges developer credentials (
api_key,secret) for a short-lived JWT viaPOST /developer/v1/sessions - Authenticated requests: All subsequent requests use
Authorization: Bearer <auth_token> - Expiry: JWT expiry is enforced by ANAPI (configured via
developer_api_token_expiration_interval_hours)
Event Lifecycle
Anear events follow a specific lifecycle managed by the Anear Event Machine (AEM):
- Created: Event is created in the AEM but not yet visible. No participants can join yet.
- Announced: Developer calls
anearEvent.announceEvent(). Event becomes visible to potential participants. Critical: This step is required for participants to join! - Live: Developer calls
anearEvent.startEvent(). Event is actively running. - Complete/Cancelled: Developer calls
anearEvent.closeEvent()oranearEvent.cancelEvent(). Critical: You MUST explicitly call one of these methods before your AppM reaches afinalstate.
Your AppM state names are completely independent of these AEM states. You can start your AppM in any state you want - the AEM lifecycle runs in parallel.
Channel Architecture
Ably.io Channels
eventChannel: Event control messagesactionsChannel: Participant presence events + ACTION clicksparticipantsDisplayChannel: Group display messages for all participantsspectatorsDisplayChannel: Display messages for all spectatorsprivateChannel: Individual participant private displays (per participant)
Message Flow
- Presence Events: Ably presence API →
actionsChannel→ XState events - Action Events: Participant clicks →
actionsChannel→ App logic - Display Events: App state transitions →
AppMachineTransition→ Channel rendering
Action Payload Tokenization
JSAPI tokenizes anear-action payloads for participant-private displays to prevent client-side payload inspection/tampering.
Render-time behavior
- App templates still use
action(...)in Pug. - For private participant renders,
DisplayEventProcessorreplaces action JSON with short opaque tokens. - JSAPI stores an ephemeral lookup map per participant and render cycle:
participantActionLookup[participantId][token] = { APP_EVENT: payload }
- The lookup map is replaced on each render cycle.
Action-ingest behavior (AEM)
When an ACTION is received on the actions channel:
- AEM tries token resolution from
participantActionLookup. - If token lookup fails, AEM accepts JSON payloads marked as trusted ABR client payloads (
__anearClient: true, forms also include__anearForm: true). - Any unrecognized/unmarked payload is dropped and not forwarded to AppM.
Forms
anear-form submissions remain JSON-based (they carry user-entered values). ABR marks form payloads with __anearForm: true and __anearClient: true; AEM strips these markers before forwarding payload to AppM.
Security model
- Tokenized button actions remove app-specific payload details from client-visible markup.
- Forged/replayed unknown tokens are rejected by default.
- AppM guards are still required for domain correctness (turn validity, ownership checks, ranges, etc.).
Participant Presence Events
The JSAPI handles participant lifecycle through presence events:
| Event | When It Fires | AppM Receives |
|-------|---------------|---------------|
| PARTICIPANT_ENTER | New participant joins | HOST_ENTER or PARTICIPANT_ENTER (role-specific) |
| PARTICIPANT_RECONNECT | Participant rejoins after disconnect | HOST_RECONNECT or PARTICIPANT_RECONNECT |
| PARTICIPANT_EXIT | Participant permanently leaves | HOST_EXIT or PARTICIPANT_EXIT |
| PARTICIPANT_DISCONNECT | Temporary connection loss | HOST_DISCONNECT or PARTICIPANT_DISCONNECT |
| ACTION | Participant performs an action | Custom event (e.g., MOVE, ANSWER) |
Key Point: The AEM uses role-specific events (HOST_* vs PARTICIPANT_*) based on whether the user is the event creator/host. Your AppM never needs to check isHost - it just reacts to the specific events it receives.
Display Rendering
Meta Properties
App developers control what participants see using XState meta properties:
meta: {
// Legacy formats (still supported)
eachParticipant: 'TemplateName', // String template name
allParticipants: 'TemplateName', // String template name
spectators: 'TemplateName', // String template name
// Object format (timeout metadata is legacy; prefer <anear-timeout> in PUG for participant-local UX)
eachParticipant: {
view: 'TemplateName',
timeout: (appContext, participantId) => 30000,
props: { message: 'Your turn!' }
},
// Selective participant rendering (function)
eachParticipant: (appContext, event) => {
const participantId = event.participantId
if (participantId) {
return [{ participantId, view: 'ThanksScreen', timeout: null }]
}
return []
},
// Host-specific display
host: { view: 'HostScreen', props: { questionCount: 10 } }
}Template Context
PUG templates receive rich context including:
app: Application XState contextparticipants: All participants mapparticipant: Individual participant datameta: Display metadata (state, event, timeout, viewer)props: Custom data from themetablock- PUG helpers:
cdnImg(),action()for interactive elements
Timeout Management
Participant-local Timeouts
Participant-local timeout UX is handled by browser runtime (<anear-timeout>). JSAPI does not emit PARTICIPANT_TIMEOUT; timeout-driven actions should arrive as normal ACTION payloads (for example ACTION_TIMEOUT if your app uses that event name).
//- Active player's controls wrapped with browser-owned timeout UX
anear-timeout(duration='30000')
anear-action(payload=action("MOVE")) MoveGroup Timeouts
For coordinated actions where all participants must respond:
meta: {
allParticipants: { view: 'QuestionScreen', timeout: 20000 }
},
on: {
ANSWER: [
{
guard: ({ event }) => event.finalAction === true,
actions: ['handleAllResponded']
},
{
actions: ['handleIndividualResponse']
}
],
ACTIONS_TIMEOUT: {
actions: ['handleTimeout']
}
}Asset Management
The JSAPI automatically handles asset management:
- CSS Upload: All CSS files in
assets/css/are uploaded to CloudFront - Image Upload: All images in
assets/images/are uploaded to CloudFront - Template Preloading: All PUG templates are preloaded and cached
- Hash-based dedupe: Asset uploads are decided by SHA-256 content hash comparison against S3 metadata (not file timestamps)
CI-Managed Assets Mode
For containerized deployments, you can publish assets during CI and skip runtime asset sync:
ANEARAPP_SKIP_RUNTIME_ASSET_SYNC=trueWhen this variable is enabled, AnearCoreServiceMachine skips image/font/CSS upload states and proceeds directly to template compilation and event lifecycle listening.
CI helpers are available under:
anear-js-api/lib/ci/publishAssets.jsanear-js-api/lib/ci/registerAppVersion.js
Versioned Lifecycle Channels
ACSM subscribes to versioned lifecycle channels derived from ANAPI app metadata:
anear:<appId>:v:<appVersion>:e
ANAPI must return latest_app_version in developer app payloads for startup channel resolution.
Asset Structure
assets/
├── css/
│ └── app.css
├── fonts/
│ └── YourCustomFont.ttf
└── images/
├── Background.png
└── ...The anearEvent Object
The anearEvent object is passed to your MachineFactory and provides access to Anear's core functionality:
// Event lifecycle management
await anearEvent.announceEvent() // Transition to announced
await anearEvent.startEvent() // Transition to live
await anearEvent.closeEvent() // Transition to complete
await anearEvent.cancelEvent() // Transition to cancelled
// allParticipants timeout management
anearEvent.cancelAllParticipantsTimeout() // Cancel active allParticipants timeout orchestrationXState Integration
State Machine Factory Pattern
const MachineFactory = anearEvent => {
const expandedConfig = {
predictableActionArguments: true,
...Config
}
const machine = createMachine(
expandedConfig,
Functions(anearEvent)
)
return machine.withContext(Context)
}Context Structure
const Context = {
C: Object.freeze(Constants), // Available in templates
// Game-specific state
playerIds: {},
gameState: 'WAITING',
// ... other state
}Event Termination
Critical: You must explicitly call closeEvent() or cancelEvent() before your AppM reaches a final state:
gameOver: {
entry: ['saveGameStats', 'closeEvent'],
type: 'final'
}
gameAborted: {
entry: ['logAbortReason', 'cancelEvent'],
type: 'final'
}If your AppM reaches a final state without calling a termination method, the AEM will log an error and forcibly cancel the event.
RENDERED Event Synchronization
The RENDERED event is helpful for synchronizing display rendering with state transitions. It ensures that displays are fully processed before the AppM proceeds to the next state.
Use RENDERED for:
- Display-only states with no user interaction
- Start/end states in event flows
- States where participants need time to see important displays
Skip RENDERED for:
- Interactive states that will transition naturally
- States waiting on user actions or timeouts
Key Benefits
- Abstraction: Hides Ably.io complexity from app developers
- Declarative: XState meta properties define display behavior
- Flexible: Rich context system enables dynamic content
- Scalable: Handles multiple concurrent events efficiently
- Reliable: Built-in reconnection and timeout management
- Consistent: XState-based architecture throughout
Documentation
For detailed information, see:
- ANEAR_GLOBAL_ARCHITECTURE.md: Complete system architecture and API reference
- ANEAR_DEVELOPER_GUIDE.md: Practical development guide with examples (in
anear-hsm-testproject) - RENDER_USAGE_EXAMPLES.md: Examples of display rendering patterns
Example Projects
- anear-hsm-test: Tic-Tac-Toe game demonstrating standard patterns
- sparkpoll: Q&A app with selective participant rendering
- neonbluff: Dice game with complex timeout management
- perimeter-chat: Chat room demonstrating open-house events
- event-chat: Embeddable chat widget for child apps
License
GPL-3.0-or-later
