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 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 (partial) from event for ${context.anearEvent.id}`, initialContext)
2308
+ logger.debug(`[AEM] Using initial_context from event ${context.anearEvent.id}:`, JSON.stringify(initialContext))
2309
2309
  }
2310
2310
  }
2311
2311
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "anear-js-api",
3
- "version": "1.4.0",
3
+ "version": "1.4.2",
4
4
  "description": "Javascript Developer API for Anear Apps",
5
5
  "main": "lib/index.js",
6
6
  "scripts": {