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.
- package/EACH_PARTICIPANT_LIFECYCLE.md +230 -0
- package/RENDER_USAGE_EXAMPLES.md +14 -14
- package/lib/models/AnearEvent.js +12 -0
- package/lib/models/AnearParticipant.js +6 -6
- package/lib/state_machines/AnearEventMachine.js +304 -104
- package/lib/state_machines/AnearParticipantMachine.js +19 -18
- package/lib/utils/AppMachineTransition.js +75 -14
- package/lib/utils/DisplayEventProcessor.js +148 -37
- package/lib/utils/FontAssetsUploader.js +10 -0
- package/lib/utils/PugHelpers.js +13 -2
- package/lib/utils/RealtimeMessaging.js +1 -1
- package/lib/utils/RenderContextBuilder.js +23 -6
- package/package.json +1 -1
|
@@ -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
|
+
```
|
package/RENDER_USAGE_EXAMPLES.md
CHANGED
|
@@ -18,15 +18,15 @@ const Config = {
|
|
|
18
18
|
states: {
|
|
19
19
|
registration: {
|
|
20
20
|
meta: {
|
|
21
|
-
|
|
21
|
+
allParticipants: 'ViewableGameBoard',
|
|
22
22
|
spectators: 'ViewableGameBoard'
|
|
23
23
|
}
|
|
24
24
|
},
|
|
25
25
|
gameInProgress: {
|
|
26
26
|
meta: {
|
|
27
|
-
|
|
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
|
-
'
|
|
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
|
-
'
|
|
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
|
-
'
|
|
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
|
-
'
|
|
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 '
|
|
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`
|
package/lib/models/AnearEvent.js
CHANGED
|
@@ -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
|