anear-js-api 1.6.2 → 1.6.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.
@@ -204,7 +204,8 @@ const AnearEventMachineContext = (
204
204
  pendingCancelAction: null, // ACTION to forward after cancellation completes: { participantId, appEventName, payload }
205
205
  consecutiveTimeoutCount: 0, // Global counter tracking consecutive timeouts across all participants for dead-man switch
206
206
  consecutiveAllParticipantsTimeoutCount: 0, // Counter tracking consecutive allParticipants timeouts where ALL participants timed out
207
- eventStats: null // Set on createAppEventMachine onDone (initial or rehydrated); createInitialEventStats() shape
207
+ eventStats: null, // Set on createAppEventMachine onDone (initial or rehydrated); createInitialEventStats() shape
208
+ lastRenderResult: null // { anyDisplaySent } from renderDisplay onDone; used to send noDisplaysSent on RENDERED and avoid re-render loops when all participants exited
208
209
  })
209
210
 
210
211
  const ActiveEventGlobalEvents = {
@@ -425,7 +426,7 @@ const ActiveEventStatesConfig = {
425
426
  input: ({ context, event }) => ({ context, event }),
426
427
  onDone: {
427
428
  target: 'notifyingPausedRenderComplete',
428
- actions: ['clearParticipantTimeoutSuppression']
429
+ actions: ['setLastRenderResult', 'clearParticipantTimeoutSuppression']
429
430
  // v5 note: internal transitions are the default (v4 had `internal: true`)
430
431
  },
431
432
  onError: {
@@ -464,7 +465,7 @@ const ActiveEventStatesConfig = {
464
465
  input: ({ context, event }) => ({ context, event }),
465
466
  onDone: {
466
467
  target: 'notifyingRenderComplete',
467
- actions: ['clearParticipantTimeoutSuppression']
468
+ actions: ['setLastRenderResult', 'clearParticipantTimeoutSuppression']
468
469
  // v5 note: internal transitions are the default (v4 had `internal: true`)
469
470
  },
470
471
  onError: {
@@ -580,7 +581,7 @@ const ActiveEventStatesConfig = {
580
581
  input: ({ context, event }) => ({ context, event }),
581
582
  onDone: {
582
583
  target: 'notifyingRenderComplete',
583
- actions: ['clearParticipantTimeoutSuppression']
584
+ actions: ['setLastRenderResult', 'clearParticipantTimeoutSuppression']
584
585
  // v5 note: internal transitions are the default (v4 had `internal: true`)
585
586
  },
586
587
  onError: {
@@ -763,11 +764,11 @@ const ActiveEventStatesConfig = {
763
764
  {
764
765
  guard: 'isParticipantsTimeoutActive',
765
766
  target: 'notifyingRenderCompleteWithTimeout',
766
- actions: ['setupParticipantsTimeout', 'setupCancelActionType', 'clearParticipantTimeoutSuppression']
767
+ actions: ['setLastRenderResult', 'setupParticipantsTimeout', 'setupCancelActionType', 'clearParticipantTimeoutSuppression']
767
768
  },
768
769
  {
769
770
  target: 'notifyingRenderComplete',
770
- actions: ['setupCancelActionType', 'clearParticipantTimeoutSuppression']
771
+ actions: ['setLastRenderResult', 'setupCancelActionType', 'clearParticipantTimeoutSuppression']
771
772
  // v5 note: internal transitions are the default (v4 had `internal: true`)
772
773
  }
773
774
  ],
@@ -816,13 +817,13 @@ const ActiveEventStatesConfig = {
816
817
  {
817
818
  guard: 'isParticipantsTimeoutActive',
818
819
  target: 'notifyingRenderComplete',
819
- actions: ['setupCancelActionType', 'clearParticipantTimeoutSuppression']
820
+ actions: ['setLastRenderResult', 'setupCancelActionType', 'clearParticipantTimeoutSuppression']
820
821
  // Note: We do NOT call setupParticipantsTimeout here because we're already in waitAllParticipantsResponse
821
822
  // and the timer is still running from the parent state (preserved by nested state pattern).
822
823
  },
823
824
  {
824
825
  target: '#waitingForActions',
825
- actions: ['setupCancelActionType', 'clearParticipantTimeoutSuppression', 'notifyAppMachineRendered']
826
+ actions: ['setLastRenderResult', 'setupCancelActionType', 'clearParticipantTimeoutSuppression', 'notifyAppMachineRendered']
826
827
  }
827
828
  ],
828
829
  onError: {
@@ -1474,6 +1475,10 @@ const AnearEventMachineFunctions = ({
1474
1475
  return { ...s, liveDurationMs: (s.liveDurationMs || 0) + add, liveStartedAt: null }
1475
1476
  }
1476
1477
  }),
1478
+ setLastRenderResult: assign(({ event }) => {
1479
+ const output = event?.output ?? event?.data
1480
+ return { lastRenderResult: output && typeof output === 'object' ? output : null }
1481
+ }),
1477
1482
  notifyAppMachineRendered: ({ context }) => {
1478
1483
  if (context.appEventMachine) {
1479
1484
  logger.debug('[AEM] Sending RENDERED to AppM')
@@ -121,14 +121,12 @@ const AppMachineTransition = (anearEvent) => {
121
121
  }
122
122
 
123
123
  if (event.type === 'RENDERED') {
124
- // Re-examine meta on RENDERED; send RENDER_DISPLAY only when state/context actually changed.
125
- // Exception: eachParticipant states re-send once on RENDERED so participants who had a
126
- // display queued (e.g. joined while another was publishing) still get the view. No opt-in.
127
- const hasEachParticipant = (rawMeta && typeof rawMeta === 'object' && rawMeta.eachParticipant != null) ||
128
- metaObjects.some(m => m && typeof m === 'object' && m.eachParticipant != null)
124
+ // RENDERED means "display processing done; AppM transitions out of the display state."
125
+ // Do not re-send RENDER_DISPLAY on RENDERED. New participants get their initial view via
126
+ // PARTICIPANT_ENTER flow, not by re-running eachParticipant here.
129
127
  const sameSignature = baseSignature === lastBaseSignature
130
- if (sameSignature && !hasEachParticipant) {
131
- logger.debug(`[AppMachineTransition] RENDERED re-examination: no change, skipping RENDER_DISPLAY stateName='${stateName}'`)
128
+ if (sameSignature) {
129
+ logger.debug(`[AppMachineTransition] RENDERED: no change, skipping RENDER_DISPLAY stateName='${stateName}'`)
132
130
  return
133
131
  }
134
132
  lastBaseSignature = baseSignature
@@ -81,16 +81,18 @@ class DisplayEventProcessor {
81
81
  // ignore precompute errors; fall back to runtime values
82
82
  }
83
83
 
84
- const publishPromises = displayEvents.map(event => {
85
- const { publishPromise, timeout } = this._processSingle(event)
84
+ const results = displayEvents.map(event => this._processSingle(event))
85
+ const publishPromises = results.map(r => r.publishPromise)
86
+ const anyDisplaySent = results.some(r => r.displaySent === true)
87
+
88
+ results.forEach(({ timeout }) => {
86
89
  if (timeout) {
87
90
  participantsTimeout = timeout
88
91
  }
89
- return publishPromise
90
92
  })
91
93
 
92
94
  return Promise.all(publishPromises).then(() => {
93
- return { participantsTimeout, cancelActionType }
95
+ return { participantsTimeout, cancelActionType, anyDisplaySent }
94
96
  })
95
97
  }
96
98
 
@@ -141,6 +143,7 @@ class DisplayEventProcessor {
141
143
  // display event, but if not, it falls back to the viewer from the meta block.
142
144
  const displayViewer = viewer || appRenderContext.meta.viewer
143
145
 
146
+ let displaySent = false
144
147
  switch (displayViewer) {
145
148
  case 'allParticipants':
146
149
  const templateName = normalizedPath.replace(C.PugSuffix, '')
@@ -176,6 +179,7 @@ class DisplayEventProcessor {
176
179
  'PARTICIPANTS_DISPLAY',
177
180
  formattedDisplayMessage()
178
181
  )
182
+ displaySent = true
179
183
  break
180
184
 
181
185
  case 'spectators':
@@ -187,6 +191,7 @@ class DisplayEventProcessor {
187
191
  if (!this.spectatorsDisplayChannel) {
188
192
  logger.debug('[DisplayEventProcessor] spectatorsDisplayChannel is null; skipping SPECTATORS_DISPLAY publish')
189
193
  publishPromise = Promise.resolve()
194
+ displaySent = false
190
195
  break
191
196
  }
192
197
 
@@ -220,24 +225,29 @@ class DisplayEventProcessor {
220
225
  'SPECTATORS_DISPLAY',
221
226
  formattedDisplayMessage()
222
227
  )
228
+ displaySent = true
223
229
  break
224
230
 
225
- case 'eachParticipant':
231
+ case 'eachParticipant': {
226
232
  const templateNameEach = normalizedPath.replace(C.PugSuffix, '')
227
233
  if (participantId === 'ALL_PARTICIPANTS') {
228
234
  // Legacy behavior - send to all participants (uses timeoutFn)
229
235
  logger.info(`[DisplayEventProcessor] RENDER_DISPLAY template=${templateNameEach} viewer=eachParticipant participantId=ALL_PARTICIPANTS`)
230
236
  publishPromise = this._processPrivateParticipantDisplays(template, templateRenderContext, timeoutFn)
237
+ displaySent = true // We attempt to send to all in index; at least one may receive
231
238
  } else if (participantId) {
232
239
  // Selective participant rendering - send to specific participant only (uses displayTimeout)
233
240
  logger.info(`[DisplayEventProcessor] RENDER_DISPLAY template=${templateNameEach} viewer=eachParticipant participantId=${participantId}`)
234
- publishPromise = this._processSelectiveParticipantDisplay(template, templateRenderContext, null, participantId, displayTimeout)
241
+ const result = this._processSelectiveParticipantDisplayWithSent(template, templateRenderContext, null, participantId, displayTimeout)
242
+ publishPromise = result.promise
243
+ displaySent = result.displaySent
235
244
  } else {
236
245
  // Fallback - should not happen with unified approach
237
246
  logger.warn(`[DisplayEventProcessor] Unexpected participant display event without participantId`)
238
247
  publishPromise = Promise.resolve()
239
248
  }
240
249
  break
250
+ }
241
251
 
242
252
  case 'host':
243
253
  const templateNameHost = normalizedPath.replace(C.PugSuffix, '')
@@ -277,12 +287,13 @@ class DisplayEventProcessor {
277
287
  templateRenderContext,
278
288
  hostOwnMsecs !== null ? (() => hostOwnMsecs) : null
279
289
  )
290
+ displaySent = true
280
291
  break
281
292
 
282
293
  default:
283
294
  throw new Error(`Unknown display viewer: ${displayViewer}`)
284
295
  }
285
- return { publishPromise, timeout }
296
+ return { publishPromise, timeout, displaySent }
286
297
  }
287
298
 
288
299
  _processHostDisplay(template, templateRenderContext, timeoutFn) {
@@ -329,30 +340,35 @@ class DisplayEventProcessor {
329
340
  return Promise.resolve()
330
341
  }
331
342
 
332
- _processSelectiveParticipantDisplay(template, templateRenderContext, timeoutFn, participantId, displayTimeout = null) {
343
+ /**
344
+ * Returns { promise, displaySent } for selective participant display.
345
+ * When participant is not found (exited), we treat as delivered (displaySent: false) so the
346
+ * render cycle can complete and we avoid infinite RENDER_DISPLAY/RENDERED loops.
347
+ */
348
+ _processSelectiveParticipantDisplayWithSent(template, templateRenderContext, timeoutFn, participantId, displayTimeout = null) {
333
349
  const participantStruct = this.participantsIndex.get(participantId)
334
350
 
335
351
  if (!participantStruct) {
336
- logger.warn(`[DisplayEventProcessor] Participant ${participantId} not found for selective display`)
337
- return Promise.resolve()
352
+ logger.debug(`[DisplayEventProcessor] Participant ${participantId} not found for selective display (exited); treating as delivered`)
353
+ return { promise: Promise.resolve(), displaySent: false }
338
354
  }
339
355
 
340
356
  // Exclude the host from receiving displays targeted at participants
341
357
  if (participantStruct.info.isHost) {
342
358
  logger.warn(`[DisplayEventProcessor] Cannot send participant display to host ${participantId}`)
343
- return Promise.resolve()
359
+ return { promise: Promise.resolve(), displaySent: false }
344
360
  }
345
361
 
346
362
  // Skip inactive participants (idle in open house events)
347
363
  if (participantStruct.info.active === false) {
348
364
  logger.debug(`[DisplayEventProcessor] Skipping display for inactive participant ${participantId}`)
349
- return Promise.resolve()
365
+ return { promise: Promise.resolve(), displaySent: false }
350
366
  }
351
367
 
352
368
  const participantMachine = this.participantMachines[participantId]
353
369
  if (!participantMachine) {
354
- logger.warn(`[DisplayEventProcessor] Participant machine not found for ${participantId}`)
355
- return Promise.resolve()
370
+ logger.debug(`[DisplayEventProcessor] Participant machine not found for ${participantId} (exited); treating as delivered`)
371
+ return { promise: Promise.resolve(), displaySent: false }
356
372
  }
357
373
 
358
374
  // Use displayTimeout if available, otherwise fall back to timeoutFn
@@ -361,7 +377,12 @@ class DisplayEventProcessor {
361
377
  timeoutFn
362
378
 
363
379
  this._sendPrivateDisplay(participantMachine, participantId, template, templateRenderContext, effectiveTimeoutFn)
364
- return Promise.resolve()
380
+ return { promise: Promise.resolve(), displaySent: true }
381
+ }
382
+
383
+ _processSelectiveParticipantDisplay(template, templateRenderContext, timeoutFn, participantId, displayTimeout = null) {
384
+ const { promise } = this._processSelectiveParticipantDisplayWithSent(template, templateRenderContext, timeoutFn, participantId, displayTimeout)
385
+ return promise
365
386
  }
366
387
 
367
388
  _sendPrivateDisplay(participantMachine, participantId, template, templateRenderContext, timeoutFn) {
@@ -93,8 +93,6 @@ class RealtimeMessaging {
93
93
 
94
94
  const clientOptions = this.ablyClientOptions(appId)
95
95
 
96
- logger.debug("[RTM] Ably Client Options", clientOptions)
97
-
98
96
  this.ablyRealtime = new Ably.Realtime(clientOptions)
99
97
 
100
98
  this.ablyRealtime.connection.on((stateChange) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "anear-js-api",
3
- "version": "1.6.2",
3
+ "version": "1.6.4",
4
4
  "description": "Javascript Developer API for Anear Apps",
5
5
  "main": "lib/index.js",
6
6
  "scripts": {