anear-js-api 0.4.39 → 0.5.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.
@@ -0,0 +1,230 @@
1
+ # The `eachParticipant` Display Lifecycle
2
+
3
+ This document provides a detailed, step-by-by-step breakdown of how a display targeted at `eachParticipant` travels from the application's state machine (AppM) to an individual participant's browser client.
4
+
5
+ ### The Goal
6
+
7
+ We want to render a specific view (`QuestionScreen.pug`) for a single user (`participant-123`) and make sure they answer within 10 seconds.
8
+
9
+ ### The Big Picture
10
+
11
+ The core idea is to translate a declarative `meta` block in your application's state machine (AppM) into concrete HTML content that gets delivered to a specific participant's device. This process involves a chain of components:
12
+
13
+ `AppMachineTransition` -> `AnearEventMachine` -> `DisplayEventProcessor` -> `AnearParticipantMachine` -> **Ably Message**
14
+
15
+ Let's break down each step.
16
+
17
+ ---
18
+
19
+ ### Part 1: The Trigger (Your AppM & `AppMachineTransition.js`)
20
+
21
+ It all starts in your application-specific state machine (e.g., `anear-q-and-a/StateMachine.js`). When your machine enters a state that needs to display something to participants, you define a `meta` object.
22
+
23
+ **Context:** The `meta` object is how your AppM communicates rendering intentions to the Anear platform. The `AppMachineTransition` module is a subscriber that listens for *every* state change in your AppM. Its job is to parse that `meta` object and translate it into a standardized command for the rest of the system.
24
+
25
+ #### Code Example (Your AppM)
26
+ Imagine your Q&A machine enters the `askQuestion` state. The state definition would look like this:
27
+
28
+ ```javascript
29
+ // anear-q-and-a/StateMachine.js
30
+ // ...
31
+ confirmMove: {
32
+ meta: {
33
+ // 'eachParticipant' is a function for selective rendering.
34
+ // This example shows how to target a single participant based on the
35
+ // event that triggered this state transition.
36
+ eachParticipant: (appContext, event) => {
37
+ // 'event' is the event that led to this state, e.g., { type: 'MOVE', participantId: 'p1', ... }
38
+ const movingParticipantId = event.participantId;
39
+
40
+ if (!movingParticipantId) return []; // Always return an array
41
+
42
+ // We only want to send a display to the participant who just moved.
43
+ return [{
44
+ participantId: movingParticipantId,
45
+ view: 'participant/MoveConfirmation', // A view confirming their move was received
46
+ timeout: 2000 // A short timeout for the confirmation display
47
+ }];
48
+ }
49
+ }
50
+ }
51
+ // ...
52
+ ```
53
+
54
+ #### Lifecycle Step 1: Parsing the `meta` block
55
+
56
+ When your AppM transitions to `confirmMove`, the `AppMachineTransition` function is invoked.
57
+
58
+ * **File:** `/Users/machvee/dev/anear-js-api/lib/utils/AppMachineTransition.js`
59
+ * **Description:** It detects the `meta.eachParticipant` property. Since it's a function, it executes it, receiving the array of display instructions we defined above. It then iterates through that array. For each instruction, it creates a standardized "display event" object.
60
+ * **Code Reference (`AppMachineTransition.js` lines 62-95):**
61
+
62
+ ```javascript
63
+ // ... inside AppMachineTransition.js
64
+ if (meta.eachParticipant) {
65
+ if (typeof meta.eachParticipant === 'function') {
66
+ // This is our path: the selective rendering function
67
+ const participantRenderFunc = meta.eachParticipant;
68
+ const participantDisplays = participantRenderFunc(appContext, event); // Executes our function from the AppM
69
+
70
+ // ... loops through the returned array ...
71
+ participantDisplays.forEach(participantDisplay => {
72
+ if (participantDisplay && participantDisplay.participantId && participantDisplay.view) {
73
+ const timeout = participantDisplay.timeout || null;
74
+
75
+ // Packages the info into a standard object
76
+ displayEvent = RenderContextBuilder.buildDisplayEvent(
77
+ participantDisplay.view,
78
+ baseAppRenderContext,
79
+ 'eachParticipant',
80
+ participantDisplay.participantId,
81
+ timeout
82
+ );
83
+ displayEvents.push(displayEvent);
84
+ }
85
+ });
86
+ } // ...
87
+ }
88
+ ```
89
+
90
+ #### Lifecycle Step 2: Sending the Command
91
+
92
+ After processing all `meta` properties, `AppMachineTransition` bundles all the generated `displayEvent` objects into a single event and sends it to the `AnearEventMachine` (AEM).
93
+
94
+ * **File:** `/Users/machvee/dev/anear-js-api/lib/utils/AppMachineTransition.js`
95
+ * **Code Reference (lines 137-140):**
96
+
97
+ ```javascript
98
+ if (displayEvents.length > 0) {
99
+ // Sends one event with a list of all rendering jobs
100
+ anearEvent.send('RENDER_DISPLAY', { displayEvents });
101
+ }
102
+ ```
103
+
104
+ ---
105
+
106
+ ### Part 2: The Router (`AnearEventMachine.js`)
107
+
108
+ **Context:** The `AnearEventMachine` (AEM) is the central orchestrator for an event. When it receives the `RENDER_DISPLAY` event, its role isn't to render anything itself, but to delegate the task to a specialized processor.
109
+
110
+ #### Lifecycle Step 3: Delegation
111
+
112
+ The AEM enters a rendering state (e.g., `announceRendering` or `liveRendering`) and invokes its `renderDisplay` service.
113
+
114
+ * **File:** `/Users/machvee/dev/anear-js-api/lib/state_machines/AnearEventMachine.js`
115
+ * **Description:** This service is simple: it creates an instance of `DisplayEventProcessor` and tells it to handle the `displayEvents` array we sent in the previous step.
116
+ * **Code Reference (lines 1144-1148):**
117
+
118
+ ```javascript
119
+ // ... inside AnearEventMachine.js
120
+ services: {
121
+ renderDisplay: async (context, event) => {
122
+ // The event here contains our { displayEvents } payload
123
+ const displayEventProcessor = new DisplayEventProcessor(context);
124
+ return await displayEventProcessor.processAndPublish(event.displayEvents);
125
+ },
126
+ // ...
127
+ ```
128
+
129
+ ---
130
+
131
+ ### Part 3: The Processor (`DisplayEventProcessor.js`)
132
+
133
+ **Context:** This class is the workhorse. It knows how to handle different display targets (`allParticipants`, `spectators`, `host`, and our target, `eachParticipant`). It's responsible for compiling the Pug templates and figuring out *who* gets the final HTML.
134
+
135
+ #### Lifecycle Step 4: Processing and Routing the Display
136
+
137
+ The `DisplayEventProcessor` loops through each `displayEvent` and, based on the target, decides what to do.
138
+
139
+ * **File:** `/Users/machvee/dev/anear-js-api/lib/utils/DisplayEventProcessor.js`
140
+ * **Description:** For an `eachParticipant` target, it doesn't publish to a public channel. Instead, it performs a critical handoff: it finds the specific participant's own state machine (`AnearParticipantMachine`) and sends a *private* `RENDER_DISPLAY` event directly to that machine. This is the key to sending a message to only one person.
141
+ * **Code Reference (lines 142-156 and 229-256):**
142
+
143
+ ```javascript
144
+ // ... inside DisplayEventProcessor.js _processSingle
145
+ case 'eachParticipant':
146
+ // ...
147
+ // It determines we need to send to a specific participant
148
+ publishPromise = this._processSelectiveParticipantDisplay(
149
+ template,
150
+ templateRenderContext,
151
+ null,
152
+ participantId, // e.g., 'participant-123'
153
+ displayTimeout // e.g., 10000
154
+ );
155
+ // ...
156
+
157
+ // ... inside _sendPrivateDisplay
158
+ _sendPrivateDisplay(participantMachine, participantId, template, templateRenderContext, timeoutFn) {
159
+ // ... it compiles the pug template into HTML ...
160
+ const privateHtml = template(privateRenderContext);
161
+
162
+ const renderMessage = { content: privateHtml };
163
+ if (timeout !== null) {
164
+ renderMessage.timeout = timeout; // Attaches the 10000ms timeout
165
+ }
166
+
167
+ // CRITICAL: It sends an event to the specific participant's machine, NOT to Ably.
168
+ participantMachine.send('RENDER_DISPLAY', renderMessage);
169
+ }
170
+ ```
171
+ ---
172
+
173
+ ### Part 4: The Target & Delivery (`AnearParticipantMachine.js`)
174
+
175
+ **Context:** Each participant in an event has their own instance of the `AnearParticipantMachine` (APM). This machine manages their connection, timeouts, and, most importantly, their private Ably channel.
176
+
177
+ #### Lifecycle Step 5: Receiving the Private Display Command
178
+
179
+ The target participant's APM receives the `RENDER_DISPLAY` event from the `DisplayEventProcessor`.
180
+
181
+ * **File:** `/Users/machvee/dev/anear-js-api/lib/state_machines/AnearParticipantMachine.js`
182
+ * **Description:** The APM transitions to its own `renderDisplay` state, where it invokes its `publishPrivateDisplay` service. The payload it received (`renderMessage`) contains the final HTML and the 10-second timeout.
183
+ * **Code Reference (lines 96-98 and 147-160):**
184
+
185
+ ```javascript
186
+ // ... inside AnearParticipantMachine.js
187
+ idle: {
188
+ on: {
189
+ RENDER_DISPLAY: {
190
+ target: '#renderDisplay'
191
+ },
192
+ // ...
193
+ renderDisplay: {
194
+ id: 'renderDisplay',
195
+ invoke: {
196
+ src: 'publishPrivateDisplay', // This is the final step
197
+ onDone: [
198
+ // After publishing, it starts waiting for the user's ACTION
199
+ { cond: 'hasActionTimeout', actions: 'updateActionTimeout', target: 'waitParticipantResponse' },
200
+ { target: 'idle', internal: true }
201
+ ],
202
+ // ...
203
+ ```
204
+
205
+ #### Lifecycle Step 6: Sending the Ably Message
206
+
207
+ This is the final hop. The `publishPrivateDisplay` service does one thing: it publishes the HTML to the participant's private channel.
208
+
209
+ * **File:** `/Users/machvee/dev/anear-js-api/lib/state_machines/AnearParticipantMachine.js`
210
+ * **Description:** It uses the `RealtimeMessaging` utility to send the message over Ably. The participant's browser, which is subscribed to this unique channel, receives the message and updates the DOM.
211
+ * **Code Reference (lines 348-358):**
212
+
213
+ ```javascript
214
+ // ... inside AnearParticipantMachine.js
215
+ services: {
216
+ publishPrivateDisplay: async (context, event) => {
217
+ // event.content is the final HTML from the DisplayEventProcessor
218
+ const displayMessage = { content: event.content };
219
+
220
+ await RealtimeMessaging.publish(
221
+ context.privateChannel, // The participant's unique channel
222
+ 'PRIVATE_DISPLAY',
223
+ displayMessage
224
+ );
225
+
226
+ // It returns the timeout so the onDone transition knows to start the timer
227
+ return { timeout: event.timeout };
228
+ },
229
+ // ...
230
+ ```
@@ -18,15 +18,15 @@ const Config = {
18
18
  states: {
19
19
  registration: {
20
20
  meta: {
21
- participants: 'ViewableGameBoard',
21
+ allParticipants: 'ViewableGameBoard',
22
22
  spectators: 'ViewableGameBoard'
23
23
  }
24
24
  },
25
25
  gameInProgress: {
26
26
  meta: {
27
- participant: {
28
- view: 'PlayableGameBoard',
29
- timeout: calcParticipantTimeout
27
+ eachParticipant: {
28
+ view: 'PlayableGameBoard',
29
+ timeout: calcParticipantTimeout
30
30
  },
31
31
  spectators: 'ViewableGameBoard'
32
32
  }
@@ -51,26 +51,26 @@ const actions = {
51
51
  renderGameOver: (context, event) => {
52
52
  anearEvent.render(
53
53
  'GameOver', // viewPath
54
- 'participants', // displayType
54
+ 'allParticipants', // displayType
55
55
  context, // appContext (AppM's context)
56
56
  event, // event that triggered this render
57
57
  null, // timeout (null for no timeout)
58
58
  { winner: context.winningPlayerId } // additional props
59
59
  )
60
60
  },
61
-
61
+
62
62
  // Render with timeout for participant displays
63
63
  renderWithTimeout: (context, event) => {
64
64
  anearEvent.render(
65
65
  'WaitingForMove',
66
- 'participant',
66
+ 'eachParticipant',
67
67
  context,
68
68
  event,
69
69
  (appContext, participantId) => 30000, // 30 second timeout
70
70
  { currentPlayer: context.currentPlayerToken }
71
71
  )
72
72
  },
73
-
73
+
74
74
  // Render for spectators
75
75
  renderForSpectators: (context, event) => {
76
76
  anearEvent.render(
@@ -95,11 +95,11 @@ const actions = {
95
95
  // Render winner display
96
96
  anearEvent.render(
97
97
  'WinnerDisplay',
98
- 'participants',
98
+ 'allParticipants',
99
99
  context,
100
100
  event,
101
101
  null,
102
- {
102
+ {
103
103
  winner: context.winner,
104
104
  finalScore: context.score,
105
105
  gameDuration: context.gameDuration
@@ -109,14 +109,14 @@ const actions = {
109
109
  // Render tie display
110
110
  anearEvent.render(
111
111
  'TieDisplay',
112
- 'participants',
112
+ 'allParticipants',
113
113
  context,
114
114
  event,
115
115
  null,
116
116
  { finalScore: context.score }
117
117
  )
118
118
  }
119
-
119
+
120
120
  // Now close the event
121
121
  anearEvent.closeEvent()
122
122
  }
@@ -128,10 +128,10 @@ const actions = {
128
128
  ### `anearEvent.render(viewPath, displayType, appContext, event, timeout, props)`
129
129
 
130
130
  - **`viewPath`** (string): Template/view path to render (e.g., 'GameBoard', 'GameOver')
131
- - **`displayType`** (string): One of 'participants', 'participant', or 'spectators'
131
+ - **`displayType`** (string): One of 'allParticipants', 'eachParticipant', or 'spectators'
132
132
  - **`appContext`** (Object): The AppM's context object (available in scope)
133
133
  - **`event`** (Object): The event that triggered this render (available in scope)
134
- - **`timeout`** (Function|number|null):
134
+ - **`timeout`** (Function|number|null):
135
135
  - `null`: No timeout
136
136
  - `number`: Fixed timeout in milliseconds
137
137
  - `Function`: Dynamic timeout function `(appContext, participantId) => msecs`
@@ -111,6 +111,18 @@ class AnearEvent extends JsonApiResource {
111
111
  return this.attributes.hosted
112
112
  }
113
113
 
114
+ get name() {
115
+ return this.attributes.name
116
+ }
117
+
118
+ get description() {
119
+ return this.attributes.description
120
+ }
121
+
122
+ get createdAt() {
123
+ return this.attributes['created-at']
124
+ }
125
+
114
126
  get participantTimeout() {
115
127
  // TODO: This probably should be set for each publishEventPrivateMessage
116
128
  // and then referenceable as an anear-data attribute in the html. That way
@@ -18,7 +18,8 @@ class AnearParticipant extends JsonApiResource {
18
18
  id: this.data.id,
19
19
  name: this.name,
20
20
  avatarUrl: this.avatarUrl,
21
- geoLocation: this.geoLocation
21
+ geoLocation: this.geoLocation,
22
+ isHost: this.isHost
22
23
  }
23
24
  }
24
25
 
@@ -62,6 +63,10 @@ class AnearParticipant extends JsonApiResource {
62
63
  return this.attributes['private-channel-name']
63
64
  }
64
65
 
66
+ get isHost() {
67
+ return this.userType === HostUserType
68
+ }
69
+
65
70
  setMachine(service) {
66
71
  if (service) {
67
72
  this.send = service.send.bind(service)
@@ -69,11 +74,6 @@ class AnearParticipant extends JsonApiResource {
69
74
  this.send = () => {}
70
75
  }
71
76
  }
72
-
73
-
74
- isHost() {
75
- return this.userType === HostUserType
76
- }
77
77
  }
78
78
 
79
79
  module.exports = AnearParticipant