anear-js-api 1.2.1 → 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.
- package/lib/api/ApiService.js +1 -1
- package/lib/models/AnearEvent.js +8 -0
- package/lib/state_machines/AnearEventMachine.js +391 -24
- package/lib/state_machines/AnearParticipantMachine.js +87 -30
- package/lib/utils/AppMachineTransition.js +22 -2
- package/lib/utils/Constants.js +4 -0
- package/lib/utils/DisplayEventProcessor.js +2 -2
- package/package.json +1 -1
- package/tests/JsonApiArrayResource.test.js +1 -1
package/lib/api/ApiService.js
CHANGED
|
@@ -101,7 +101,7 @@ class ApiService {
|
|
|
101
101
|
}
|
|
102
102
|
|
|
103
103
|
prepareGetRequest(resource, params={}) {
|
|
104
|
-
const urlString = this.idParamUrlString(resource, params)
|
|
104
|
+
const urlString = this.idParamUrlString(resource, params)
|
|
105
105
|
return new fetch.Request(
|
|
106
106
|
urlString, {
|
|
107
107
|
method: 'GET',
|
package/lib/models/AnearEvent.js
CHANGED
|
@@ -97,6 +97,14 @@ class AnearEvent extends JsonApiResource {
|
|
|
97
97
|
this.send({ type: "BOOT_PARTICIPANT", data: { participantId, reason } })
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
+
cancelAllParticipantTimeouts() {
|
|
101
|
+
// Cancel all active participant-level timeouts. This sends CANCEL_TIMEOUT
|
|
102
|
+
// to all APMs, which will benignly clear any active timeout timers.
|
|
103
|
+
// Useful for scenarios where a game event (e.g., LIAR call) should immediately
|
|
104
|
+
// cancel all individual participant timeouts.
|
|
105
|
+
this.send({ type: "CANCEL_ALL_PARTICIPANT_TIMEOUTS" })
|
|
106
|
+
}
|
|
107
|
+
|
|
100
108
|
render(viewPath, displayType, appContext, event, timeout = null, props = {}) {
|
|
101
109
|
// Explicit render method for guaranteed rendering control
|
|
102
110
|
// This complements the meta: {} approach for when you need explicit control
|
|
@@ -162,7 +162,12 @@ const AnearEventMachineContext = (
|
|
|
162
162
|
spectatorsDisplayChannel: null, // display all spectators
|
|
163
163
|
participants: {},
|
|
164
164
|
participantMachines: {},
|
|
165
|
-
participantsActionTimeout: null
|
|
165
|
+
participantsActionTimeout: null,
|
|
166
|
+
suppressParticipantTimeouts: false, // Flag to suppress PARTICIPANT_TIMEOUT events (cleared when new timers are set up via render)
|
|
167
|
+
eachParticipantCancelActionType: null, // ACTION type that triggers cancellation for eachParticipant timeouts
|
|
168
|
+
pendingCancelConfirmations: null, // Set of participantIds waiting for TIMER_CANCELED confirmation
|
|
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
|
|
166
171
|
})
|
|
167
172
|
|
|
168
173
|
const ActiveEventGlobalEvents = {
|
|
@@ -274,7 +279,7 @@ const ActiveEventStatesConfig = {
|
|
|
274
279
|
],
|
|
275
280
|
on: {
|
|
276
281
|
ACTION: {
|
|
277
|
-
actions: ['processParticipantAction']
|
|
282
|
+
actions: ['resetParticipantTimeoutCount', 'processParticipantAction']
|
|
278
283
|
},
|
|
279
284
|
RENDER_DISPLAY: {
|
|
280
285
|
target: '#createdRendering'
|
|
@@ -378,6 +383,7 @@ const ActiveEventStatesConfig = {
|
|
|
378
383
|
input: ({ context, event }) => ({ context, event }),
|
|
379
384
|
onDone: {
|
|
380
385
|
target: 'notifyingPausedRenderComplete',
|
|
386
|
+
actions: ['clearParticipantTimeoutSuppression']
|
|
381
387
|
// v5 note: internal transitions are the default (v4 had `internal: true`)
|
|
382
388
|
},
|
|
383
389
|
onError: {
|
|
@@ -416,6 +422,7 @@ const ActiveEventStatesConfig = {
|
|
|
416
422
|
input: ({ context, event }) => ({ context, event }),
|
|
417
423
|
onDone: {
|
|
418
424
|
target: 'notifyingRenderComplete',
|
|
425
|
+
actions: ['clearParticipantTimeoutSuppression']
|
|
419
426
|
// v5 note: internal transitions are the default (v4 had `internal: true`)
|
|
420
427
|
},
|
|
421
428
|
onError: {
|
|
@@ -441,7 +448,7 @@ const ActiveEventStatesConfig = {
|
|
|
441
448
|
initial: 'transitioning',
|
|
442
449
|
on: {
|
|
443
450
|
ACTION: {
|
|
444
|
-
actions: ['processParticipantAction']
|
|
451
|
+
actions: ['resetParticipantTimeoutCount', 'processParticipantAction']
|
|
445
452
|
},
|
|
446
453
|
RENDER_DISPLAY: {
|
|
447
454
|
target: '#announceRendering'
|
|
@@ -484,6 +491,9 @@ const ActiveEventStatesConfig = {
|
|
|
484
491
|
BOOT_PARTICIPANT: {
|
|
485
492
|
actions: 'sendBootEventToParticipantMachine'
|
|
486
493
|
},
|
|
494
|
+
CANCEL_ALL_PARTICIPANT_TIMEOUTS: {
|
|
495
|
+
actions: 'cancelAllParticipantTimeouts'
|
|
496
|
+
},
|
|
487
497
|
SPECTATOR_ENTER: {
|
|
488
498
|
actions: 'sendSpectatorEnterToAppEventMachine'
|
|
489
499
|
}
|
|
@@ -527,6 +537,7 @@ const ActiveEventStatesConfig = {
|
|
|
527
537
|
input: ({ context, event }) => ({ context, event }),
|
|
528
538
|
onDone: {
|
|
529
539
|
target: 'notifyingRenderComplete',
|
|
540
|
+
actions: ['clearParticipantTimeoutSuppression']
|
|
530
541
|
// v5 note: internal transitions are the default (v4 had `internal: true`)
|
|
531
542
|
},
|
|
532
543
|
onError: {
|
|
@@ -616,6 +627,14 @@ const ActiveEventStatesConfig = {
|
|
|
616
627
|
},
|
|
617
628
|
BOOT_PARTICIPANT: {
|
|
618
629
|
actions: 'sendBootEventToParticipantMachine'
|
|
630
|
+
},
|
|
631
|
+
CANCEL_ALL_PARTICIPANT_TIMEOUTS: {
|
|
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']
|
|
619
638
|
}
|
|
620
639
|
},
|
|
621
640
|
states: {
|
|
@@ -634,14 +653,49 @@ const ActiveEventStatesConfig = {
|
|
|
634
653
|
waitingForActions: {
|
|
635
654
|
id: 'waitingForActions',
|
|
636
655
|
entry: () => logger.debug('[AEM] live state...waiting for actions'),
|
|
656
|
+
initial: 'idle',
|
|
657
|
+
states: {
|
|
658
|
+
idle: {},
|
|
659
|
+
waitingForCancelConfirmations: {
|
|
660
|
+
entry: ['sendCancelTimeoutToAllAPMs', 'setupPendingCancelConfirmations'],
|
|
661
|
+
on: {
|
|
662
|
+
TIMER_CANCELED: {
|
|
663
|
+
actions: ['removeFromPendingCancelConfirmations']
|
|
664
|
+
},
|
|
665
|
+
PARTICIPANT_EXIT: {
|
|
666
|
+
actions: ['removeFromPendingCancelConfirmations']
|
|
667
|
+
},
|
|
668
|
+
// Drop only ACTION and PARTICIPANT_TIMEOUT events during cancellation wait
|
|
669
|
+
// All other events (presence events, etc.) will bubble up to parent handlers
|
|
670
|
+
ACTION: {
|
|
671
|
+
actions: ['dropEventDuringCancellation']
|
|
672
|
+
},
|
|
673
|
+
PARTICIPANT_TIMEOUT: {
|
|
674
|
+
actions: ['dropEventDuringCancellation']
|
|
675
|
+
}
|
|
676
|
+
},
|
|
677
|
+
always: {
|
|
678
|
+
guard: 'allCancelConfirmationsReceived',
|
|
679
|
+
actions: ['forwardPendingCancelAction', 'clearCancelState'],
|
|
680
|
+
target: '#waitingForActions.idle'
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
},
|
|
637
684
|
on: {
|
|
638
685
|
ACTION: {
|
|
639
686
|
actions: ['processParticipantAction']
|
|
640
687
|
},
|
|
641
688
|
// Forward per-participant timeouts (from APM) to the AppM
|
|
642
689
|
PARTICIPANT_TIMEOUT: {
|
|
643
|
-
actions: ['processParticipantTimeout'],
|
|
690
|
+
actions: ['trackParticipantTimeout', 'processParticipantTimeout'],
|
|
644
691
|
// v5 note: internal transitions are the default (v4 had `internal: true`)
|
|
692
|
+
},
|
|
693
|
+
TRIGGER_CANCEL_SEQUENCE: {
|
|
694
|
+
target: '.waitingForCancelConfirmations'
|
|
695
|
+
},
|
|
696
|
+
TIMER_CANCELED: {
|
|
697
|
+
// Ignore if not in cancellation state (defensive)
|
|
698
|
+
actions: []
|
|
645
699
|
}
|
|
646
700
|
}
|
|
647
701
|
},
|
|
@@ -654,10 +708,11 @@ const ActiveEventStatesConfig = {
|
|
|
654
708
|
{
|
|
655
709
|
guard: 'isParticipantsTimeoutActive',
|
|
656
710
|
target: 'notifyingRenderCompleteWithTimeout',
|
|
657
|
-
actions: 'setupParticipantsTimeout'
|
|
711
|
+
actions: ['setupParticipantsTimeout', 'setupCancelActionType', 'clearParticipantTimeoutSuppression']
|
|
658
712
|
},
|
|
659
713
|
{
|
|
660
714
|
target: 'notifyingRenderComplete',
|
|
715
|
+
actions: ['setupCancelActionType', 'clearParticipantTimeoutSuppression']
|
|
661
716
|
// v5 note: internal transitions are the default (v4 had `internal: true`)
|
|
662
717
|
}
|
|
663
718
|
],
|
|
@@ -693,13 +748,70 @@ const ActiveEventStatesConfig = {
|
|
|
693
748
|
target: 'handleParticipantsTimeout'
|
|
694
749
|
}
|
|
695
750
|
},
|
|
751
|
+
initial: 'waiting',
|
|
752
|
+
states: {
|
|
753
|
+
waiting: {
|
|
754
|
+
// The actual waiting state. Timer continues running when we transition to .rendering.
|
|
755
|
+
},
|
|
756
|
+
rendering: {
|
|
757
|
+
invoke: {
|
|
758
|
+
src: 'renderDisplay',
|
|
759
|
+
input: ({ context, event }) => ({ context, event }),
|
|
760
|
+
onDone: [
|
|
761
|
+
{
|
|
762
|
+
guard: 'isParticipantsTimeoutActive',
|
|
763
|
+
target: 'notifyingRenderComplete',
|
|
764
|
+
actions: ['setupCancelActionType', 'clearParticipantTimeoutSuppression']
|
|
765
|
+
// Note: We do NOT call setupParticipantsTimeout here because we're already in waitAllParticipantsResponse
|
|
766
|
+
// and the timer is still running from the parent state (preserved by nested state pattern).
|
|
767
|
+
},
|
|
768
|
+
{
|
|
769
|
+
target: '#waitingForActions',
|
|
770
|
+
actions: ['setupCancelActionType', 'clearParticipantTimeoutSuppression', 'notifyAppMachineRendered']
|
|
771
|
+
}
|
|
772
|
+
],
|
|
773
|
+
onError: {
|
|
774
|
+
target: '#activeEvent.failure'
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
},
|
|
778
|
+
notifyingRenderComplete: {
|
|
779
|
+
after: {
|
|
780
|
+
timeoutRendered: {
|
|
781
|
+
// Target sibling state (stays within parent, timer continues)
|
|
782
|
+
target: 'waiting',
|
|
783
|
+
actions: ['notifyAppMachineRendered']
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
},
|
|
696
788
|
on: {
|
|
697
|
-
ACTION:
|
|
698
|
-
|
|
699
|
-
|
|
789
|
+
ACTION: [
|
|
790
|
+
{
|
|
791
|
+
// Check if this is a cancel ACTION for allParticipants timeout
|
|
792
|
+
guard: ({ context, event }) => {
|
|
793
|
+
const { cancelActionType } = context.participantsActionTimeout || {}
|
|
794
|
+
if (!cancelActionType) return false
|
|
795
|
+
const eventMessagePayload = JSON.parse(event.data.payload)
|
|
796
|
+
const [appEventName] = Object.entries(eventMessagePayload)[0]
|
|
797
|
+
return cancelActionType === appEventName
|
|
798
|
+
},
|
|
799
|
+
actions: ['storeCancelActionForAllParticipants'],
|
|
800
|
+
target: 'handleParticipantsTimeoutCanceled'
|
|
801
|
+
},
|
|
802
|
+
{
|
|
803
|
+
actions: ['resetParticipantTimeoutCount', 'processAndForwardAction', 'processParticipantResponse'],
|
|
804
|
+
// v5 note: internal transitions are the default (v4 had `internal: true`)
|
|
805
|
+
}
|
|
806
|
+
],
|
|
807
|
+
CANCEL_ALL_PARTICIPANT_TIMEOUTS: {
|
|
808
|
+
actions: ['cancelAllParticipantTimeouts'],
|
|
809
|
+
target: 'handleParticipantsTimeoutCanceled'
|
|
700
810
|
},
|
|
701
811
|
RENDER_DISPLAY: {
|
|
702
|
-
|
|
812
|
+
// Re-render without exiting waitAllParticipantsResponse.
|
|
813
|
+
// This preserves the parent's after: timer because we stay within the parent state.
|
|
814
|
+
target: '.rendering'
|
|
703
815
|
},
|
|
704
816
|
PARTICIPANT_LEAVE: [
|
|
705
817
|
{
|
|
@@ -720,6 +832,10 @@ const ActiveEventStatesConfig = {
|
|
|
720
832
|
entry: ['sendActionsTimeoutToAppM', 'clearParticipantsTimeout'],
|
|
721
833
|
always: 'waitingForActions'
|
|
722
834
|
},
|
|
835
|
+
handleParticipantsTimeoutCanceled: {
|
|
836
|
+
entry: ['sendActionsTimeoutToAppMCanceled', 'clearParticipantsTimeout', 'forwardPendingCancelActionIfExists'],
|
|
837
|
+
always: 'waitingForActions'
|
|
838
|
+
},
|
|
723
839
|
participantEntering: {
|
|
724
840
|
// a PARTICIPANT_ENTER received from a new user JOIN click. Unless already exists,
|
|
725
841
|
// create an AnearParticipantMachine instance.
|
|
@@ -1591,6 +1707,114 @@ const AnearEventMachineFunctions = ({
|
|
|
1591
1707
|
sendParticipantExitEvents: ({ context }) => {
|
|
1592
1708
|
Object.values(context.participantMachines).forEach(pm => pm.send({ type: 'PARTICIPANT_EXIT' }))
|
|
1593
1709
|
},
|
|
1710
|
+
cancelAllParticipantTimeouts: assign(({ context }) => {
|
|
1711
|
+
// Send CANCEL_TIMEOUT to all participant machines to cancel any active
|
|
1712
|
+
// participant-level timeouts. This is useful when a game event (e.g., LIAR call)
|
|
1713
|
+
// should immediately cancel all individual participant timeouts.
|
|
1714
|
+
// APMs that are not in waitParticipantResponse state will benignly ignore this event.
|
|
1715
|
+
logger.info(`[AEM] Cancelling all participant timeouts (manual) for ${Object.keys(context.participantMachines).length} participants`)
|
|
1716
|
+
Object.values(context.participantMachines).forEach(pm => pm.send({ type: 'CANCEL_TIMEOUT' }))
|
|
1717
|
+
|
|
1718
|
+
// Note: If we're in waitAllParticipantsResponse state, the nested state's handler
|
|
1719
|
+
// will also handle this event and cancel the allParticipants timeout.
|
|
1720
|
+
|
|
1721
|
+
// Set suppression flag to ignore stale PARTICIPANT_TIMEOUT events from cancelled timers.
|
|
1722
|
+
// This flag will be cleared when new timers are set up (via render completion).
|
|
1723
|
+
return {
|
|
1724
|
+
suppressParticipantTimeouts: true
|
|
1725
|
+
}
|
|
1726
|
+
}),
|
|
1727
|
+
sendCancelTimeoutToAllAPMs: ({ context }) => {
|
|
1728
|
+
const participantCount = Object.keys(context.participantMachines).length
|
|
1729
|
+
logger.info(`[AEM] Sending CANCEL_TIMEOUT to all ${participantCount} APMs for declarative cancel ACTION`)
|
|
1730
|
+
Object.values(context.participantMachines).forEach(pm => {
|
|
1731
|
+
pm.send({ type: 'CANCEL_TIMEOUT' })
|
|
1732
|
+
})
|
|
1733
|
+
},
|
|
1734
|
+
setupPendingCancelConfirmations: assign(({ context, event }) => {
|
|
1735
|
+
// Get all active participants with timers (participants in waitParticipantResponse state)
|
|
1736
|
+
const activeParticipantIds = getActiveParticipantIds(context)
|
|
1737
|
+
const pendingConfirmations = new Set(activeParticipantIds)
|
|
1738
|
+
|
|
1739
|
+
logger.info(`[AEM] Starting cancellation sequence for ACTION ${event.data.appEventName}. Waiting for ${pendingConfirmations.size} APM confirmations: ${[...pendingConfirmations].join(', ')}`)
|
|
1740
|
+
|
|
1741
|
+
return {
|
|
1742
|
+
pendingCancelConfirmations: pendingConfirmations,
|
|
1743
|
+
pendingCancelAction: {
|
|
1744
|
+
participantId: event.data.participantId,
|
|
1745
|
+
appEventName: event.data.appEventName,
|
|
1746
|
+
payload: event.data.payload
|
|
1747
|
+
},
|
|
1748
|
+
suppressParticipantTimeouts: true // Drop PARTICIPANT_TIMEOUT events
|
|
1749
|
+
}
|
|
1750
|
+
}),
|
|
1751
|
+
removeFromPendingCancelConfirmations: assign(({ context, event }) => {
|
|
1752
|
+
const participantId = event.participantId || event.data?.id
|
|
1753
|
+
if (!context.pendingCancelConfirmations || !participantId) return {}
|
|
1754
|
+
|
|
1755
|
+
const newPending = new Set(context.pendingCancelConfirmations)
|
|
1756
|
+
newPending.delete(participantId)
|
|
1757
|
+
const remaining = newPending.size
|
|
1758
|
+
|
|
1759
|
+
logger.debug(`[AEM] TIMER_CANCELED received from ${participantId}. ${remaining} confirmations remaining.`)
|
|
1760
|
+
|
|
1761
|
+
return {
|
|
1762
|
+
pendingCancelConfirmations: newPending
|
|
1763
|
+
}
|
|
1764
|
+
}),
|
|
1765
|
+
forwardPendingCancelAction: ({ context }) => {
|
|
1766
|
+
if (!context.pendingCancelAction) {
|
|
1767
|
+
logger.warn(`[AEM] forwardPendingCancelAction called but pendingCancelAction is null`)
|
|
1768
|
+
return
|
|
1769
|
+
}
|
|
1770
|
+
|
|
1771
|
+
const { participantId, appEventName, payload } = context.pendingCancelAction
|
|
1772
|
+
|
|
1773
|
+
const actionEventPayload = {
|
|
1774
|
+
participantId,
|
|
1775
|
+
payload
|
|
1776
|
+
}
|
|
1777
|
+
|
|
1778
|
+
logger.info(`[AEM] All cancellation confirmations received. Forwarding cancel ACTION ${appEventName} from ${participantId} to AppM and APM`)
|
|
1779
|
+
|
|
1780
|
+
// Forward to AppM
|
|
1781
|
+
context.appEventMachine.send({ type: appEventName, ...actionEventPayload })
|
|
1782
|
+
|
|
1783
|
+
// Forward to APM
|
|
1784
|
+
const participantMachine = context.participantMachines[participantId]
|
|
1785
|
+
if (participantMachine) {
|
|
1786
|
+
participantMachine.send({ type: 'ACTION', ...actionEventPayload })
|
|
1787
|
+
} else {
|
|
1788
|
+
logger.warn(`[AEM] Participant machine not found for ${participantId} when forwarding cancel ACTION`)
|
|
1789
|
+
}
|
|
1790
|
+
},
|
|
1791
|
+
clearCancelState: assign({
|
|
1792
|
+
pendingCancelConfirmations: null,
|
|
1793
|
+
pendingCancelAction: null,
|
|
1794
|
+
suppressParticipantTimeouts: false
|
|
1795
|
+
}),
|
|
1796
|
+
dropEventDuringCancellation: ({ context, event }) => {
|
|
1797
|
+
// Drop/ignore ACTION and PARTICIPANT_TIMEOUT events received during cancellation wait
|
|
1798
|
+
// All other events (presence events, etc.) are not handled here and will bubble up to parent handlers
|
|
1799
|
+
logger.debug(`[AEM] Dropping ${event.type} event during cancellation wait`)
|
|
1800
|
+
},
|
|
1801
|
+
clearParticipantTimeoutSuppression: assign({
|
|
1802
|
+
suppressParticipantTimeouts: false
|
|
1803
|
+
}),
|
|
1804
|
+
setupCancelActionType: assign(({ context, event }) => {
|
|
1805
|
+
const cancelActionType =
|
|
1806
|
+
(event && event.output && event.output.cancelActionType) ||
|
|
1807
|
+
(event && event.data && event.data.cancelActionType) ||
|
|
1808
|
+
null
|
|
1809
|
+
|
|
1810
|
+
if (cancelActionType) {
|
|
1811
|
+
logger.debug(`[AEM] Setting up cancel action type for eachParticipant timeout: ${cancelActionType}`)
|
|
1812
|
+
}
|
|
1813
|
+
|
|
1814
|
+
return {
|
|
1815
|
+
eachParticipantCancelActionType: cancelActionType
|
|
1816
|
+
}
|
|
1817
|
+
}),
|
|
1594
1818
|
updateParticipantPresence: ({ context, event }) => {
|
|
1595
1819
|
const participantId = event.data.id;
|
|
1596
1820
|
const participant = context.participants[participantId];
|
|
@@ -1613,7 +1837,18 @@ const AnearEventMachineFunctions = ({
|
|
|
1613
1837
|
const eventName = getPresenceEventName(participant, 'UPDATE');
|
|
1614
1838
|
context.appEventMachine.send({ type: eventName, data: event.data });
|
|
1615
1839
|
},
|
|
1616
|
-
|
|
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
|
+
}),
|
|
1851
|
+
processParticipantAction: ({ context, event, self }) => {
|
|
1617
1852
|
// event.data.participantId,
|
|
1618
1853
|
// event.data.payload: {"appEventMachineACTION": {action event keys and values}}
|
|
1619
1854
|
// e.g. {"MOVE":{"x":1, "y":2}}
|
|
@@ -1623,7 +1858,25 @@ const AnearEventMachineFunctions = ({
|
|
|
1623
1858
|
const [appEventName, payload] = Object.entries(eventMessagePayload)[0]
|
|
1624
1859
|
|
|
1625
1860
|
logger.debug(`[AEM] got Event ${appEventName} from payload from participant ${participantId}`)
|
|
1861
|
+
logger.debug(`[AEM] eachParticipantCancelActionType is: ${context.eachParticipantCancelActionType}`)
|
|
1862
|
+
|
|
1863
|
+
// Check if this ACTION should trigger cancellation for eachParticipant timeout
|
|
1864
|
+
if (context.eachParticipantCancelActionType === appEventName) {
|
|
1865
|
+
logger.info(`[AEM] Intercepting cancel ACTION ${appEventName} from ${participantId} (eachParticipant timeout cancellation)`)
|
|
1866
|
+
// Trigger cancellation sequence via self-send
|
|
1867
|
+
self.send({
|
|
1868
|
+
type: 'TRIGGER_CANCEL_SEQUENCE',
|
|
1869
|
+
data: {
|
|
1870
|
+
participantId,
|
|
1871
|
+
appEventName,
|
|
1872
|
+
payload,
|
|
1873
|
+
event // Store original event for reference
|
|
1874
|
+
}
|
|
1875
|
+
})
|
|
1876
|
+
return // Don't forward yet, wait for cancellation to complete
|
|
1877
|
+
}
|
|
1626
1878
|
|
|
1879
|
+
// Normal flow: forward immediately
|
|
1627
1880
|
const actionEventPayload = {
|
|
1628
1881
|
participantId,
|
|
1629
1882
|
payload
|
|
@@ -1643,6 +1896,9 @@ const AnearEventMachineFunctions = ({
|
|
|
1643
1896
|
// This assumes the current participant IS a non-responder.
|
|
1644
1897
|
const isFinalAction = nonResponders.size === 1 && nonResponders.has(participantId);
|
|
1645
1898
|
|
|
1899
|
+
if (isFinalAction) {
|
|
1900
|
+
logger.debug(`[AEM] Final ACTION received from ${participantId}, allParticipants timeout cancelled (all responded)`)
|
|
1901
|
+
}
|
|
1646
1902
|
logger.info(`[AEM] Participants FINAL ACTION is ${isFinalAction}`)
|
|
1647
1903
|
|
|
1648
1904
|
// Forward to AppM with the finalAction flag
|
|
@@ -1662,10 +1918,61 @@ const AnearEventMachineFunctions = ({
|
|
|
1662
1918
|
participantMachine.send({ type: 'ACTION', ...apm_Payload });
|
|
1663
1919
|
}
|
|
1664
1920
|
},
|
|
1665
|
-
|
|
1666
|
-
|
|
1921
|
+
storeCancelActionForAllParticipants: assign(({ context, event }) => {
|
|
1922
|
+
const participantId = event.data.participantId
|
|
1923
|
+
const eventMessagePayload = JSON.parse(event.data.payload)
|
|
1924
|
+
const [appEventName, payload] = Object.entries(eventMessagePayload)[0]
|
|
1925
|
+
|
|
1926
|
+
logger.info(`[AEM] Intercepting cancel ACTION ${appEventName} for allParticipants timeout from ${participantId}`)
|
|
1927
|
+
|
|
1928
|
+
return {
|
|
1929
|
+
pendingCancelAction: {
|
|
1930
|
+
participantId,
|
|
1931
|
+
appEventName,
|
|
1932
|
+
payload
|
|
1933
|
+
}
|
|
1934
|
+
}
|
|
1935
|
+
}),
|
|
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 }) => {
|
|
1951
|
+
// Suppress PARTICIPANT_TIMEOUT events after cancelAllParticipantTimeouts() until new timers
|
|
1952
|
+
// are set up via render. This handles race conditions where an APM's timer fires just
|
|
1953
|
+
// before receiving CANCEL_TIMEOUT. The flag is cleared when renders complete (new timers set up).
|
|
1954
|
+
if (context.suppressParticipantTimeouts) {
|
|
1955
|
+
logger.debug(`[AEM] Suppressing PARTICIPANT_TIMEOUT from ${event.participantId} (cancelled timers, waiting for new render)`)
|
|
1956
|
+
return
|
|
1957
|
+
}
|
|
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 })
|
|
1971
|
+
},
|
|
1667
1972
|
setupParticipantsTimeout: assign(({ context, event }) => {
|
|
1668
|
-
// Only set up a new timeout if one is provided by the display render workflow
|
|
1973
|
+
// Only set up a new timeout if one is provided by the display render workflow
|
|
1974
|
+
// AND we don't already have an active timeout (i.e., we're entering from waitingForActions,
|
|
1975
|
+
// not re-rendering during waitAllParticipantsResponse).
|
|
1669
1976
|
//
|
|
1670
1977
|
// v5 note: `renderDisplay` is implemented as a Promise actor (fromPromise).
|
|
1671
1978
|
// The actor's resolved value is delivered on the done event as `event.output`
|
|
@@ -1676,21 +1983,29 @@ const AnearEventMachineFunctions = ({
|
|
|
1676
1983
|
(event && event.data && event.data.participantsTimeout) ||
|
|
1677
1984
|
null
|
|
1678
1985
|
|
|
1679
|
-
//
|
|
1680
|
-
|
|
1986
|
+
// Only set up a NEW timeout if one is provided and we don't already have an active timeout.
|
|
1987
|
+
// If we're re-rendering during waitAllParticipantsResponse, the timer is still running
|
|
1988
|
+
// from the parent state (preserved by nested state pattern), so we don't overwrite it.
|
|
1989
|
+
if (participantsTimeout && !context.participantsActionTimeout) {
|
|
1681
1990
|
const timeoutMsecs = participantsTimeout.msecs
|
|
1682
1991
|
const allParticipantIds = getActiveParticipantIds(context)
|
|
1683
|
-
|
|
1992
|
+
const cancelActionType =
|
|
1993
|
+
(event && event.output && event.output.cancelActionType) ||
|
|
1994
|
+
(event && event.data && event.data.cancelActionType) ||
|
|
1995
|
+
null
|
|
1996
|
+
|
|
1997
|
+
logger.debug(`[AEM] Setting up allParticipants timeout: ${timeoutMsecs}ms for ${allParticipantIds.length} participants: ${allParticipantIds.join(', ')}${cancelActionType ? `, cancel: ${cancelActionType}` : ''}`)
|
|
1684
1998
|
|
|
1685
1999
|
return {
|
|
1686
2000
|
participantsActionTimeout: {
|
|
1687
2001
|
msecs: timeoutMsecs,
|
|
1688
2002
|
startedAt: Date.now(),
|
|
1689
|
-
nonResponders: new Set(allParticipantIds)
|
|
2003
|
+
nonResponders: new Set(allParticipantIds),
|
|
2004
|
+
cancelActionType: cancelActionType
|
|
1690
2005
|
}
|
|
1691
2006
|
}
|
|
1692
2007
|
}
|
|
1693
|
-
// If no timeout data, return empty object to not change context
|
|
2008
|
+
// If no timeout data or timeout already exists, return empty object to not change context
|
|
1694
2009
|
return {};
|
|
1695
2010
|
}),
|
|
1696
2011
|
processParticipantResponse: assign(({ context, event }) => {
|
|
@@ -1698,6 +2013,9 @@ const AnearEventMachineFunctions = ({
|
|
|
1698
2013
|
const { nonResponders, ...rest } = context.participantsActionTimeout
|
|
1699
2014
|
const newNonResponders = new Set(nonResponders)
|
|
1700
2015
|
newNonResponders.delete(participantId)
|
|
2016
|
+
const remainingCount = newNonResponders.size
|
|
2017
|
+
|
|
2018
|
+
logger.debug(`[AEM] Participant ${participantId} responded, removing from allParticipants timeout. ${remainingCount} participants remaining.`)
|
|
1701
2019
|
|
|
1702
2020
|
return {
|
|
1703
2021
|
participantsActionTimeout: {
|
|
@@ -1726,15 +2044,53 @@ const AnearEventMachineFunctions = ({
|
|
|
1726
2044
|
}
|
|
1727
2045
|
};
|
|
1728
2046
|
}),
|
|
1729
|
-
sendActionsTimeoutToAppM: ({ context }) => {
|
|
2047
|
+
sendActionsTimeoutToAppM: ({ context, self }) => {
|
|
1730
2048
|
const { nonResponders, msecs } = context.participantsActionTimeout
|
|
1731
2049
|
const nonResponderIds = [...nonResponders]
|
|
1732
|
-
|
|
2050
|
+
const allActiveParticipantIds = getActiveParticipantIds(context)
|
|
2051
|
+
|
|
2052
|
+
logger.info(`[AEM] allParticipants timeout expired after ${msecs}ms. Non-responders: ${nonResponderIds.join(', ')}`)
|
|
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
|
+
}
|
|
1733
2060
|
|
|
1734
2061
|
if (context.appEventMachine) {
|
|
1735
2062
|
context.appEventMachine.send({ type: 'ACTIONS_TIMEOUT', timeout: msecs, nonResponderIds })
|
|
1736
2063
|
}
|
|
1737
2064
|
},
|
|
2065
|
+
sendActionsTimeoutToAppMCanceled: ({ context }) => {
|
|
2066
|
+
const { nonResponders, msecs } = context.participantsActionTimeout
|
|
2067
|
+
const nonResponderIds = [...nonResponders]
|
|
2068
|
+
logger.info(`[AEM] allParticipants timeout cancelled after ${msecs}ms. Non-responders at cancellation: ${nonResponderIds.join(', ')}`)
|
|
2069
|
+
|
|
2070
|
+
if (context.appEventMachine) {
|
|
2071
|
+
context.appEventMachine.send({ type: 'ACTIONS_TIMEOUT', timeout: msecs, nonResponderIds, canceled: true })
|
|
2072
|
+
}
|
|
2073
|
+
},
|
|
2074
|
+
forwardPendingCancelActionIfExists: ({ context }) => {
|
|
2075
|
+
if (!context.pendingCancelAction) return // No cancel ACTION to forward
|
|
2076
|
+
|
|
2077
|
+
const { participantId, appEventName, payload } = context.pendingCancelAction
|
|
2078
|
+
const actionEventPayload = { participantId, payload }
|
|
2079
|
+
|
|
2080
|
+
// Forward to AppM
|
|
2081
|
+
context.appEventMachine.send({ type: appEventName, ...actionEventPayload })
|
|
2082
|
+
|
|
2083
|
+
// Forward to APM
|
|
2084
|
+
const participantMachine = context.participantMachines[participantId]
|
|
2085
|
+
if (participantMachine) {
|
|
2086
|
+
participantMachine.send({ type: 'ACTION', ...actionEventPayload })
|
|
2087
|
+
}
|
|
2088
|
+
|
|
2089
|
+
logger.info(`[AEM] Forwarded cancel ACTION ${appEventName} from ${participantId} to AppM and APM after allParticipants cancellation`)
|
|
2090
|
+
|
|
2091
|
+
// Clear the pending action
|
|
2092
|
+
context.pendingCancelAction = null
|
|
2093
|
+
},
|
|
1738
2094
|
clearParticipantsTimeout: assign({
|
|
1739
2095
|
participantsActionTimeout: null
|
|
1740
2096
|
}),
|
|
@@ -1744,6 +2100,9 @@ const AnearEventMachineFunctions = ({
|
|
|
1744
2100
|
|
|
1745
2101
|
logger.debug(`[AEM] cleaning up exiting participant ${participantId}`)
|
|
1746
2102
|
|
|
2103
|
+
// Clean up timeout counts for exiting participant
|
|
2104
|
+
const { [participantId]: removedTimeoutCount, ...remainingTimeoutCounts } = context.consecutiveTimeoutCounts
|
|
2105
|
+
|
|
1747
2106
|
if (participant) {
|
|
1748
2107
|
const isOpenHouse = context.anearEvent.openHouse || false
|
|
1749
2108
|
|
|
@@ -1766,7 +2125,8 @@ const AnearEventMachineFunctions = ({
|
|
|
1766
2125
|
...context.participants,
|
|
1767
2126
|
[participantId]: updatedParticipant
|
|
1768
2127
|
},
|
|
1769
|
-
participantMachines: remainingMachines
|
|
2128
|
+
participantMachines: remainingMachines,
|
|
2129
|
+
consecutiveTimeoutCounts: remainingTimeoutCounts
|
|
1770
2130
|
}
|
|
1771
2131
|
} else {
|
|
1772
2132
|
// For non-open-house events: remove both participant and machine (existing behavior)
|
|
@@ -1782,11 +2142,14 @@ const AnearEventMachineFunctions = ({
|
|
|
1782
2142
|
|
|
1783
2143
|
return {
|
|
1784
2144
|
participants: remainingParticipants,
|
|
1785
|
-
participantMachines: remainingMachines
|
|
2145
|
+
participantMachines: remainingMachines,
|
|
2146
|
+
consecutiveTimeoutCounts: remainingTimeoutCounts
|
|
1786
2147
|
}
|
|
1787
2148
|
}
|
|
1788
2149
|
} else {
|
|
1789
|
-
return {
|
|
2150
|
+
return {
|
|
2151
|
+
consecutiveTimeoutCounts: remainingTimeoutCounts
|
|
2152
|
+
}
|
|
1790
2153
|
}
|
|
1791
2154
|
}),
|
|
1792
2155
|
notifyCoreServiceMachineExit: ({ context }) => {
|
|
@@ -1985,8 +2348,9 @@ const AnearEventMachineFunctions = ({
|
|
|
1985
2348
|
renderDisplay: fromPromise(async ({ input }) => {
|
|
1986
2349
|
const { context, event } = input
|
|
1987
2350
|
const displayEventProcessor = new DisplayEventProcessor(context)
|
|
2351
|
+
const cancelActionType = event.cancelActionType || null
|
|
1988
2352
|
|
|
1989
|
-
return await displayEventProcessor.processAndPublish(event.displayEvents)
|
|
2353
|
+
return await displayEventProcessor.processAndPublish(event.displayEvents, cancelActionType)
|
|
1990
2354
|
}),
|
|
1991
2355
|
getAttachedCreatorOrHost: fromPromise(async ({ input }) => {
|
|
1992
2356
|
const { context } = input
|
|
@@ -2162,6 +2526,9 @@ const AnearEventMachineFunctions = ({
|
|
|
2162
2526
|
},
|
|
2163
2527
|
allParticipantsResponded: ({ context }) => {
|
|
2164
2528
|
return context.participantsActionTimeout && context.participantsActionTimeout.nonResponders.size === 0
|
|
2529
|
+
},
|
|
2530
|
+
allCancelConfirmationsReceived: ({ context }) => {
|
|
2531
|
+
return !context.pendingCancelConfirmations || context.pendingCancelConfirmations.size === 0
|
|
2165
2532
|
}
|
|
2166
2533
|
},
|
|
2167
2534
|
delays: {
|
|
@@ -161,6 +161,7 @@ const AnearParticipantMachineConfig = participantId => ({
|
|
|
161
161
|
initial: 'idle',
|
|
162
162
|
states: {
|
|
163
163
|
idle: {
|
|
164
|
+
entry: ['ensureTimerCleared'],
|
|
164
165
|
on: {
|
|
165
166
|
RENDER_DISPLAY: {
|
|
166
167
|
target: '#renderDisplay'
|
|
@@ -168,6 +169,12 @@ const AnearParticipantMachineConfig = participantId => ({
|
|
|
168
169
|
PRIVATE_DISPLAY: {
|
|
169
170
|
target: '#renderDisplay'
|
|
170
171
|
},
|
|
172
|
+
CANCEL_TIMEOUT: {
|
|
173
|
+
// Acknowledge CANCEL_TIMEOUT even when idle (no active timer).
|
|
174
|
+
// This ensures the AEM's cancellation sequence completes for all participants.
|
|
175
|
+
actions: ['sendTimerCanceled'],
|
|
176
|
+
// v5 note: internal transitions are the default (v4 had `internal: true`)
|
|
177
|
+
},
|
|
171
178
|
PARTICIPANT_DISCONNECT: {
|
|
172
179
|
target: 'waitReconnect'
|
|
173
180
|
},
|
|
@@ -235,8 +242,7 @@ const AnearParticipantMachineConfig = participantId => ({
|
|
|
235
242
|
onDone: [
|
|
236
243
|
{ guard: 'hasActionTimeout', actions: 'updateActionTimeout', target: 'waitParticipantResponse' },
|
|
237
244
|
// If this display does not configure a timeout, explicitly clear any
|
|
238
|
-
// previously-running action timeout.
|
|
239
|
-
// from an older prompt can be "resumed" later and immediately fire.
|
|
245
|
+
// previously-running action timeout.
|
|
240
246
|
{ actions: 'nullActionTimeout', target: 'idle' }
|
|
241
247
|
],
|
|
242
248
|
onError: {
|
|
@@ -251,6 +257,7 @@ const AnearParticipantMachineConfig = participantId => ({
|
|
|
251
257
|
}
|
|
252
258
|
},
|
|
253
259
|
waitParticipantResponse: {
|
|
260
|
+
entry: 'logWaitParticipantResponseEntry',
|
|
254
261
|
always: {
|
|
255
262
|
guard: 'isTimeoutImmediate',
|
|
256
263
|
actions: 'logImmediateTimeout',
|
|
@@ -262,16 +269,41 @@ const AnearParticipantMachineConfig = participantId => ({
|
|
|
262
269
|
target: '#participantTimedOut'
|
|
263
270
|
}
|
|
264
271
|
},
|
|
272
|
+
initial: 'waiting',
|
|
273
|
+
states: {
|
|
274
|
+
waiting: {
|
|
275
|
+
// The actual waiting state. Timer continues running when we transition to .rendering.
|
|
276
|
+
},
|
|
277
|
+
rendering: {
|
|
278
|
+
invoke: {
|
|
279
|
+
src: 'publishPrivateDisplay',
|
|
280
|
+
input: ({ context, event }) => ({ context, event }),
|
|
281
|
+
onDone: {
|
|
282
|
+
// Transition back to waiting - timer continues because we never exited waitParticipantResponse
|
|
283
|
+
// Target sibling state (stays within parent, timer continues)
|
|
284
|
+
target: 'waiting'
|
|
285
|
+
},
|
|
286
|
+
onError: {
|
|
287
|
+
target: '#error'
|
|
288
|
+
}
|
|
289
|
+
},
|
|
290
|
+
on: {
|
|
291
|
+
PARTICIPANT_EXIT: {
|
|
292
|
+
actions: 'logExit',
|
|
293
|
+
target: '#cleanupAndExit'
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
},
|
|
265
298
|
on: {
|
|
266
299
|
RENDER_DISPLAY: {
|
|
267
|
-
// Re-render the current prompt
|
|
268
|
-
//
|
|
269
|
-
|
|
270
|
-
// actionTimeoutStart so the participant does not get extra time.
|
|
271
|
-
target: '#renderDisplay'
|
|
300
|
+
// Re-render the current prompt without exiting waitParticipantResponse.
|
|
301
|
+
// This preserves the parent's after: timer because we stay within the parent state.
|
|
302
|
+
target: '.rendering'
|
|
272
303
|
},
|
|
273
304
|
PRIVATE_DISPLAY: {
|
|
274
|
-
|
|
305
|
+
// Same as RENDER_DISPLAY - re-render without interrupting timer
|
|
306
|
+
target: '.rendering'
|
|
275
307
|
},
|
|
276
308
|
PARTICIPANT_EXIT: {
|
|
277
309
|
actions: 'logExit',
|
|
@@ -285,6 +317,14 @@ const AnearParticipantMachineConfig = participantId => ({
|
|
|
285
317
|
],
|
|
286
318
|
target: 'idle'
|
|
287
319
|
},
|
|
320
|
+
CANCEL_TIMEOUT: {
|
|
321
|
+
// Cancel the active timeout and transition to idle. This is used
|
|
322
|
+
// when the AppM wants to immediately cancel all participant timeouts
|
|
323
|
+
// (e.g., when a LIAR call interrupts the wait state).
|
|
324
|
+
// Always send TIMER_CANCELED to acknowledge, even if no timer was active.
|
|
325
|
+
actions: ['nullActionTimeout', 'sendTimerCanceled'],
|
|
326
|
+
target: 'idle'
|
|
327
|
+
},
|
|
288
328
|
PARTICIPANT_DISCONNECT: {
|
|
289
329
|
actions: 'updateRemainingTimeoutOnDisconnect',
|
|
290
330
|
target: 'waitReconnect'
|
|
@@ -297,7 +337,7 @@ const AnearParticipantMachineConfig = participantId => ({
|
|
|
297
337
|
},
|
|
298
338
|
participantTimedOut: {
|
|
299
339
|
id: 'participantTimedOut',
|
|
300
|
-
entry: ['sendTimeoutEvents', 'nullActionTimeout'],
|
|
340
|
+
entry: ['logParticipantTimedOut', 'sendTimeoutEvents', 'nullActionTimeout'],
|
|
301
341
|
always: 'idle'
|
|
302
342
|
},
|
|
303
343
|
cleanupAndExit: {
|
|
@@ -412,6 +452,7 @@ const AnearParticipantMachineFunctions = {
|
|
|
412
452
|
}
|
|
413
453
|
}),
|
|
414
454
|
sendTimeoutEvents: ({ context }) => {
|
|
455
|
+
logger.info(`[APM] Timer expired for ${context.anearParticipant.id}, sending PARTICIPANT_TIMEOUT to AEM`)
|
|
415
456
|
// AEM currently expects `event.participantId` (not nested under `data`).
|
|
416
457
|
context.anearEvent.send({
|
|
417
458
|
type: 'PARTICIPANT_TIMEOUT',
|
|
@@ -419,6 +460,13 @@ const AnearParticipantMachineFunctions = {
|
|
|
419
460
|
})
|
|
420
461
|
if (context.appParticipantMachine) context.appParticipantMachine.send({ type: 'PARTICIPANT_TIMEOUT' })
|
|
421
462
|
},
|
|
463
|
+
sendTimerCanceled: ({ context }) => {
|
|
464
|
+
logger.debug(`[APM] Sending TIMER_CANCELED to AEM for ${context.anearParticipant.id}`)
|
|
465
|
+
context.anearEvent.send({
|
|
466
|
+
type: 'TIMER_CANCELED',
|
|
467
|
+
participantId: context.anearParticipant.id
|
|
468
|
+
})
|
|
469
|
+
},
|
|
422
470
|
sendActionToAppParticipantMachine: ({ context, event }) => {
|
|
423
471
|
if (context.appParticipantMachine) context.appParticipantMachine.send(event)
|
|
424
472
|
},
|
|
@@ -429,34 +477,41 @@ const AnearParticipantMachineFunctions = {
|
|
|
429
477
|
const timeoutFromEvent = event?.output?.timeout ?? event?.data?.timeout
|
|
430
478
|
const now = CurrentDateTimestamp()
|
|
431
479
|
|
|
432
|
-
//
|
|
433
|
-
//
|
|
434
|
-
//
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
if (
|
|
438
|
-
|
|
439
|
-
const base = context.actionTimeoutMsecs != null ? context.actionTimeoutMsecs : timeoutFromEvent
|
|
440
|
-
const remaining = base - elapsed
|
|
441
|
-
const remainingMsecs = remaining > 0 ? remaining : 0
|
|
442
|
-
|
|
443
|
-
logger.debug(`[APM] Resuming timer for ${context.anearParticipant.id} with ${remainingMsecs}ms remaining`)
|
|
444
|
-
return {
|
|
445
|
-
actionTimeoutMsecs: remainingMsecs,
|
|
446
|
-
actionTimeoutStart: now
|
|
447
|
-
}
|
|
480
|
+
// Start a new timer. This is only called when transitioning from idle to waitParticipantResponse.
|
|
481
|
+
// When RENDER_DISPLAY arrives during waitParticipantResponse, we transition to the nested
|
|
482
|
+
// .rendering state, which preserves the parent's after: timer (no resume needed).
|
|
483
|
+
const existingMsecs = context.actionTimeoutMsecs
|
|
484
|
+
const existingStart = context.actionTimeoutStart
|
|
485
|
+
if (existingMsecs != null || existingStart != null) {
|
|
486
|
+
logger.debug(`[APM] WARNING: Starting new timer for ${context.anearParticipant.id} but existing timer state found: msecs=${existingMsecs}, start=${existingStart}`)
|
|
448
487
|
}
|
|
449
|
-
|
|
450
|
-
// First time this participant sees this prompt: start a fresh timer.
|
|
451
488
|
logger.debug(`[APM] Starting new timer for ${context.anearParticipant.id} with ${timeoutFromEvent}ms`)
|
|
452
489
|
return {
|
|
453
490
|
actionTimeoutMsecs: timeoutFromEvent,
|
|
454
491
|
actionTimeoutStart: now
|
|
455
492
|
}
|
|
456
493
|
}),
|
|
457
|
-
nullActionTimeout: assign({
|
|
458
|
-
|
|
459
|
-
|
|
494
|
+
nullActionTimeout: assign(({ context }) => {
|
|
495
|
+
if (context.actionTimeoutMsecs != null || context.actionTimeoutStart != null) {
|
|
496
|
+
logger.debug(`[APM] Cancelling timer for ${context.anearParticipant.id} (ACTION received or timeout cleared)`)
|
|
497
|
+
}
|
|
498
|
+
return {
|
|
499
|
+
actionTimeoutMsecs: null,
|
|
500
|
+
actionTimeoutStart: null
|
|
501
|
+
}
|
|
502
|
+
}),
|
|
503
|
+
ensureTimerCleared: assign(({ context }) => {
|
|
504
|
+
// Defensive: Ensure timer state is cleared when entering idle state.
|
|
505
|
+
// This prevents stale timer state from affecting new displays, especially
|
|
506
|
+
// if a RENDER_DISPLAY arrives before timeout cleanup completes.
|
|
507
|
+
if (context.actionTimeoutMsecs != null || context.actionTimeoutStart != null) {
|
|
508
|
+
logger.debug(`[APM] Clearing stale timer state for ${context.anearParticipant.id} on idle entry (msecs=${context.actionTimeoutMsecs}, start=${context.actionTimeoutStart})`)
|
|
509
|
+
return {
|
|
510
|
+
actionTimeoutMsecs: null,
|
|
511
|
+
actionTimeoutStart: null
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
return {}
|
|
460
515
|
}),
|
|
461
516
|
updateRemainingTimeoutOnDisconnect: assign(({ context }) => {
|
|
462
517
|
if (context.actionTimeoutStart) {
|
|
@@ -492,6 +547,8 @@ const AnearParticipantMachineFunctions = {
|
|
|
492
547
|
},
|
|
493
548
|
logAttached: ({ context }) => logger.debug(`[APM] Got ATTACHED for privateChannel for ${context.anearParticipant.id}`),
|
|
494
549
|
logLive: ({ context }) => logger.debug(`[APM] Participant ${context.anearParticipant.id} is LIVE!`),
|
|
550
|
+
logWaitParticipantResponseEntry: ({ context }) => logger.debug(`[APM] ENTERING waitParticipantResponse state for ${context.anearParticipant.id} - timer will be (re)started if context provides timeout`),
|
|
551
|
+
logParticipantTimedOut: ({ context }) => logger.debug(`[APM] ENTERING participantTimedOut state for ${context.anearParticipant.id} - timeout occurred, clearing timer and sending PARTICIPANT_TIMEOUT`),
|
|
495
552
|
logExit: (_args) => logger.debug('[APM] got PARTICIPANT_EXIT. Exiting...'),
|
|
496
553
|
logDisconnected: ({ context }) => logger.info(`[APM] Participant ${context.anearParticipant.id} has DISCONNECTED`),
|
|
497
554
|
logActionTimeoutWhileDisconnected: ({ context }) => logger.info(`[APM] Participant ${context.anearParticipant.id} timed out on action while disconnected.`),
|
|
@@ -94,6 +94,7 @@ const AppMachineTransition = (anearEvent) => {
|
|
|
94
94
|
const appStateName = _stringifiedState(value)
|
|
95
95
|
|
|
96
96
|
const displayEvents = []
|
|
97
|
+
let cancelActionType = null // Track cancel property from meta configuration
|
|
97
98
|
|
|
98
99
|
// Process all meta objects to handle unpredictable AppM structures
|
|
99
100
|
metaObjects.forEach(meta => {
|
|
@@ -112,6 +113,11 @@ const AppMachineTransition = (anearEvent) => {
|
|
|
112
113
|
const { viewPath, props } = _extractViewAndProps(meta.allParticipants)
|
|
113
114
|
timeoutFn = RenderContextBuilder.buildTimeoutFn(viewer, meta.allParticipants.timeout)
|
|
114
115
|
|
|
116
|
+
// Extract cancel property if present
|
|
117
|
+
if (meta.allParticipants.cancel && !cancelActionType) {
|
|
118
|
+
cancelActionType = meta.allParticipants.cancel
|
|
119
|
+
}
|
|
120
|
+
|
|
115
121
|
displayEvent = RenderContextBuilder.buildDisplayEvent(
|
|
116
122
|
viewPath,
|
|
117
123
|
RenderContextBuilder.buildAppRenderContext(appContext, appStateName, event.type, viewer, timeoutFn),
|
|
@@ -127,7 +133,8 @@ const AppMachineTransition = (anearEvent) => {
|
|
|
127
133
|
// Check if participant is a function (new selective rendering format)
|
|
128
134
|
viewer = 'eachParticipant'
|
|
129
135
|
if (typeof meta.eachParticipant === 'function') {
|
|
130
|
-
// New selective participant rendering
|
|
136
|
+
// New selective participant rendering - cancel property is not supported in function format
|
|
137
|
+
// (would need to be in the object format, see legacy handling below)
|
|
131
138
|
const participantRenderFunc = meta.eachParticipant
|
|
132
139
|
const participantDisplays = participantRenderFunc(appContext, event)
|
|
133
140
|
|
|
@@ -165,6 +172,11 @@ const AppMachineTransition = (anearEvent) => {
|
|
|
165
172
|
const { viewPath, props } = _extractViewAndProps(meta.eachParticipant)
|
|
166
173
|
const timeoutFn = RenderContextBuilder.buildTimeoutFn('participant', meta.eachParticipant.timeout)
|
|
167
174
|
|
|
175
|
+
// Extract cancel property if present (for legacy eachParticipant format)
|
|
176
|
+
if (meta.eachParticipant.cancel && !cancelActionType) {
|
|
177
|
+
cancelActionType = meta.eachParticipant.cancel
|
|
178
|
+
}
|
|
179
|
+
|
|
168
180
|
const renderContext = RenderContextBuilder.buildAppRenderContext(appContext, appStateName, event.type, 'eachParticipant', timeoutFn)
|
|
169
181
|
displayEvent = RenderContextBuilder.buildDisplayEvent(
|
|
170
182
|
viewPath,
|
|
@@ -213,7 +225,15 @@ const AppMachineTransition = (anearEvent) => {
|
|
|
213
225
|
|
|
214
226
|
if (displayEvents.length > 0) {
|
|
215
227
|
logger.debug(`[AppMachineTransition] sending RENDER_DISPLAY with ${displayEvents.length} displayEvents`)
|
|
216
|
-
|
|
228
|
+
const renderDisplayPayload = {
|
|
229
|
+
type: 'RENDER_DISPLAY',
|
|
230
|
+
displayEvents
|
|
231
|
+
}
|
|
232
|
+
// Include cancel property only if defined
|
|
233
|
+
if (cancelActionType) {
|
|
234
|
+
renderDisplayPayload.cancelActionType = cancelActionType
|
|
235
|
+
}
|
|
236
|
+
anearEvent.send(renderDisplayPayload)
|
|
217
237
|
}
|
|
218
238
|
}
|
|
219
239
|
|
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',
|
|
@@ -56,7 +56,7 @@ class DisplayEventProcessor {
|
|
|
56
56
|
this.participantsIndex = this._buildParticipantsIndex(anearEventMachineContext.participants)
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
-
processAndPublish(displayEvents) {
|
|
59
|
+
processAndPublish(displayEvents, cancelActionType = null) {
|
|
60
60
|
let participantsTimeout = null
|
|
61
61
|
|
|
62
62
|
// Pre-compute allParticipants timeout for this render batch so other viewers
|
|
@@ -88,7 +88,7 @@ class DisplayEventProcessor {
|
|
|
88
88
|
})
|
|
89
89
|
|
|
90
90
|
return Promise.all(publishPromises).then(() => {
|
|
91
|
-
return { participantsTimeout }
|
|
91
|
+
return { participantsTimeout, cancelActionType }
|
|
92
92
|
})
|
|
93
93
|
}
|
|
94
94
|
|
package/package.json
CHANGED