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
|
@@ -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,32 @@ 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)
|
|
139
|
+
timeoutFn = RenderContextBuilder.buildTimeoutFn(viewer, meta.spectators.timeout)
|
|
128
140
|
|
|
129
141
|
displayEvent = RenderContextBuilder.buildDisplayEvent(
|
|
130
142
|
viewPath,
|
|
131
|
-
RenderContextBuilder.buildAppRenderContext(appContext, appStateName, event.type, viewer)
|
|
143
|
+
RenderContextBuilder.buildAppRenderContext(appContext, appStateName, event.type, viewer, timeoutFn),
|
|
144
|
+
viewer,
|
|
145
|
+
null,
|
|
146
|
+
null,
|
|
147
|
+
props
|
|
132
148
|
)
|
|
133
149
|
displayEvents.push(displayEvent)
|
|
134
150
|
}
|
|
@@ -140,28 +156,28 @@ const AppMachineTransition = (anearEvent) => {
|
|
|
140
156
|
}
|
|
141
157
|
}
|
|
142
158
|
|
|
143
|
-
//
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
}
|
|
159
|
+
// Note: When AppM reaches a final state, the service.onDone() callback
|
|
160
|
+
// in AnearEventMachine.js will fire and send APPM_FINAL to trigger cleanup.
|
|
161
|
+
// Apps should explicitly call anearEvent.closeEvent() or anearEvent.cancelEvent()
|
|
162
|
+
// as entry actions in final states if they need API state transitions.
|
|
148
163
|
}
|
|
149
164
|
}
|
|
150
165
|
|
|
151
|
-
const
|
|
152
|
-
if (!config) return null
|
|
153
|
-
|
|
166
|
+
const _extractViewAndProps = (config) => {
|
|
167
|
+
if (!config) return { viewPath: null, props: {} }
|
|
168
|
+
|
|
169
|
+
if (typeof config === 'string') {
|
|
170
|
+
return { viewPath: config, props: {} }
|
|
171
|
+
}
|
|
172
|
+
|
|
154
173
|
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
|
|
174
|
+
const viewPath = config.view
|
|
175
|
+
const props = config.props || {}
|
|
176
|
+
return { viewPath, props }
|
|
162
177
|
}
|
|
178
|
+
|
|
163
179
|
logger.warn(`[AppMachineTransition] Unknown meta format: ${JSON.stringify(config)}`)
|
|
164
|
-
return null
|
|
180
|
+
return { viewPath: null, props: {} }
|
|
165
181
|
}
|
|
166
182
|
|
|
167
183
|
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',
|
|
@@ -6,10 +6,17 @@
|
|
|
6
6
|
* {
|
|
7
7
|
* app: application XState context
|
|
8
8
|
* meta: {
|
|
9
|
-
* viewer: // 'eachParticipant', 'allParticipants', 'spectators'
|
|
9
|
+
* viewer: // 'eachParticipant', 'allParticipants', 'spectators', 'host'
|
|
10
10
|
* state: // Stringified state name (e.g., 'live.registration.waitForOpponentToJoin')
|
|
11
|
-
* event: // The triggering event for this transition ('
|
|
12
|
-
* timeout
|
|
11
|
+
* event: // The triggering event for this transition ('PARTIPANT_ENTER')
|
|
12
|
+
* // timeout exposed to views for display purposes only; actual timers are attached
|
|
13
|
+
* // only to the viewer that configured a timeout in AppM meta.
|
|
14
|
+
* // Shape: { msecs: number, remainingMsecs: number }
|
|
15
|
+
* timeout: // optional
|
|
16
|
+
* }
|
|
17
|
+
* props: {
|
|
18
|
+
* // All properties from the `props` object in the AppM's meta block.
|
|
19
|
+
* // e.g., meta: { view: 'foo', props: { title: 'Hello' } } -> `props.title`
|
|
13
20
|
* }
|
|
14
21
|
*
|
|
15
22
|
* // When 'eachParticipant' displayType
|
|
@@ -38,6 +45,8 @@ const C = require('./Constants')
|
|
|
38
45
|
class DisplayEventProcessor {
|
|
39
46
|
constructor(anearEventMachineContext) {
|
|
40
47
|
this.anearEvent = anearEventMachineContext.anearEvent
|
|
48
|
+
// Snapshot of current AEM timeout context for this render
|
|
49
|
+
this.participantsActionTimeout = anearEventMachineContext.participantsActionTimeout
|
|
41
50
|
this.pugTemplates = anearEventMachineContext.pugTemplates
|
|
42
51
|
this.pugHelpers = anearEventMachineContext.pugHelpers
|
|
43
52
|
this.participantMachines = anearEventMachineContext.participantMachines
|
|
@@ -50,6 +59,26 @@ class DisplayEventProcessor {
|
|
|
50
59
|
processAndPublish(displayEvents) {
|
|
51
60
|
let participantsTimeout = null
|
|
52
61
|
|
|
62
|
+
// Pre-compute allParticipants timeout for this render batch so other viewers
|
|
63
|
+
// (host/spectators) can mirror it visually even before AEM context is updated
|
|
64
|
+
try {
|
|
65
|
+
const allEvent = displayEvents.find(e => (e.viewer || e.appRenderContext?.meta?.viewer) === 'allParticipants')
|
|
66
|
+
if (allEvent) {
|
|
67
|
+
const { timeout: displayTimeout, appRenderContext } = allEvent
|
|
68
|
+
const timeoutFn = appRenderContext?.meta?.timeoutFn
|
|
69
|
+
if (displayTimeout !== null && displayTimeout > 0) {
|
|
70
|
+
this.precomputedParticipantsTimeout = { msecs: displayTimeout, startedAt: Date.now() }
|
|
71
|
+
} else if (timeoutFn) {
|
|
72
|
+
const msecs = timeoutFn(appRenderContext.app)
|
|
73
|
+
if (typeof msecs === 'number' && msecs > 0) {
|
|
74
|
+
this.precomputedParticipantsTimeout = { msecs, startedAt: Date.now() }
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
} catch (_e) {
|
|
79
|
+
// ignore precompute errors; fall back to runtime values
|
|
80
|
+
}
|
|
81
|
+
|
|
53
82
|
const publishPromises = displayEvents.map(event => {
|
|
54
83
|
const { publishPromise, timeout } = this._processSingle(event)
|
|
55
84
|
if (timeout) {
|
|
@@ -77,7 +106,7 @@ class DisplayEventProcessor {
|
|
|
77
106
|
}
|
|
78
107
|
|
|
79
108
|
_processSingle(displayEvent) {
|
|
80
|
-
const { viewPath, appRenderContext,
|
|
109
|
+
const { viewPath, appRenderContext, viewer, participantId, timeout: displayTimeout, props } = displayEvent
|
|
81
110
|
const timeoutFn = appRenderContext.meta.timeoutFn
|
|
82
111
|
|
|
83
112
|
const normalizedPath = viewPath.endsWith(C.PugSuffix) ? viewPath : `${viewPath}${C.PugSuffix}`
|
|
@@ -93,7 +122,8 @@ class DisplayEventProcessor {
|
|
|
93
122
|
...appRenderContext,
|
|
94
123
|
anearEvent: this.anearEvent,
|
|
95
124
|
participants: this.participantsIndex,
|
|
96
|
-
...this.pugHelpers
|
|
125
|
+
...this.pugHelpers,
|
|
126
|
+
props
|
|
97
127
|
}
|
|
98
128
|
|
|
99
129
|
const formattedDisplayMessage = () => {
|
|
@@ -104,11 +134,11 @@ class DisplayEventProcessor {
|
|
|
104
134
|
return displayMessage
|
|
105
135
|
}
|
|
106
136
|
|
|
107
|
-
// The
|
|
108
|
-
//
|
|
109
|
-
const
|
|
137
|
+
// The viewer determines who sees the view. It can be set directly on the
|
|
138
|
+
// display event, but if not, it falls back to the viewer from the meta block.
|
|
139
|
+
const displayViewer = viewer || appRenderContext.meta.viewer
|
|
110
140
|
|
|
111
|
-
switch (
|
|
141
|
+
switch (displayViewer) {
|
|
112
142
|
case 'allParticipants':
|
|
113
143
|
logger.debug(`[DisplayEventProcessor] Publishing ${viewPath} to PARTICIPANTS_DISPLAY`)
|
|
114
144
|
|
|
@@ -122,6 +152,20 @@ class DisplayEventProcessor {
|
|
|
122
152
|
}
|
|
123
153
|
}
|
|
124
154
|
|
|
155
|
+
// Expose timeout to template context for countdown bars
|
|
156
|
+
if (timeout) {
|
|
157
|
+
const startedAt = (this.participantsActionTimeout && this.participantsActionTimeout.startedAt) || (this.precomputedParticipantsTimeout && this.precomputedParticipantsTimeout.startedAt)
|
|
158
|
+
const now = Date.now()
|
|
159
|
+
const remainingMsecs = startedAt ? Math.max(0, timeout.msecs - (now - startedAt)) : timeout.msecs
|
|
160
|
+
templateRenderContext.meta = {
|
|
161
|
+
...templateRenderContext.meta,
|
|
162
|
+
timeout: { msecs: timeout.msecs, remainingMsecs }
|
|
163
|
+
}
|
|
164
|
+
logger.debug(`[DisplayEventProcessor] allParticipants timeout resolved to ${timeout.msecs}ms (remaining ${remainingMsecs}ms)`)
|
|
165
|
+
} else {
|
|
166
|
+
logger.debug(`[DisplayEventProcessor] no allParticipants timeout resolved`)
|
|
167
|
+
}
|
|
168
|
+
|
|
125
169
|
publishPromise = RealtimeMessaging.publish(
|
|
126
170
|
this.participantsDisplayChannel,
|
|
127
171
|
'PARTICIPANTS_DISPLAY',
|
|
@@ -132,6 +176,30 @@ class DisplayEventProcessor {
|
|
|
132
176
|
case 'spectators':
|
|
133
177
|
logger.debug(`[DisplayEventProcessor] Publishing ${viewPath} to SPECTATORS_DISPLAY`)
|
|
134
178
|
|
|
179
|
+
// Spectators never have action timeouts; they can display a visual bar only.
|
|
180
|
+
// Prefer their own configured timeout for visuals, otherwise mirror the
|
|
181
|
+
// allParticipants collective timeout (remaining time) if one is running.
|
|
182
|
+
let specTimeout = null
|
|
183
|
+
if (timeoutFn) {
|
|
184
|
+
const msecs = timeoutFn(appRenderContext.app)
|
|
185
|
+
if (typeof msecs === 'number' && msecs > 0) {
|
|
186
|
+
specTimeout = { msecs, remainingMsecs: msecs }
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
if (!specTimeout) {
|
|
190
|
+
const pat = this.precomputedParticipantsTimeout || this.participantsActionTimeout
|
|
191
|
+
if (pat && typeof pat.msecs === 'number') {
|
|
192
|
+
const now = Date.now()
|
|
193
|
+
const start = pat.startedAt || Date.now()
|
|
194
|
+
const remainingMsecs = Math.max(0, pat.msecs - (now - start))
|
|
195
|
+
specTimeout = { msecs: pat.msecs, remainingMsecs }
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
if (specTimeout) {
|
|
199
|
+
templateRenderContext.meta = { ...templateRenderContext.meta, timeout: specTimeout }
|
|
200
|
+
logger.debug(`[DisplayEventProcessor] spectators visual timeout set to msecs=${specTimeout.msecs}, remaining=${specTimeout.remainingMsecs}`)
|
|
201
|
+
}
|
|
202
|
+
|
|
135
203
|
publishPromise = RealtimeMessaging.publish(
|
|
136
204
|
this.spectatorsDisplayChannel,
|
|
137
205
|
'SPECTATORS_DISPLAY',
|
|
@@ -157,12 +225,45 @@ class DisplayEventProcessor {
|
|
|
157
225
|
|
|
158
226
|
case 'host':
|
|
159
227
|
logger.debug(`[DisplayEventProcessor] Processing RENDER_DISPLAY ${viewPath} for host`)
|
|
228
|
+
// Host may optionally have a real timeout (if configured). Otherwise, host
|
|
229
|
+
// mirrors the allParticipants visual timer without starting a host timeout.
|
|
230
|
+
let hostOwnMsecs = null
|
|
231
|
+
if (timeoutFn) {
|
|
232
|
+
const msecs = timeoutFn(appRenderContext.app)
|
|
233
|
+
if (typeof msecs === 'number' && msecs > 0) hostOwnMsecs = msecs
|
|
234
|
+
}
|
|
235
|
+
if (hostOwnMsecs !== null) {
|
|
236
|
+
templateRenderContext.meta = {
|
|
237
|
+
...templateRenderContext.meta,
|
|
238
|
+
timeout: { msecs: hostOwnMsecs, remainingMsecs: hostOwnMsecs }
|
|
239
|
+
}
|
|
240
|
+
logger.debug(`[DisplayEventProcessor] host timeout resolved to ${hostOwnMsecs}ms`)
|
|
241
|
+
} else {
|
|
242
|
+
// Visual-only: mirror participants timeout if it exists
|
|
243
|
+
const pat = this.precomputedParticipantsTimeout || this.participantsActionTimeout
|
|
244
|
+
if (pat && typeof pat.msecs === 'number') {
|
|
245
|
+
const now = Date.now()
|
|
246
|
+
const start = pat.startedAt || Date.now()
|
|
247
|
+
const remainingMsecs = Math.max(0, pat.msecs - (now - start))
|
|
248
|
+
templateRenderContext.meta = {
|
|
249
|
+
...templateRenderContext.meta,
|
|
250
|
+
timeout: { msecs: pat.msecs, remainingMsecs }
|
|
251
|
+
}
|
|
252
|
+
logger.debug(`[DisplayEventProcessor] host visual timeout from allParticipants msecs=${pat.msecs}, remaining=${remainingMsecs}`)
|
|
253
|
+
} else {
|
|
254
|
+
logger.debug(`[DisplayEventProcessor] host timeout not set`)
|
|
255
|
+
}
|
|
256
|
+
}
|
|
160
257
|
|
|
161
|
-
publishPromise = this._processHostDisplay(
|
|
258
|
+
publishPromise = this._processHostDisplay(
|
|
259
|
+
template,
|
|
260
|
+
templateRenderContext,
|
|
261
|
+
hostOwnMsecs !== null ? (() => hostOwnMsecs) : null
|
|
262
|
+
)
|
|
162
263
|
break
|
|
163
264
|
|
|
164
265
|
default:
|
|
165
|
-
throw new Error(`Unknown display
|
|
266
|
+
throw new Error(`Unknown display viewer: ${displayViewer}`)
|
|
166
267
|
}
|
|
167
268
|
return { publishPromise, timeout }
|
|
168
269
|
}
|
|
@@ -245,7 +346,8 @@ class DisplayEventProcessor {
|
|
|
245
346
|
}
|
|
246
347
|
|
|
247
348
|
if (timeout !== null) {
|
|
248
|
-
|
|
349
|
+
// APM will compute remaining on its own; start remaining at full msecs here
|
|
350
|
+
privateRenderContext.timeout = { msecs: timeout, remainingMsecs: timeout }
|
|
249
351
|
}
|
|
250
352
|
|
|
251
353
|
const privateHtml = template(privateRenderContext)
|
|
@@ -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) {
|