anear-js-api 1.2.2 → 1.2.3

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.
@@ -166,7 +166,8 @@ const AnearEventMachineContext = (
166
166
  suppressParticipantTimeouts: false, // Flag to suppress PARTICIPANT_TIMEOUT events (cleared when new timers are set up via render)
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
- pendingCancelAction: null // ACTION to forward after cancellation completes: { participantId, appEventName, payload }
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
171
  })
171
172
 
172
173
  const ActiveEventGlobalEvents = {
@@ -278,7 +279,7 @@ const ActiveEventStatesConfig = {
278
279
  ],
279
280
  on: {
280
281
  ACTION: {
281
- actions: ['processParticipantAction']
282
+ actions: ['resetParticipantTimeoutCount', 'processParticipantAction']
282
283
  },
283
284
  RENDER_DISPLAY: {
284
285
  target: '#createdRendering'
@@ -447,7 +448,7 @@ const ActiveEventStatesConfig = {
447
448
  initial: 'transitioning',
448
449
  on: {
449
450
  ACTION: {
450
- actions: ['processParticipantAction']
451
+ actions: ['resetParticipantTimeoutCount', 'processParticipantAction']
451
452
  },
452
453
  RENDER_DISPLAY: {
453
454
  target: '#announceRendering'
@@ -629,6 +630,11 @@ const ActiveEventStatesConfig = {
629
630
  },
630
631
  CANCEL_ALL_PARTICIPANT_TIMEOUTS: {
631
632
  actions: 'cancelAllParticipantTimeouts'
633
+ },
634
+ // Handle PARTICIPANT_TIMEOUT even when in liveRendering state
635
+ // (events bubble up from child states, so we need to handle them here)
636
+ PARTICIPANT_TIMEOUT: {
637
+ actions: ['trackParticipantTimeout', 'processParticipantTimeout']
632
638
  }
633
639
  },
634
640
  states: {
@@ -681,7 +687,7 @@ const ActiveEventStatesConfig = {
681
687
  },
682
688
  // Forward per-participant timeouts (from APM) to the AppM
683
689
  PARTICIPANT_TIMEOUT: {
684
- actions: ['processParticipantTimeout'],
690
+ actions: ['trackParticipantTimeout', 'processParticipantTimeout'],
685
691
  // v5 note: internal transitions are the default (v4 had `internal: true`)
686
692
  },
687
693
  TRIGGER_CANCEL_SEQUENCE: {
@@ -794,7 +800,7 @@ const ActiveEventStatesConfig = {
794
800
  target: 'handleParticipantsTimeoutCanceled'
795
801
  },
796
802
  {
797
- actions: ['processAndForwardAction', 'processParticipantResponse'],
803
+ actions: ['resetParticipantTimeoutCount', 'processAndForwardAction', 'processParticipantResponse'],
798
804
  // v5 note: internal transitions are the default (v4 had `internal: true`)
799
805
  }
800
806
  ],
@@ -1831,6 +1837,17 @@ const AnearEventMachineFunctions = ({
1831
1837
  const eventName = getPresenceEventName(participant, 'UPDATE');
1832
1838
  context.appEventMachine.send({ type: eventName, data: event.data });
1833
1839
  },
1840
+ resetParticipantTimeoutCount: assign(({ context, event }) => {
1841
+ 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 }
1848
+ }
1849
+ return {}
1850
+ }),
1834
1851
  processParticipantAction: ({ context, event, self }) => {
1835
1852
  // event.data.participantId,
1836
1853
  // event.data.payload: {"appEventMachineACTION": {action event keys and values}}
@@ -1916,7 +1933,21 @@ const AnearEventMachineFunctions = ({
1916
1933
  }
1917
1934
  }
1918
1935
  }),
1919
- processParticipantTimeout: ({ context, event }) => {
1936
+ trackParticipantTimeout: assign(({ context, event }) => {
1937
+ const participantId = event.participantId
1938
+ const currentCount = context.consecutiveTimeoutCounts[participantId] || 0
1939
+ const newCount = currentCount + 1
1940
+
1941
+ logger.debug(`[AEM] Participant ${participantId} timeout count: ${currentCount} -> ${newCount}`)
1942
+
1943
+ return {
1944
+ consecutiveTimeoutCounts: {
1945
+ ...context.consecutiveTimeoutCounts,
1946
+ [participantId]: newCount
1947
+ }
1948
+ }
1949
+ }),
1950
+ processParticipantTimeout: ({ context, event, self }) => {
1920
1951
  // Suppress PARTICIPANT_TIMEOUT events after cancelAllParticipantTimeouts() until new timers
1921
1952
  // are set up via render. This handles race conditions where an APM's timer fires just
1922
1953
  // before receiving CANCEL_TIMEOUT. The flag is cleared when renders complete (new timers set up).
@@ -1924,7 +1955,19 @@ const AnearEventMachineFunctions = ({
1924
1955
  logger.debug(`[AEM] Suppressing PARTICIPANT_TIMEOUT from ${event.participantId} (cancelled timers, waiting for new render)`)
1925
1956
  return
1926
1957
  }
1927
- context.appEventMachine.send({ type: 'PARTICIPANT_TIMEOUT', participantId: event.participantId })
1958
+
1959
+ const participantId = event.participantId
1960
+ const timeoutCount = context.consecutiveTimeoutCounts[participantId] || 0
1961
+ const maxConsecutiveTimeouts = C.DEAD_MAN_SWITCH.MAX_CONSECUTIVE_PARTICIPANT_TIMEOUTS
1962
+
1963
+ // Check dead-man switch: if participant has exceeded consecutive timeout threshold, cancel event
1964
+ if (timeoutCount >= maxConsecutiveTimeouts) {
1965
+ logger.warn(`[AEM] Dead-man switch triggered: Participant ${participantId} has ${timeoutCount} consecutive timeouts (threshold: ${maxConsecutiveTimeouts}). Auto-canceling event.`)
1966
+ self.send({ type: 'CANCEL' })
1967
+ return
1968
+ }
1969
+
1970
+ context.appEventMachine.send({ type: 'PARTICIPANT_TIMEOUT', participantId })
1928
1971
  },
1929
1972
  setupParticipantsTimeout: assign(({ context, event }) => {
1930
1973
  // Only set up a new timeout if one is provided by the display render workflow
@@ -2001,11 +2044,20 @@ const AnearEventMachineFunctions = ({
2001
2044
  }
2002
2045
  };
2003
2046
  }),
2004
- sendActionsTimeoutToAppM: ({ context }) => {
2047
+ sendActionsTimeoutToAppM: ({ context, self }) => {
2005
2048
  const { nonResponders, msecs } = context.participantsActionTimeout
2006
2049
  const nonResponderIds = [...nonResponders]
2050
+ const allActiveParticipantIds = getActiveParticipantIds(context)
2051
+
2007
2052
  logger.info(`[AEM] allParticipants timeout expired after ${msecs}ms. Non-responders: ${nonResponderIds.join(', ')}`)
2008
2053
 
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
2059
+ }
2060
+
2009
2061
  if (context.appEventMachine) {
2010
2062
  context.appEventMachine.send({ type: 'ACTIONS_TIMEOUT', timeout: msecs, nonResponderIds })
2011
2063
  }
@@ -2048,6 +2100,9 @@ const AnearEventMachineFunctions = ({
2048
2100
 
2049
2101
  logger.debug(`[AEM] cleaning up exiting participant ${participantId}`)
2050
2102
 
2103
+ // Clean up timeout counts for exiting participant
2104
+ const { [participantId]: removedTimeoutCount, ...remainingTimeoutCounts } = context.consecutiveTimeoutCounts
2105
+
2051
2106
  if (participant) {
2052
2107
  const isOpenHouse = context.anearEvent.openHouse || false
2053
2108
 
@@ -2070,7 +2125,8 @@ const AnearEventMachineFunctions = ({
2070
2125
  ...context.participants,
2071
2126
  [participantId]: updatedParticipant
2072
2127
  },
2073
- participantMachines: remainingMachines
2128
+ participantMachines: remainingMachines,
2129
+ consecutiveTimeoutCounts: remainingTimeoutCounts
2074
2130
  }
2075
2131
  } else {
2076
2132
  // For non-open-house events: remove both participant and machine (existing behavior)
@@ -2086,11 +2142,14 @@ const AnearEventMachineFunctions = ({
2086
2142
 
2087
2143
  return {
2088
2144
  participants: remainingParticipants,
2089
- participantMachines: remainingMachines
2145
+ participantMachines: remainingMachines,
2146
+ consecutiveTimeoutCounts: remainingTimeoutCounts
2090
2147
  }
2091
2148
  }
2092
2149
  } else {
2093
- return {}
2150
+ return {
2151
+ consecutiveTimeoutCounts: remainingTimeoutCounts
2152
+ }
2094
2153
  }
2095
2154
  }),
2096
2155
  notifyCoreServiceMachineExit: ({ context }) => {
@@ -16,6 +16,10 @@ module.exports = {
16
16
  RECONNECT: 30 * 1000, // 30 seconds
17
17
  BOOT_EXIT: 2 * 1000 // 5 seconds
18
18
  },
19
+ DEAD_MAN_SWITCH: {
20
+ // Maximum consecutive timeouts per participant before auto-canceling event
21
+ MAX_CONSECUTIVE_PARTICIPANT_TIMEOUTS: 4
22
+ },
19
23
  EventStates: {
20
24
  CREATED: 'created',
21
25
  STARTED: 'started',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "anear-js-api",
3
- "version": "1.2.2",
3
+ "version": "1.2.3",
4
4
  "description": "Javascript Developer API for Anear Apps",
5
5
  "main": "lib/index.js",
6
6
  "scripts": {