anear-js-api 1.2.3 → 1.3.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.
@@ -167,7 +167,8 @@ const AnearEventMachineContext = (
167
167
  eachParticipantCancelActionType: null, // ACTION type that triggers cancellation for eachParticipant timeouts
168
168
  pendingCancelConfirmations: null, // Set of participantIds waiting for TIMER_CANCELED confirmation
169
169
  pendingCancelAction: null, // ACTION to forward after cancellation completes: { participantId, appEventName, payload }
170
- consecutiveTimeoutCounts: {} // { participantId: count } tracking consecutive timeouts per participant for dead-man switch
170
+ consecutiveTimeoutCount: 0, // Global counter tracking consecutive timeouts across all participants for dead-man switch
171
+ consecutiveAllParticipantsTimeoutCount: 0 // Counter tracking consecutive allParticipants timeouts where ALL participants timed out
171
172
  })
172
173
 
173
174
  const ActiveEventGlobalEvents = {
@@ -683,7 +684,7 @@ const ActiveEventStatesConfig = {
683
684
  },
684
685
  on: {
685
686
  ACTION: {
686
- actions: ['processParticipantAction']
687
+ actions: ['resetParticipantTimeoutCount', 'processParticipantAction']
687
688
  },
688
689
  // Forward per-participant timeouts (from APM) to the AppM
689
690
  PARTICIPANT_TIMEOUT: {
@@ -741,7 +742,7 @@ const ActiveEventStatesConfig = {
741
742
  always: {
742
743
  guard: 'allParticipantsResponded',
743
744
  target: 'waitingForActions',
744
- actions: ['clearParticipantsTimeout']
745
+ actions: ['sendActionsCompleteToAppM', 'clearParticipantsTimeout']
745
746
  },
746
747
  after: {
747
748
  participantsActionTimeout: {
@@ -1839,14 +1840,17 @@ const AnearEventMachineFunctions = ({
1839
1840
  },
1840
1841
  resetParticipantTimeoutCount: assign(({ context, event }) => {
1841
1842
  const participantId = event.data.participantId
1842
- // Reset consecutive timeout count when participant sends an action
1843
- if (context.consecutiveTimeoutCounts[participantId]) {
1844
- logger.debug(`[AEM] Resetting timeout count for participant ${participantId} (action received)`)
1845
- const newCounts = { ...context.consecutiveTimeoutCounts }
1846
- delete newCounts[participantId]
1847
- return { consecutiveTimeoutCounts: newCounts }
1843
+ // Reset global consecutive timeout counts when ANY participant sends an action
1844
+ const updates = {}
1845
+ if (context.consecutiveTimeoutCount > 0) {
1846
+ logger.debug(`[AEM] Resetting global timeout count from ${context.consecutiveTimeoutCount} to 0 (action received from ${participantId})`)
1847
+ updates.consecutiveTimeoutCount = 0
1848
1848
  }
1849
- return {}
1849
+ if (context.consecutiveAllParticipantsTimeoutCount > 0) {
1850
+ logger.debug(`[AEM] Resetting allParticipants timeout count from ${context.consecutiveAllParticipantsTimeoutCount} to 0 (action received from ${participantId})`)
1851
+ updates.consecutiveAllParticipantsTimeoutCount = 0
1852
+ }
1853
+ return updates
1850
1854
  }),
1851
1855
  processParticipantAction: ({ context, event, self }) => {
1852
1856
  // event.data.participantId,
@@ -1935,16 +1939,13 @@ const AnearEventMachineFunctions = ({
1935
1939
  }),
1936
1940
  trackParticipantTimeout: assign(({ context, event }) => {
1937
1941
  const participantId = event.participantId
1938
- const currentCount = context.consecutiveTimeoutCounts[participantId] || 0
1942
+ const currentCount = context.consecutiveTimeoutCount || 0
1939
1943
  const newCount = currentCount + 1
1940
1944
 
1941
- logger.debug(`[AEM] Participant ${participantId} timeout count: ${currentCount} -> ${newCount}`)
1945
+ logger.debug(`[AEM] Global timeout count (any participant): ${currentCount} -> ${newCount} (timeout from ${participantId})`)
1942
1946
 
1943
1947
  return {
1944
- consecutiveTimeoutCounts: {
1945
- ...context.consecutiveTimeoutCounts,
1946
- [participantId]: newCount
1947
- }
1948
+ consecutiveTimeoutCount: newCount
1948
1949
  }
1949
1950
  }),
1950
1951
  processParticipantTimeout: ({ context, event, self }) => {
@@ -1957,12 +1958,13 @@ const AnearEventMachineFunctions = ({
1957
1958
  }
1958
1959
 
1959
1960
  const participantId = event.participantId
1960
- const timeoutCount = context.consecutiveTimeoutCounts[participantId] || 0
1961
+ const timeoutCount = context.consecutiveTimeoutCount || 0
1961
1962
  const maxConsecutiveTimeouts = C.DEAD_MAN_SWITCH.MAX_CONSECUTIVE_PARTICIPANT_TIMEOUTS
1962
1963
 
1963
- // Check dead-man switch: if participant has exceeded consecutive timeout threshold, cancel event
1964
+ // Check dead-man switch: if global consecutive timeout threshold reached, cancel event
1965
+ // This detects when the game is "dead" (no actions happening) across all participants
1964
1966
  if (timeoutCount >= maxConsecutiveTimeouts) {
1965
- logger.warn(`[AEM] Dead-man switch triggered: Participant ${participantId} has ${timeoutCount} consecutive timeouts (threshold: ${maxConsecutiveTimeouts}). Auto-canceling event.`)
1967
+ logger.warn(`[AEM] Dead-man switch triggered: ${timeoutCount} consecutive timeouts across all participants (threshold: ${maxConsecutiveTimeouts}). Auto-canceling event.`)
1966
1968
  self.send({ type: 'CANCEL' })
1967
1969
  return
1968
1970
  }
@@ -2044,30 +2046,70 @@ const AnearEventMachineFunctions = ({
2044
2046
  }
2045
2047
  };
2046
2048
  }),
2047
- sendActionsTimeoutToAppM: ({ context, self }) => {
2049
+ sendActionsCompleteToAppM: assign(({ context }) => {
2050
+ // When all participants respond, the final ACTION already has finalAction: true
2051
+ // No need to send a separate ACTIONS_COMPLETE event - the finalAction flag handles it
2052
+ // Reset allParticipants timeout counter when all participants respond
2053
+ if (context.consecutiveAllParticipantsTimeoutCount > 0) {
2054
+ logger.debug(`[AEM] Resetting allParticipants timeout count from ${context.consecutiveAllParticipantsTimeoutCount} to 0 (all participants responded)`)
2055
+ return { consecutiveAllParticipantsTimeoutCount: 0 }
2056
+ }
2057
+ return {}
2058
+ }),
2059
+ sendActionsTimeoutToAppM: assign(({ context, self }) => {
2048
2060
  const { nonResponders, msecs } = context.participantsActionTimeout
2049
2061
  const nonResponderIds = [...nonResponders]
2050
2062
  const allActiveParticipantIds = getActiveParticipantIds(context)
2051
2063
 
2052
2064
  logger.info(`[AEM] allParticipants timeout expired after ${msecs}ms. Non-responders: ${nonResponderIds.join(', ')}`)
2053
2065
 
2054
- // Dead-man switch: if all participants timed out, auto-cancel the event
2055
- if (nonResponders.size === allActiveParticipantIds.length && allActiveParticipantIds.length > 0) {
2056
- logger.warn(`[AEM] Dead-man switch triggered: All ${allActiveParticipantIds.length} participants timed out in allParticipants timeout. Auto-canceling event.`)
2057
- self.send({ type: 'CANCEL' })
2058
- return
2066
+ // Track consecutive allParticipants timeouts where ALL participants timed out
2067
+ const allTimedOut = nonResponders.size === allActiveParticipantIds.length && allActiveParticipantIds.length > 0
2068
+ const updates = {}
2069
+
2070
+ if (allTimedOut) {
2071
+ const currentCount = context.consecutiveAllParticipantsTimeoutCount || 0
2072
+ const newCount = currentCount + 1
2073
+ logger.debug(`[AEM] All participants timed out in allParticipants timeout. Count: ${currentCount} -> ${newCount}`)
2074
+ updates.consecutiveAllParticipantsTimeoutCount = newCount
2075
+
2076
+ // Dead-man switch: if 4 consecutive allParticipants timeouts where all timed out, cancel event
2077
+ const maxConsecutiveTimeouts = C.DEAD_MAN_SWITCH.MAX_CONSECUTIVE_PARTICIPANT_TIMEOUTS
2078
+ if (newCount >= maxConsecutiveTimeouts) {
2079
+ logger.warn(`[AEM] Dead-man switch triggered: ${maxConsecutiveTimeouts} consecutive allParticipants timeouts where all participants timed out. Auto-canceling event.`)
2080
+ self.send({ type: 'CANCEL' })
2081
+ return updates
2082
+ }
2083
+ } else {
2084
+ // Not all timed out - reset the counter
2085
+ if (context.consecutiveAllParticipantsTimeoutCount > 0) {
2086
+ logger.debug(`[AEM] Resetting allParticipants timeout count from ${context.consecutiveAllParticipantsTimeoutCount} to 0 (not all participants timed out)`)
2087
+ updates.consecutiveAllParticipantsTimeoutCount = 0
2088
+ }
2059
2089
  }
2060
2090
 
2061
2091
  if (context.appEventMachine) {
2092
+ // Send ACTIONS_TIMEOUT when timeout expires
2093
+ logger.info(`[AEM] Sending ACTIONS_TIMEOUT (timeout expired)`)
2062
2094
  context.appEventMachine.send({ type: 'ACTIONS_TIMEOUT', timeout: msecs, nonResponderIds })
2063
2095
  }
2064
- },
2096
+
2097
+ return updates
2098
+ }),
2065
2099
  sendActionsTimeoutToAppMCanceled: ({ context }) => {
2066
2100
  const { nonResponders, msecs } = context.participantsActionTimeout
2067
2101
  const nonResponderIds = [...nonResponders]
2068
2102
  logger.info(`[AEM] allParticipants timeout cancelled after ${msecs}ms. Non-responders at cancellation: ${nonResponderIds.join(', ')}`)
2069
2103
 
2070
2104
  if (context.appEventMachine) {
2105
+ // Send ACTIONS_TIMEOUT when timeout is canceled (backward compatibility)
2106
+ context.appEventMachine.send({
2107
+ type: 'ACTIONS_TIMEOUT',
2108
+ timeout: msecs,
2109
+ nonResponderIds,
2110
+ canceled: true
2111
+ })
2112
+ // Keep ACTIONS_TIMEOUT for backward compatibility (deprecated)
2071
2113
  context.appEventMachine.send({ type: 'ACTIONS_TIMEOUT', timeout: msecs, nonResponderIds, canceled: true })
2072
2114
  }
2073
2115
  },
@@ -2100,9 +2142,6 @@ const AnearEventMachineFunctions = ({
2100
2142
 
2101
2143
  logger.debug(`[AEM] cleaning up exiting participant ${participantId}`)
2102
2144
 
2103
- // Clean up timeout counts for exiting participant
2104
- const { [participantId]: removedTimeoutCount, ...remainingTimeoutCounts } = context.consecutiveTimeoutCounts
2105
-
2106
2145
  if (participant) {
2107
2146
  const isOpenHouse = context.anearEvent.openHouse || false
2108
2147
 
@@ -2125,8 +2164,7 @@ const AnearEventMachineFunctions = ({
2125
2164
  ...context.participants,
2126
2165
  [participantId]: updatedParticipant
2127
2166
  },
2128
- participantMachines: remainingMachines,
2129
- consecutiveTimeoutCounts: remainingTimeoutCounts
2167
+ participantMachines: remainingMachines
2130
2168
  }
2131
2169
  } else {
2132
2170
  // For non-open-house events: remove both participant and machine (existing behavior)
@@ -2142,14 +2180,11 @@ const AnearEventMachineFunctions = ({
2142
2180
 
2143
2181
  return {
2144
2182
  participants: remainingParticipants,
2145
- participantMachines: remainingMachines,
2146
- consecutiveTimeoutCounts: remainingTimeoutCounts
2183
+ participantMachines: remainingMachines
2147
2184
  }
2148
2185
  }
2149
2186
  } else {
2150
- return {
2151
- consecutiveTimeoutCounts: remainingTimeoutCounts
2152
- }
2187
+ return {}
2153
2188
  }
2154
2189
  }),
2155
2190
  notifyCoreServiceMachineExit: ({ context }) => {
@@ -365,9 +365,20 @@ class DisplayEventProcessor {
365
365
  if (timeoutFn) {
366
366
  // For participant displays, any non-null / positive timeout value should
367
367
  // start a real per-participant timer on that participant's APM.
368
- const msecs = timeoutFn(appCtx, participantStruct)
369
- if (typeof msecs === 'number' && msecs > 0) {
370
- timeout = msecs
368
+ if (!participantStruct) {
369
+ logger.warn(`[DisplayEventProcessor] Participant struct not found for ${participantId}, skipping timeout calculation`)
370
+ } else {
371
+ try {
372
+ const msecs = timeoutFn(appCtx, participantStruct)
373
+ if (typeof msecs === 'number' && msecs > 0) {
374
+ timeout = msecs
375
+ } else {
376
+ // Log when timeout function returns null/undefined to help diagnose issues
377
+ logger.debug(`[DisplayEventProcessor] Timeout function returned ${msecs === null ? 'null' : msecs === undefined ? 'undefined' : `non-numeric value: ${msecs}`} for ${participantId}`)
378
+ }
379
+ } catch (error) {
380
+ logger.warn(`[DisplayEventProcessor] Error calculating timeout for ${participantId}: ${error.message}`)
381
+ }
371
382
  }
372
383
  }
373
384
  const privateRenderContext = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "anear-js-api",
3
- "version": "1.2.3",
3
+ "version": "1.3.0",
4
4
  "description": "Javascript Developer API for Anear Apps",
5
5
  "main": "lib/index.js",
6
6
  "scripts": {