anear-js-api 1.4.0 → 1.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +382 -0
- package/lib/state_machines/AnearEventMachine.js +1 -1
- package/package.json +1 -1
package/README.md
ADDED
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
# Anear JavaScript API (JSAPI)
|
|
2
|
+
|
|
3
|
+
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.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
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.
|
|
8
|
+
|
|
9
|
+
### Key Philosophy
|
|
10
|
+
|
|
11
|
+
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.
|
|
12
|
+
|
|
13
|
+
### System Purpose
|
|
14
|
+
|
|
15
|
+
The anear-js-api abstracts away:
|
|
16
|
+
- Ably.io channel management and messaging
|
|
17
|
+
- Event lifecycle state transitions (`created` → `announce` → `live` → `closed`)
|
|
18
|
+
- Participant presence tracking and coordination
|
|
19
|
+
- Display rendering and template compilation
|
|
20
|
+
- Asset management (CSS, images) and CDN uploads
|
|
21
|
+
- Timeout management for participant actions
|
|
22
|
+
- Reconnection handling and error recovery
|
|
23
|
+
|
|
24
|
+
## Architecture
|
|
25
|
+
|
|
26
|
+
### State Machine Hierarchy
|
|
27
|
+
|
|
28
|
+
```
|
|
29
|
+
AnearCoreServiceMachine (Root)
|
|
30
|
+
├── AnearEventMachine (Per Event)
|
|
31
|
+
│ ├── AnearParticipantMachine (Per Participant)
|
|
32
|
+
│ └── AppEventMachine (Developer's App Logic)
|
|
33
|
+
└── AppParticipantMachine (Optional, Per Participant)
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### Core Components
|
|
37
|
+
|
|
38
|
+
#### AnearCoreServiceMachine
|
|
39
|
+
- **Purpose**: Highest-level parent state machine managing realtime messaging and event lifecycle
|
|
40
|
+
- **Responsibilities**:
|
|
41
|
+
- Initialize Ably.io realtime messaging
|
|
42
|
+
- Manage multiple concurrent events via `anearEventMachines` object
|
|
43
|
+
- Handle app data fetching and asset uploads (CSS, images)
|
|
44
|
+
- Load and compile PUG templates
|
|
45
|
+
- Listen for `CREATE_EVENT` messages from backend via Ably REST API
|
|
46
|
+
- **Key States**: `waitForContextUpdate` → `fetchAppDataWithRetry` → `initRealtimeMessaging` → `waitAnearEventLifecycleCommand`
|
|
47
|
+
|
|
48
|
+
#### AnearEventMachine (AEM)
|
|
49
|
+
- **Purpose**: Manages individual event lifecycle and participant coordination
|
|
50
|
+
- **Responsibilities**:
|
|
51
|
+
- Handle event lifecycle states: `created` → `announce` → `live` → `closed`
|
|
52
|
+
- Route messages between participants and app logic
|
|
53
|
+
- Manage participant presence (enter/leave/exit)
|
|
54
|
+
- Coordinate display rendering for all channels
|
|
55
|
+
- Handle individual participant timeouts
|
|
56
|
+
- **Key States**: `registerCreator` → `eventCreated` → `announce` → `live` → `closeEvent`
|
|
57
|
+
|
|
58
|
+
#### AnearParticipantMachine (APM)
|
|
59
|
+
- **Purpose**: Manages individual participant state and private communications
|
|
60
|
+
- **Responsibilities**:
|
|
61
|
+
- Handle participant private channel communications
|
|
62
|
+
- Manage participant timeouts and reconnection logic
|
|
63
|
+
- Track participant activity and idle states
|
|
64
|
+
- Route participant-specific actions to app logic
|
|
65
|
+
- **Key States**: `setup` → `live` → `waitReconnect` → `cleanupAndExit`
|
|
66
|
+
|
|
67
|
+
### Separation of Concerns: JSAPI vs. AppM
|
|
68
|
+
|
|
69
|
+
A key architectural principle is the clear separation of concerns between the Anear JSAPI (the "engine") and the App Event Machine (the "application").
|
|
70
|
+
|
|
71
|
+
#### JSAPI (AEM & APM): The Engine Room
|
|
72
|
+
|
|
73
|
+
The JSAPI is responsible for all the underlying infrastructure and communication mechanics. Its concerns are purely technical:
|
|
74
|
+
|
|
75
|
+
- **Event Lifecycle & State:** Manages the low-level state transitions of an event (`created` → `announce` → `live` → `closed`) and the lifecycle of participants.
|
|
76
|
+
- **Real-time Communication:** Handles all Ably channel setup, messaging, presence events, and connection state.
|
|
77
|
+
- **Orderly Operations:** Ensures smooth, reliable startup and shutdown of all services and participant connections.
|
|
78
|
+
- **Notifier:** The JSAPI acts as a notifier. It informs the AppM of important events (`PARTICIPANT_ENTER`, `PARTICIPANT_DISCONNECT`, `ACTION`, `PARTICIPANT_TIMEOUT`) but does not decide what these events mean for the application.
|
|
79
|
+
|
|
80
|
+
#### AppM: The Application & User Experience
|
|
81
|
+
|
|
82
|
+
The AppM developer is responsible for everything related to the specific event's logic and what the user sees:
|
|
83
|
+
|
|
84
|
+
- **Game/Event Logic:** Controls the flow, rules, and state of the interactive event.
|
|
85
|
+
- **User Experience (UX/UI):** Defines what the user sees at every stage via `meta` display properties in its state nodes. All display content is the AppM's responsibility.
|
|
86
|
+
- **Decision Maker:** The AppM is the decision-maker. When the JSAPI notifies it of an event (e.g., "a participant timed out"), the AppM decides what to do. Should the game end? Should the participant be removed? Should the state change? That is application-level logic.
|
|
87
|
+
|
|
88
|
+
This strict separation allows AppM developers to focus entirely on their application logic without needing to manage the complexities of real-time infrastructure.
|
|
89
|
+
|
|
90
|
+
## Getting Started
|
|
91
|
+
|
|
92
|
+
### Installation
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
npm install anear-js-api
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### Basic Setup
|
|
99
|
+
|
|
100
|
+
```javascript
|
|
101
|
+
// index.js
|
|
102
|
+
const { AnearService } = require('anear-js-api')
|
|
103
|
+
const MachineFactory = require('./StateMachine')
|
|
104
|
+
|
|
105
|
+
// Starts and instantiates the service
|
|
106
|
+
AnearService(MachineFactory)
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### Required Environment Variables
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
ANEARAPP_API_KEY=your_api_key
|
|
113
|
+
ANEARAPP_API_SECRET=your_api_secret
|
|
114
|
+
ANEARAPP_API_VERSION=v1
|
|
115
|
+
ANEARAPP_API_URL=http://api.lvh.me:3001/developer # Optional, for local development
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### Developer API Authentication
|
|
119
|
+
|
|
120
|
+
`anear-js-api` communicates with the **ANAPI Developer API** (`/developer/v1/*` endpoints) using JWT authentication:
|
|
121
|
+
|
|
122
|
+
1. **Challenge (credentials → JWT)**: On startup, exchanges developer credentials (`api_key`, `secret`) for a short-lived JWT via `POST /developer/v1/sessions`
|
|
123
|
+
2. **Authenticated requests**: All subsequent requests use `Authorization: Bearer <auth_token>`
|
|
124
|
+
3. **Expiry**: JWT expiry is enforced by ANAPI (configured via `developer_api_token_expiration_interval_hours`)
|
|
125
|
+
|
|
126
|
+
## Event Lifecycle
|
|
127
|
+
|
|
128
|
+
Anear events follow a specific lifecycle managed by the Anear Event Machine (AEM):
|
|
129
|
+
|
|
130
|
+
1. **Created**: Event is created in the AEM but not yet visible. No participants can join yet.
|
|
131
|
+
2. **Announced**: Developer calls `anearEvent.announceEvent()`. Event becomes visible to potential participants. **Critical**: This step is required for participants to join!
|
|
132
|
+
3. **Live**: Developer calls `anearEvent.startEvent()`. Event is actively running.
|
|
133
|
+
4. **Complete/Cancelled**: Developer calls `anearEvent.closeEvent()` or `anearEvent.cancelEvent()`. **Critical**: You MUST explicitly call one of these methods before your AppM reaches a `final` state.
|
|
134
|
+
|
|
135
|
+
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.
|
|
136
|
+
|
|
137
|
+
## Channel Architecture
|
|
138
|
+
|
|
139
|
+
### Ably.io Channels
|
|
140
|
+
|
|
141
|
+
- **`eventChannel`**: Event control messages
|
|
142
|
+
- **`actionsChannel`**: Participant presence events + ACTION clicks
|
|
143
|
+
- **`participantsDisplayChannel`**: Group display messages for all participants
|
|
144
|
+
- **`spectatorsDisplayChannel`**: Display messages for all spectators
|
|
145
|
+
- **`privateChannel`**: Individual participant private displays (per participant)
|
|
146
|
+
|
|
147
|
+
### Message Flow
|
|
148
|
+
|
|
149
|
+
1. **Presence Events**: Ably presence API → `actionsChannel` → XState events
|
|
150
|
+
2. **Action Events**: Participant clicks → `actionsChannel` → App logic
|
|
151
|
+
3. **Display Events**: App state transitions → `AppMachineTransition` → Channel rendering
|
|
152
|
+
|
|
153
|
+
## Participant Presence Events
|
|
154
|
+
|
|
155
|
+
The JSAPI handles participant lifecycle through presence events:
|
|
156
|
+
|
|
157
|
+
| Event | When It Fires | AppM Receives |
|
|
158
|
+
|-------|---------------|---------------|
|
|
159
|
+
| `PARTICIPANT_ENTER` | New participant joins | `HOST_ENTER` or `PARTICIPANT_ENTER` (role-specific) |
|
|
160
|
+
| `PARTICIPANT_RECONNECT` | Participant rejoins after disconnect | `HOST_RECONNECT` or `PARTICIPANT_RECONNECT` |
|
|
161
|
+
| `PARTICIPANT_EXIT` | Participant permanently leaves | `HOST_EXIT` or `PARTICIPANT_EXIT` |
|
|
162
|
+
| `PARTICIPANT_DISCONNECT` | Temporary connection loss | `HOST_DISCONNECT` or `PARTICIPANT_DISCONNECT` |
|
|
163
|
+
| `PARTICIPANT_TIMEOUT` | Participant fails to respond in time | `PARTICIPANT_TIMEOUT` |
|
|
164
|
+
| `ACTION` | Participant performs an action | Custom event (e.g., `MOVE`, `ANSWER`) |
|
|
165
|
+
|
|
166
|
+
**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.
|
|
167
|
+
|
|
168
|
+
## Display Rendering
|
|
169
|
+
|
|
170
|
+
### Meta Properties
|
|
171
|
+
|
|
172
|
+
App developers control what participants see using XState `meta` properties:
|
|
173
|
+
|
|
174
|
+
```javascript
|
|
175
|
+
meta: {
|
|
176
|
+
// Legacy formats (still supported)
|
|
177
|
+
eachParticipant: 'TemplateName', // String template name
|
|
178
|
+
allParticipants: 'TemplateName', // String template name
|
|
179
|
+
spectators: 'TemplateName', // String template name
|
|
180
|
+
|
|
181
|
+
// Object format with timeout
|
|
182
|
+
eachParticipant: {
|
|
183
|
+
view: 'TemplateName',
|
|
184
|
+
timeout: (appContext, participantId) => 30000,
|
|
185
|
+
props: { message: 'Your turn!' }
|
|
186
|
+
},
|
|
187
|
+
|
|
188
|
+
// Selective participant rendering (function)
|
|
189
|
+
eachParticipant: (appContext, event) => {
|
|
190
|
+
const participantId = event.participantId
|
|
191
|
+
if (participantId) {
|
|
192
|
+
return [{ participantId, view: 'ThanksScreen', timeout: null }]
|
|
193
|
+
}
|
|
194
|
+
return []
|
|
195
|
+
},
|
|
196
|
+
|
|
197
|
+
// Host-specific display
|
|
198
|
+
host: { view: 'HostScreen', props: { questionCount: 10 } }
|
|
199
|
+
}
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
### Template Context
|
|
203
|
+
|
|
204
|
+
PUG templates receive rich context including:
|
|
205
|
+
- `app`: Application XState context
|
|
206
|
+
- `participants`: All participants map
|
|
207
|
+
- `participant`: Individual participant data
|
|
208
|
+
- `meta`: Display metadata (state, event, timeout, viewer)
|
|
209
|
+
- `props`: Custom data from the `meta` block
|
|
210
|
+
- PUG helpers: `cdnImg()`, `action()` for interactive elements
|
|
211
|
+
|
|
212
|
+
## Timeout Management
|
|
213
|
+
|
|
214
|
+
### Individual Timeouts
|
|
215
|
+
|
|
216
|
+
For turn-based actions where each participant has their own timer:
|
|
217
|
+
|
|
218
|
+
```javascript
|
|
219
|
+
meta: {
|
|
220
|
+
eachParticipant: {
|
|
221
|
+
view: 'PlayableGameBoard',
|
|
222
|
+
timeout: (context, participant) => {
|
|
223
|
+
// Only current player gets timeout
|
|
224
|
+
return context.currentPlayerId === participant.info.id ? 30000 : null
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
},
|
|
228
|
+
on: {
|
|
229
|
+
MOVE: 'nextTurn',
|
|
230
|
+
PARTICIPANT_TIMEOUT: 'handleTimeout'
|
|
231
|
+
}
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
### Group Timeouts
|
|
235
|
+
|
|
236
|
+
For coordinated actions where all participants must respond:
|
|
237
|
+
|
|
238
|
+
```javascript
|
|
239
|
+
meta: {
|
|
240
|
+
allParticipants: { view: 'QuestionScreen', timeout: 20000 }
|
|
241
|
+
},
|
|
242
|
+
on: {
|
|
243
|
+
ANSWER: [
|
|
244
|
+
{
|
|
245
|
+
guard: ({ event }) => event.finalAction === true,
|
|
246
|
+
actions: ['handleAllResponded']
|
|
247
|
+
},
|
|
248
|
+
{
|
|
249
|
+
actions: ['handleIndividualResponse']
|
|
250
|
+
}
|
|
251
|
+
],
|
|
252
|
+
ACTIONS_TIMEOUT: {
|
|
253
|
+
actions: ['handleTimeout']
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
## Asset Management
|
|
259
|
+
|
|
260
|
+
The JSAPI automatically handles asset management:
|
|
261
|
+
|
|
262
|
+
1. **CSS Upload**: All CSS files in `assets/css/` are uploaded to CloudFront
|
|
263
|
+
2. **Image Upload**: All images in `assets/images/` are uploaded to CloudFront
|
|
264
|
+
3. **Template Preloading**: All PUG templates are preloaded and cached
|
|
265
|
+
|
|
266
|
+
### Asset Structure
|
|
267
|
+
|
|
268
|
+
```
|
|
269
|
+
assets/
|
|
270
|
+
├── css/
|
|
271
|
+
│ └── app.css
|
|
272
|
+
├── fonts/
|
|
273
|
+
│ └── YourCustomFont.ttf
|
|
274
|
+
└── images/
|
|
275
|
+
├── Background.png
|
|
276
|
+
└── ...
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
## The anearEvent Object
|
|
280
|
+
|
|
281
|
+
The `anearEvent` object is passed to your `MachineFactory` and provides access to Anear's core functionality:
|
|
282
|
+
|
|
283
|
+
```javascript
|
|
284
|
+
// Event lifecycle management
|
|
285
|
+
await anearEvent.announceEvent() // Transition to announced
|
|
286
|
+
await anearEvent.startEvent() // Transition to live
|
|
287
|
+
await anearEvent.closeEvent() // Transition to complete
|
|
288
|
+
await anearEvent.cancelEvent() // Transition to cancelled
|
|
289
|
+
|
|
290
|
+
// Participant timeout management
|
|
291
|
+
anearEvent.cancelAllParticipantTimeouts() // Cancel all active timeouts
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
## XState Integration
|
|
295
|
+
|
|
296
|
+
### State Machine Factory Pattern
|
|
297
|
+
|
|
298
|
+
```javascript
|
|
299
|
+
const MachineFactory = anearEvent => {
|
|
300
|
+
const expandedConfig = {
|
|
301
|
+
predictableActionArguments: true,
|
|
302
|
+
...Config
|
|
303
|
+
}
|
|
304
|
+
const machine = createMachine(
|
|
305
|
+
expandedConfig,
|
|
306
|
+
Functions(anearEvent)
|
|
307
|
+
)
|
|
308
|
+
return machine.withContext(Context)
|
|
309
|
+
}
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
### Context Structure
|
|
313
|
+
|
|
314
|
+
```javascript
|
|
315
|
+
const Context = {
|
|
316
|
+
C: Object.freeze(Constants), // Available in templates
|
|
317
|
+
// Game-specific state
|
|
318
|
+
playerIds: {},
|
|
319
|
+
gameState: 'WAITING',
|
|
320
|
+
// ... other state
|
|
321
|
+
}
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
## Event Termination
|
|
325
|
+
|
|
326
|
+
**Critical**: You must explicitly call `closeEvent()` or `cancelEvent()` before your AppM reaches a `final` state:
|
|
327
|
+
|
|
328
|
+
```javascript
|
|
329
|
+
gameOver: {
|
|
330
|
+
entry: ['saveGameStats', 'closeEvent'],
|
|
331
|
+
type: 'final'
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
gameAborted: {
|
|
335
|
+
entry: ['logAbortReason', 'cancelEvent'],
|
|
336
|
+
type: 'final'
|
|
337
|
+
}
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
If your AppM reaches a `final` state without calling a termination method, the AEM will log an error and forcibly cancel the event.
|
|
341
|
+
|
|
342
|
+
## RENDERED Event Synchronization
|
|
343
|
+
|
|
344
|
+
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.
|
|
345
|
+
|
|
346
|
+
**Use RENDERED for:**
|
|
347
|
+
- Display-only states with no user interaction
|
|
348
|
+
- Start/end states in event flows
|
|
349
|
+
- States where participants need time to see important displays
|
|
350
|
+
|
|
351
|
+
**Skip RENDERED for:**
|
|
352
|
+
- Interactive states that will transition naturally
|
|
353
|
+
- States waiting on user actions or timeouts
|
|
354
|
+
|
|
355
|
+
## Key Benefits
|
|
356
|
+
|
|
357
|
+
1. **Abstraction**: Hides Ably.io complexity from app developers
|
|
358
|
+
2. **Declarative**: XState meta properties define display behavior
|
|
359
|
+
3. **Flexible**: Rich context system enables dynamic content
|
|
360
|
+
4. **Scalable**: Handles multiple concurrent events efficiently
|
|
361
|
+
5. **Reliable**: Built-in reconnection and timeout management
|
|
362
|
+
6. **Consistent**: XState-based architecture throughout
|
|
363
|
+
|
|
364
|
+
## Documentation
|
|
365
|
+
|
|
366
|
+
For detailed information, see:
|
|
367
|
+
|
|
368
|
+
- **[ANEAR_GLOBAL_ARCHITECTURE.md](./ANEAR_GLOBAL_ARCHITECTURE.md)**: Complete system architecture and API reference
|
|
369
|
+
- **[ANEAR_DEVELOPER_GUIDE.md](../anear-hsm-test/ANEAR_DEVELOPER_GUIDE.md)**: Practical development guide with examples (in `anear-hsm-test` project)
|
|
370
|
+
- **[RENDER_USAGE_EXAMPLES.md](./RENDER_USAGE_EXAMPLES.md)**: Examples of display rendering patterns
|
|
371
|
+
|
|
372
|
+
## Example Projects
|
|
373
|
+
|
|
374
|
+
- **anear-hsm-test**: Tic-Tac-Toe game demonstrating standard patterns
|
|
375
|
+
- **sparkpoll**: Q&A app with selective participant rendering
|
|
376
|
+
- **neonbluff**: Dice game with complex timeout management
|
|
377
|
+
- **perimeter-chat**: Chat room demonstrating open-house events
|
|
378
|
+
- **event-chat**: Embeddable chat widget for child apps
|
|
379
|
+
|
|
380
|
+
## License
|
|
381
|
+
|
|
382
|
+
GPL-3.0-or-later
|
|
@@ -2305,7 +2305,7 @@ const AnearEventMachineFunctions = ({
|
|
|
2305
2305
|
context.anearEvent.attributes.initial_context
|
|
2306
2306
|
if (initialContext && typeof initialContext === 'object' && !Array.isArray(initialContext)) {
|
|
2307
2307
|
actorInput = initialContext
|
|
2308
|
-
logger.debug(`[AEM] Using initial_context
|
|
2308
|
+
logger.debug(`[AEM] Using initial_context from event ${context.anearEvent.id}:`, JSON.stringify(initialContext))
|
|
2309
2309
|
}
|
|
2310
2310
|
}
|
|
2311
2311
|
|