anear-js-api 1.6.6 → 1.8.0
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/lib/state_machines/AnearParticipantMachine.js +14 -1
- package/lib/utils/AppMachineTransition.js +171 -57
- package/lib/utils/DisplayEventProcessor.js +135 -68
- package/lib/utils/RenderContextBuilder.js +42 -2
- package/package.json +1 -1
- package/tests/AppMachineTransition.test.js +57 -0
- package/tests/DisplayEventProcessor.test.js +87 -0
|
@@ -654,7 +654,20 @@ const AnearParticipantMachineFunctions = {
|
|
|
654
654
|
actors: {
|
|
655
655
|
publishPrivateDisplay: fromPromise(async ({ input }) => {
|
|
656
656
|
const { context, event } = input
|
|
657
|
-
|
|
657
|
+
let displayMessage
|
|
658
|
+
if (event.targets != null && typeof event.targets === 'object' && !Array.isArray(event.targets)) {
|
|
659
|
+
displayMessage = { targets: event.targets }
|
|
660
|
+
} else if (event.anchor != null && event.content !== undefined) {
|
|
661
|
+
const val = (event.type === 'replace' || event.type === 'append')
|
|
662
|
+
? { content: event.content, type: event.type }
|
|
663
|
+
: event.content
|
|
664
|
+
displayMessage = { targets: { [event.anchor]: val } }
|
|
665
|
+
} else {
|
|
666
|
+
displayMessage = { content: event.content }
|
|
667
|
+
}
|
|
668
|
+
if (event.timeout != null) {
|
|
669
|
+
displayMessage.timeout = event.timeout
|
|
670
|
+
}
|
|
658
671
|
EventStats.recordPublish(context.eventStats, context.privateChannel, 'PRIVATE_DISPLAY', displayMessage)
|
|
659
672
|
await RealtimeMessaging.publish(context.privateChannel, 'PRIVATE_DISPLAY', displayMessage)
|
|
660
673
|
return { timeout: event.timeout }
|
|
@@ -169,37 +169,46 @@ const AppMachineTransition = (anearEvent) => {
|
|
|
169
169
|
|
|
170
170
|
if (meta.allParticipants) {
|
|
171
171
|
viewer = 'allParticipants'
|
|
172
|
-
const
|
|
173
|
-
|
|
172
|
+
const extracted = _extractViewAndProps(meta.allParticipants)
|
|
173
|
+
const config = meta.allParticipants
|
|
174
|
+
timeoutFn = RenderContextBuilder.buildTimeoutFn(viewer, typeof config === 'object' && !Array.isArray(config) ? config.timeout : undefined)
|
|
174
175
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
cancelActionType = meta.allParticipants.cancel
|
|
176
|
+
if (typeof config === 'object' && !Array.isArray(config) && config.cancel && !cancelActionType) {
|
|
177
|
+
cancelActionType = config.cancel
|
|
178
178
|
}
|
|
179
179
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
180
|
+
if (extracted.targets && extracted.targets.length > 0) {
|
|
181
|
+
displayEvent = RenderContextBuilder.buildDisplayEventWithTargets(
|
|
182
|
+
extracted.targets,
|
|
183
|
+
RenderContextBuilder.buildAppRenderContext(appContext, appStateName, event.type, viewer, timeoutFn),
|
|
184
|
+
viewer,
|
|
185
|
+
null,
|
|
186
|
+
null
|
|
187
|
+
)
|
|
188
|
+
} else {
|
|
189
|
+
displayEvent = RenderContextBuilder.buildDisplayEvent(
|
|
190
|
+
extracted.viewPath,
|
|
191
|
+
RenderContextBuilder.buildAppRenderContext(appContext, appStateName, event.type, viewer, timeoutFn),
|
|
192
|
+
viewer,
|
|
193
|
+
null,
|
|
194
|
+
null,
|
|
195
|
+
extracted.props,
|
|
196
|
+
extracted.anchor,
|
|
197
|
+
extracted.type,
|
|
198
|
+
extracted.content,
|
|
199
|
+
extracted.contentType
|
|
200
|
+
)
|
|
201
|
+
}
|
|
188
202
|
displayEvents.push(displayEvent)
|
|
189
203
|
}
|
|
190
204
|
|
|
191
205
|
if (meta.eachParticipant) {
|
|
192
|
-
// Check if participant is a function (new selective rendering format)
|
|
193
206
|
viewer = 'eachParticipant'
|
|
194
207
|
if (typeof meta.eachParticipant === 'function') {
|
|
195
|
-
// New selective participant rendering - cancel property is not supported in function format
|
|
196
|
-
// (would need to be in the object format, see legacy handling below)
|
|
197
208
|
const participantRenderFunc = meta.eachParticipant
|
|
198
209
|
const participantDisplays = participantRenderFunc(appContext, event)
|
|
199
210
|
|
|
200
211
|
if (Array.isArray(participantDisplays)) {
|
|
201
|
-
// Build the base render context once and reuse it for all participant displays
|
|
202
|
-
// This avoids redundant context building when multiple participants receive different views
|
|
203
212
|
const baseAppRenderContext = RenderContextBuilder.buildAppRenderContext(
|
|
204
213
|
appContext,
|
|
205
214
|
appStateName,
|
|
@@ -208,27 +217,79 @@ const AppMachineTransition = (anearEvent) => {
|
|
|
208
217
|
null
|
|
209
218
|
)
|
|
210
219
|
|
|
220
|
+
const byParticipant = new Map()
|
|
211
221
|
participantDisplays.forEach(participantDisplay => {
|
|
212
|
-
if (participantDisplay
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
222
|
+
if (!participantDisplay || !participantDisplay.participantId) return
|
|
223
|
+
const participantViewPath = participantDisplay.view || participantDisplay.viewPath || null
|
|
224
|
+
const hasDirectContent = participantDisplay.content !== undefined && participantDisplay.content !== null
|
|
225
|
+
if (!participantViewPath && !hasDirectContent) return
|
|
226
|
+
const pid = participantDisplay.participantId
|
|
227
|
+
const targetSpec = {
|
|
228
|
+
viewPath: participantViewPath,
|
|
229
|
+
view: participantViewPath,
|
|
230
|
+
anchor: participantDisplay.anchor != null ? participantDisplay.anchor : null,
|
|
231
|
+
type: (participantDisplay.type === 'replace' || participantDisplay.type === 'append') ? participantDisplay.type : null,
|
|
232
|
+
props: participantDisplay.props || {},
|
|
233
|
+
content: hasDirectContent ? participantDisplay.content : null,
|
|
234
|
+
contentType: (participantDisplay.contentType === 'html' || participantDisplay.contentType === 'text') ? participantDisplay.contentType : null
|
|
235
|
+
}
|
|
236
|
+
const existing = byParticipant.get(pid)
|
|
237
|
+
if (existing) {
|
|
238
|
+
existing.targets.push(targetSpec)
|
|
239
|
+
if (participantDisplay.timeout != null && existing.timeout == null) {
|
|
240
|
+
existing.timeout = participantDisplay.timeout
|
|
241
|
+
}
|
|
242
|
+
} else {
|
|
243
|
+
byParticipant.set(pid, {
|
|
244
|
+
participantId: pid,
|
|
245
|
+
targets: [targetSpec],
|
|
246
|
+
timeout: participantDisplay.timeout ?? null
|
|
247
|
+
})
|
|
248
|
+
}
|
|
249
|
+
})
|
|
216
250
|
|
|
251
|
+
byParticipant.forEach(({ participantId: pid, targets: targetSpecs, timeout: to }) => {
|
|
252
|
+
if (targetSpecs.length === 1 && !targetSpecs[0].anchor) {
|
|
253
|
+
displayEvent = RenderContextBuilder.buildDisplayEvent(
|
|
254
|
+
targetSpecs[0].viewPath,
|
|
255
|
+
baseAppRenderContext,
|
|
256
|
+
viewer,
|
|
257
|
+
pid,
|
|
258
|
+
to,
|
|
259
|
+
targetSpecs[0].props,
|
|
260
|
+
null,
|
|
261
|
+
null,
|
|
262
|
+
targetSpecs[0].content,
|
|
263
|
+
targetSpecs[0].contentType
|
|
264
|
+
)
|
|
265
|
+
} else if (targetSpecs.length === 1) {
|
|
217
266
|
displayEvent = RenderContextBuilder.buildDisplayEvent(
|
|
218
|
-
|
|
219
|
-
baseAppRenderContext,
|
|
267
|
+
targetSpecs[0].viewPath,
|
|
268
|
+
baseAppRenderContext,
|
|
269
|
+
viewer,
|
|
270
|
+
pid,
|
|
271
|
+
to,
|
|
272
|
+
targetSpecs[0].props,
|
|
273
|
+
targetSpecs[0].anchor,
|
|
274
|
+
targetSpecs[0].type,
|
|
275
|
+
targetSpecs[0].content,
|
|
276
|
+
targetSpecs[0].contentType
|
|
277
|
+
)
|
|
278
|
+
} else {
|
|
279
|
+
displayEvent = RenderContextBuilder.buildDisplayEventWithTargets(
|
|
280
|
+
targetSpecs,
|
|
281
|
+
baseAppRenderContext,
|
|
220
282
|
viewer,
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
props
|
|
283
|
+
pid,
|
|
284
|
+
to
|
|
224
285
|
)
|
|
225
|
-
displayEvents.push(displayEvent)
|
|
226
286
|
}
|
|
287
|
+
displayEvents.push(displayEvent)
|
|
227
288
|
})
|
|
228
289
|
}
|
|
229
290
|
} else {
|
|
230
291
|
// Legacy participant rendering - normalize to selective format
|
|
231
|
-
const { viewPath, props } = _extractViewAndProps(meta.eachParticipant)
|
|
292
|
+
const { viewPath, props, anchor, type, content, contentType } = _extractViewAndProps(meta.eachParticipant)
|
|
232
293
|
const timeoutFn = RenderContextBuilder.buildTimeoutFn('participant', meta.eachParticipant.timeout)
|
|
233
294
|
|
|
234
295
|
// Extract cancel property if present (for legacy eachParticipant format)
|
|
@@ -243,7 +304,11 @@ const AppMachineTransition = (anearEvent) => {
|
|
|
243
304
|
viewer,
|
|
244
305
|
'ALL_PARTICIPANTS', // Special marker for "all participants"
|
|
245
306
|
null,
|
|
246
|
-
props
|
|
307
|
+
props,
|
|
308
|
+
anchor,
|
|
309
|
+
type,
|
|
310
|
+
content,
|
|
311
|
+
contentType
|
|
247
312
|
)
|
|
248
313
|
displayEvents.push(displayEvent)
|
|
249
314
|
}
|
|
@@ -251,33 +316,63 @@ const AppMachineTransition = (anearEvent) => {
|
|
|
251
316
|
|
|
252
317
|
if (meta.host) {
|
|
253
318
|
viewer = 'host'
|
|
254
|
-
const
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
RenderContextBuilder.
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
319
|
+
const extracted = _extractViewAndProps(meta.host)
|
|
320
|
+
const config = meta.host
|
|
321
|
+
timeoutFn = RenderContextBuilder.buildTimeoutFn(viewer, typeof config === 'object' && !Array.isArray(config) ? config.timeout : undefined)
|
|
322
|
+
|
|
323
|
+
if (extracted.targets && extracted.targets.length > 0) {
|
|
324
|
+
displayEvent = RenderContextBuilder.buildDisplayEventWithTargets(
|
|
325
|
+
extracted.targets,
|
|
326
|
+
RenderContextBuilder.buildAppRenderContext(appContext, appStateName, event.type, viewer, timeoutFn),
|
|
327
|
+
viewer,
|
|
328
|
+
null,
|
|
329
|
+
null
|
|
330
|
+
)
|
|
331
|
+
} else {
|
|
332
|
+
displayEvent = RenderContextBuilder.buildDisplayEvent(
|
|
333
|
+
extracted.viewPath,
|
|
334
|
+
RenderContextBuilder.buildAppRenderContext(appContext, appStateName, event.type, viewer, timeoutFn),
|
|
335
|
+
viewer,
|
|
336
|
+
null,
|
|
337
|
+
null,
|
|
338
|
+
extracted.props,
|
|
339
|
+
extracted.anchor,
|
|
340
|
+
extracted.type,
|
|
341
|
+
extracted.content,
|
|
342
|
+
extracted.contentType
|
|
343
|
+
)
|
|
344
|
+
}
|
|
265
345
|
displayEvents.push(displayEvent)
|
|
266
346
|
}
|
|
267
347
|
|
|
268
348
|
if (meta.spectators) {
|
|
269
349
|
viewer = 'spectators'
|
|
270
|
-
const
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
RenderContextBuilder.
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
350
|
+
const extracted = _extractViewAndProps(meta.spectators)
|
|
351
|
+
const config = meta.spectators
|
|
352
|
+
timeoutFn = RenderContextBuilder.buildTimeoutFn(viewer, typeof config === 'object' && !Array.isArray(config) ? config.timeout : undefined)
|
|
353
|
+
|
|
354
|
+
if (extracted.targets && extracted.targets.length > 0) {
|
|
355
|
+
displayEvent = RenderContextBuilder.buildDisplayEventWithTargets(
|
|
356
|
+
extracted.targets,
|
|
357
|
+
RenderContextBuilder.buildAppRenderContext(appContext, appStateName, event.type, viewer, timeoutFn),
|
|
358
|
+
viewer,
|
|
359
|
+
null,
|
|
360
|
+
null
|
|
361
|
+
)
|
|
362
|
+
} else {
|
|
363
|
+
displayEvent = RenderContextBuilder.buildDisplayEvent(
|
|
364
|
+
extracted.viewPath,
|
|
365
|
+
RenderContextBuilder.buildAppRenderContext(appContext, appStateName, event.type, viewer, timeoutFn),
|
|
366
|
+
viewer,
|
|
367
|
+
null,
|
|
368
|
+
null,
|
|
369
|
+
extracted.props,
|
|
370
|
+
extracted.anchor,
|
|
371
|
+
extracted.type,
|
|
372
|
+
extracted.content,
|
|
373
|
+
extracted.contentType
|
|
374
|
+
)
|
|
375
|
+
}
|
|
281
376
|
displayEvents.push(displayEvent)
|
|
282
377
|
}
|
|
283
378
|
})
|
|
@@ -303,20 +398,39 @@ const AppMachineTransition = (anearEvent) => {
|
|
|
303
398
|
}
|
|
304
399
|
|
|
305
400
|
const _extractViewAndProps = (config) => {
|
|
306
|
-
if (!config) return { viewPath: null, props: {} }
|
|
401
|
+
if (!config) return { viewPath: null, props: {}, anchor: null, type: null, content: null, contentType: null, targets: null }
|
|
307
402
|
|
|
308
403
|
if (typeof config === 'string') {
|
|
309
|
-
return { viewPath: config, props: {} }
|
|
404
|
+
return { viewPath: config, props: {}, anchor: null, type: null, content: null, contentType: null, targets: null }
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
if (Array.isArray(config)) {
|
|
408
|
+
const targets = config
|
|
409
|
+
.filter(t => t && ((t.view || t.viewPath) || (t.content !== undefined && t.content !== null)))
|
|
410
|
+
.map(t => ({
|
|
411
|
+
viewPath: t.view || t.viewPath,
|
|
412
|
+
view: t.view || t.viewPath,
|
|
413
|
+
anchor: t.anchor != null ? t.anchor : null,
|
|
414
|
+
type: (t.type === 'replace' || t.type === 'append') ? t.type : null,
|
|
415
|
+
props: t.props || {},
|
|
416
|
+
content: (t.content !== undefined && t.content !== null) ? t.content : null,
|
|
417
|
+
contentType: (t.contentType === 'html' || t.contentType === 'text') ? t.contentType : null
|
|
418
|
+
}))
|
|
419
|
+
return { viewPath: null, props: {}, anchor: null, type: null, content: null, contentType: null, targets: targets.length > 0 ? targets : null }
|
|
310
420
|
}
|
|
311
421
|
|
|
312
422
|
if (typeof config === 'object') {
|
|
313
|
-
const viewPath = config.view
|
|
423
|
+
const viewPath = config.view || config.viewPath || null
|
|
314
424
|
const props = config.props || {}
|
|
315
|
-
|
|
425
|
+
const anchor = config.anchor != null ? config.anchor : null
|
|
426
|
+
const type = (config.type === 'replace' || config.type === 'append') ? config.type : null
|
|
427
|
+
const content = (config.content !== undefined && config.content !== null) ? config.content : null
|
|
428
|
+
const contentType = (config.contentType === 'html' || config.contentType === 'text') ? config.contentType : null
|
|
429
|
+
return { viewPath, props, anchor, type, content, contentType, targets: null }
|
|
316
430
|
}
|
|
317
431
|
|
|
318
432
|
logger.warn(`[AppMachineTransition] Unknown meta format: ${JSON.stringify(config)}`)
|
|
319
|
-
return { viewPath: null, props: {} }
|
|
433
|
+
return { viewPath: null, props: {}, anchor: null, type: null, content: null, contentType: null, targets: null }
|
|
320
434
|
}
|
|
321
435
|
|
|
322
436
|
const _stringifiedState = (stateValue) => {
|
|
@@ -111,32 +111,53 @@ class DisplayEventProcessor {
|
|
|
111
111
|
}
|
|
112
112
|
|
|
113
113
|
_processSingle(displayEvent) {
|
|
114
|
-
const { viewPath, appRenderContext, viewer, participantId, timeout: displayTimeout, props } = displayEvent
|
|
114
|
+
const { viewPath, targets: targetsArray, appRenderContext, viewer, participantId, timeout: displayTimeout, props, anchor, type, content, contentType } = displayEvent
|
|
115
115
|
const timeoutFn = appRenderContext.meta.timeoutFn
|
|
116
116
|
|
|
117
|
-
const
|
|
118
|
-
const template = this.pugTemplates[normalizedPath]
|
|
119
|
-
let publishPromise
|
|
120
|
-
let timeout = null
|
|
121
|
-
|
|
122
|
-
if (!template) {
|
|
123
|
-
throw new Error(`Template not found: ${normalizedPath}`)
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
const templateRenderContext = {
|
|
117
|
+
const baseContext = {
|
|
127
118
|
...appRenderContext,
|
|
128
119
|
anearEvent: this.anearEvent,
|
|
129
120
|
participants: this.participantsIndex,
|
|
130
|
-
...this.pugHelpers
|
|
131
|
-
props
|
|
121
|
+
...this.pugHelpers
|
|
132
122
|
}
|
|
133
123
|
|
|
134
|
-
const
|
|
135
|
-
|
|
136
|
-
|
|
124
|
+
const templateRenderContext = { ...baseContext, props: props || {} }
|
|
125
|
+
const normalizedPath = viewPath ? ((viewPath || '').endsWith(C.PugSuffix) ? viewPath : `${viewPath}${C.PugSuffix}`) : null
|
|
126
|
+
const template = normalizedPath ? this.pugTemplates[normalizedPath] : null
|
|
127
|
+
let publishPromise
|
|
128
|
+
let timeout = null
|
|
129
|
+
|
|
130
|
+
const formattedDisplayMessage = (contextOverrides = {}) => {
|
|
131
|
+
const ctx = { ...templateRenderContext, ...contextOverrides }
|
|
132
|
+
if (Array.isArray(targetsArray) && targetsArray.length > 0) {
|
|
133
|
+
const targetsObj = {}
|
|
134
|
+
for (const t of targetsArray) {
|
|
135
|
+
const a = t.anchor
|
|
136
|
+
if (a == null) continue
|
|
137
|
+
targetsObj[a] = this._buildTargetValue(t, ctx)
|
|
138
|
+
}
|
|
139
|
+
return { targets: targetsObj }
|
|
137
140
|
}
|
|
138
141
|
|
|
139
|
-
|
|
142
|
+
if (content !== null && content !== undefined) {
|
|
143
|
+
const directContent = this._stringifyContent(content)
|
|
144
|
+
if (anchor != null) {
|
|
145
|
+
return { targets: { [anchor]: this._buildTargetValue({ content: directContent, contentType, type }, ctx) } }
|
|
146
|
+
}
|
|
147
|
+
return { content: directContent }
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (!template) throw new Error(`Template not found: ${normalizedPath || viewPath || '[unknown]'}`)
|
|
151
|
+
const html = template(ctx)
|
|
152
|
+
if (anchor != null) {
|
|
153
|
+
const val = this._buildTargetValue({ content: html, contentType: contentType || 'html', type }, ctx)
|
|
154
|
+
return { targets: { [anchor]: val } }
|
|
155
|
+
}
|
|
156
|
+
return { content: html }
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (!Array.isArray(targetsArray) && content == null && !template) {
|
|
160
|
+
throw new Error(`Template not found: ${normalizedPath || viewPath || '[unknown]'}`)
|
|
140
161
|
}
|
|
141
162
|
|
|
142
163
|
// The viewer determines who sees the view. It can be set directly on the
|
|
@@ -146,7 +167,7 @@ class DisplayEventProcessor {
|
|
|
146
167
|
let displaySent = false
|
|
147
168
|
switch (displayViewer) {
|
|
148
169
|
case 'allParticipants':
|
|
149
|
-
const templateName = normalizedPath.replace(C.PugSuffix, '')
|
|
170
|
+
const templateName = normalizedPath ? normalizedPath.replace(C.PugSuffix, '') : '[inline-content]'
|
|
150
171
|
logger.info(`[DisplayEventProcessor] RENDER_DISPLAY template=${templateName} viewer=allParticipants`)
|
|
151
172
|
|
|
152
173
|
// Use display timeout if available, otherwise fall back to timeoutFn
|
|
@@ -183,7 +204,7 @@ class DisplayEventProcessor {
|
|
|
183
204
|
break
|
|
184
205
|
|
|
185
206
|
case 'spectators':
|
|
186
|
-
const templateNameSpectators = normalizedPath.replace(C.PugSuffix, '')
|
|
207
|
+
const templateNameSpectators = normalizedPath ? normalizedPath.replace(C.PugSuffix, '') : '[inline-content]'
|
|
187
208
|
logger.info(`[DisplayEventProcessor] RENDER_DISPLAY template=${templateNameSpectators} viewer=spectators`)
|
|
188
209
|
|
|
189
210
|
// If this event/app does not support spectators, the spectatorsDisplayChannel
|
|
@@ -229,20 +250,17 @@ class DisplayEventProcessor {
|
|
|
229
250
|
break
|
|
230
251
|
|
|
231
252
|
case 'eachParticipant': {
|
|
232
|
-
const templateNameEach = normalizedPath.replace(C.PugSuffix, '')
|
|
253
|
+
const templateNameEach = (targetsArray?.[0]?.viewPath || targetsArray?.[0]?.view || normalizedPath || '[inline-content]').replace(C.PugSuffix, '')
|
|
233
254
|
if (participantId === 'ALL_PARTICIPANTS') {
|
|
234
|
-
// Legacy behavior - send to all participants (uses timeoutFn)
|
|
235
255
|
logger.info(`[DisplayEventProcessor] RENDER_DISPLAY template=${templateNameEach} viewer=eachParticipant participantId=ALL_PARTICIPANTS`)
|
|
236
|
-
publishPromise = this._processPrivateParticipantDisplays(template, templateRenderContext, timeoutFn)
|
|
237
|
-
displaySent = true
|
|
256
|
+
publishPromise = this._processPrivateParticipantDisplays(template, templateRenderContext, timeoutFn, anchor, type, targetsArray, content, contentType)
|
|
257
|
+
displaySent = true
|
|
238
258
|
} else if (participantId) {
|
|
239
|
-
// Selective participant rendering - send to specific participant only (uses displayTimeout)
|
|
240
259
|
logger.info(`[DisplayEventProcessor] RENDER_DISPLAY template=${templateNameEach} viewer=eachParticipant participantId=${participantId}`)
|
|
241
|
-
const result = this._processSelectiveParticipantDisplayWithSent(template, templateRenderContext, null, participantId, displayTimeout)
|
|
260
|
+
const result = this._processSelectiveParticipantDisplayWithSent(template, templateRenderContext, null, participantId, displayTimeout, anchor, type, targetsArray, content, contentType)
|
|
242
261
|
publishPromise = result.promise
|
|
243
262
|
displaySent = result.displaySent
|
|
244
263
|
} else {
|
|
245
|
-
// Fallback - should not happen with unified approach
|
|
246
264
|
logger.warn(`[DisplayEventProcessor] Unexpected participant display event without participantId`)
|
|
247
265
|
publishPromise = Promise.resolve()
|
|
248
266
|
}
|
|
@@ -250,7 +268,7 @@ class DisplayEventProcessor {
|
|
|
250
268
|
}
|
|
251
269
|
|
|
252
270
|
case 'host':
|
|
253
|
-
const templateNameHost = normalizedPath.replace(C.PugSuffix, '')
|
|
271
|
+
const templateNameHost = normalizedPath ? normalizedPath.replace(C.PugSuffix, '') : '[inline-content]'
|
|
254
272
|
logger.info(`[DisplayEventProcessor] RENDER_DISPLAY template=${templateNameHost} viewer=host`)
|
|
255
273
|
// Host may optionally have a real timeout (if configured). Otherwise, host
|
|
256
274
|
// mirrors the allParticipants visual timer without starting a host timeout.
|
|
@@ -285,7 +303,12 @@ class DisplayEventProcessor {
|
|
|
285
303
|
publishPromise = this._processHostDisplay(
|
|
286
304
|
template,
|
|
287
305
|
templateRenderContext,
|
|
288
|
-
hostOwnMsecs !== null ? (() => hostOwnMsecs) : null
|
|
306
|
+
hostOwnMsecs !== null ? (() => hostOwnMsecs) : null,
|
|
307
|
+
anchor,
|
|
308
|
+
type,
|
|
309
|
+
targetsArray,
|
|
310
|
+
content,
|
|
311
|
+
contentType
|
|
289
312
|
)
|
|
290
313
|
displaySent = true
|
|
291
314
|
break
|
|
@@ -296,7 +319,7 @@ class DisplayEventProcessor {
|
|
|
296
319
|
return { publishPromise, timeout, displaySent }
|
|
297
320
|
}
|
|
298
321
|
|
|
299
|
-
_processHostDisplay(template, templateRenderContext, timeoutFn) {
|
|
322
|
+
_processHostDisplay(template, templateRenderContext, timeoutFn, anchor = null, type = null, targetsArray = null, content = null, contentType = null) {
|
|
300
323
|
const hostEntry = Object.entries(this.participants).find(
|
|
301
324
|
([id, participantInfo]) => participantInfo.isHost
|
|
302
325
|
);
|
|
@@ -311,29 +334,23 @@ class DisplayEventProcessor {
|
|
|
311
334
|
if (!hostMachine) {
|
|
312
335
|
throw new Error(`[DisplayEventProcessor] Host participant machine not found for hostId: ${hostId}`)
|
|
313
336
|
}
|
|
314
|
-
this._sendPrivateDisplay(hostMachine, hostId, template, templateRenderContext, timeoutFn)
|
|
337
|
+
this._sendPrivateDisplay(hostMachine, hostId, template, templateRenderContext, timeoutFn, anchor, type, targetsArray, content, contentType)
|
|
315
338
|
return Promise.resolve()
|
|
316
339
|
}
|
|
317
340
|
|
|
318
|
-
_processPrivateParticipantDisplays(template, templateRenderContext, timeoutFn) {
|
|
341
|
+
_processPrivateParticipantDisplays(template, templateRenderContext, timeoutFn, anchor = null, type = null, targetsArray = null, content = null, contentType = null) {
|
|
319
342
|
Object.values(this.participantsIndex.all).forEach(
|
|
320
343
|
participantStruct => {
|
|
321
|
-
// Exclude the host from receiving displays targeted at all participants.
|
|
322
|
-
// Host-specific displays should use the `meta: { host: '...' }` syntax.
|
|
323
344
|
if (participantStruct.info.isHost) return;
|
|
324
|
-
|
|
325
|
-
// Skip inactive participants (idle in open house events)
|
|
326
|
-
// Note: _buildParticipantsIndex already filters these out, but adding check for safety
|
|
327
345
|
if (participantStruct.info.active === false) return;
|
|
328
346
|
|
|
329
347
|
const participantId = participantStruct.info.id
|
|
330
348
|
const participantMachine = this.participantMachines[participantId]
|
|
331
|
-
// Skip if no machine (shouldn't happen since we filter active participants, but safety check)
|
|
332
349
|
if (!participantMachine) {
|
|
333
350
|
logger.warn(`[DisplayEventProcessor] Participant machine not found for ${participantId}, skipping display`)
|
|
334
351
|
return
|
|
335
352
|
}
|
|
336
|
-
this._sendPrivateDisplay(participantMachine, participantId, template, templateRenderContext, timeoutFn)
|
|
353
|
+
this._sendPrivateDisplay(participantMachine, participantId, template, templateRenderContext, timeoutFn, anchor, type, targetsArray, content, contentType)
|
|
337
354
|
}
|
|
338
355
|
)
|
|
339
356
|
|
|
@@ -345,7 +362,7 @@ class DisplayEventProcessor {
|
|
|
345
362
|
* When participant is not found (exited), we treat as delivered (displaySent: false) so the
|
|
346
363
|
* render cycle can complete and we avoid infinite RENDER_DISPLAY/RENDERED loops.
|
|
347
364
|
*/
|
|
348
|
-
_processSelectiveParticipantDisplayWithSent(template, templateRenderContext, timeoutFn, participantId, displayTimeout = null) {
|
|
365
|
+
_processSelectiveParticipantDisplayWithSent(template, templateRenderContext, timeoutFn, participantId, displayTimeout = null, anchor = null, type = null, targetsArray = null, content = null, contentType = null) {
|
|
349
366
|
const participantStruct = this.participantsIndex.get(participantId)
|
|
350
367
|
|
|
351
368
|
if (!participantStruct) {
|
|
@@ -353,13 +370,11 @@ class DisplayEventProcessor {
|
|
|
353
370
|
return { promise: Promise.resolve(), displaySent: false }
|
|
354
371
|
}
|
|
355
372
|
|
|
356
|
-
// Exclude the host from receiving displays targeted at participants
|
|
357
373
|
if (participantStruct.info.isHost) {
|
|
358
374
|
logger.warn(`[DisplayEventProcessor] Cannot send participant display to host ${participantId}`)
|
|
359
375
|
return { promise: Promise.resolve(), displaySent: false }
|
|
360
376
|
}
|
|
361
377
|
|
|
362
|
-
// Skip inactive participants (idle in open house events)
|
|
363
378
|
if (participantStruct.info.active === false) {
|
|
364
379
|
logger.debug(`[DisplayEventProcessor] Skipping display for inactive participant ${participantId}`)
|
|
365
380
|
return { promise: Promise.resolve(), displaySent: false }
|
|
@@ -371,29 +386,26 @@ class DisplayEventProcessor {
|
|
|
371
386
|
return { promise: Promise.resolve(), displaySent: false }
|
|
372
387
|
}
|
|
373
388
|
|
|
374
|
-
// Use displayTimeout if available, otherwise fall back to timeoutFn
|
|
375
389
|
const effectiveTimeoutFn = displayTimeout !== null ?
|
|
376
390
|
() => displayTimeout :
|
|
377
391
|
timeoutFn
|
|
378
392
|
|
|
379
|
-
this._sendPrivateDisplay(participantMachine, participantId, template, templateRenderContext, effectiveTimeoutFn)
|
|
393
|
+
this._sendPrivateDisplay(participantMachine, participantId, template, templateRenderContext, effectiveTimeoutFn, anchor, type, targetsArray, content, contentType)
|
|
380
394
|
return { promise: Promise.resolve(), displaySent: true }
|
|
381
395
|
}
|
|
382
396
|
|
|
383
|
-
_processSelectiveParticipantDisplay(template, templateRenderContext, timeoutFn, participantId, displayTimeout = null) {
|
|
384
|
-
const { promise } = this._processSelectiveParticipantDisplayWithSent(template, templateRenderContext, timeoutFn, participantId, displayTimeout)
|
|
397
|
+
_processSelectiveParticipantDisplay(template, templateRenderContext, timeoutFn, participantId, displayTimeout = null, anchor = null, type = null, targetsArray = null, content = null, contentType = null) {
|
|
398
|
+
const { promise } = this._processSelectiveParticipantDisplayWithSent(template, templateRenderContext, timeoutFn, participantId, displayTimeout, anchor, type, targetsArray, content, contentType)
|
|
385
399
|
return promise
|
|
386
400
|
}
|
|
387
401
|
|
|
388
|
-
_sendPrivateDisplay(participantMachine, participantId, template, templateRenderContext, timeoutFn) {
|
|
402
|
+
_sendPrivateDisplay(participantMachine, participantId, template, templateRenderContext, timeoutFn, anchor = null, type = null, targetsArray = null, content = null, contentType = null) {
|
|
389
403
|
let timeout = null
|
|
390
404
|
|
|
391
405
|
const participantStruct = this.participantsIndex.get(participantId)
|
|
392
406
|
const appCtx = templateRenderContext.app || {}
|
|
393
407
|
|
|
394
408
|
if (timeoutFn) {
|
|
395
|
-
// For participant displays, any non-null / positive timeout value should
|
|
396
|
-
// start a real per-participant timer on that participant's APM.
|
|
397
409
|
if (!participantStruct) {
|
|
398
410
|
logger.warn(`[DisplayEventProcessor] Participant struct not found for ${participantId}, skipping timeout calculation`)
|
|
399
411
|
} else {
|
|
@@ -402,7 +414,6 @@ class DisplayEventProcessor {
|
|
|
402
414
|
if (typeof msecs === 'number' && msecs > 0) {
|
|
403
415
|
timeout = msecs
|
|
404
416
|
} else {
|
|
405
|
-
// Log when timeout function returns null/undefined to help diagnose issues
|
|
406
417
|
logger.debug(`[DisplayEventProcessor] Timeout function returned ${msecs === null ? 'null' : msecs === undefined ? 'undefined' : `non-numeric value: ${msecs}`} for ${participantId}`)
|
|
407
418
|
}
|
|
408
419
|
} catch (error) {
|
|
@@ -415,16 +426,8 @@ class DisplayEventProcessor {
|
|
|
415
426
|
participant: participantStruct
|
|
416
427
|
}
|
|
417
428
|
|
|
418
|
-
// Build visual timeout (meta.timeout) used by the countdown bar.
|
|
419
|
-
// For participant displays we support two patterns:
|
|
420
|
-
// 1) Global participants timeout (allParticipants flow) – mirror the
|
|
421
|
-
// shared participantsActionTimeout so EVERYONE sees the same bar,
|
|
422
|
-
// regardless of whether their own APM is timed.
|
|
423
|
-
// 2) Per-participant timeout (eachParticipant flow) – no global timeout;
|
|
424
|
-
// only the timed participant gets a bar, derived from their own APM.
|
|
425
429
|
let visualTimeout = null
|
|
426
430
|
try {
|
|
427
|
-
// First, prefer a shared participants timeout if one exists (e.g., move timer).
|
|
428
431
|
const pat = this.precomputedParticipantsTimeout || this.participantsActionTimeout
|
|
429
432
|
if (pat && typeof pat.msecs === 'number') {
|
|
430
433
|
const now = Date.now()
|
|
@@ -432,10 +435,6 @@ class DisplayEventProcessor {
|
|
|
432
435
|
const remainingMsecs = Math.max(0, pat.msecs - (now - start))
|
|
433
436
|
visualTimeout = { msecs: pat.msecs, remainingMsecs }
|
|
434
437
|
} else {
|
|
435
|
-
// Fallback: no global timeout.
|
|
436
|
-
// If this participant currently has an active APM timer, show a bar based on that.
|
|
437
|
-
// This enables "visual-only" re-renders (timeout=null) during an active wait without
|
|
438
|
-
// attempting to restart/reset the timer.
|
|
439
438
|
const state = participantMachine?.state
|
|
440
439
|
const ctx = state?.context
|
|
441
440
|
if (ctx && ctx.actionTimeoutStart && ctx.actionTimeoutMsecs != null && ctx.actionTimeoutMsecs > 0) {
|
|
@@ -448,13 +447,11 @@ class DisplayEventProcessor {
|
|
|
448
447
|
remainingMsecs
|
|
449
448
|
}
|
|
450
449
|
} else if (timeout !== null) {
|
|
451
|
-
// Backward compatibility: if we have an explicit timeout, mirror it.
|
|
452
450
|
visualTimeout = { msecs: timeout, remainingMsecs: timeout }
|
|
453
451
|
}
|
|
454
452
|
}
|
|
455
453
|
} catch (_e) {
|
|
456
|
-
//
|
|
457
|
-
// let the templates fall back to their default behavior.
|
|
454
|
+
// ignore
|
|
458
455
|
}
|
|
459
456
|
|
|
460
457
|
if (visualTimeout) {
|
|
@@ -464,16 +461,86 @@ class DisplayEventProcessor {
|
|
|
464
461
|
}
|
|
465
462
|
}
|
|
466
463
|
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
464
|
+
let renderMessage
|
|
465
|
+
if (Array.isArray(targetsArray) && targetsArray.length > 0) {
|
|
466
|
+
const targetsObj = {}
|
|
467
|
+
for (const t of targetsArray) {
|
|
468
|
+
const a = t.anchor
|
|
469
|
+
if (a == null) continue
|
|
470
|
+
targetsObj[a] = this._buildTargetValue(t, privateRenderContext)
|
|
471
|
+
}
|
|
472
|
+
renderMessage = { targets: targetsObj }
|
|
473
|
+
} else if (content !== null && content !== undefined) {
|
|
474
|
+
const directContent = this._stringifyContent(content)
|
|
475
|
+
if (anchor != null) {
|
|
476
|
+
renderMessage = { targets: { [anchor]: this._buildTargetValue({ content: directContent, contentType, type }, privateRenderContext) } }
|
|
477
|
+
} else {
|
|
478
|
+
renderMessage = { content: directContent }
|
|
479
|
+
}
|
|
480
|
+
} else {
|
|
481
|
+
if (!template) {
|
|
482
|
+
throw new Error(`[DisplayEventProcessor] Template not found for participant ${participantId}`)
|
|
483
|
+
}
|
|
484
|
+
const privateHtml = template(privateRenderContext)
|
|
485
|
+
if (anchor != null) {
|
|
486
|
+
const val = this._buildTargetValue({ content: privateHtml, contentType: contentType || 'html', type }, privateRenderContext)
|
|
487
|
+
renderMessage = { targets: { [anchor]: val } }
|
|
488
|
+
} else {
|
|
489
|
+
renderMessage = { content: privateHtml }
|
|
490
|
+
}
|
|
491
|
+
}
|
|
470
492
|
if (timeout !== null) {
|
|
471
493
|
renderMessage.timeout = timeout
|
|
472
494
|
}
|
|
473
495
|
|
|
474
|
-
// v5: send events as objects
|
|
475
496
|
participantMachine.send({ type: 'RENDER_DISPLAY', ...renderMessage })
|
|
476
497
|
}
|
|
498
|
+
|
|
499
|
+
_normalizeAnchorUpdateType(type) {
|
|
500
|
+
return (type === 'replace' || type === 'append') ? type : null
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
_normalizeContentType(contentType) {
|
|
504
|
+
return (contentType === 'html' || contentType === 'text') ? contentType : null
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
_stringifyContent(content) {
|
|
508
|
+
if (typeof content === 'string') return content
|
|
509
|
+
return String(content)
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
_buildTargetValue(targetSpec, baseContext) {
|
|
513
|
+
const ty = this._normalizeAnchorUpdateType(targetSpec?.type)
|
|
514
|
+
const ct = this._normalizeContentType(targetSpec?.contentType)
|
|
515
|
+
|
|
516
|
+
if (targetSpec && targetSpec.content !== null && targetSpec.content !== undefined) {
|
|
517
|
+
const directContent = this._stringifyContent(targetSpec.content)
|
|
518
|
+
if (ty || ct) {
|
|
519
|
+
const payload = { content: directContent }
|
|
520
|
+
if (ty) payload.type = ty
|
|
521
|
+
if (ct) payload.contentType = ct
|
|
522
|
+
return payload
|
|
523
|
+
}
|
|
524
|
+
return directContent
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
const pathSource = targetSpec?.viewPath || targetSpec?.view
|
|
528
|
+
if (!pathSource) {
|
|
529
|
+
throw new Error('[DisplayEventProcessor] Target spec requires either content or view/viewPath')
|
|
530
|
+
}
|
|
531
|
+
const path = pathSource.endsWith(C.PugSuffix) ? pathSource : `${pathSource}${C.PugSuffix}`
|
|
532
|
+
const tmpl = this.pugTemplates[path]
|
|
533
|
+
if (!tmpl) throw new Error(`Template not found: ${path}`)
|
|
534
|
+
const tCtx = { ...baseContext, props: targetSpec?.props || {} }
|
|
535
|
+
const html = tmpl(tCtx)
|
|
536
|
+
if (ty || ct) {
|
|
537
|
+
const payload = { content: html }
|
|
538
|
+
if (ty) payload.type = ty
|
|
539
|
+
if (ct) payload.contentType = ct
|
|
540
|
+
return payload
|
|
541
|
+
}
|
|
542
|
+
return html
|
|
543
|
+
}
|
|
477
544
|
}
|
|
478
545
|
|
|
479
546
|
module.exports = DisplayEventProcessor
|
|
@@ -57,15 +57,19 @@ class RenderContextBuilder {
|
|
|
57
57
|
|
|
58
58
|
/**
|
|
59
59
|
* Build display event object
|
|
60
|
-
* @param {string} viewPath - Template/view path
|
|
60
|
+
* @param {string|null} viewPath - Template/view path
|
|
61
61
|
* @param {Object} appRenderContext - Context object
|
|
62
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
65
|
* @param {Object} props - Optional arguments for the view
|
|
66
|
+
* @param {string} anchor - Optional anchor name for single-anchor inject
|
|
67
|
+
* @param {string} type - Optional 'replace' | 'append' for anchor updates; default 'replace'
|
|
68
|
+
* @param {string|number|null} content - Optional direct content payload for full/anchor updates
|
|
69
|
+
* @param {string|null} contentType - Optional content kind: 'html' | 'text'
|
|
66
70
|
* @returns {Object} Display event object
|
|
67
71
|
*/
|
|
68
|
-
static buildDisplayEvent(viewPath, appRenderContext, viewer = null, participantId = null, timeout = null, props = {}) {
|
|
72
|
+
static buildDisplayEvent(viewPath, appRenderContext, viewer = null, participantId = null, timeout = null, props = {}, anchor = null, type = null, content = null, contentType = null) {
|
|
69
73
|
const displayEvent = {
|
|
70
74
|
viewPath,
|
|
71
75
|
appRenderContext,
|
|
@@ -84,6 +88,42 @@ class RenderContextBuilder {
|
|
|
84
88
|
displayEvent.timeout = timeout
|
|
85
89
|
}
|
|
86
90
|
|
|
91
|
+
if (anchor != null) {
|
|
92
|
+
displayEvent.anchor = anchor
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (type != null && (type === 'replace' || type === 'append')) {
|
|
96
|
+
displayEvent.type = type
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (content !== null && content !== undefined) {
|
|
100
|
+
displayEvent.content = content
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (contentType != null && (contentType === 'html' || contentType === 'text')) {
|
|
104
|
+
displayEvent.contentType = contentType
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return displayEvent
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Build display event object with multiple targets
|
|
112
|
+
* @param {Array} targets - Array of { viewPath?, anchor?, type?, props?, content?, contentType? }
|
|
113
|
+
* @param {Object} appRenderContext - Context object
|
|
114
|
+
* @param {string} viewer - Viewer type
|
|
115
|
+
* @param {string} participantId - Optional participant ID for selective rendering
|
|
116
|
+
* @param {number} timeout - Optional timeout in milliseconds
|
|
117
|
+
* @returns {Object} Display event object with targets array
|
|
118
|
+
*/
|
|
119
|
+
static buildDisplayEventWithTargets(targets, appRenderContext, viewer = null, participantId = null, timeout = null) {
|
|
120
|
+
const displayEvent = {
|
|
121
|
+
targets: targets,
|
|
122
|
+
appRenderContext
|
|
123
|
+
}
|
|
124
|
+
if (viewer) displayEvent.viewer = viewer
|
|
125
|
+
if (participantId) displayEvent.participantId = participantId
|
|
126
|
+
if (timeout !== null) displayEvent.timeout = timeout
|
|
87
127
|
return displayEvent
|
|
88
128
|
}
|
|
89
129
|
}
|
package/package.json
CHANGED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
const AppMachineTransition = require('../lib/utils/AppMachineTransition')
|
|
2
|
+
|
|
3
|
+
describe('AppMachineTransition eachParticipant content targets', () => {
|
|
4
|
+
test('accepts direct content without a view in function-based eachParticipant meta', () => {
|
|
5
|
+
const anearEvent = { send: jest.fn() }
|
|
6
|
+
const onTransition = AppMachineTransition(anearEvent)
|
|
7
|
+
|
|
8
|
+
const state = {
|
|
9
|
+
context: { scoreByParticipant: { p1: 98, p2: 77 } },
|
|
10
|
+
value: 'play',
|
|
11
|
+
getMeta: () => ({
|
|
12
|
+
play: {
|
|
13
|
+
eachParticipant: (appContext) => ([
|
|
14
|
+
{
|
|
15
|
+
participantId: 'p1',
|
|
16
|
+
anchor: 'seat_p1_score',
|
|
17
|
+
content: String(appContext.scoreByParticipant.p1),
|
|
18
|
+
contentType: 'text',
|
|
19
|
+
type: 'replace'
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
participantId: 'p2',
|
|
23
|
+
anchor: 'seat_p2_score',
|
|
24
|
+
content: String(appContext.scoreByParticipant.p2),
|
|
25
|
+
contentType: 'text',
|
|
26
|
+
type: 'replace'
|
|
27
|
+
}
|
|
28
|
+
])
|
|
29
|
+
}
|
|
30
|
+
})
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
onTransition(state, { type: 'TURN_COMPLETE' }, { source: 'test' })
|
|
34
|
+
|
|
35
|
+
expect(anearEvent.send).toHaveBeenCalledTimes(1)
|
|
36
|
+
const payload = anearEvent.send.mock.calls[0][0]
|
|
37
|
+
expect(payload.type).toBe('RENDER_DISPLAY')
|
|
38
|
+
expect(payload.displayEvents).toHaveLength(2)
|
|
39
|
+
expect(payload.displayEvents[0]).toMatchObject({
|
|
40
|
+
viewer: 'eachParticipant',
|
|
41
|
+
participantId: 'p1',
|
|
42
|
+
anchor: 'seat_p1_score',
|
|
43
|
+
content: '98',
|
|
44
|
+
contentType: 'text',
|
|
45
|
+
type: 'replace'
|
|
46
|
+
})
|
|
47
|
+
expect(payload.displayEvents[1]).toMatchObject({
|
|
48
|
+
viewer: 'eachParticipant',
|
|
49
|
+
participantId: 'p2',
|
|
50
|
+
anchor: 'seat_p2_score',
|
|
51
|
+
content: '77',
|
|
52
|
+
contentType: 'text',
|
|
53
|
+
type: 'replace'
|
|
54
|
+
})
|
|
55
|
+
})
|
|
56
|
+
})
|
|
57
|
+
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
const DisplayEventProcessor = require('../lib/utils/DisplayEventProcessor')
|
|
2
|
+
|
|
3
|
+
const makeProcessor = (overrides = {}) => {
|
|
4
|
+
const participantMachine = { send: jest.fn(), state: { context: {} } }
|
|
5
|
+
const baseContext = {
|
|
6
|
+
anearEvent: {},
|
|
7
|
+
participantsActionTimeout: null,
|
|
8
|
+
pugTemplates: {},
|
|
9
|
+
pugHelpers: {},
|
|
10
|
+
participantMachines: { p1: participantMachine },
|
|
11
|
+
participantsDisplayChannel: 'participants-channel',
|
|
12
|
+
spectatorsDisplayChannel: null,
|
|
13
|
+
participants: {
|
|
14
|
+
p1: { id: 'p1', isHost: false, active: true }
|
|
15
|
+
},
|
|
16
|
+
eventStats: null,
|
|
17
|
+
...overrides
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return {
|
|
21
|
+
processor: new DisplayEventProcessor(baseContext),
|
|
22
|
+
participantMachine
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
describe('DisplayEventProcessor anchor updates', () => {
|
|
27
|
+
test('sends direct text content to an anchor for eachParticipant', () => {
|
|
28
|
+
const { processor, participantMachine } = makeProcessor()
|
|
29
|
+
|
|
30
|
+
processor._processSingle({
|
|
31
|
+
appRenderContext: { app: { score: 98 }, meta: { viewer: 'eachParticipant', timeoutFn: null } },
|
|
32
|
+
viewer: 'eachParticipant',
|
|
33
|
+
participantId: 'p1',
|
|
34
|
+
anchor: 'seat_p1_score',
|
|
35
|
+
content: 98,
|
|
36
|
+
contentType: 'text',
|
|
37
|
+
type: 'replace',
|
|
38
|
+
props: {}
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
expect(participantMachine.send).toHaveBeenCalledWith({
|
|
42
|
+
type: 'RENDER_DISPLAY',
|
|
43
|
+
targets: {
|
|
44
|
+
seat_p1_score: {
|
|
45
|
+
content: '98',
|
|
46
|
+
contentType: 'text',
|
|
47
|
+
type: 'replace'
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
})
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
test('supports multi-target participant updates mixing text and html', () => {
|
|
54
|
+
const { processor, participantMachine } = makeProcessor({
|
|
55
|
+
pugTemplates: {
|
|
56
|
+
'seat_label.pug': () => '<span>Seat A</span>'
|
|
57
|
+
}
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
processor._processSingle({
|
|
61
|
+
appRenderContext: { app: {}, meta: { viewer: 'eachParticipant', timeoutFn: null } },
|
|
62
|
+
viewer: 'eachParticipant',
|
|
63
|
+
participantId: 'p1',
|
|
64
|
+
targets: [
|
|
65
|
+
{ anchor: 'seat_p1_score', content: 12, contentType: 'text', type: 'replace' },
|
|
66
|
+
{ anchor: 'seat_p1_label', viewPath: 'seat_label', type: 'replace' }
|
|
67
|
+
],
|
|
68
|
+
props: {}
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
expect(participantMachine.send).toHaveBeenCalledWith({
|
|
72
|
+
type: 'RENDER_DISPLAY',
|
|
73
|
+
targets: {
|
|
74
|
+
seat_p1_score: {
|
|
75
|
+
content: '12',
|
|
76
|
+
contentType: 'text',
|
|
77
|
+
type: 'replace'
|
|
78
|
+
},
|
|
79
|
+
seat_p1_label: {
|
|
80
|
+
content: '<span>Seat A</span>',
|
|
81
|
+
type: 'replace'
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
})
|
|
85
|
+
})
|
|
86
|
+
})
|
|
87
|
+
|