anear-js-api 1.2.3 → 1.3.1

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 = {
@@ -657,7 +658,8 @@ const ActiveEventStatesConfig = {
657
658
  states: {
658
659
  idle: {},
659
660
  waitingForCancelConfirmations: {
660
- entry: ['sendCancelTimeoutToAllAPMs', 'setupPendingCancelConfirmations'],
661
+ // Note: Setup actions are called in the transitions that lead here, not in entry
662
+ // This allows different setup for declarative cancel (TRIGGER_CANCEL_SEQUENCE) vs manual cancel (CANCEL_ALL_PARTICIPANT_TIMEOUTS)
661
663
  on: {
662
664
  TIMER_CANCELED: {
663
665
  actions: ['removeFromPendingCancelConfirmations']
@@ -676,21 +678,26 @@ const ActiveEventStatesConfig = {
676
678
  },
677
679
  always: {
678
680
  guard: 'allCancelConfirmationsReceived',
679
- actions: ['forwardPendingCancelAction', 'clearCancelState'],
681
+ actions: ['forwardPendingCancelActionIfExists', 'clearCancelState'],
680
682
  target: '#waitingForActions.idle'
681
683
  }
682
684
  }
683
685
  },
684
686
  on: {
685
687
  ACTION: {
686
- actions: ['processParticipantAction']
688
+ actions: ['resetParticipantTimeoutCount', 'processParticipantAction']
687
689
  },
688
690
  // Forward per-participant timeouts (from APM) to the AppM
689
691
  PARTICIPANT_TIMEOUT: {
690
692
  actions: ['trackParticipantTimeout', 'processParticipantTimeout'],
691
693
  // v5 note: internal transitions are the default (v4 had `internal: true`)
692
694
  },
695
+ CANCEL_ALL_PARTICIPANT_TIMEOUTS: {
696
+ actions: ['setupPendingCancelConfirmationsForManualCancel', 'sendCancelTimeoutToAllAPMs'],
697
+ target: '.waitingForCancelConfirmations'
698
+ },
693
699
  TRIGGER_CANCEL_SEQUENCE: {
700
+ actions: ['sendCancelTimeoutToAllAPMs', 'setupPendingCancelConfirmations'],
694
701
  target: '.waitingForCancelConfirmations'
695
702
  },
696
703
  TIMER_CANCELED: {
@@ -741,7 +748,7 @@ const ActiveEventStatesConfig = {
741
748
  always: {
742
749
  guard: 'allParticipantsResponded',
743
750
  target: 'waitingForActions',
744
- actions: ['clearParticipantsTimeout']
751
+ actions: ['sendActionsCompleteToAppM', 'clearParticipantsTimeout']
745
752
  },
746
753
  after: {
747
754
  participantsActionTimeout: {
@@ -1748,6 +1755,19 @@ const AnearEventMachineFunctions = ({
1748
1755
  suppressParticipantTimeouts: true // Drop PARTICIPANT_TIMEOUT events
1749
1756
  }
1750
1757
  }),
1758
+ setupPendingCancelConfirmationsForManualCancel: assign(({ context }) => {
1759
+ // Get all active participants (for manual cancelAllParticipantTimeouts call)
1760
+ const activeParticipantIds = getActiveParticipantIds(context)
1761
+ const pendingConfirmations = new Set(activeParticipantIds)
1762
+
1763
+ logger.info(`[AEM] Starting manual cancellation sequence (cancelAllParticipantTimeouts). Waiting for ${pendingConfirmations.size} APM confirmations: ${[...pendingConfirmations].join(', ')}`)
1764
+
1765
+ return {
1766
+ pendingCancelConfirmations: pendingConfirmations,
1767
+ pendingCancelAction: null, // No ACTION to forward for manual cancel
1768
+ suppressParticipantTimeouts: true // Drop PARTICIPANT_TIMEOUT events
1769
+ }
1770
+ }),
1751
1771
  removeFromPendingCancelConfirmations: assign(({ context, event }) => {
1752
1772
  const participantId = event.participantId || event.data?.id
1753
1773
  if (!context.pendingCancelConfirmations || !participantId) return {}
@@ -1839,14 +1859,17 @@ const AnearEventMachineFunctions = ({
1839
1859
  },
1840
1860
  resetParticipantTimeoutCount: assign(({ context, event }) => {
1841
1861
  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 }
1862
+ // Reset global consecutive timeout counts when ANY participant sends an action
1863
+ const updates = {}
1864
+ if (context.consecutiveTimeoutCount > 0) {
1865
+ logger.debug(`[AEM] Resetting global timeout count from ${context.consecutiveTimeoutCount} to 0 (action received from ${participantId})`)
1866
+ updates.consecutiveTimeoutCount = 0
1848
1867
  }
1849
- return {}
1868
+ if (context.consecutiveAllParticipantsTimeoutCount > 0) {
1869
+ logger.debug(`[AEM] Resetting allParticipants timeout count from ${context.consecutiveAllParticipantsTimeoutCount} to 0 (action received from ${participantId})`)
1870
+ updates.consecutiveAllParticipantsTimeoutCount = 0
1871
+ }
1872
+ return updates
1850
1873
  }),
1851
1874
  processParticipantAction: ({ context, event, self }) => {
1852
1875
  // event.data.participantId,
@@ -1935,16 +1958,13 @@ const AnearEventMachineFunctions = ({
1935
1958
  }),
1936
1959
  trackParticipantTimeout: assign(({ context, event }) => {
1937
1960
  const participantId = event.participantId
1938
- const currentCount = context.consecutiveTimeoutCounts[participantId] || 0
1961
+ const currentCount = context.consecutiveTimeoutCount || 0
1939
1962
  const newCount = currentCount + 1
1940
1963
 
1941
- logger.debug(`[AEM] Participant ${participantId} timeout count: ${currentCount} -> ${newCount}`)
1964
+ logger.debug(`[AEM] Global timeout count (any participant): ${currentCount} -> ${newCount} (timeout from ${participantId})`)
1942
1965
 
1943
1966
  return {
1944
- consecutiveTimeoutCounts: {
1945
- ...context.consecutiveTimeoutCounts,
1946
- [participantId]: newCount
1947
- }
1967
+ consecutiveTimeoutCount: newCount
1948
1968
  }
1949
1969
  }),
1950
1970
  processParticipantTimeout: ({ context, event, self }) => {
@@ -1957,12 +1977,13 @@ const AnearEventMachineFunctions = ({
1957
1977
  }
1958
1978
 
1959
1979
  const participantId = event.participantId
1960
- const timeoutCount = context.consecutiveTimeoutCounts[participantId] || 0
1980
+ const timeoutCount = context.consecutiveTimeoutCount || 0
1961
1981
  const maxConsecutiveTimeouts = C.DEAD_MAN_SWITCH.MAX_CONSECUTIVE_PARTICIPANT_TIMEOUTS
1962
1982
 
1963
- // Check dead-man switch: if participant has exceeded consecutive timeout threshold, cancel event
1983
+ // Check dead-man switch: if global consecutive timeout threshold reached, cancel event
1984
+ // This detects when the game is "dead" (no actions happening) across all participants
1964
1985
  if (timeoutCount >= maxConsecutiveTimeouts) {
1965
- logger.warn(`[AEM] Dead-man switch triggered: Participant ${participantId} has ${timeoutCount} consecutive timeouts (threshold: ${maxConsecutiveTimeouts}). Auto-canceling event.`)
1986
+ logger.warn(`[AEM] Dead-man switch triggered: ${timeoutCount} consecutive timeouts across all participants (threshold: ${maxConsecutiveTimeouts}). Auto-canceling event.`)
1966
1987
  self.send({ type: 'CANCEL' })
1967
1988
  return
1968
1989
  }
@@ -2044,30 +2065,70 @@ const AnearEventMachineFunctions = ({
2044
2065
  }
2045
2066
  };
2046
2067
  }),
2047
- sendActionsTimeoutToAppM: ({ context, self }) => {
2068
+ sendActionsCompleteToAppM: assign(({ context }) => {
2069
+ // When all participants respond, the final ACTION already has finalAction: true
2070
+ // No need to send a separate ACTIONS_COMPLETE event - the finalAction flag handles it
2071
+ // Reset allParticipants timeout counter when all participants respond
2072
+ if (context.consecutiveAllParticipantsTimeoutCount > 0) {
2073
+ logger.debug(`[AEM] Resetting allParticipants timeout count from ${context.consecutiveAllParticipantsTimeoutCount} to 0 (all participants responded)`)
2074
+ return { consecutiveAllParticipantsTimeoutCount: 0 }
2075
+ }
2076
+ return {}
2077
+ }),
2078
+ sendActionsTimeoutToAppM: assign(({ context, self }) => {
2048
2079
  const { nonResponders, msecs } = context.participantsActionTimeout
2049
2080
  const nonResponderIds = [...nonResponders]
2050
2081
  const allActiveParticipantIds = getActiveParticipantIds(context)
2051
2082
 
2052
2083
  logger.info(`[AEM] allParticipants timeout expired after ${msecs}ms. Non-responders: ${nonResponderIds.join(', ')}`)
2053
2084
 
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
2085
+ // Track consecutive allParticipants timeouts where ALL participants timed out
2086
+ const allTimedOut = nonResponders.size === allActiveParticipantIds.length && allActiveParticipantIds.length > 0
2087
+ const updates = {}
2088
+
2089
+ if (allTimedOut) {
2090
+ const currentCount = context.consecutiveAllParticipantsTimeoutCount || 0
2091
+ const newCount = currentCount + 1
2092
+ logger.debug(`[AEM] All participants timed out in allParticipants timeout. Count: ${currentCount} -> ${newCount}`)
2093
+ updates.consecutiveAllParticipantsTimeoutCount = newCount
2094
+
2095
+ // Dead-man switch: if 4 consecutive allParticipants timeouts where all timed out, cancel event
2096
+ const maxConsecutiveTimeouts = C.DEAD_MAN_SWITCH.MAX_CONSECUTIVE_PARTICIPANT_TIMEOUTS
2097
+ if (newCount >= maxConsecutiveTimeouts) {
2098
+ logger.warn(`[AEM] Dead-man switch triggered: ${maxConsecutiveTimeouts} consecutive allParticipants timeouts where all participants timed out. Auto-canceling event.`)
2099
+ self.send({ type: 'CANCEL' })
2100
+ return updates
2101
+ }
2102
+ } else {
2103
+ // Not all timed out - reset the counter
2104
+ if (context.consecutiveAllParticipantsTimeoutCount > 0) {
2105
+ logger.debug(`[AEM] Resetting allParticipants timeout count from ${context.consecutiveAllParticipantsTimeoutCount} to 0 (not all participants timed out)`)
2106
+ updates.consecutiveAllParticipantsTimeoutCount = 0
2107
+ }
2059
2108
  }
2060
2109
 
2061
2110
  if (context.appEventMachine) {
2111
+ // Send ACTIONS_TIMEOUT when timeout expires
2112
+ logger.info(`[AEM] Sending ACTIONS_TIMEOUT (timeout expired)`)
2062
2113
  context.appEventMachine.send({ type: 'ACTIONS_TIMEOUT', timeout: msecs, nonResponderIds })
2063
2114
  }
2064
- },
2115
+
2116
+ return updates
2117
+ }),
2065
2118
  sendActionsTimeoutToAppMCanceled: ({ context }) => {
2066
2119
  const { nonResponders, msecs } = context.participantsActionTimeout
2067
2120
  const nonResponderIds = [...nonResponders]
2068
2121
  logger.info(`[AEM] allParticipants timeout cancelled after ${msecs}ms. Non-responders at cancellation: ${nonResponderIds.join(', ')}`)
2069
2122
 
2070
2123
  if (context.appEventMachine) {
2124
+ // Send ACTIONS_TIMEOUT when timeout is canceled (backward compatibility)
2125
+ context.appEventMachine.send({
2126
+ type: 'ACTIONS_TIMEOUT',
2127
+ timeout: msecs,
2128
+ nonResponderIds,
2129
+ canceled: true
2130
+ })
2131
+ // Keep ACTIONS_TIMEOUT for backward compatibility (deprecated)
2071
2132
  context.appEventMachine.send({ type: 'ACTIONS_TIMEOUT', timeout: msecs, nonResponderIds, canceled: true })
2072
2133
  }
2073
2134
  },
@@ -2100,9 +2161,6 @@ const AnearEventMachineFunctions = ({
2100
2161
 
2101
2162
  logger.debug(`[AEM] cleaning up exiting participant ${participantId}`)
2102
2163
 
2103
- // Clean up timeout counts for exiting participant
2104
- const { [participantId]: removedTimeoutCount, ...remainingTimeoutCounts } = context.consecutiveTimeoutCounts
2105
-
2106
2164
  if (participant) {
2107
2165
  const isOpenHouse = context.anearEvent.openHouse || false
2108
2166
 
@@ -2125,8 +2183,7 @@ const AnearEventMachineFunctions = ({
2125
2183
  ...context.participants,
2126
2184
  [participantId]: updatedParticipant
2127
2185
  },
2128
- participantMachines: remainingMachines,
2129
- consecutiveTimeoutCounts: remainingTimeoutCounts
2186
+ participantMachines: remainingMachines
2130
2187
  }
2131
2188
  } else {
2132
2189
  // For non-open-house events: remove both participant and machine (existing behavior)
@@ -2142,14 +2199,11 @@ const AnearEventMachineFunctions = ({
2142
2199
 
2143
2200
  return {
2144
2201
  participants: remainingParticipants,
2145
- participantMachines: remainingMachines,
2146
- consecutiveTimeoutCounts: remainingTimeoutCounts
2202
+ participantMachines: remainingMachines
2147
2203
  }
2148
2204
  }
2149
2205
  } else {
2150
- return {
2151
- consecutiveTimeoutCounts: remainingTimeoutCounts
2152
- }
2206
+ return {}
2153
2207
  }
2154
2208
  }),
2155
2209
  notifyCoreServiceMachineExit: ({ context }) => {
@@ -102,6 +102,14 @@ const AppMachineTransition = (anearEvent) => {
102
102
  let timeoutFn
103
103
  let displayEvent
104
104
 
105
+ // Extract cancel from meta level (for function-based eachParticipant or top-level cancel)
106
+ // Supports both "cancel" (developer-friendly) and "cancelActionType" (internal)
107
+ if (meta.cancel && !cancelActionType) {
108
+ cancelActionType = meta.cancel
109
+ } else if (meta.cancelActionType && !cancelActionType) {
110
+ cancelActionType = meta.cancelActionType
111
+ }
112
+
105
113
  // Validate that participants: and participant: are not both defined
106
114
  if (meta.allParticipants && meta.eachParticipant) {
107
115
  logger.error(`[AppMachineTransition] Invalid meta configuration: both 'allParticipants' and 'eachParticipant' are defined. Only one can be used per state.`)
@@ -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.1",
4
4
  "description": "Javascript Developer API for Anear Apps",
5
5
  "main": "lib/index.js",
6
6
  "scripts": {