anear-js-api 0.5.2 → 0.5.3
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 +15 -8
- package/lib/state_machines/AnearCoreServiceMachine.js +12 -3
- package/lib/state_machines/AnearEventMachine.js +331 -101
- package/lib/state_machines/AnearParticipantMachine.js +55 -1
- package/lib/utils/AppMachineTransition.js +44 -29
- package/lib/utils/Constants.js +2 -1
- package/lib/utils/DisplayEventProcessor.js +13 -8
- 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
|
|
@@ -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,
|
|
@@ -34,6 +34,11 @@ const getAllParticipantIds = (context) => {
|
|
|
34
34
|
return Object.keys(context.participants);
|
|
35
35
|
};
|
|
36
36
|
|
|
37
|
+
const getPresenceEventName = (participant, presenceAction) => {
|
|
38
|
+
const role = participant && participant.isHost ? 'HOST' : 'PARTICIPANT';
|
|
39
|
+
return `${role}_${presenceAction}`;
|
|
40
|
+
};
|
|
41
|
+
|
|
37
42
|
const RealtimeMessaging = require('../utils/RealtimeMessaging')
|
|
38
43
|
const AppMachineTransition = require('../utils/AppMachineTransition')
|
|
39
44
|
const DisplayEventProcessor = require('../utils/DisplayEventProcessor')
|
|
@@ -51,7 +56,8 @@ const AnearEventMachineContext = (
|
|
|
51
56
|
pugTemplates,
|
|
52
57
|
pugHelpers,
|
|
53
58
|
appEventMachineFactory,
|
|
54
|
-
appParticipantMachineFactory
|
|
59
|
+
appParticipantMachineFactory,
|
|
60
|
+
rehydrate = false
|
|
55
61
|
) => ({
|
|
56
62
|
anearEvent,
|
|
57
63
|
coreServiceMachine,
|
|
@@ -59,6 +65,7 @@ const AnearEventMachineContext = (
|
|
|
59
65
|
pugHelpers,
|
|
60
66
|
appEventMachineFactory,
|
|
61
67
|
appParticipantMachineFactory,
|
|
68
|
+
rehydrate,
|
|
62
69
|
appEventMachine: null,
|
|
63
70
|
eventChannel: null, // event control messages
|
|
64
71
|
actionsChannel: null, // participant presence/live actions
|
|
@@ -72,11 +79,14 @@ const AnearEventMachineContext = (
|
|
|
72
79
|
const DeferredStates = [
|
|
73
80
|
'PARTICIPANT_ENTER',
|
|
74
81
|
'PARTICIPANT_LEAVE',
|
|
82
|
+
'PARTICIPANT_UPDATE',
|
|
75
83
|
'SPECTATOR_ENTER',
|
|
76
84
|
'PARTICIPANT_TIMEOUT',
|
|
77
85
|
'ACTION',
|
|
78
86
|
'CANCEL',
|
|
79
|
-
'CLOSE'
|
|
87
|
+
'CLOSE',
|
|
88
|
+
'SAVE',
|
|
89
|
+
'PAUSE'
|
|
80
90
|
]
|
|
81
91
|
|
|
82
92
|
const DeferredStatesPlus = (...additionalStates) => DeferredStates.concat(additionalStates)
|
|
@@ -94,6 +104,10 @@ const ActiveEventGlobalEvents = {
|
|
|
94
104
|
CANCEL: {
|
|
95
105
|
// appM does an abrupt shutdown of the event
|
|
96
106
|
target: '#canceled'
|
|
107
|
+
},
|
|
108
|
+
APPM_FINAL: {
|
|
109
|
+
// AppM reached a root-level final → cleanup only (no ANAPI transition)
|
|
110
|
+
target: '#activeEvent.shutdownOnly'
|
|
97
111
|
}
|
|
98
112
|
}
|
|
99
113
|
|
|
@@ -186,10 +200,6 @@ const ActiveEventStatesConfig = {
|
|
|
186
200
|
target: '#createdRendering'
|
|
187
201
|
},
|
|
188
202
|
PARTICIPANT_LEAVE: [
|
|
189
|
-
{
|
|
190
|
-
cond: 'isHostLeaving',
|
|
191
|
-
actions: ['sendHostExitToAppMachine', 'sendExitToParticipantMachine']
|
|
192
|
-
},
|
|
193
203
|
{
|
|
194
204
|
cond: 'isPermanentLeave',
|
|
195
205
|
actions: ['sendExitToAppMachine', 'sendExitToParticipantMachine']
|
|
@@ -198,6 +208,25 @@ const ActiveEventStatesConfig = {
|
|
|
198
208
|
actions: ['processDisconnectEvents']
|
|
199
209
|
}
|
|
200
210
|
],
|
|
211
|
+
PARTICIPANT_UPDATE: [
|
|
212
|
+
// In the future, the AnearBrowser may periodically send presence updates to the AEM
|
|
213
|
+
// which will include latitude/longitude direction, etc. for Apps that require this
|
|
214
|
+
// level of approved user location tracking. These types of explicit presence.updates
|
|
215
|
+
// will include a type field in the event payload to indicate the type of update.
|
|
216
|
+
// Otherwise, the Realtime messaging system in the mobile client browser may be
|
|
217
|
+
// sending this presence updated implicityly without a type because it has detected
|
|
218
|
+
// that the user left the event and came back. If we receive this and the participant exists,
|
|
219
|
+
// this is a PARTICIPANT_RECONNECT scenario. The User likely navigated away from the
|
|
220
|
+
// event and came back. We need to update the AppM with a PARTICIPANT_RECONNECT event
|
|
221
|
+
// so they can refresh the view for the returning participant.
|
|
222
|
+
{
|
|
223
|
+
cond: 'isReconnectUpdate',
|
|
224
|
+
actions: 'processReconnectEvents'
|
|
225
|
+
},
|
|
226
|
+
{
|
|
227
|
+
actions: ['updateParticipantGeoLocation', 'processUpdateEvents']
|
|
228
|
+
}
|
|
229
|
+
],
|
|
201
230
|
PARTICIPANT_ENTER: [
|
|
202
231
|
{
|
|
203
232
|
cond: 'participantExists',
|
|
@@ -224,6 +253,42 @@ const ActiveEventStatesConfig = {
|
|
|
224
253
|
},
|
|
225
254
|
START: {
|
|
226
255
|
target: '#activeEvent.live'
|
|
256
|
+
},
|
|
257
|
+
PAUSE: {
|
|
258
|
+
target: '#pausingEvent'
|
|
259
|
+
},
|
|
260
|
+
SAVE: {
|
|
261
|
+
target: 'savingAppEventContext'
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
},
|
|
265
|
+
pausingEvent: {
|
|
266
|
+
id: 'pausingEvent',
|
|
267
|
+
deferred: DeferredStates,
|
|
268
|
+
invoke: {
|
|
269
|
+
src: 'saveAppEventContext',
|
|
270
|
+
onDone: {
|
|
271
|
+
actions: ['sendPausedAckToAppMachine'],
|
|
272
|
+
target: '#waitingAnnounce',
|
|
273
|
+
internal: true
|
|
274
|
+
},
|
|
275
|
+
onError: {
|
|
276
|
+
target: '#activeEvent.failure'
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
},
|
|
280
|
+
savingAppEventContext: {
|
|
281
|
+
id: 'savingAppEventContext',
|
|
282
|
+
invoke: {
|
|
283
|
+
src: 'saveAppEventContext',
|
|
284
|
+
onDone: {
|
|
285
|
+
actions: ['sendSavedAckToAppMachine'],
|
|
286
|
+
target: '#waitingAnnounce',
|
|
287
|
+
internal: true
|
|
288
|
+
},
|
|
289
|
+
onError: {
|
|
290
|
+
// If save fails, log and remain in waitingAnnounce; AppM may retry/handle error UI
|
|
291
|
+
target: '#waitingAnnounce'
|
|
227
292
|
}
|
|
228
293
|
}
|
|
229
294
|
},
|
|
@@ -265,10 +330,6 @@ const ActiveEventStatesConfig = {
|
|
|
265
330
|
target: '#announceRendering'
|
|
266
331
|
},
|
|
267
332
|
PARTICIPANT_LEAVE: [
|
|
268
|
-
{
|
|
269
|
-
cond: 'isHostLeaving',
|
|
270
|
-
actions: ['sendHostExitToAppMachine', 'sendExitToParticipantMachine']
|
|
271
|
-
},
|
|
272
333
|
{
|
|
273
334
|
cond: 'isPermanentLeave',
|
|
274
335
|
actions: ['sendExitToAppMachine', 'sendExitToParticipantMachine']
|
|
@@ -289,6 +350,9 @@ const ActiveEventStatesConfig = {
|
|
|
289
350
|
target: '#newParticipantJoining'
|
|
290
351
|
}
|
|
291
352
|
],
|
|
353
|
+
BOOT_PARTICIPANT: {
|
|
354
|
+
actions: 'sendBootEventToParticipantMachine'
|
|
355
|
+
},
|
|
292
356
|
SPECTATOR_ENTER: {
|
|
293
357
|
actions: 'sendSpectatorEnterToAppEventMachine'
|
|
294
358
|
}
|
|
@@ -385,10 +449,6 @@ const ActiveEventStatesConfig = {
|
|
|
385
449
|
target: '#liveRendering'
|
|
386
450
|
},
|
|
387
451
|
PARTICIPANT_LEAVE: [
|
|
388
|
-
{
|
|
389
|
-
cond: 'isHostLeaving',
|
|
390
|
-
actions: ['sendHostExitToAppMachine', 'sendExitToParticipantMachine']
|
|
391
|
-
},
|
|
392
452
|
{
|
|
393
453
|
cond: 'isPermanentLeave',
|
|
394
454
|
actions: ['sendExitToAppMachine', 'sendExitToParticipantMachine']
|
|
@@ -407,8 +467,8 @@ const ActiveEventStatesConfig = {
|
|
|
407
467
|
target: '.participantEntering'
|
|
408
468
|
}
|
|
409
469
|
],
|
|
410
|
-
|
|
411
|
-
|
|
470
|
+
BOOT_PARTICIPANT: {
|
|
471
|
+
actions: 'sendBootEventToParticipantMachine'
|
|
412
472
|
}
|
|
413
473
|
},
|
|
414
474
|
states: {
|
|
@@ -491,8 +551,8 @@ const ActiveEventStatesConfig = {
|
|
|
491
551
|
},
|
|
492
552
|
PARTICIPANT_LEAVE: [
|
|
493
553
|
{
|
|
494
|
-
cond: '
|
|
495
|
-
actions: ['removeLeavingParticipantFromTimeout', '
|
|
554
|
+
cond: 'isBooting',
|
|
555
|
+
actions: ['removeLeavingParticipantFromTimeout', 'sendExitToParticipantMachine']
|
|
496
556
|
},
|
|
497
557
|
{
|
|
498
558
|
cond: 'isPermanentLeave',
|
|
@@ -539,29 +599,6 @@ const ActiveEventStatesConfig = {
|
|
|
539
599
|
}
|
|
540
600
|
}
|
|
541
601
|
},
|
|
542
|
-
paused: {
|
|
543
|
-
initial: 'transitioning',
|
|
544
|
-
states: {
|
|
545
|
-
transitioning: {
|
|
546
|
-
deferred: DeferredStatesPlus('RESUME', 'CLOSE'),
|
|
547
|
-
invoke: {
|
|
548
|
-
src: 'transitionToPaused',
|
|
549
|
-
onDone: {
|
|
550
|
-
target: 'waitingForResume'
|
|
551
|
-
},
|
|
552
|
-
onError: {
|
|
553
|
-
target: '#activeEvent.failure'
|
|
554
|
-
}
|
|
555
|
-
}
|
|
556
|
-
},
|
|
557
|
-
waitingForResume: {
|
|
558
|
-
on: {
|
|
559
|
-
RESUME: '#activeEvent.live',
|
|
560
|
-
CLOSE: '#activeEvent.closeEvent'
|
|
561
|
-
}
|
|
562
|
-
}
|
|
563
|
-
}
|
|
564
|
-
},
|
|
565
602
|
closeEvent: {
|
|
566
603
|
id: 'closeEvent',
|
|
567
604
|
initial: 'notifyingParticipants',
|
|
@@ -631,7 +668,7 @@ const ActiveEventStatesConfig = {
|
|
|
631
668
|
}
|
|
632
669
|
},
|
|
633
670
|
finalizing: {
|
|
634
|
-
|
|
671
|
+
// canceled path does ANAPI transition to canceled, then detaches
|
|
635
672
|
invoke: {
|
|
636
673
|
src: 'eventTransitionCanceled',
|
|
637
674
|
onDone: 'detaching',
|
|
@@ -652,6 +689,42 @@ const ActiveEventStatesConfig = {
|
|
|
652
689
|
}
|
|
653
690
|
}
|
|
654
691
|
},
|
|
692
|
+
shutdownOnly: {
|
|
693
|
+
id: 'shutdownOnly',
|
|
694
|
+
initial: 'notifyingParticipants',
|
|
695
|
+
states: {
|
|
696
|
+
notifyingParticipants: {
|
|
697
|
+
entry: 'sendParticipantExitEvents',
|
|
698
|
+
always: 'waitForParticipantsToExit'
|
|
699
|
+
},
|
|
700
|
+
waitForParticipantsToExit: {
|
|
701
|
+
entry: context => logger.debug(`[AEM] Entering waitForParticipantsToExit with ${getAllParticipantIds(context).length} participants`),
|
|
702
|
+
always: [
|
|
703
|
+
{
|
|
704
|
+
cond: context => getAllParticipantIds(context).length === 0,
|
|
705
|
+
target: 'detaching'
|
|
706
|
+
}
|
|
707
|
+
],
|
|
708
|
+
on: {
|
|
709
|
+
PARTICIPANT_LEAVE: {
|
|
710
|
+
actions: () => logger.debug('[AEM] Ignoring PARTICIPANT_LEAVE during orchestrated shutdown.')
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
},
|
|
714
|
+
detaching: {
|
|
715
|
+
deferred: DeferredStates,
|
|
716
|
+
invoke: {
|
|
717
|
+
src: 'detachChannels',
|
|
718
|
+
onDone: {
|
|
719
|
+
target: '#activeEvent.doneExit'
|
|
720
|
+
},
|
|
721
|
+
onError: {
|
|
722
|
+
target: '#activeEvent.failure'
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
},
|
|
655
728
|
review: {
|
|
656
729
|
on: {
|
|
657
730
|
NEXT: 'reward'
|
|
@@ -708,7 +781,10 @@ const CreateEventChannelsAndAppMachineConfig = {
|
|
|
708
781
|
states: {
|
|
709
782
|
setupEventChannel: {
|
|
710
783
|
// get(eventChannelName) and setup state-change callbacks
|
|
711
|
-
entry: [(c,e) =>
|
|
784
|
+
entry: [(c,e) => {
|
|
785
|
+
const eventType = c.rehydrate ? 'LOAD_EVENT' : 'CREATE_EVENT'
|
|
786
|
+
logger.debug(`[AEM] === ${eventType} ${c.anearEvent.id} ===`)
|
|
787
|
+
}, 'createEventChannel'],
|
|
712
788
|
invoke: {
|
|
713
789
|
src: 'attachToEventChannel',
|
|
714
790
|
onDone: {
|
|
@@ -720,7 +796,18 @@ const CreateEventChannelsAndAppMachineConfig = {
|
|
|
720
796
|
}
|
|
721
797
|
},
|
|
722
798
|
on: {
|
|
723
|
-
ATTACHED:
|
|
799
|
+
ATTACHED: 'enterEventPresence'
|
|
800
|
+
}
|
|
801
|
+
},
|
|
802
|
+
enterEventPresence: {
|
|
803
|
+
invoke: {
|
|
804
|
+
src: 'enterEventPresence',
|
|
805
|
+
onDone: {
|
|
806
|
+
actions: ['subscribeToEventMessages'],
|
|
807
|
+
target: 'setupParticipantsDisplayChannel',
|
|
808
|
+
internal: true
|
|
809
|
+
},
|
|
810
|
+
onError: {
|
|
724
811
|
actions: ['subscribeToEventMessages'],
|
|
725
812
|
target: 'setupParticipantsDisplayChannel'
|
|
726
813
|
}
|
|
@@ -782,9 +869,16 @@ const CreateEventChannelsAndAppMachineConfig = {
|
|
|
782
869
|
}
|
|
783
870
|
},
|
|
784
871
|
createAppEventMachine: {
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
872
|
+
invoke: {
|
|
873
|
+
src: 'createAppEventMachine',
|
|
874
|
+
onDone: {
|
|
875
|
+
actions: ['setAppEventMachine'],
|
|
876
|
+
target: '#activeEvent',
|
|
877
|
+
internal: true
|
|
878
|
+
},
|
|
879
|
+
onError: {
|
|
880
|
+
target: '#activeEvent.failure'
|
|
881
|
+
}
|
|
788
882
|
}
|
|
789
883
|
}
|
|
790
884
|
}
|
|
@@ -806,12 +900,20 @@ const AnearEventMachineStatesConfig = eventId => ({
|
|
|
806
900
|
|
|
807
901
|
const AnearEventMachineFunctions = ({
|
|
808
902
|
actions: {
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
903
|
+
sendPausedAckToAppMachine: (context, _event) => {
|
|
904
|
+
if (context.appEventMachine) {
|
|
905
|
+
context.appEventMachine.send('PAUSED')
|
|
906
|
+
}
|
|
907
|
+
},
|
|
908
|
+
sendSavedAckToAppMachine: (context, _event) => {
|
|
909
|
+
if (context.appEventMachine) {
|
|
910
|
+
context.appEventMachine.send('SAVED')
|
|
911
|
+
}
|
|
912
|
+
},
|
|
913
|
+
setAppEventMachine: assign({
|
|
914
|
+
appEventMachine: (context, event) => {
|
|
915
|
+
const service = event.data.service
|
|
916
|
+
return service
|
|
815
917
|
}
|
|
816
918
|
}),
|
|
817
919
|
notifyAppMachineRendered: (context, event) => {
|
|
@@ -924,8 +1026,7 @@ const AnearEventMachineFunctions = ({
|
|
|
924
1026
|
return;
|
|
925
1027
|
}
|
|
926
1028
|
|
|
927
|
-
const
|
|
928
|
-
const eventName = isHost ? 'HOST_ENTER' : 'PARTICIPANT_ENTER';
|
|
1029
|
+
const eventName = getPresenceEventName(anearParticipant, 'ENTER');
|
|
929
1030
|
const eventPayload = { participant: participantInfo };
|
|
930
1031
|
|
|
931
1032
|
logger.debug(`[AEM] Sending ${eventName} for ${anearParticipant.id}`);
|
|
@@ -934,11 +1035,10 @@ const AnearEventMachineFunctions = ({
|
|
|
934
1035
|
processDisconnectEvents: (context, event) => {
|
|
935
1036
|
const participantId = event.data.id;
|
|
936
1037
|
const participant = context.participants[participantId];
|
|
937
|
-
const
|
|
938
|
-
const eventName = isHost ? 'HOST_DISCONNECT' : 'PARTICIPANT_DISCONNECT';
|
|
1038
|
+
const eventName = getPresenceEventName(participant, 'DISCONNECT');
|
|
939
1039
|
const participantMachine = context.participantMachines[participantId];
|
|
940
1040
|
|
|
941
|
-
logger.debug(`[AEM] processing
|
|
1041
|
+
logger.debug(`[AEM] processing ${eventName} for ${participantId}`);
|
|
942
1042
|
if (participantMachine) {
|
|
943
1043
|
participantMachine.send('PARTICIPANT_DISCONNECT');
|
|
944
1044
|
}
|
|
@@ -947,31 +1047,79 @@ const AnearEventMachineFunctions = ({
|
|
|
947
1047
|
processReconnectEvents: (context, event) => {
|
|
948
1048
|
const participantId = event.data.id;
|
|
949
1049
|
const participant = context.participants[participantId];
|
|
950
|
-
const
|
|
951
|
-
const eventName = isHost ? 'HOST_RECONNECT' : 'PARTICIPANT_RECONNECT';
|
|
1050
|
+
const eventName = getPresenceEventName(participant, 'RECONNECT');
|
|
952
1051
|
const participantMachine = context.participantMachines[participantId];
|
|
953
1052
|
|
|
954
|
-
logger.debug(`[AEM] processing
|
|
1053
|
+
logger.debug(`[AEM] processing ${eventName} for ${participantId}`);
|
|
955
1054
|
if (participantMachine) {
|
|
956
1055
|
participantMachine.send('PARTICIPANT_RECONNECT');
|
|
957
1056
|
}
|
|
958
1057
|
context.appEventMachine.send(eventName, { participantId });
|
|
959
1058
|
},
|
|
1059
|
+
updateParticipantGeoLocation: assign({
|
|
1060
|
+
participants: (context, event) => {
|
|
1061
|
+
const { id, geoLocation } = event.data;
|
|
1062
|
+
const participantInfoToUpdate = context.participants[id];
|
|
1063
|
+
|
|
1064
|
+
if (!participantInfoToUpdate) return context.participants;
|
|
1065
|
+
|
|
1066
|
+
// Create a new, updated info object
|
|
1067
|
+
const updatedParticipantInfo = {
|
|
1068
|
+
...participantInfoToUpdate,
|
|
1069
|
+
geoLocation
|
|
1070
|
+
};
|
|
1071
|
+
|
|
1072
|
+
// Return a new participants object with the updated participant info
|
|
1073
|
+
return {
|
|
1074
|
+
...context.participants,
|
|
1075
|
+
[id]: updatedParticipantInfo
|
|
1076
|
+
};
|
|
1077
|
+
}
|
|
1078
|
+
}),
|
|
1079
|
+
processUpdateEvents: (context, event) => {
|
|
1080
|
+
const { id } = event.data;
|
|
1081
|
+
// NOTE: get the full AnearParticipant object from the AEM instance,
|
|
1082
|
+
// NOT the plain info object from the context.
|
|
1083
|
+
const participant = context.participants[id];
|
|
1084
|
+
|
|
1085
|
+
if (!participant) return;
|
|
1086
|
+
|
|
1087
|
+
const eventName = getPresenceEventName(participant, 'UPDATE');
|
|
1088
|
+
const participantMachine = context.participantMachines[id];
|
|
1089
|
+
|
|
1090
|
+
// AppM gets the role-specific event
|
|
1091
|
+
const appMPayload = { type: eventName, participant };
|
|
1092
|
+
|
|
1093
|
+
logger.debug(`[AEM] processing ${eventName} for ${id}`);
|
|
1094
|
+
if (participantMachine) {
|
|
1095
|
+
// APM always gets the generic event
|
|
1096
|
+
const apmPayload = { type: 'PARTICIPANT_UPDATE', participant };
|
|
1097
|
+
participantMachine.send(apmPayload);
|
|
1098
|
+
}
|
|
1099
|
+
context.appEventMachine.send(appMPayload);
|
|
1100
|
+
},
|
|
960
1101
|
sendExitToAppMachine: (context, event) => {
|
|
961
|
-
// coming from an action channel message, event.data.id
|
|
962
1102
|
const participantId = event.data.id;
|
|
963
|
-
const
|
|
964
|
-
if (
|
|
965
|
-
|
|
966
|
-
|
|
1103
|
+
const participant = context.participants[participantId];
|
|
1104
|
+
if (participant) {
|
|
1105
|
+
const eventName = getPresenceEventName(participant, 'EXIT');
|
|
1106
|
+
logger.debug(`[AEM] sending ${eventName} to AppM for participant ${participantId}`);
|
|
1107
|
+
context.appEventMachine.send(eventName, { participantId });
|
|
967
1108
|
} else {
|
|
968
1109
|
logger.warn(`[AEM] Participant info not found for id ${participantId} during sendExitToAppMachine`);
|
|
969
1110
|
}
|
|
970
1111
|
},
|
|
971
|
-
|
|
972
|
-
const participantId = event.data
|
|
973
|
-
|
|
974
|
-
|
|
1112
|
+
sendBootEventToParticipantMachine: (context, event) => {
|
|
1113
|
+
const { participantId, reason } = event.data;
|
|
1114
|
+
const participantMachine = context.participantMachines[participantId];
|
|
1115
|
+
if (participantMachine) {
|
|
1116
|
+
participantMachine.send({
|
|
1117
|
+
type: 'BOOT_PARTICIPANT',
|
|
1118
|
+
data: { reason }
|
|
1119
|
+
})
|
|
1120
|
+
} else {
|
|
1121
|
+
logger.warn(`[AEM] Participant machine not found for id ${participantId} during sendBootEventToParticipantMachine`);
|
|
1122
|
+
}
|
|
975
1123
|
},
|
|
976
1124
|
sendExitToParticipantMachine: (context, event) => {
|
|
977
1125
|
// coming from an action channel message, event.data.id
|
|
@@ -986,18 +1134,27 @@ const AnearEventMachineFunctions = ({
|
|
|
986
1134
|
sendParticipantExitEvents: context => {
|
|
987
1135
|
Object.values(context.participantMachines).forEach(pm => pm.send('PARTICIPANT_EXIT'))
|
|
988
1136
|
},
|
|
1137
|
+
updateParticipantPresence: (context, event) => {
|
|
1138
|
+
const participantId = event.data.id;
|
|
1139
|
+
const participant = context.participants[participantId];
|
|
1140
|
+
const participantMachine = context.participantMachines[participantId];
|
|
989
1141
|
|
|
1142
|
+
if (!participant) {
|
|
1143
|
+
logger.warn(`[AEM] Participant info not found for id ${participantId} during updateParticipantPresence`);
|
|
1144
|
+
return;
|
|
1145
|
+
}
|
|
990
1146
|
|
|
991
|
-
|
|
992
|
-
// lookup the participantMachine and update its context
|
|
993
|
-
const participantMachine = context.participantMachines[event.data.id]
|
|
1147
|
+
// APM always gets the generic event
|
|
994
1148
|
if (participantMachine) {
|
|
995
1149
|
// opportunity to send presence data update like geoLocation, and
|
|
996
1150
|
// to inform app that a participant still has interest in the possibly long
|
|
997
1151
|
// running, light-participation event
|
|
998
|
-
participantMachine.send('PARTICIPANT_UPDATE', event.data)
|
|
1152
|
+
participantMachine.send('PARTICIPANT_UPDATE', event.data);
|
|
999
1153
|
}
|
|
1000
|
-
|
|
1154
|
+
|
|
1155
|
+
// AppM gets the role-specific event
|
|
1156
|
+
const eventName = getPresenceEventName(participant, 'UPDATE');
|
|
1157
|
+
context.appEventMachine.send(eventName, event.data);
|
|
1001
1158
|
},
|
|
1002
1159
|
processParticipantAction: (context, event) => {
|
|
1003
1160
|
// event.data.participantId,
|
|
@@ -1053,16 +1210,22 @@ const AnearEventMachineFunctions = ({
|
|
|
1053
1210
|
{ participantId: event.participantId }
|
|
1054
1211
|
),
|
|
1055
1212
|
setupParticipantsTimeout: assign((context, event) => {
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1213
|
+
// Only set up a new timeout if one is provided in the event data.
|
|
1214
|
+
// This prevents overwriting an existing timeout during a simple re-render.
|
|
1215
|
+
if (event.data && event.data.participantsTimeout) {
|
|
1216
|
+
const timeoutMsecs = event.data.participantsTimeout.msecs
|
|
1217
|
+
const allParticipantIds = getPlayingParticipantIds(context)
|
|
1218
|
+
logger.debug(`[AEM] Starting participants action timeout for ${timeoutMsecs}ms. Responders: ${allParticipantIds.join(', ')}`)
|
|
1059
1219
|
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1220
|
+
return {
|
|
1221
|
+
participantsActionTimeout: {
|
|
1222
|
+
msecs: timeoutMsecs,
|
|
1223
|
+
nonResponders: new Set(allParticipantIds)
|
|
1224
|
+
}
|
|
1064
1225
|
}
|
|
1065
1226
|
}
|
|
1227
|
+
// If no timeout data, return empty object to not change context
|
|
1228
|
+
return {};
|
|
1066
1229
|
}),
|
|
1067
1230
|
processParticipantResponse: assign((context, event) => {
|
|
1068
1231
|
const participantId = event.data.participantId
|
|
@@ -1078,17 +1241,24 @@ const AnearEventMachineFunctions = ({
|
|
|
1078
1241
|
}
|
|
1079
1242
|
}),
|
|
1080
1243
|
removeLeavingParticipantFromTimeout: assign((context, event) => {
|
|
1081
|
-
const participantId = event.data.id
|
|
1082
|
-
const
|
|
1083
|
-
|
|
1084
|
-
|
|
1244
|
+
const participantId = event.data.id;
|
|
1245
|
+
const participant = context.participants[participantId];
|
|
1246
|
+
|
|
1247
|
+
// If there's no active timeout, or if the leaving participant is the host, do nothing.
|
|
1248
|
+
if (!context.participantsActionTimeout || (participant && participant.isHost)) {
|
|
1249
|
+
return {};
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
const { nonResponders, ...rest } = context.participantsActionTimeout;
|
|
1253
|
+
const newNonResponders = new Set(nonResponders);
|
|
1254
|
+
newNonResponders.delete(participantId);
|
|
1085
1255
|
|
|
1086
1256
|
return {
|
|
1087
1257
|
participantsActionTimeout: {
|
|
1088
1258
|
...rest,
|
|
1089
1259
|
nonResponders: newNonResponders
|
|
1090
1260
|
}
|
|
1091
|
-
}
|
|
1261
|
+
};
|
|
1092
1262
|
}),
|
|
1093
1263
|
sendActionsTimeoutToAppM: (context, _event) => {
|
|
1094
1264
|
const { nonResponders, msecs } = context.participantsActionTimeout
|
|
@@ -1139,6 +1309,63 @@ const AnearEventMachineFunctions = ({
|
|
|
1139
1309
|
logInvalidParticipantEnter: (c, e) => logger.info("[AEM] Error: Unexepected PARTICIPANT_ENTER with id: ", e.data.id),
|
|
1140
1310
|
},
|
|
1141
1311
|
services: {
|
|
1312
|
+
saveAppEventContext: async (context, event) => {
|
|
1313
|
+
// Events like PAUSE/SAVE are sent as send('PAUSE', { appmContext: {...} })
|
|
1314
|
+
// event.appmContext -> { context, resumeEvent }
|
|
1315
|
+
const appmContext = event?.appmContext || {}
|
|
1316
|
+
const payload = {
|
|
1317
|
+
eventId: context.anearEvent.id,
|
|
1318
|
+
savedAt: new Date().toISOString(),
|
|
1319
|
+
...appmContext
|
|
1320
|
+
}
|
|
1321
|
+
await AnearApi.saveAppEventContext(context.anearEvent.id, payload)
|
|
1322
|
+
return 'done'
|
|
1323
|
+
},
|
|
1324
|
+
enterEventPresence: async (context, _event) => {
|
|
1325
|
+
const data = { actor: 'AEM', eventId: context.anearEvent.id, start: Date.now() }
|
|
1326
|
+
await RealtimeMessaging.setPresence(context.eventChannel, data)
|
|
1327
|
+
return { service: started }
|
|
1328
|
+
},
|
|
1329
|
+
createAppEventMachine: async (context, _event) => {
|
|
1330
|
+
// Build the AppM, optionally rehydrating from saved app_event_context
|
|
1331
|
+
const baseMachine = context.appEventMachineFactory(context.anearEvent)
|
|
1332
|
+
|
|
1333
|
+
let machineToStart = baseMachine
|
|
1334
|
+
let resumeEvent = null
|
|
1335
|
+
|
|
1336
|
+
if (context.rehydrate) {
|
|
1337
|
+
try {
|
|
1338
|
+
const { appmContext } = await AnearApi.getLatestAppEventContext(context.anearEvent.id)
|
|
1339
|
+
if (appmContext && typeof appmContext === 'object') {
|
|
1340
|
+
const savedContext = appmContext.context
|
|
1341
|
+
resumeEvent = appmContext.resumeEvent
|
|
1342
|
+
if (savedContext && typeof savedContext === 'object') {
|
|
1343
|
+
machineToStart = baseMachine.withContext(savedContext)
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
} catch (e) {
|
|
1347
|
+
// Log and proceed without rehydration
|
|
1348
|
+
logger.warn('[AEM] Failed to fetch or parse app_event_context. Starting clean.', e)
|
|
1349
|
+
}
|
|
1350
|
+
}
|
|
1351
|
+
|
|
1352
|
+
const service = interpret(machineToStart)
|
|
1353
|
+
service.subscribe(AppMachineTransition(context.anearEvent))
|
|
1354
|
+
// Auto-cleanup when AppM final: notify AEM
|
|
1355
|
+
try {
|
|
1356
|
+
service.onDone(() => {
|
|
1357
|
+
logger.debug('[AEM] AppM reached final state, sending APPM_FINAL for cleanup-only shutdown')
|
|
1358
|
+
context.anearEvent.send('APPM_FINAL')
|
|
1359
|
+
})
|
|
1360
|
+
} catch (_e) {}
|
|
1361
|
+
const started = service.start()
|
|
1362
|
+
|
|
1363
|
+
if (resumeEvent && resumeEvent.type) {
|
|
1364
|
+
started.send(resumeEvent)
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
return { service: started }
|
|
1368
|
+
},
|
|
1142
1369
|
renderDisplay: async (context, event) => {
|
|
1143
1370
|
const displayEventProcessor = new DisplayEventProcessor(context)
|
|
1144
1371
|
|
|
@@ -1201,9 +1428,6 @@ const AnearEventMachineFunctions = ({
|
|
|
1201
1428
|
transitionToCanceled: (context, event) => {
|
|
1202
1429
|
return AnearApi.transitionEvent(context.anearEvent.id, 'canceled')
|
|
1203
1430
|
},
|
|
1204
|
-
transitionToPaused: (context, event) => {
|
|
1205
|
-
return AnearApi.transitionEvent(context.anearEvent.id, 'paused')
|
|
1206
|
-
},
|
|
1207
1431
|
eventTransitionClosed: async (context, event) => {
|
|
1208
1432
|
// This service handles the transition of the event to 'closed' via AnearApi
|
|
1209
1433
|
// and the publishing of the 'EVENT_TRANSITION' message to ABRs.
|
|
@@ -1224,6 +1448,10 @@ const AnearEventMachineFunctions = ({
|
|
|
1224
1448
|
}
|
|
1225
1449
|
},
|
|
1226
1450
|
guards: {
|
|
1451
|
+
isReconnectUpdate: (context, event) => {
|
|
1452
|
+
// participant exists and event.data.type is undefined
|
|
1453
|
+
return context.participants[event.data.id] && !event.data.type
|
|
1454
|
+
},
|
|
1227
1455
|
isPermanentLeave: (context, event) => {
|
|
1228
1456
|
// The remote client has left the event. This is a permanent exit
|
|
1229
1457
|
// from the event
|
|
@@ -1237,18 +1465,18 @@ const AnearEventMachineFunctions = ({
|
|
|
1237
1465
|
return false
|
|
1238
1466
|
}
|
|
1239
1467
|
},
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
const participant = context.participants[participantId];
|
|
1243
|
-
return participant && participant.isHost;
|
|
1468
|
+
isBooting: (_c, event) => {
|
|
1469
|
+
return event.data.type === 'BOOTED'
|
|
1244
1470
|
},
|
|
1245
1471
|
participantExists: (context, event) => !!context.participants[event.data.id],
|
|
1246
|
-
eventCreatorIsHost: (context,
|
|
1247
|
-
isOpenHouseEvent: (context,
|
|
1472
|
+
eventCreatorIsHost: (context, _e) => context.anearEvent.hosted,
|
|
1473
|
+
isOpenHouseEvent: (context, _e) => context.anearEvent.openHouse || false,
|
|
1248
1474
|
isParticipantsTimeoutActive: (context, event) => {
|
|
1249
|
-
|
|
1475
|
+
const isStartingNewTimeout = event.data && event.data.participantsTimeout && event.data.participantsTimeout.msecs > 0;
|
|
1476
|
+
const isTimeoutAlreadyRunning = context.participantsActionTimeout !== null;
|
|
1477
|
+
return isStartingNewTimeout || isTimeoutAlreadyRunning;
|
|
1250
1478
|
},
|
|
1251
|
-
allParticipantsResponded: (context,
|
|
1479
|
+
allParticipantsResponded: (context, _e) => {
|
|
1252
1480
|
return context.participantsActionTimeout && context.participantsActionTimeout.nonResponders.size === 0
|
|
1253
1481
|
}
|
|
1254
1482
|
},
|
|
@@ -1258,7 +1486,7 @@ const AnearEventMachineFunctions = ({
|
|
|
1258
1486
|
timeoutEventAnnounce: context => C.TIMEOUT_MSECS.ANNOUNCE,
|
|
1259
1487
|
timeoutEventStart: context => C.TIMEOUT_MSECS.START,
|
|
1260
1488
|
timeoutRendered: context => C.TIMEOUT_MSECS.RENDERED_EVENT_DELAY,
|
|
1261
|
-
participantsActionTimeout: (context,
|
|
1489
|
+
participantsActionTimeout: (context, _e) => {
|
|
1262
1490
|
return context.participantsActionTimeout.msecs
|
|
1263
1491
|
}
|
|
1264
1492
|
}
|
|
@@ -1269,7 +1497,8 @@ const AnearEventMachine = (anearEvent, {
|
|
|
1269
1497
|
pugTemplates,
|
|
1270
1498
|
appEventMachineFactory,
|
|
1271
1499
|
appParticipantMachineFactory,
|
|
1272
|
-
imageAssetsUrl
|
|
1500
|
+
imageAssetsUrl,
|
|
1501
|
+
rehydrate = false
|
|
1273
1502
|
}) => {
|
|
1274
1503
|
const expandedConfig = {predictableActionArguments: true, ...AnearEventMachineStatesConfig(anearEvent.id)}
|
|
1275
1504
|
|
|
@@ -1282,7 +1511,8 @@ const AnearEventMachine = (anearEvent, {
|
|
|
1282
1511
|
pugTemplates,
|
|
1283
1512
|
pugHelpers,
|
|
1284
1513
|
appEventMachineFactory,
|
|
1285
|
-
appParticipantMachineFactory
|
|
1514
|
+
appParticipantMachineFactory,
|
|
1515
|
+
rehydrate
|
|
1286
1516
|
)
|
|
1287
1517
|
|
|
1288
1518
|
const service = interpret(eventMachine.withContext(anearEventMachineContext))
|
|
@@ -109,6 +109,10 @@ const AnearParticipantMachineConfig = participantId => ({
|
|
|
109
109
|
PARTICIPANT_RECONNECT: {
|
|
110
110
|
actions: 'logReconnected',
|
|
111
111
|
internal: true
|
|
112
|
+
},
|
|
113
|
+
BOOT_PARTICIPANT: {
|
|
114
|
+
actions: 'logBooted',
|
|
115
|
+
target: '#cleanupAndExit'
|
|
112
116
|
}
|
|
113
117
|
}
|
|
114
118
|
},
|
|
@@ -151,11 +155,17 @@ const AnearParticipantMachineConfig = participantId => ({
|
|
|
151
155
|
src: 'publishPrivateDisplay',
|
|
152
156
|
onDone: [
|
|
153
157
|
{ cond: 'hasActionTimeout', actions: 'updateActionTimeout', target: 'waitParticipantResponse' },
|
|
154
|
-
{ target: 'idle'
|
|
158
|
+
{ target: 'idle' }
|
|
155
159
|
],
|
|
156
160
|
onError: {
|
|
157
161
|
target: '#error'
|
|
158
162
|
}
|
|
163
|
+
},
|
|
164
|
+
on: {
|
|
165
|
+
PARTICIPANT_EXIT: {
|
|
166
|
+
actions: 'logExit',
|
|
167
|
+
target: '#cleanupAndExit'
|
|
168
|
+
}
|
|
159
169
|
}
|
|
160
170
|
},
|
|
161
171
|
waitParticipantResponse: {
|
|
@@ -223,6 +233,35 @@ const AnearParticipantMachineConfig = participantId => ({
|
|
|
223
233
|
}
|
|
224
234
|
}
|
|
225
235
|
},
|
|
236
|
+
booting: {
|
|
237
|
+
initial: 'publishingShutdown',
|
|
238
|
+
states: {
|
|
239
|
+
publishingShutdown: {
|
|
240
|
+
invoke: {
|
|
241
|
+
id: 'publishBootMessage',
|
|
242
|
+
src: 'publishBootMessageService',
|
|
243
|
+
onDone: 'waitingForClientExit',
|
|
244
|
+
onError: '#cleanupAndExit'
|
|
245
|
+
}
|
|
246
|
+
},
|
|
247
|
+
waitingForClientExit: {
|
|
248
|
+
after: {
|
|
249
|
+
bootCleanupTimeout: {
|
|
250
|
+
target: '#cleanupAndExit'
|
|
251
|
+
}
|
|
252
|
+
},
|
|
253
|
+
on: {
|
|
254
|
+
PARTICIPANT_EXIT: '#cleanupAndExit',
|
|
255
|
+
ACTION: {
|
|
256
|
+
internal: true
|
|
257
|
+
},
|
|
258
|
+
PARTICIPANT_DISCONNECT: {
|
|
259
|
+
internal: true
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
},
|
|
226
265
|
done: {
|
|
227
266
|
id: 'done',
|
|
228
267
|
entry: ['notifyEventMachineExit'],
|
|
@@ -356,6 +395,20 @@ const AnearParticipantMachineFunctions = {
|
|
|
356
395
|
|
|
357
396
|
return { timeout: event.timeout }
|
|
358
397
|
},
|
|
398
|
+
publishBootMessageService: async (context, event) => {
|
|
399
|
+
const { reason } = event.data;
|
|
400
|
+
const payload = {
|
|
401
|
+
content: {
|
|
402
|
+
reason: reason || 'You have been removed from the event.'
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
await RealtimeMessaging.publish(
|
|
407
|
+
context.privateChannel,
|
|
408
|
+
'FORCE_SHUTDOWN',
|
|
409
|
+
payload
|
|
410
|
+
)
|
|
411
|
+
},
|
|
359
412
|
attachToPrivateChannel: (context, event) => RealtimeMessaging.attachTo(context.privateChannel),
|
|
360
413
|
detachPrivateChannel: async (context, event) => {
|
|
361
414
|
return context.privateChannel ? RealtimeMessaging.detachAll([context.privateChannel]) : Promise.resolve()
|
|
@@ -368,6 +421,7 @@ const AnearParticipantMachineFunctions = {
|
|
|
368
421
|
wasMidTurnOnDisconnect: context => context.actionTimeoutMsecs !== null && context.actionTimeoutMsecs > 0
|
|
369
422
|
},
|
|
370
423
|
delays: {
|
|
424
|
+
bootCleanupTimeout: () => C.TIMEOUT_MSECS.BOOT_EXIT,
|
|
371
425
|
actionTimeout: context => context.actionTimeoutMsecs,
|
|
372
426
|
dynamicReconnectTimeout: context => {
|
|
373
427
|
// If an action timeout is active, use its remaining time.
|
|
@@ -37,7 +37,6 @@ const AppMachineTransition = (anearEvent) => {
|
|
|
37
37
|
// Process all meta objects to handle unpredictable AppM structures
|
|
38
38
|
metaObjects.forEach(meta => {
|
|
39
39
|
let viewer
|
|
40
|
-
let viewPath
|
|
41
40
|
let timeoutFn
|
|
42
41
|
let displayEvent
|
|
43
42
|
|
|
@@ -49,18 +48,23 @@ const AppMachineTransition = (anearEvent) => {
|
|
|
49
48
|
|
|
50
49
|
if (meta.allParticipants) {
|
|
51
50
|
viewer = 'allParticipants'
|
|
52
|
-
viewPath =
|
|
51
|
+
const { viewPath, props } = _extractViewAndProps(meta.allParticipants)
|
|
53
52
|
timeoutFn = RenderContextBuilder.buildTimeoutFn(viewer, meta.allParticipants.timeout)
|
|
54
53
|
|
|
55
54
|
displayEvent = RenderContextBuilder.buildDisplayEvent(
|
|
56
55
|
viewPath,
|
|
57
|
-
RenderContextBuilder.buildAppRenderContext(appContext, appStateName, event.type, viewer, timeoutFn)
|
|
56
|
+
RenderContextBuilder.buildAppRenderContext(appContext, appStateName, event.type, viewer, timeoutFn),
|
|
57
|
+
viewer,
|
|
58
|
+
null,
|
|
59
|
+
null,
|
|
60
|
+
props
|
|
58
61
|
)
|
|
59
62
|
displayEvents.push(displayEvent)
|
|
60
63
|
}
|
|
61
64
|
|
|
62
65
|
if (meta.eachParticipant) {
|
|
63
66
|
// Check if participant is a function (new selective rendering format)
|
|
67
|
+
viewer = 'eachParticipant'
|
|
64
68
|
if (typeof meta.eachParticipant === 'function') {
|
|
65
69
|
// New selective participant rendering
|
|
66
70
|
const participantRenderFunc = meta.eachParticipant
|
|
@@ -73,7 +77,7 @@ const AppMachineTransition = (anearEvent) => {
|
|
|
73
77
|
appContext,
|
|
74
78
|
appStateName,
|
|
75
79
|
event.type,
|
|
76
|
-
|
|
80
|
+
viewer,
|
|
77
81
|
null
|
|
78
82
|
)
|
|
79
83
|
|
|
@@ -81,13 +85,15 @@ const AppMachineTransition = (anearEvent) => {
|
|
|
81
85
|
if (participantDisplay && participantDisplay.participantId && participantDisplay.view) {
|
|
82
86
|
// For selective rendering, timeout is handled directly in the participant display object
|
|
83
87
|
const timeout = participantDisplay.timeout || null
|
|
88
|
+
const props = participantDisplay.props || {}
|
|
84
89
|
|
|
85
90
|
displayEvent = RenderContextBuilder.buildDisplayEvent(
|
|
86
91
|
participantDisplay.view,
|
|
87
92
|
baseAppRenderContext, // Reuse the same base context
|
|
88
|
-
|
|
93
|
+
viewer,
|
|
89
94
|
participantDisplay.participantId,
|
|
90
|
-
timeout
|
|
95
|
+
timeout,
|
|
96
|
+
props
|
|
91
97
|
)
|
|
92
98
|
displayEvents.push(displayEvent)
|
|
93
99
|
}
|
|
@@ -95,15 +101,17 @@ const AppMachineTransition = (anearEvent) => {
|
|
|
95
101
|
}
|
|
96
102
|
} else {
|
|
97
103
|
// Legacy participant rendering - normalize to selective format
|
|
98
|
-
const viewPath =
|
|
104
|
+
const { viewPath, props } = _extractViewAndProps(meta.eachParticipant)
|
|
99
105
|
const timeoutFn = RenderContextBuilder.buildTimeoutFn('participant', meta.eachParticipant.timeout)
|
|
100
106
|
|
|
101
107
|
const renderContext = RenderContextBuilder.buildAppRenderContext(appContext, appStateName, event.type, 'eachParticipant', timeoutFn),
|
|
102
108
|
displayEvent = RenderContextBuilder.buildDisplayEvent(
|
|
103
109
|
viewPath,
|
|
104
110
|
renderContext,
|
|
105
|
-
|
|
106
|
-
'ALL_PARTICIPANTS' // Special marker for "all participants"
|
|
111
|
+
viewer,
|
|
112
|
+
'ALL_PARTICIPANTS', // Special marker for "all participants"
|
|
113
|
+
null,
|
|
114
|
+
props
|
|
107
115
|
)
|
|
108
116
|
displayEvents.push(displayEvent)
|
|
109
117
|
}
|
|
@@ -111,24 +119,31 @@ const AppMachineTransition = (anearEvent) => {
|
|
|
111
119
|
|
|
112
120
|
if (meta.host) {
|
|
113
121
|
viewer = 'host'
|
|
114
|
-
viewPath =
|
|
122
|
+
const { viewPath, props } = _extractViewAndProps(meta.host)
|
|
115
123
|
timeoutFn = RenderContextBuilder.buildTimeoutFn(viewer, meta.host.timeout)
|
|
116
124
|
|
|
117
125
|
displayEvent = RenderContextBuilder.buildDisplayEvent(
|
|
118
126
|
viewPath,
|
|
119
127
|
RenderContextBuilder.buildAppRenderContext(appContext, appStateName, event.type, viewer, timeoutFn),
|
|
120
|
-
|
|
128
|
+
viewer,
|
|
129
|
+
null,
|
|
130
|
+
null,
|
|
131
|
+
props
|
|
121
132
|
)
|
|
122
133
|
displayEvents.push(displayEvent)
|
|
123
134
|
}
|
|
124
135
|
|
|
125
136
|
if (meta.spectators) {
|
|
126
137
|
viewer = 'spectators'
|
|
127
|
-
viewPath =
|
|
138
|
+
const { viewPath, props } = _extractViewAndProps(meta.spectators)
|
|
128
139
|
|
|
129
140
|
displayEvent = RenderContextBuilder.buildDisplayEvent(
|
|
130
141
|
viewPath,
|
|
131
|
-
RenderContextBuilder.buildAppRenderContext(appContext, appStateName, event.type, viewer)
|
|
142
|
+
RenderContextBuilder.buildAppRenderContext(appContext, appStateName, event.type, viewer),
|
|
143
|
+
viewer,
|
|
144
|
+
null,
|
|
145
|
+
null,
|
|
146
|
+
props
|
|
132
147
|
)
|
|
133
148
|
displayEvents.push(displayEvent)
|
|
134
149
|
}
|
|
@@ -140,28 +155,28 @@ const AppMachineTransition = (anearEvent) => {
|
|
|
140
155
|
}
|
|
141
156
|
}
|
|
142
157
|
|
|
143
|
-
//
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
}
|
|
158
|
+
// Note: When AppM reaches a final state, the service.onDone() callback
|
|
159
|
+
// in AnearEventMachine.js will fire and send APPM_FINAL to trigger cleanup.
|
|
160
|
+
// Apps should explicitly call anearEvent.closeEvent() or anearEvent.cancelEvent()
|
|
161
|
+
// as entry actions in final states if they need API state transitions.
|
|
148
162
|
}
|
|
149
163
|
}
|
|
150
164
|
|
|
151
|
-
const
|
|
152
|
-
if (!config) return null
|
|
153
|
-
|
|
165
|
+
const _extractViewAndProps = (config) => {
|
|
166
|
+
if (!config) return { viewPath: null, props: {} }
|
|
167
|
+
|
|
168
|
+
if (typeof config === 'string') {
|
|
169
|
+
return { viewPath: config, props: {} }
|
|
170
|
+
}
|
|
171
|
+
|
|
154
172
|
if (typeof config === 'object') {
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
if (config.path) return config.path
|
|
159
|
-
// If no view path found, log warning but don't throw
|
|
160
|
-
logger.warn(`[AppMachineTransition] No view path found in config: ${JSON.stringify(config)}`)
|
|
161
|
-
return null
|
|
173
|
+
const viewPath = config.view
|
|
174
|
+
const props = config.props || {}
|
|
175
|
+
return { viewPath, props }
|
|
162
176
|
}
|
|
177
|
+
|
|
163
178
|
logger.warn(`[AppMachineTransition] Unknown meta format: ${JSON.stringify(config)}`)
|
|
164
|
-
return null
|
|
179
|
+
return { viewPath: null, props: {} }
|
|
165
180
|
}
|
|
166
181
|
|
|
167
182
|
const _stringifiedState = (stateValue) => {
|
package/lib/utils/Constants.js
CHANGED
|
@@ -13,7 +13,8 @@ module.exports = {
|
|
|
13
13
|
ANNOUNCE: 5 * 60 * 1000, // 5 minutes
|
|
14
14
|
START: 5 * 60 * 1000, // 5 minutes
|
|
15
15
|
RENDERED_EVENT_DELAY: 100, // 100 milliseconds
|
|
16
|
-
RECONNECT: 30 * 1000 // 30 seconds
|
|
16
|
+
RECONNECT: 30 * 1000, // 30 seconds
|
|
17
|
+
BOOT_EXIT: 2 * 1000 // 5 seconds
|
|
17
18
|
},
|
|
18
19
|
EventStates: {
|
|
19
20
|
CREATED: 'created',
|
|
@@ -8,9 +8,13 @@
|
|
|
8
8
|
* meta: {
|
|
9
9
|
* viewer: // 'eachParticipant', 'allParticipants', 'spectators'
|
|
10
10
|
* state: // Stringified state name (e.g., 'live.registration.waitForOpponentToJoin')
|
|
11
|
-
* event: // The triggering event for this transition ('
|
|
11
|
+
* event: // The triggering event for this transition ('PARTIPANT_ENTER')
|
|
12
12
|
* timeout: // null|| func}
|
|
13
13
|
* }
|
|
14
|
+
* props: {
|
|
15
|
+
* // All properties from the `props` object in the AppM's meta block.
|
|
16
|
+
* // e.g., meta: { view: 'foo', props: { title: 'Hello' } } -> `props.title`
|
|
17
|
+
* }
|
|
14
18
|
*
|
|
15
19
|
* // When 'eachParticipant' displayType
|
|
16
20
|
* allParticipants: // all: Map of all participants [info, context] for this event, get(id) => info, context
|
|
@@ -77,7 +81,7 @@ class DisplayEventProcessor {
|
|
|
77
81
|
}
|
|
78
82
|
|
|
79
83
|
_processSingle(displayEvent) {
|
|
80
|
-
const { viewPath, appRenderContext,
|
|
84
|
+
const { viewPath, appRenderContext, viewer, participantId, timeout: displayTimeout, props } = displayEvent
|
|
81
85
|
const timeoutFn = appRenderContext.meta.timeoutFn
|
|
82
86
|
|
|
83
87
|
const normalizedPath = viewPath.endsWith(C.PugSuffix) ? viewPath : `${viewPath}${C.PugSuffix}`
|
|
@@ -93,7 +97,8 @@ class DisplayEventProcessor {
|
|
|
93
97
|
...appRenderContext,
|
|
94
98
|
anearEvent: this.anearEvent,
|
|
95
99
|
participants: this.participantsIndex,
|
|
96
|
-
...this.pugHelpers
|
|
100
|
+
...this.pugHelpers,
|
|
101
|
+
props
|
|
97
102
|
}
|
|
98
103
|
|
|
99
104
|
const formattedDisplayMessage = () => {
|
|
@@ -104,11 +109,11 @@ class DisplayEventProcessor {
|
|
|
104
109
|
return displayMessage
|
|
105
110
|
}
|
|
106
111
|
|
|
107
|
-
// The
|
|
108
|
-
//
|
|
109
|
-
const
|
|
112
|
+
// The viewer determines who sees the view. It can be set directly on the
|
|
113
|
+
// display event, but if not, it falls back to the viewer from the meta block.
|
|
114
|
+
const displayViewer = viewer || appRenderContext.meta.viewer
|
|
110
115
|
|
|
111
|
-
switch (
|
|
116
|
+
switch (displayViewer) {
|
|
112
117
|
case 'allParticipants':
|
|
113
118
|
logger.debug(`[DisplayEventProcessor] Publishing ${viewPath} to PARTICIPANTS_DISPLAY`)
|
|
114
119
|
|
|
@@ -162,7 +167,7 @@ class DisplayEventProcessor {
|
|
|
162
167
|
break
|
|
163
168
|
|
|
164
169
|
default:
|
|
165
|
-
throw new Error(`Unknown display
|
|
170
|
+
throw new Error(`Unknown display viewer: ${displayViewer}`)
|
|
166
171
|
}
|
|
167
172
|
return { publishPromise, timeout }
|
|
168
173
|
}
|
|
@@ -85,6 +85,15 @@ class RealtimeMessaging {
|
|
|
85
85
|
return channel.publish(msgType, message)
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
+
async setPresence(channel, data) {
|
|
89
|
+
try {
|
|
90
|
+
await channel.presence.enter(data)
|
|
91
|
+
logger.debug(`[RTM] presence.enter on ${channel.name} with`, data)
|
|
92
|
+
} catch (e) {
|
|
93
|
+
logger.warn(`[RTM] presence.enter failed on ${channel.name}`, e)
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
88
97
|
/**
|
|
89
98
|
* Detach a list of Ably channels.
|
|
90
99
|
* If any detach fails, Promise.all will reject.
|
|
@@ -59,19 +59,21 @@ class RenderContextBuilder {
|
|
|
59
59
|
* Build display event object
|
|
60
60
|
* @param {string} viewPath - Template/view path
|
|
61
61
|
* @param {Object} appRenderContext - Context object
|
|
62
|
-
* @param {string}
|
|
62
|
+
* @param {string} viewer - Optional viewer type (host, participant, etc.)
|
|
63
63
|
* @param {string} participantId - Optional participant ID for selective rendering
|
|
64
64
|
* @param {number} timeout - Optional timeout in milliseconds
|
|
65
|
+
* @param {Object} props - Optional arguments for the view
|
|
65
66
|
* @returns {Object} Display event object
|
|
66
67
|
*/
|
|
67
|
-
static buildDisplayEvent(viewPath, appRenderContext,
|
|
68
|
+
static buildDisplayEvent(viewPath, appRenderContext, viewer = null, participantId = null, timeout = null, props = {}) {
|
|
68
69
|
const displayEvent = {
|
|
69
70
|
viewPath,
|
|
70
|
-
appRenderContext
|
|
71
|
+
appRenderContext,
|
|
72
|
+
props
|
|
71
73
|
}
|
|
72
74
|
|
|
73
|
-
if (
|
|
74
|
-
displayEvent.
|
|
75
|
+
if (viewer) {
|
|
76
|
+
displayEvent.viewer = viewer
|
|
75
77
|
}
|
|
76
78
|
|
|
77
79
|
if (participantId) {
|