anear-js-api 0.5.2 → 0.5.4
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 +5 -91
- package/RENDER_USAGE_EXAMPLES.md +6 -2
- package/lib/api/AnearApi.js +22 -0
- package/lib/api/ApiService.js +12 -0
- package/lib/models/AnearEvent.js +19 -8
- package/lib/state_machines/AnearCoreServiceMachine.js +12 -3
- package/lib/state_machines/AnearEventMachine.js +332 -101
- package/lib/state_machines/AnearParticipantMachine.js +55 -1
- package/lib/utils/AppMachineTransition.js +45 -29
- package/lib/utils/Constants.js +2 -1
- package/lib/utils/DisplayEventProcessor.js +114 -12
- package/lib/utils/RealtimeMessaging.js +9 -0
- package/lib/utils/RenderContextBuilder.js +7 -5
- package/package.json +1 -1
|
@@ -43,6 +43,10 @@ confirmMove: {
|
|
|
43
43
|
return [{
|
|
44
44
|
participantId: movingParticipantId,
|
|
45
45
|
view: 'participant/MoveConfirmation', // A view confirming their move was received
|
|
46
|
+
props: {
|
|
47
|
+
message: 'Your move has been recorded. Waiting for opponent...',
|
|
48
|
+
move: event.moveData
|
|
49
|
+
},
|
|
46
50
|
timeout: 2000 // A short timeout for the confirmation display
|
|
47
51
|
}];
|
|
48
52
|
}
|
|
@@ -137,94 +141,4 @@ services: {
|
|
|
137
141
|
The `DisplayEventProcessor` loops through each `displayEvent` and, based on the target, decides what to do.
|
|
138
142
|
|
|
139
143
|
* **File:** `/Users/machvee/dev/anear-js-api/lib/utils/DisplayEventProcessor.js`
|
|
140
|
-
* **Description:** For an `eachParticipant`
|
|
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
|
-
```
|
|
144
|
+
* **Description:** For an `eachParticipant`
|
package/RENDER_USAGE_EXAMPLES.md
CHANGED
|
@@ -26,7 +26,11 @@ const Config = {
|
|
|
26
26
|
meta: {
|
|
27
27
|
eachParticipant: {
|
|
28
28
|
view: 'PlayableGameBoard',
|
|
29
|
-
timeout: calcParticipantTimeout
|
|
29
|
+
timeout: calcParticipantTimeout,
|
|
30
|
+
props: {
|
|
31
|
+
title: "Your Move!",
|
|
32
|
+
highlightLastMove: true
|
|
33
|
+
}
|
|
30
34
|
},
|
|
31
35
|
spectators: 'ViewableGameBoard'
|
|
32
36
|
}
|
|
@@ -135,7 +139,7 @@ const actions = {
|
|
|
135
139
|
- `null`: No timeout
|
|
136
140
|
- `number`: Fixed timeout in milliseconds
|
|
137
141
|
- `Function`: Dynamic timeout function `(appContext, participantId) => msecs`
|
|
138
|
-
- **`props`** (Object): Additional properties
|
|
142
|
+
- **`props`** (Object): Additional properties made available at the root of the pug template's render context (optional)
|
|
139
143
|
|
|
140
144
|
## When to Use Each Approach
|
|
141
145
|
|
package/lib/api/AnearApi.js
CHANGED
|
@@ -79,6 +79,28 @@ class AnearApi extends ApiService {
|
|
|
79
79
|
logger.debug('getAppFontAssetsUploadUrls response:', attrs)
|
|
80
80
|
return attrs
|
|
81
81
|
}
|
|
82
|
+
|
|
83
|
+
async saveAppEventContext(eventId, appmContext) {
|
|
84
|
+
logger.debug(`API: POST developer events/${eventId}/app_event_context`)
|
|
85
|
+
const path = `events/${eventId}/app_event_context`
|
|
86
|
+
return this.postRaw(path, { appm_context: appmContext })
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async getLatestAppEventContext(eventId) {
|
|
90
|
+
logger.debug(`API: GET developer events/${eventId}/app_event_context`)
|
|
91
|
+
const path = `events/${eventId}/app_event_context`
|
|
92
|
+
const json = await this.get(path)
|
|
93
|
+
const attrs = json.data && json.data.attributes ? json.data.attributes : {}
|
|
94
|
+
const eventIdAttr = attrs['event-id']
|
|
95
|
+
const raw = attrs['appm-context']
|
|
96
|
+
let appmContext = null
|
|
97
|
+
try {
|
|
98
|
+
appmContext = typeof raw === 'string' ? JSON.parse(raw) : raw
|
|
99
|
+
} catch (e) {
|
|
100
|
+
// leave appmContext as null if parsing fails
|
|
101
|
+
}
|
|
102
|
+
return { eventId: eventIdAttr, appmContext }
|
|
103
|
+
}
|
|
82
104
|
}
|
|
83
105
|
|
|
84
106
|
// Instantiate and export the API immediately
|
package/lib/api/ApiService.js
CHANGED
|
@@ -56,6 +56,18 @@ class ApiService {
|
|
|
56
56
|
return this.issueRequest(request)
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
+
postRaw(path, body={}) {
|
|
60
|
+
const urlString = `${this.api_base_url}/${path}`
|
|
61
|
+
const request = new fetch.Request(
|
|
62
|
+
urlString, {
|
|
63
|
+
method: 'POST',
|
|
64
|
+
headers: this.default_headers,
|
|
65
|
+
body: JSON.stringify(body)
|
|
66
|
+
}
|
|
67
|
+
)
|
|
68
|
+
return this.issueRequest(request)
|
|
69
|
+
}
|
|
70
|
+
|
|
59
71
|
put(resource, id, attributes, relationships={}) {
|
|
60
72
|
const payload = this.formatPayload(resource, attributes, relationships)
|
|
61
73
|
const urlString = `${this.api_base_url}/${resource}/${id}`
|
package/lib/models/AnearEvent.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use strict"
|
|
2
2
|
const logger = require('../utils/Logger')
|
|
3
|
+
const anearApi = require('../api/AnearApi')
|
|
3
4
|
|
|
4
5
|
const JsonApiResource = require('./JsonApiResource')
|
|
5
6
|
|
|
@@ -46,6 +47,20 @@ class AnearEvent extends JsonApiResource {
|
|
|
46
47
|
this.send("CLOSE")
|
|
47
48
|
}
|
|
48
49
|
|
|
50
|
+
pauseEvent(context, resumeEvent = { type: 'RESUME' }) {
|
|
51
|
+
// Persist via AEM and acknowledge with PAUSED
|
|
52
|
+
this.send('PAUSE', { appmContext: { context, resumeEvent } })
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
saveEvent(context, resumeEvent = { type: 'RESUME' }) {
|
|
56
|
+
// Delegate save to the AEM; AEM will persist via ANAPI and acknowledge with SAVED
|
|
57
|
+
this.send('SAVE', { appmContext: { context, resumeEvent } })
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
bootParticipant(participantId, reason) {
|
|
61
|
+
this.send("BOOT_PARTICIPANT", { participantId, reason })
|
|
62
|
+
}
|
|
63
|
+
|
|
49
64
|
render(viewPath, displayType, appContext, event, timeout = null, props = {}) {
|
|
50
65
|
// Explicit render method for guaranteed rendering control
|
|
51
66
|
// This complements the meta: {} approach for when you need explicit control
|
|
@@ -80,14 +95,6 @@ class AnearEvent extends JsonApiResource {
|
|
|
80
95
|
})
|
|
81
96
|
}
|
|
82
97
|
|
|
83
|
-
pauseEvent() {
|
|
84
|
-
this.send("PAUSE")
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
resumeEvent() {
|
|
88
|
-
this.send("RESUME")
|
|
89
|
-
}
|
|
90
|
-
|
|
91
98
|
getClonedFromEvent() {
|
|
92
99
|
// if the current event was a clone of previous event, fetch if from
|
|
93
100
|
// Peristence and return
|
|
@@ -138,6 +145,10 @@ class AnearEvent extends JsonApiResource {
|
|
|
138
145
|
return this.app.attributes["participant-timeout"]
|
|
139
146
|
}
|
|
140
147
|
|
|
148
|
+
get appIconUrl() {
|
|
149
|
+
return this.app.attributes["icon-url"]
|
|
150
|
+
}
|
|
151
|
+
|
|
141
152
|
hasFlag(flagName) {
|
|
142
153
|
return this.attributes.flags.includes(flagName)
|
|
143
154
|
}
|
|
@@ -134,7 +134,7 @@ const AnearCoreServiceMachineConfig = appId => ({
|
|
|
134
134
|
}
|
|
135
135
|
},
|
|
136
136
|
waitAnearEventLifecycleCommand: {
|
|
137
|
-
// The Anear API backend will send CREATE_EVENT messages with the event JSON data
|
|
137
|
+
// The Anear API backend will send CREATE_EVENT or LOAD_EVENT messages with the event JSON data
|
|
138
138
|
// to this createEventMessages Channel when it needs to create a new instance of an
|
|
139
139
|
// Event
|
|
140
140
|
entry: (context) => logger.debug(`Waiting on ${context.appData.data.attributes['short-name']} lifecycle command`),
|
|
@@ -142,6 +142,9 @@ const AnearCoreServiceMachineConfig = appId => ({
|
|
|
142
142
|
CREATE_EVENT: {
|
|
143
143
|
actions: ['startNewEventMachine']
|
|
144
144
|
},
|
|
145
|
+
LOAD_EVENT: {
|
|
146
|
+
actions: ['startNewEventMachine']
|
|
147
|
+
},
|
|
145
148
|
EVENT_MACHINE_EXIT: {
|
|
146
149
|
actions: [
|
|
147
150
|
'cleanupEventMachine'
|
|
@@ -234,6 +237,11 @@ const AnearCoreServiceMachineFunctions = {
|
|
|
234
237
|
context.coreServiceMachine,
|
|
235
238
|
'CREATE_EVENT'
|
|
236
239
|
)
|
|
240
|
+
RealtimeMessaging.subscribe(
|
|
241
|
+
context.newEventCreationChannel,
|
|
242
|
+
context.coreServiceMachine,
|
|
243
|
+
'LOAD_EVENT'
|
|
244
|
+
)
|
|
237
245
|
},
|
|
238
246
|
startNewEventMachine: assign(
|
|
239
247
|
{
|
|
@@ -242,11 +250,12 @@ const AnearCoreServiceMachineFunctions = {
|
|
|
242
250
|
const anearEvent = new AnearEvent(eventJSON)
|
|
243
251
|
|
|
244
252
|
if (context.anearEventMachines[anearEvent.id]) {
|
|
245
|
-
logger.info(`[ACSM] Event machine for ${anearEvent.id} already exists. Ignoring
|
|
253
|
+
logger.info(`[ACSM] Event machine for ${anearEvent.id} already exists. Ignoring ${event.type}.`)
|
|
246
254
|
return context.anearEventMachines
|
|
247
255
|
}
|
|
248
256
|
|
|
249
|
-
const
|
|
257
|
+
const isLoadEvent = event.type === 'LOAD_EVENT'
|
|
258
|
+
const service = AnearEventMachine(anearEvent, { ...context, rehydrate: isLoadEvent })
|
|
250
259
|
|
|
251
260
|
return {
|
|
252
261
|
...context.anearEventMachines,
|