anear-js-api 1.2.2 → 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.
|
@@ -166,7 +166,9 @@ 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
|
+
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
|
|
170
172
|
})
|
|
171
173
|
|
|
172
174
|
const ActiveEventGlobalEvents = {
|
|
@@ -278,7 +280,7 @@ const ActiveEventStatesConfig = {
|
|
|
278
280
|
],
|
|
279
281
|
on: {
|
|
280
282
|
ACTION: {
|
|
281
|
-
actions: ['processParticipantAction']
|
|
283
|
+
actions: ['resetParticipantTimeoutCount', 'processParticipantAction']
|
|
282
284
|
},
|
|
283
285
|
RENDER_DISPLAY: {
|
|
284
286
|
target: '#createdRendering'
|
|
@@ -447,7 +449,7 @@ const ActiveEventStatesConfig = {
|
|
|
447
449
|
initial: 'transitioning',
|
|
448
450
|
on: {
|
|
449
451
|
ACTION: {
|
|
450
|
-
actions: ['processParticipantAction']
|
|
452
|
+
actions: ['resetParticipantTimeoutCount', 'processParticipantAction']
|
|
451
453
|
},
|
|
452
454
|
RENDER_DISPLAY: {
|
|
453
455
|
target: '#announceRendering'
|
|
@@ -629,6 +631,11 @@ const ActiveEventStatesConfig = {
|
|
|
629
631
|
},
|
|
630
632
|
CANCEL_ALL_PARTICIPANT_TIMEOUTS: {
|
|
631
633
|
actions: 'cancelAllParticipantTimeouts'
|
|
634
|
+
},
|
|
635
|
+
// Handle PARTICIPANT_TIMEOUT even when in liveRendering state
|
|
636
|
+
// (events bubble up from child states, so we need to handle them here)
|
|
637
|
+
PARTICIPANT_TIMEOUT: {
|
|
638
|
+
actions: ['trackParticipantTimeout', 'processParticipantTimeout']
|
|
632
639
|
}
|
|
633
640
|
},
|
|
634
641
|
states: {
|
|
@@ -677,11 +684,11 @@ const ActiveEventStatesConfig = {
|
|
|
677
684
|
},
|
|
678
685
|
on: {
|
|
679
686
|
ACTION: {
|
|
680
|
-
actions: ['processParticipantAction']
|
|
687
|
+
actions: ['resetParticipantTimeoutCount', 'processParticipantAction']
|
|
681
688
|
},
|
|
682
689
|
// Forward per-participant timeouts (from APM) to the AppM
|
|
683
690
|
PARTICIPANT_TIMEOUT: {
|
|
684
|
-
actions: ['processParticipantTimeout'],
|
|
691
|
+
actions: ['trackParticipantTimeout', 'processParticipantTimeout'],
|
|
685
692
|
// v5 note: internal transitions are the default (v4 had `internal: true`)
|
|
686
693
|
},
|
|
687
694
|
TRIGGER_CANCEL_SEQUENCE: {
|
|
@@ -735,7 +742,7 @@ const ActiveEventStatesConfig = {
|
|
|
735
742
|
always: {
|
|
736
743
|
guard: 'allParticipantsResponded',
|
|
737
744
|
target: 'waitingForActions',
|
|
738
|
-
actions: ['clearParticipantsTimeout']
|
|
745
|
+
actions: ['sendActionsCompleteToAppM', 'clearParticipantsTimeout']
|
|
739
746
|
},
|
|
740
747
|
after: {
|
|
741
748
|
participantsActionTimeout: {
|
|
@@ -794,7 +801,7 @@ const ActiveEventStatesConfig = {
|
|
|
794
801
|
target: 'handleParticipantsTimeoutCanceled'
|
|
795
802
|
},
|
|
796
803
|
{
|
|
797
|
-
actions: ['processAndForwardAction', 'processParticipantResponse'],
|
|
804
|
+
actions: ['resetParticipantTimeoutCount', 'processAndForwardAction', 'processParticipantResponse'],
|
|
798
805
|
// v5 note: internal transitions are the default (v4 had `internal: true`)
|
|
799
806
|
}
|
|
800
807
|
],
|
|
@@ -1831,6 +1838,20 @@ const AnearEventMachineFunctions = ({
|
|
|
1831
1838
|
const eventName = getPresenceEventName(participant, 'UPDATE');
|
|
1832
1839
|
context.appEventMachine.send({ type: eventName, data: event.data });
|
|
1833
1840
|
},
|
|
1841
|
+
resetParticipantTimeoutCount: assign(({ context, event }) => {
|
|
1842
|
+
const participantId = event.data.participantId
|
|
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
|
+
}
|
|
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
|
|
1854
|
+
}),
|
|
1834
1855
|
processParticipantAction: ({ context, event, self }) => {
|
|
1835
1856
|
// event.data.participantId,
|
|
1836
1857
|
// event.data.payload: {"appEventMachineACTION": {action event keys and values}}
|
|
@@ -1916,7 +1937,18 @@ const AnearEventMachineFunctions = ({
|
|
|
1916
1937
|
}
|
|
1917
1938
|
}
|
|
1918
1939
|
}),
|
|
1919
|
-
|
|
1940
|
+
trackParticipantTimeout: assign(({ context, event }) => {
|
|
1941
|
+
const participantId = event.participantId
|
|
1942
|
+
const currentCount = context.consecutiveTimeoutCount || 0
|
|
1943
|
+
const newCount = currentCount + 1
|
|
1944
|
+
|
|
1945
|
+
logger.debug(`[AEM] Global timeout count (any participant): ${currentCount} -> ${newCount} (timeout from ${participantId})`)
|
|
1946
|
+
|
|
1947
|
+
return {
|
|
1948
|
+
consecutiveTimeoutCount: newCount
|
|
1949
|
+
}
|
|
1950
|
+
}),
|
|
1951
|
+
processParticipantTimeout: ({ context, event, self }) => {
|
|
1920
1952
|
// Suppress PARTICIPANT_TIMEOUT events after cancelAllParticipantTimeouts() until new timers
|
|
1921
1953
|
// are set up via render. This handles race conditions where an APM's timer fires just
|
|
1922
1954
|
// before receiving CANCEL_TIMEOUT. The flag is cleared when renders complete (new timers set up).
|
|
@@ -1924,7 +1956,20 @@ const AnearEventMachineFunctions = ({
|
|
|
1924
1956
|
logger.debug(`[AEM] Suppressing PARTICIPANT_TIMEOUT from ${event.participantId} (cancelled timers, waiting for new render)`)
|
|
1925
1957
|
return
|
|
1926
1958
|
}
|
|
1927
|
-
|
|
1959
|
+
|
|
1960
|
+
const participantId = event.participantId
|
|
1961
|
+
const timeoutCount = context.consecutiveTimeoutCount || 0
|
|
1962
|
+
const maxConsecutiveTimeouts = C.DEAD_MAN_SWITCH.MAX_CONSECUTIVE_PARTICIPANT_TIMEOUTS
|
|
1963
|
+
|
|
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
|
|
1966
|
+
if (timeoutCount >= maxConsecutiveTimeouts) {
|
|
1967
|
+
logger.warn(`[AEM] Dead-man switch triggered: ${timeoutCount} consecutive timeouts across all participants (threshold: ${maxConsecutiveTimeouts}). Auto-canceling event.`)
|
|
1968
|
+
self.send({ type: 'CANCEL' })
|
|
1969
|
+
return
|
|
1970
|
+
}
|
|
1971
|
+
|
|
1972
|
+
context.appEventMachine.send({ type: 'PARTICIPANT_TIMEOUT', participantId })
|
|
1928
1973
|
},
|
|
1929
1974
|
setupParticipantsTimeout: assign(({ context, event }) => {
|
|
1930
1975
|
// Only set up a new timeout if one is provided by the display render workflow
|
|
@@ -2001,21 +2046,70 @@ const AnearEventMachineFunctions = ({
|
|
|
2001
2046
|
}
|
|
2002
2047
|
};
|
|
2003
2048
|
}),
|
|
2004
|
-
|
|
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 }) => {
|
|
2005
2060
|
const { nonResponders, msecs } = context.participantsActionTimeout
|
|
2006
2061
|
const nonResponderIds = [...nonResponders]
|
|
2062
|
+
const allActiveParticipantIds = getActiveParticipantIds(context)
|
|
2063
|
+
|
|
2007
2064
|
logger.info(`[AEM] allParticipants timeout expired after ${msecs}ms. Non-responders: ${nonResponderIds.join(', ')}`)
|
|
2008
2065
|
|
|
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
|
+
}
|
|
2089
|
+
}
|
|
2090
|
+
|
|
2009
2091
|
if (context.appEventMachine) {
|
|
2092
|
+
// Send ACTIONS_TIMEOUT when timeout expires
|
|
2093
|
+
logger.info(`[AEM] Sending ACTIONS_TIMEOUT (timeout expired)`)
|
|
2010
2094
|
context.appEventMachine.send({ type: 'ACTIONS_TIMEOUT', timeout: msecs, nonResponderIds })
|
|
2011
2095
|
}
|
|
2012
|
-
|
|
2096
|
+
|
|
2097
|
+
return updates
|
|
2098
|
+
}),
|
|
2013
2099
|
sendActionsTimeoutToAppMCanceled: ({ context }) => {
|
|
2014
2100
|
const { nonResponders, msecs } = context.participantsActionTimeout
|
|
2015
2101
|
const nonResponderIds = [...nonResponders]
|
|
2016
2102
|
logger.info(`[AEM] allParticipants timeout cancelled after ${msecs}ms. Non-responders at cancellation: ${nonResponderIds.join(', ')}`)
|
|
2017
2103
|
|
|
2018
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)
|
|
2019
2113
|
context.appEventMachine.send({ type: 'ACTIONS_TIMEOUT', timeout: msecs, nonResponderIds, canceled: true })
|
|
2020
2114
|
}
|
|
2021
2115
|
},
|
package/lib/utils/Constants.js
CHANGED
|
@@ -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',
|
|
@@ -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 = {
|