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
|
-
|
|
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
|
-
|
|
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: ['
|
|
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
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
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
|
-
|
|
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.
|
|
1961
|
+
const currentCount = context.consecutiveTimeoutCount || 0
|
|
1939
1962
|
const newCount = currentCount + 1
|
|
1940
1963
|
|
|
1941
|
-
logger.debug(`[AEM]
|
|
1964
|
+
logger.debug(`[AEM] Global timeout count (any participant): ${currentCount} -> ${newCount} (timeout from ${participantId})`)
|
|
1942
1965
|
|
|
1943
1966
|
return {
|
|
1944
|
-
|
|
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.
|
|
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
|
|
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:
|
|
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
|
-
|
|
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
|
-
//
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
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
|
-
|
|
369
|
-
|
|
370
|
-
|
|
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 = {
|