anear-js-api 2.1.1 → 2.2.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.
- package/EACH_PARTICIPANT_LIFECYCLE.md +3 -1
- package/README.md +12 -19
- package/lib/AnearService.js +3 -3
- package/lib/models/AnearEvent.js +2 -4
- package/lib/state_machines/AnearCoreServiceMachine.js +3 -5
- package/lib/state_machines/AnearEventMachine.js +210 -366
- package/lib/utils/AssetFileCollector.js +0 -2
- package/lib/utils/DisplayEventProcessor.js +21 -40
- package/package.json +1 -1
- package/tests/DisplayEventProcessor.test.js +39 -26
- package/lib/state_machines/AnearParticipantMachine.js +0 -765
|
@@ -24,7 +24,6 @@
|
|
|
24
24
|
* - Constructed by ACSM with:
|
|
25
25
|
* - `anearEvent` → the event JSON model (id, appId, metadata)
|
|
26
26
|
* - `appEventMachineFactory`
|
|
27
|
-
* - `appParticipantMachineFactory` (optional)
|
|
28
27
|
* - ACSM context fields:
|
|
29
28
|
* - `appData` (app metadata from ANAPI)
|
|
30
29
|
* - `pugTemplates` (preloaded, compiled templates)
|
|
@@ -52,7 +51,7 @@
|
|
|
52
51
|
* 1. AEM is created and started by the ACSM in response to CREATE_EVENT or LOAD_EVENT.
|
|
53
52
|
* 2. AEM configures its event-scoped Ably channels and subscriptions.
|
|
54
53
|
* 3. AEM wires realtime messages (presence, actions, host commands) into the
|
|
55
|
-
* AppEventMachine
|
|
54
|
+
* AppEventMachine.
|
|
56
55
|
* 4. On state changes, AEM:
|
|
57
56
|
* - Renders the appropriate Pug templates using `context.pugTemplates`.
|
|
58
57
|
* - Publishes updated views and state snapshots out via Ably (participants,
|
|
@@ -140,7 +139,6 @@ const DisplayEventProcessor = require('../utils/DisplayEventProcessor')
|
|
|
140
139
|
const PugHelpers = require('../utils/PugHelpers')
|
|
141
140
|
|
|
142
141
|
const AnearApi = require('../api/AnearApi')
|
|
143
|
-
const AnearParticipantMachine = require('../state_machines/AnearParticipantMachine')
|
|
144
142
|
const AnearParticipant = require('../models/AnearParticipant')
|
|
145
143
|
|
|
146
144
|
const CurrentDateTimestamp = _ => new Date().getTime()
|
|
@@ -179,7 +177,6 @@ const AnearEventMachineContext = (
|
|
|
179
177
|
pugTemplates,
|
|
180
178
|
pugHelpers,
|
|
181
179
|
appEventMachineFactory,
|
|
182
|
-
appParticipantMachineFactory,
|
|
183
180
|
rehydrate = false
|
|
184
181
|
) => ({
|
|
185
182
|
anearEvent,
|
|
@@ -187,7 +184,6 @@ const AnearEventMachineContext = (
|
|
|
187
184
|
pugTemplates,
|
|
188
185
|
pugHelpers,
|
|
189
186
|
appEventMachineFactory,
|
|
190
|
-
appParticipantMachineFactory,
|
|
191
187
|
rehydrate,
|
|
192
188
|
appEventMachine: null,
|
|
193
189
|
appMachineTransition: null,
|
|
@@ -196,11 +192,10 @@ const AnearEventMachineContext = (
|
|
|
196
192
|
participantsDisplayChannel: null, // display all participants
|
|
197
193
|
spectatorsDisplayChannel: null, // display all spectators
|
|
198
194
|
participants: {},
|
|
199
|
-
|
|
195
|
+
participantPrivateChannels: {},
|
|
196
|
+
exitedParticipantPrivateChannels: [],
|
|
200
197
|
participantsActionTimeout: null,
|
|
201
|
-
suppressParticipantTimeouts: false, // Flag to suppress PARTICIPANT_TIMEOUT events (cleared when new timers are set up via render)
|
|
202
198
|
eachParticipantCancelActionType: null, // ACTION type that triggers cancellation for eachParticipant timeouts
|
|
203
|
-
pendingCancelConfirmations: null, // Set of participantIds waiting for TIMER_CANCELED confirmation
|
|
204
199
|
pendingCancelAction: null, // ACTION to forward after cancellation completes: { participantId, appEventName, payload }
|
|
205
200
|
consecutiveTimeoutCount: 0, // Global counter tracking consecutive timeouts across all participants for dead-man switch
|
|
206
201
|
consecutiveAllParticipantsTimeoutCount: 0, // Counter tracking consecutive allParticipants timeouts where ALL participants timed out
|
|
@@ -209,10 +204,6 @@ const AnearEventMachineContext = (
|
|
|
209
204
|
})
|
|
210
205
|
|
|
211
206
|
const ActiveEventGlobalEvents = {
|
|
212
|
-
PARTICIPANT_MACHINE_EXIT: {
|
|
213
|
-
// the AnearParticipantMachine has hit its final state
|
|
214
|
-
actions: ['cleanupExitingParticipant']
|
|
215
|
-
},
|
|
216
207
|
RESTART: {
|
|
217
208
|
// Hosted apps can request a rewind back to ANNOUNCE without terminating the event.
|
|
218
209
|
target: '#activeEvent.restartEvent'
|
|
@@ -249,16 +240,19 @@ const ActiveEventStatesConfig = {
|
|
|
249
240
|
invoke: {
|
|
250
241
|
src: 'getAttachedCreatorOrHost',
|
|
251
242
|
input: ({ context, event }) => ({ context, event }),
|
|
252
|
-
onDone:
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
243
|
+
onDone: [
|
|
244
|
+
{
|
|
245
|
+
// If actions-channel presence.get() found creator/host, register now.
|
|
246
|
+
guard: 'hasAttachedCreatorOrHost',
|
|
247
|
+
actions: ['startNewParticipantSession', 'sendEnterToAppMachine'],
|
|
248
|
+
target: '#eventCreated'
|
|
249
|
+
},
|
|
250
|
+
{
|
|
251
|
+
// No creator/host found yet via presence.get(); wait for PARTICIPANT_ENTER.
|
|
252
|
+
actions: ['logWaitingForCreatorPresence'],
|
|
253
|
+
target: '#waiting'
|
|
254
|
+
}
|
|
255
|
+
],
|
|
262
256
|
onError: {
|
|
263
257
|
target: '#failure'
|
|
264
258
|
}
|
|
@@ -274,10 +268,6 @@ const ActiveEventStatesConfig = {
|
|
|
274
268
|
PARTICIPANT_ENTER: {
|
|
275
269
|
actions: 'logCreatorEnter',
|
|
276
270
|
target: '#waiting.fetching'
|
|
277
|
-
},
|
|
278
|
-
PARTICIPANT_MACHINE_READY: {
|
|
279
|
-
actions: ['logAPMReady', 'sendEnterToAppMachine'],
|
|
280
|
-
target: '#eventCreated'
|
|
281
271
|
}
|
|
282
272
|
}
|
|
283
273
|
},
|
|
@@ -289,8 +279,8 @@ const ActiveEventStatesConfig = {
|
|
|
289
279
|
onDone: {
|
|
290
280
|
// v5 note: onDone of a fromPromise actor provides the resolved value on `event.output`.
|
|
291
281
|
// event.output.anearParticipant available in actions
|
|
292
|
-
actions: ['
|
|
293
|
-
target: '#
|
|
282
|
+
actions: ['startNewParticipantSession', 'sendEnterToAppMachine'],
|
|
283
|
+
target: '#eventCreated'
|
|
294
284
|
},
|
|
295
285
|
onError: {
|
|
296
286
|
target: '#activeEvent.failure'
|
|
@@ -327,7 +317,7 @@ const ActiveEventStatesConfig = {
|
|
|
327
317
|
PARTICIPANT_LEAVE: [
|
|
328
318
|
{
|
|
329
319
|
guard: 'isPermanentLeave',
|
|
330
|
-
actions: ['sendExitToAppMachine', '
|
|
320
|
+
actions: ['sendExitToAppMachine', 'handleParticipantExit']
|
|
331
321
|
},
|
|
332
322
|
{
|
|
333
323
|
actions: ['processDisconnectEvents', 'setParticipantIdle']
|
|
@@ -508,7 +498,7 @@ const ActiveEventStatesConfig = {
|
|
|
508
498
|
PARTICIPANT_LEAVE: [
|
|
509
499
|
{
|
|
510
500
|
guard: 'isPermanentLeave',
|
|
511
|
-
actions: ['sendExitToAppMachine', '
|
|
501
|
+
actions: ['sendExitToAppMachine', 'handleParticipantExit']
|
|
512
502
|
},
|
|
513
503
|
{
|
|
514
504
|
actions: ['processDisconnectEvents', 'setParticipantIdle']
|
|
@@ -532,7 +522,7 @@ const ActiveEventStatesConfig = {
|
|
|
532
522
|
}
|
|
533
523
|
],
|
|
534
524
|
BOOT_PARTICIPANT: {
|
|
535
|
-
actions: '
|
|
525
|
+
actions: 'sendBootEventToParticipant'
|
|
536
526
|
},
|
|
537
527
|
CANCEL_ALL_PARTICIPANT_TIMEOUTS: {
|
|
538
528
|
actions: 'cancelAllParticipantTimeouts'
|
|
@@ -590,16 +580,15 @@ const ActiveEventStatesConfig = {
|
|
|
590
580
|
}
|
|
591
581
|
},
|
|
592
582
|
newParticipantJoining: {
|
|
593
|
-
//
|
|
594
|
-
//
|
|
595
|
-
// manages active/idle state for long-running events, and manages any ACTION timeouts.
|
|
583
|
+
// A PARTICIPANT_ENTER received from a new user JOIN click.
|
|
584
|
+
// Fetch participant and register direct session in AEM.
|
|
596
585
|
id: 'newParticipantJoining',
|
|
597
586
|
invoke: {
|
|
598
587
|
src: 'fetchParticipantData',
|
|
599
588
|
input: ({ context, event }) => ({ context, event }),
|
|
600
589
|
onDone: {
|
|
601
|
-
actions: ['
|
|
602
|
-
target: '#
|
|
590
|
+
actions: ['startNewParticipantSession', 'sendEnterToAppMachine'],
|
|
591
|
+
target: '#waitingToStart'
|
|
603
592
|
},
|
|
604
593
|
onError: {
|
|
605
594
|
target: '#activeEvent.failure'
|
|
@@ -614,16 +603,6 @@ const ActiveEventStatesConfig = {
|
|
|
614
603
|
}
|
|
615
604
|
}
|
|
616
605
|
},
|
|
617
|
-
waitParticipantReady: {
|
|
618
|
-
id: 'waitParticipantReady',
|
|
619
|
-
on: {
|
|
620
|
-
PARTICIPANT_MACHINE_READY: {
|
|
621
|
-
actions: ['sendEnterToAppMachine'],
|
|
622
|
-
target: '#waitingToStart',
|
|
623
|
-
// v5 note: internal transitions are the default (v4 had `internal: true`)
|
|
624
|
-
}
|
|
625
|
-
}
|
|
626
|
-
}
|
|
627
606
|
}
|
|
628
607
|
},
|
|
629
608
|
live: {
|
|
@@ -645,7 +624,7 @@ const ActiveEventStatesConfig = {
|
|
|
645
624
|
PARTICIPANT_LEAVE: [
|
|
646
625
|
{
|
|
647
626
|
guard: 'isPermanentLeave',
|
|
648
|
-
actions: ['sendExitToAppMachine', '
|
|
627
|
+
actions: ['sendExitToAppMachine', 'handleParticipantExit']
|
|
649
628
|
},
|
|
650
629
|
{
|
|
651
630
|
actions: ['processDisconnectEvents', 'setParticipantIdle']
|
|
@@ -670,16 +649,9 @@ const ActiveEventStatesConfig = {
|
|
|
670
649
|
actions: 'sendSpectatorEnterToAppEventMachine'
|
|
671
650
|
},
|
|
672
651
|
BOOT_PARTICIPANT: {
|
|
673
|
-
actions: '
|
|
652
|
+
actions: 'sendBootEventToParticipant'
|
|
674
653
|
},
|
|
675
|
-
CANCEL_ALL_PARTICIPANT_TIMEOUTS: {
|
|
676
|
-
actions: 'cancelAllParticipantTimeouts'
|
|
677
|
-
},
|
|
678
|
-
// Handle PARTICIPANT_TIMEOUT even when in liveRendering state
|
|
679
|
-
// (events bubble up from child states, so we need to handle them here)
|
|
680
|
-
PARTICIPANT_TIMEOUT: {
|
|
681
|
-
actions: ['trackParticipantTimeout', 'processParticipantTimeout']
|
|
682
|
-
}
|
|
654
|
+
CANCEL_ALL_PARTICIPANT_TIMEOUTS: {}
|
|
683
655
|
},
|
|
684
656
|
states: {
|
|
685
657
|
transitioning: {
|
|
@@ -705,54 +677,13 @@ const ActiveEventStatesConfig = {
|
|
|
705
677
|
},
|
|
706
678
|
initial: 'idle',
|
|
707
679
|
states: {
|
|
708
|
-
idle: {}
|
|
709
|
-
waitingForCancelConfirmations: {
|
|
710
|
-
// Note: Setup actions are called in the transitions that lead here, not in entry
|
|
711
|
-
// This allows different setup for declarative cancel (TRIGGER_CANCEL_SEQUENCE) vs manual cancel (CANCEL_ALL_PARTICIPANT_TIMEOUTS)
|
|
712
|
-
on: {
|
|
713
|
-
TIMER_CANCELED: {
|
|
714
|
-
actions: ['removeFromPendingCancelConfirmations']
|
|
715
|
-
},
|
|
716
|
-
PARTICIPANT_EXIT: {
|
|
717
|
-
actions: ['removeFromPendingCancelConfirmations']
|
|
718
|
-
},
|
|
719
|
-
// Drop only ACTION and PARTICIPANT_TIMEOUT events during cancellation wait
|
|
720
|
-
// All other events (presence events, etc.) will bubble up to parent handlers
|
|
721
|
-
ACTION: {
|
|
722
|
-
actions: ['dropEventDuringCancellation']
|
|
723
|
-
},
|
|
724
|
-
PARTICIPANT_TIMEOUT: {
|
|
725
|
-
actions: ['dropEventDuringCancellation']
|
|
726
|
-
}
|
|
727
|
-
},
|
|
728
|
-
always: {
|
|
729
|
-
guard: 'allCancelConfirmationsReceived',
|
|
730
|
-
actions: ['forwardPendingCancelActionIfExists', 'clearCancelState'],
|
|
731
|
-
target: '#waitingForActions.idle'
|
|
732
|
-
}
|
|
733
|
-
}
|
|
680
|
+
idle: {}
|
|
734
681
|
},
|
|
735
682
|
on: {
|
|
736
683
|
ACTION: {
|
|
737
684
|
actions: ['resetParticipantTimeoutCount', 'processParticipantAction']
|
|
738
685
|
},
|
|
739
|
-
|
|
740
|
-
PARTICIPANT_TIMEOUT: {
|
|
741
|
-
actions: ['trackParticipantTimeout', 'processParticipantTimeout'],
|
|
742
|
-
// v5 note: internal transitions are the default (v4 had `internal: true`)
|
|
743
|
-
},
|
|
744
|
-
CANCEL_ALL_PARTICIPANT_TIMEOUTS: {
|
|
745
|
-
actions: ['setupPendingCancelConfirmationsForManualCancel', 'sendCancelTimeoutToAllAPMs'],
|
|
746
|
-
target: '.waitingForCancelConfirmations'
|
|
747
|
-
},
|
|
748
|
-
TRIGGER_CANCEL_SEQUENCE: {
|
|
749
|
-
actions: ['sendCancelTimeoutToAllAPMs', 'setupPendingCancelConfirmations'],
|
|
750
|
-
target: '.waitingForCancelConfirmations'
|
|
751
|
-
},
|
|
752
|
-
TIMER_CANCELED: {
|
|
753
|
-
// Ignore if not in cancellation state (defensive)
|
|
754
|
-
actions: []
|
|
755
|
-
}
|
|
686
|
+
CANCEL_ALL_PARTICIPANT_TIMEOUTS: {}
|
|
756
687
|
}
|
|
757
688
|
},
|
|
758
689
|
liveRendering: {
|
|
@@ -764,11 +695,11 @@ const ActiveEventStatesConfig = {
|
|
|
764
695
|
{
|
|
765
696
|
guard: 'isParticipantsTimeoutActive',
|
|
766
697
|
target: 'notifyingRenderCompleteWithTimeout',
|
|
767
|
-
actions: ['setLastRenderResult', 'setupParticipantsTimeout', 'setupCancelActionType'
|
|
698
|
+
actions: ['setLastRenderResult', 'setupParticipantsTimeout', 'setupCancelActionType']
|
|
768
699
|
},
|
|
769
700
|
{
|
|
770
701
|
target: 'notifyingRenderComplete',
|
|
771
|
-
actions: ['setLastRenderResult', 'setupCancelActionType'
|
|
702
|
+
actions: ['setLastRenderResult', 'setupCancelActionType']
|
|
772
703
|
// v5 note: internal transitions are the default (v4 had `internal: true`)
|
|
773
704
|
}
|
|
774
705
|
],
|
|
@@ -817,13 +748,13 @@ const ActiveEventStatesConfig = {
|
|
|
817
748
|
{
|
|
818
749
|
guard: 'isParticipantsTimeoutActive',
|
|
819
750
|
target: 'notifyingRenderComplete',
|
|
820
|
-
actions: ['setLastRenderResult', 'setupCancelActionType'
|
|
751
|
+
actions: ['setLastRenderResult', 'setupCancelActionType']
|
|
821
752
|
// Note: We do NOT call setupParticipantsTimeout here because we're already in waitAllParticipantsResponse
|
|
822
753
|
// and the timer is still running from the parent state (preserved by nested state pattern).
|
|
823
754
|
},
|
|
824
755
|
{
|
|
825
756
|
target: '#waitingForActions',
|
|
826
|
-
actions: ['setLastRenderResult', 'setupCancelActionType', '
|
|
757
|
+
actions: ['setLastRenderResult', 'setupCancelActionType', 'notifyAppMachineRendered']
|
|
827
758
|
}
|
|
828
759
|
],
|
|
829
760
|
onError: {
|
|
@@ -872,11 +803,11 @@ const ActiveEventStatesConfig = {
|
|
|
872
803
|
PARTICIPANT_LEAVE: [
|
|
873
804
|
{
|
|
874
805
|
guard: 'isBooting',
|
|
875
|
-
actions: ['removeLeavingParticipantFromTimeout', '
|
|
806
|
+
actions: ['removeLeavingParticipantFromTimeout', 'handleParticipantExit']
|
|
876
807
|
},
|
|
877
808
|
{
|
|
878
809
|
guard: 'isPermanentLeave',
|
|
879
|
-
actions: ['removeLeavingParticipantFromTimeout', 'sendExitToAppMachine', '
|
|
810
|
+
actions: ['removeLeavingParticipantFromTimeout', 'sendExitToAppMachine', 'handleParticipantExit']
|
|
880
811
|
},
|
|
881
812
|
{
|
|
882
813
|
actions: ['processDisconnectEvents']
|
|
@@ -893,33 +824,21 @@ const ActiveEventStatesConfig = {
|
|
|
893
824
|
always: 'waitingForActions'
|
|
894
825
|
},
|
|
895
826
|
participantEntering: {
|
|
896
|
-
// a PARTICIPANT_ENTER received from a new user JOIN click.
|
|
897
|
-
//
|
|
898
|
-
// This machine tracks presence, geo-location (when approved by mobile participant),
|
|
899
|
-
// manages active/idle state for long-running events, and manages any ACTION timeouts.
|
|
827
|
+
// a PARTICIPANT_ENTER received from a new user JOIN click.
|
|
828
|
+
// Register direct participant session in AEM unless already exists.
|
|
900
829
|
id: 'participantEntering',
|
|
901
830
|
invoke: {
|
|
902
831
|
src: 'fetchParticipantData',
|
|
903
832
|
input: ({ context, event }) => ({ context, event }),
|
|
904
833
|
onDone: {
|
|
905
|
-
actions: ['
|
|
906
|
-
target: '
|
|
834
|
+
actions: ['startNewParticipantSession', 'sendEnterToAppMachine'],
|
|
835
|
+
target: 'waitingForActions',
|
|
907
836
|
},
|
|
908
837
|
onError: {
|
|
909
838
|
target: '#activeEvent.failure'
|
|
910
839
|
}
|
|
911
840
|
}
|
|
912
841
|
},
|
|
913
|
-
waitParticipantJoined: {
|
|
914
|
-
id: 'waitParticipantJoined',
|
|
915
|
-
on: {
|
|
916
|
-
PARTICIPANT_MACHINE_READY: {
|
|
917
|
-
actions: ['sendEnterToAppMachine'],
|
|
918
|
-
target: 'waitingForActions',
|
|
919
|
-
// v5 note: internal transitions are the default (v4 had `internal: true`)
|
|
920
|
-
}
|
|
921
|
-
}
|
|
922
|
-
}
|
|
923
842
|
}
|
|
924
843
|
},
|
|
925
844
|
restartEvent: {
|
|
@@ -990,10 +909,10 @@ const ActiveEventStatesConfig = {
|
|
|
990
909
|
// This implies they are not waiting for any game-over screen.
|
|
991
910
|
// Clean them up immediately without waiting.
|
|
992
911
|
guard: 'isPermanentLeave',
|
|
993
|
-
actions: ['
|
|
912
|
+
actions: ['handleParticipantExit', (c, e) => logger.debug(`[AEM] Participant ${e.data.id} permanently left during closeEvent shutdown.`)]
|
|
994
913
|
},
|
|
995
914
|
{
|
|
996
|
-
actions: ['
|
|
915
|
+
actions: ['handleParticipantExit', (c, e) => logger.debug(`[AEM] Participant ${e.data.id} left during closeEvent shutdown.`)]
|
|
997
916
|
}
|
|
998
917
|
]
|
|
999
918
|
}
|
|
@@ -1061,11 +980,11 @@ const ActiveEventStatesConfig = {
|
|
|
1061
980
|
// This implies they are not waiting for any game-over screen.
|
|
1062
981
|
// Clean them up immediately without waiting.
|
|
1063
982
|
guard: 'isPermanentLeave',
|
|
1064
|
-
actions: ['
|
|
983
|
+
actions: ['handleParticipantExit', (c, e) => logger.debug(`[AEM] Participant ${e.data.id} permanently left during canceled shutdown.`)]
|
|
1065
984
|
},
|
|
1066
985
|
{
|
|
1067
986
|
// Standard leave (e.g. disconnect). Clean them up too.
|
|
1068
|
-
actions: ['
|
|
987
|
+
actions: ['handleParticipantExit', (c, e) => logger.debug(`[AEM] Participant ${e.data.id} left during canceled shutdown.`)]
|
|
1069
988
|
}
|
|
1070
989
|
]
|
|
1071
990
|
}
|
|
@@ -1130,10 +1049,10 @@ const ActiveEventStatesConfig = {
|
|
|
1130
1049
|
PARTICIPANT_LEAVE: [
|
|
1131
1050
|
{
|
|
1132
1051
|
guard: 'isPermanentLeave',
|
|
1133
|
-
actions: ['
|
|
1052
|
+
actions: ['handleParticipantExit', (c, e) => logger.debug(`[AEM] Participant ${e.data.id} permanently left during shutdownOnly.`)]
|
|
1134
1053
|
},
|
|
1135
1054
|
{
|
|
1136
|
-
actions: ['
|
|
1055
|
+
actions: ['handleParticipantExit', (c, e) => logger.debug(`[AEM] Participant ${e.data.id} left during shutdownOnly.`)]
|
|
1137
1056
|
}
|
|
1138
1057
|
]
|
|
1139
1058
|
}
|
|
@@ -1197,10 +1116,10 @@ const ActiveEventStatesConfig = {
|
|
|
1197
1116
|
PARTICIPANT_LEAVE: [
|
|
1198
1117
|
{
|
|
1199
1118
|
guard: 'isPermanentLeave',
|
|
1200
|
-
actions: ['
|
|
1119
|
+
actions: ['handleParticipantExit', (c, e) => logger.debug(`[AEM] Participant ${e.data.id} permanently left during suspending.`)]
|
|
1201
1120
|
},
|
|
1202
1121
|
{
|
|
1203
|
-
actions: ['
|
|
1122
|
+
actions: ['handleParticipantExit', (c, e) => logger.debug(`[AEM] Participant ${e.data.id} left during suspending.`)]
|
|
1204
1123
|
}
|
|
1205
1124
|
]
|
|
1206
1125
|
}
|
|
@@ -1556,27 +1475,28 @@ const AnearEventMachineFunctions = ({
|
|
|
1556
1475
|
self
|
|
1557
1476
|
)
|
|
1558
1477
|
}),
|
|
1559
|
-
|
|
1478
|
+
startNewParticipantSession: assign(({ context, event, self }) => {
|
|
1560
1479
|
const anearParticipant = event?.output?.anearParticipant ?? event?.data?.anearParticipant
|
|
1561
1480
|
|
|
1562
1481
|
if (!anearParticipant) return {}
|
|
1563
1482
|
|
|
1564
1483
|
const existingParticipant = context.participants[anearParticipant.id]
|
|
1565
1484
|
const isOpenHouse = context.anearEvent.openHouse || false
|
|
1485
|
+
const privateChannelName = anearParticipant.privateChannelName
|
|
1486
|
+
const privateChannel = privateChannelName ? RealtimeMessaging.getChannel(privateChannelName, self) : null
|
|
1566
1487
|
|
|
1567
1488
|
// For open house events: allow machine recreation if participant exists but has no machine (reconnect scenario)
|
|
1568
1489
|
if (existingParticipant) {
|
|
1569
|
-
if (isOpenHouse && !context.
|
|
1570
|
-
// Open house reconnect: participant exists but
|
|
1571
|
-
logger.debug(`[AEM] recreating participant
|
|
1572
|
-
const service = AnearParticipantMachine(anearParticipant, context)
|
|
1490
|
+
if (isOpenHouse && !context.participantPrivateChannels[anearParticipant.id]) {
|
|
1491
|
+
// Open house reconnect: participant exists but private channel was removed, recreate it
|
|
1492
|
+
logger.debug(`[AEM] recreating participant private channel for reconnect: ${anearParticipant.id}`)
|
|
1573
1493
|
const now = Date.now()
|
|
1574
1494
|
const participantInfo = anearParticipant.participantInfo
|
|
1575
1495
|
|
|
1576
1496
|
return {
|
|
1577
|
-
|
|
1578
|
-
...context.
|
|
1579
|
-
[anearParticipant.id]:
|
|
1497
|
+
participantPrivateChannels: {
|
|
1498
|
+
...context.participantPrivateChannels,
|
|
1499
|
+
[anearParticipant.id]: privateChannel
|
|
1580
1500
|
},
|
|
1581
1501
|
participants: {
|
|
1582
1502
|
...context.participants,
|
|
@@ -1595,17 +1515,12 @@ const AnearEventMachineFunctions = ({
|
|
|
1595
1515
|
}
|
|
1596
1516
|
}
|
|
1597
1517
|
|
|
1598
|
-
logger.debug(
|
|
1599
|
-
|
|
1600
|
-
const service = AnearParticipantMachine(
|
|
1601
|
-
anearParticipant,
|
|
1602
|
-
context
|
|
1603
|
-
)
|
|
1518
|
+
logger.debug('[AEM] registering participant session for: ', anearParticipant)
|
|
1604
1519
|
|
|
1605
1520
|
return {
|
|
1606
|
-
|
|
1607
|
-
...context.
|
|
1608
|
-
[anearParticipant.id]:
|
|
1521
|
+
participantPrivateChannels: {
|
|
1522
|
+
...context.participantPrivateChannels,
|
|
1523
|
+
[anearParticipant.id]: privateChannel
|
|
1609
1524
|
},
|
|
1610
1525
|
participants: {
|
|
1611
1526
|
...context.participants,
|
|
@@ -1631,7 +1546,10 @@ const AnearEventMachineFunctions = ({
|
|
|
1631
1546
|
)
|
|
1632
1547
|
},
|
|
1633
1548
|
sendEnterToAppMachine: ({ context, event }) => {
|
|
1634
|
-
const anearParticipant =
|
|
1549
|
+
const anearParticipant =
|
|
1550
|
+
event?.output?.anearParticipant ??
|
|
1551
|
+
event?.data?.anearParticipant ??
|
|
1552
|
+
event?.anearParticipant;
|
|
1635
1553
|
|
|
1636
1554
|
if (!anearParticipant) {
|
|
1637
1555
|
logger.error('[AEM] sendEnterToAppMachine was called without an anearParticipant in the event', event);
|
|
@@ -1654,25 +1572,17 @@ const AnearEventMachineFunctions = ({
|
|
|
1654
1572
|
const participantId = event.data.id;
|
|
1655
1573
|
const participant = context.participants[participantId];
|
|
1656
1574
|
const eventName = getPresenceEventName(participant, 'DISCONNECT');
|
|
1657
|
-
const participantMachine = context.participantMachines[participantId];
|
|
1658
1575
|
const participantName = getParticipantName(context, participantId);
|
|
1659
1576
|
|
|
1660
1577
|
logger.info(`[AEM] Event ${context.anearEvent.id} ${eventName} participantId=${participantId} name=${participantName}`);
|
|
1661
|
-
if (participantMachine) {
|
|
1662
|
-
participantMachine.send({ type: 'PARTICIPANT_DISCONNECT' });
|
|
1663
|
-
}
|
|
1664
1578
|
context.appEventMachine.send({ type: eventName, participantId });
|
|
1665
1579
|
},
|
|
1666
1580
|
processReconnectEvents: ({ context, event }) => {
|
|
1667
1581
|
const participantId = event.data.id;
|
|
1668
1582
|
const participant = context.participants[participantId];
|
|
1669
1583
|
const eventName = getPresenceEventName(participant, 'RECONNECT');
|
|
1670
|
-
const participantMachine = context.participantMachines[participantId];
|
|
1671
1584
|
|
|
1672
1585
|
logger.info(`[AEM] Event ${context.anearEvent.id} ${eventName} participantId=${participantId}`);
|
|
1673
|
-
if (participantMachine) {
|
|
1674
|
-
participantMachine.send({ type: 'PARTICIPANT_RECONNECT' });
|
|
1675
|
-
}
|
|
1676
1586
|
if (context.appEventMachine) {
|
|
1677
1587
|
// Send participant info (including updated geoLocation) to AppM
|
|
1678
1588
|
const reconnectEvent = { type: eventName, participantId, participant }
|
|
@@ -1763,17 +1673,11 @@ const AnearEventMachineFunctions = ({
|
|
|
1763
1673
|
if (!participant) return;
|
|
1764
1674
|
|
|
1765
1675
|
const eventName = getPresenceEventName(participant, 'UPDATE');
|
|
1766
|
-
const participantMachine = context.participantMachines[id];
|
|
1767
1676
|
|
|
1768
1677
|
// AppM gets the role-specific event
|
|
1769
1678
|
const appMPayload = { type: eventName, participant };
|
|
1770
1679
|
|
|
1771
1680
|
logger.debug(`[AEM] processing ${eventName} for ${id}`);
|
|
1772
|
-
if (participantMachine) {
|
|
1773
|
-
// APM always gets the generic event
|
|
1774
|
-
const apmPayload = { type: 'PARTICIPANT_UPDATE', participant };
|
|
1775
|
-
participantMachine.send(apmPayload);
|
|
1776
|
-
}
|
|
1777
1681
|
if (context.appEventMachine) {
|
|
1778
1682
|
context.appEventMachine.send(appMPayload);
|
|
1779
1683
|
}
|
|
@@ -1790,72 +1694,98 @@ const AnearEventMachineFunctions = ({
|
|
|
1790
1694
|
logger.warn(`[AEM] Participant info not found for id ${participantId} during sendExitToAppMachine`);
|
|
1791
1695
|
}
|
|
1792
1696
|
},
|
|
1793
|
-
|
|
1697
|
+
sendBootEventToParticipant: ({ context, event }) => {
|
|
1794
1698
|
const { participantId, reason } = event.data;
|
|
1795
|
-
const
|
|
1796
|
-
if (
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1699
|
+
const participantChannel = context.participantPrivateChannels[participantId]
|
|
1700
|
+
if (participantChannel) {
|
|
1701
|
+
const payload = {
|
|
1702
|
+
content: {
|
|
1703
|
+
reason: reason || 'You have been removed from the event.'
|
|
1704
|
+
}
|
|
1705
|
+
}
|
|
1706
|
+
EventStats.recordPublish(context.eventStats, participantChannel, 'FORCE_SHUTDOWN', payload)
|
|
1707
|
+
RealtimeMessaging.publish(participantChannel, 'FORCE_SHUTDOWN', payload)
|
|
1801
1708
|
} else {
|
|
1802
|
-
logger.warn(`[AEM] Participant
|
|
1709
|
+
logger.warn(`[AEM] Participant private channel not found for id ${participantId} during BOOT_PARTICIPANT`)
|
|
1803
1710
|
}
|
|
1804
1711
|
},
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1712
|
+
handleParticipantExit: assign(({ context, event }) => {
|
|
1713
|
+
const participantId = event?.participantId || event?.data?.id
|
|
1714
|
+
if (!participantId) return {}
|
|
1715
|
+
|
|
1716
|
+
const participant = context.participants[participantId]
|
|
1717
|
+
const {
|
|
1718
|
+
[participantId]: participantChannel,
|
|
1719
|
+
...remainingChannels
|
|
1720
|
+
} = context.participantPrivateChannels
|
|
1721
|
+
|
|
1722
|
+
// Participant-initiated exits should not be treated as a forced boot.
|
|
1723
|
+
// ABR explicit exit flow already performs local teardown/navigation.
|
|
1724
|
+
|
|
1725
|
+
if (!participant) {
|
|
1726
|
+
return {
|
|
1727
|
+
participantPrivateChannels: remainingChannels,
|
|
1728
|
+
exitedParticipantPrivateChannels: participantChannel
|
|
1729
|
+
? [...(context.exitedParticipantPrivateChannels || []), participantChannel]
|
|
1730
|
+
: (context.exitedParticipantPrivateChannels || [])
|
|
1731
|
+
}
|
|
1813
1732
|
}
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1733
|
+
|
|
1734
|
+
const isOpenHouse = context.anearEvent.openHouse || false
|
|
1735
|
+
if (isOpenHouse) {
|
|
1736
|
+
const now = Date.now()
|
|
1737
|
+
return {
|
|
1738
|
+
participants: {
|
|
1739
|
+
...context.participants,
|
|
1740
|
+
[participantId]: {
|
|
1741
|
+
...participant,
|
|
1742
|
+
active: false,
|
|
1743
|
+
idleAt: now
|
|
1744
|
+
}
|
|
1745
|
+
},
|
|
1746
|
+
participantPrivateChannels: remainingChannels,
|
|
1747
|
+
exitedParticipantPrivateChannels: participantChannel
|
|
1748
|
+
? [...(context.exitedParticipantPrivateChannels || []), participantChannel]
|
|
1749
|
+
: (context.exitedParticipantPrivateChannels || [])
|
|
1750
|
+
}
|
|
1751
|
+
}
|
|
1752
|
+
|
|
1753
|
+
const {
|
|
1754
|
+
[participantId]: _removedParticipant,
|
|
1755
|
+
...remainingParticipants
|
|
1756
|
+
} = context.participants
|
|
1757
|
+
|
|
1831
1758
|
return {
|
|
1832
|
-
|
|
1759
|
+
participants: remainingParticipants,
|
|
1760
|
+
participantPrivateChannels: remainingChannels,
|
|
1761
|
+
exitedParticipantPrivateChannels: participantChannel
|
|
1762
|
+
? [...(context.exitedParticipantPrivateChannels || []), participantChannel]
|
|
1763
|
+
: (context.exitedParticipantPrivateChannels || [])
|
|
1833
1764
|
}
|
|
1834
1765
|
}),
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1766
|
+
sendParticipantExitEvents: assign(({ context }) => {
|
|
1767
|
+
// Normal event shutdown (close/cancel/suspend) should not look like a boot.
|
|
1768
|
+
// Keep last rendered display visible; clients will observe EVENT_TRANSITION and channel detach.
|
|
1769
|
+
return {
|
|
1770
|
+
participants: {},
|
|
1771
|
+
// Preserve private channel references so detachChannels can detach them during shutdown.
|
|
1772
|
+
participantPrivateChannels: context.participantPrivateChannels
|
|
1773
|
+
}
|
|
1774
|
+
}),
|
|
1775
|
+
cancelAllParticipantTimeouts: assign(() => ({
|
|
1776
|
+
participantsActionTimeout: null,
|
|
1777
|
+
pendingCancelAction: null
|
|
1778
|
+
})),
|
|
1779
|
+
sendCancelTimeoutToAllAPMs: () => {},
|
|
1843
1780
|
setupPendingCancelConfirmations: assign(({ context, event }) => {
|
|
1844
|
-
// Get all active participants with timers (participants in waitParticipantResponse state)
|
|
1845
|
-
const activeParticipantIds = getActiveParticipantIds(context)
|
|
1846
|
-
const pendingConfirmations = new Set(activeParticipantIds)
|
|
1847
|
-
|
|
1848
1781
|
const participantName = getParticipantName(context, event.data.participantId)
|
|
1849
|
-
logger.info(`[AEM] Event ${context.anearEvent.id}
|
|
1850
|
-
|
|
1782
|
+
logger.info(`[AEM] Event ${context.anearEvent.id} cancellation requested by participantId=${event.data.participantId} name=${participantName}`)
|
|
1851
1783
|
return {
|
|
1852
|
-
pendingCancelConfirmations: pendingConfirmations,
|
|
1853
1784
|
pendingCancelAction: {
|
|
1854
1785
|
participantId: event.data.participantId,
|
|
1855
1786
|
appEventName: event.data.appEventName,
|
|
1856
1787
|
payload: event.data.payload
|
|
1857
|
-
}
|
|
1858
|
-
suppressParticipantTimeouts: true // Drop PARTICIPANT_TIMEOUT events
|
|
1788
|
+
}
|
|
1859
1789
|
}
|
|
1860
1790
|
}),
|
|
1861
1791
|
setupPendingCancelConfirmationsForManualCancel: assign(({ context }) => {
|
|
@@ -1865,25 +1795,10 @@ const AnearEventMachineFunctions = ({
|
|
|
1865
1795
|
|
|
1866
1796
|
logger.info(`[AEM] Event ${context.anearEvent.id} starting manual cancellation sequence, waiting for ${pendingConfirmations.size} confirmations`)
|
|
1867
1797
|
|
|
1868
|
-
return {
|
|
1869
|
-
pendingCancelConfirmations: pendingConfirmations,
|
|
1870
|
-
pendingCancelAction: null, // No ACTION to forward for manual cancel
|
|
1871
|
-
suppressParticipantTimeouts: true // Drop PARTICIPANT_TIMEOUT events
|
|
1872
|
-
}
|
|
1798
|
+
return { pendingCancelAction: null }
|
|
1873
1799
|
}),
|
|
1874
1800
|
removeFromPendingCancelConfirmations: assign(({ context, event }) => {
|
|
1875
|
-
|
|
1876
|
-
if (!context.pendingCancelConfirmations || !participantId) return {}
|
|
1877
|
-
|
|
1878
|
-
const newPending = new Set(context.pendingCancelConfirmations)
|
|
1879
|
-
newPending.delete(participantId)
|
|
1880
|
-
const remaining = newPending.size
|
|
1881
|
-
|
|
1882
|
-
logger.debug(`[AEM] TIMER_CANCELED received from ${participantId}. ${remaining} confirmations remaining.`)
|
|
1883
|
-
|
|
1884
|
-
return {
|
|
1885
|
-
pendingCancelConfirmations: newPending
|
|
1886
|
-
}
|
|
1801
|
+
return {}
|
|
1887
1802
|
}),
|
|
1888
1803
|
forwardPendingCancelAction: ({ context }) => {
|
|
1889
1804
|
if (!context.pendingCancelAction) {
|
|
@@ -1904,27 +1819,15 @@ const AnearEventMachineFunctions = ({
|
|
|
1904
1819
|
// Forward to AppM
|
|
1905
1820
|
context.appEventMachine.send({ type: appEventName, ...actionEventPayload })
|
|
1906
1821
|
|
|
1907
|
-
// Forward to APM
|
|
1908
|
-
const participantMachine = context.participantMachines[participantId]
|
|
1909
|
-
if (participantMachine) {
|
|
1910
|
-
participantMachine.send({ type: 'ACTION', ...actionEventPayload })
|
|
1911
|
-
} else {
|
|
1912
|
-
logger.warn(`[AEM] Participant machine not found for ${participantId} when forwarding cancel ACTION`)
|
|
1913
|
-
}
|
|
1914
1822
|
},
|
|
1915
1823
|
clearCancelState: assign({
|
|
1916
|
-
|
|
1917
|
-
pendingCancelAction: null,
|
|
1918
|
-
suppressParticipantTimeouts: false
|
|
1824
|
+
pendingCancelAction: null
|
|
1919
1825
|
}),
|
|
1920
1826
|
dropEventDuringCancellation: ({ context, event }) => {
|
|
1921
|
-
// Drop/ignore
|
|
1922
|
-
// All other events (presence events, etc.) are not handled here and will bubble up to parent handlers
|
|
1827
|
+
// Drop/ignore events received during cancellation wait.
|
|
1923
1828
|
logger.debug(`[AEM] Dropping ${event.type} event during cancellation wait`)
|
|
1924
1829
|
},
|
|
1925
|
-
clearParticipantTimeoutSuppression: assign({
|
|
1926
|
-
suppressParticipantTimeouts: false
|
|
1927
|
-
}),
|
|
1830
|
+
clearParticipantTimeoutSuppression: assign({}),
|
|
1928
1831
|
setupCancelActionType: assign(({ context, event }) => {
|
|
1929
1832
|
const cancelActionType =
|
|
1930
1833
|
(event && event.output && event.output.cancelActionType) ||
|
|
@@ -1942,21 +1845,12 @@ const AnearEventMachineFunctions = ({
|
|
|
1942
1845
|
updateParticipantPresence: ({ context, event }) => {
|
|
1943
1846
|
const participantId = event.data.id;
|
|
1944
1847
|
const participant = context.participants[participantId];
|
|
1945
|
-
const participantMachine = context.participantMachines[participantId];
|
|
1946
1848
|
|
|
1947
1849
|
if (!participant) {
|
|
1948
1850
|
logger.warn(`[AEM] Participant info not found for id ${participantId} during updateParticipantPresence`);
|
|
1949
1851
|
return;
|
|
1950
1852
|
}
|
|
1951
1853
|
|
|
1952
|
-
// APM always gets the generic event
|
|
1953
|
-
if (participantMachine) {
|
|
1954
|
-
// opportunity to send presence data update like geoLocation, and
|
|
1955
|
-
// to inform app that a participant still has interest in the possibly long
|
|
1956
|
-
// running, light-participation event
|
|
1957
|
-
participantMachine.send({ type: 'PARTICIPANT_UPDATE', data: event.data });
|
|
1958
|
-
}
|
|
1959
|
-
|
|
1960
1854
|
// AppM gets the role-specific event
|
|
1961
1855
|
const eventName = getPresenceEventName(participant, 'UPDATE');
|
|
1962
1856
|
context.appEventMachine.send({ type: eventName, data: event.data });
|
|
@@ -1975,7 +1869,7 @@ const AnearEventMachineFunctions = ({
|
|
|
1975
1869
|
}
|
|
1976
1870
|
return updates
|
|
1977
1871
|
}),
|
|
1978
|
-
processParticipantAction: ({ context, event
|
|
1872
|
+
processParticipantAction: ({ context, event }) => {
|
|
1979
1873
|
// event.data.participantId,
|
|
1980
1874
|
// event.data.payload: {"appEventMachineACTION": {action event keys and values}}
|
|
1981
1875
|
// e.g. {"MOVE":{"x":1, "y":2}}
|
|
@@ -1988,32 +1882,11 @@ const AnearEventMachineFunctions = ({
|
|
|
1988
1882
|
logger.info(`[AEM] Event ${context.anearEvent.id} ACTION participantId=${participantId} name=${participantName} action=${appEventName}`)
|
|
1989
1883
|
logger.debug(`[AEM] eachParticipantCancelActionType is: ${context.eachParticipantCancelActionType}`)
|
|
1990
1884
|
|
|
1991
|
-
// Check if this ACTION should trigger cancellation for eachParticipant timeout
|
|
1992
|
-
if (context.eachParticipantCancelActionType === appEventName) {
|
|
1993
|
-
logger.info(`[AEM] Event ${context.anearEvent.id} intercepting cancel ACTION ${appEventName} from participantId=${participantId} name=${participantName} (eachParticipant timeout)`)
|
|
1994
|
-
// Trigger cancellation sequence via self-send
|
|
1995
|
-
self.send({
|
|
1996
|
-
type: 'TRIGGER_CANCEL_SEQUENCE',
|
|
1997
|
-
data: {
|
|
1998
|
-
participantId,
|
|
1999
|
-
appEventName,
|
|
2000
|
-
payload,
|
|
2001
|
-
event // Store original event for reference
|
|
2002
|
-
}
|
|
2003
|
-
})
|
|
2004
|
-
return // Don't forward yet, wait for cancellation to complete
|
|
2005
|
-
}
|
|
2006
|
-
|
|
2007
|
-
// Normal flow: forward immediately
|
|
2008
1885
|
const actionEventPayload = {
|
|
2009
1886
|
participantId,
|
|
2010
1887
|
payload
|
|
2011
1888
|
}
|
|
2012
1889
|
|
|
2013
|
-
const participantMachine = context.participantMachines[participantId]
|
|
2014
|
-
|
|
2015
|
-
participantMachine.send({ type: 'ACTION', ...actionEventPayload })
|
|
2016
|
-
|
|
2017
1890
|
context.appEventMachine.send({ type: appEventName, ...actionEventPayload })
|
|
2018
1891
|
},
|
|
2019
1892
|
processAndForwardAction: ({ context, event }) => {
|
|
@@ -2041,12 +1914,7 @@ const AnearEventMachineFunctions = ({
|
|
|
2041
1914
|
};
|
|
2042
1915
|
context.appEventMachine.send({ type: appEventName, ...appM_Payload });
|
|
2043
1916
|
|
|
2044
|
-
//
|
|
2045
|
-
const participantMachine = context.participantMachines[participantId];
|
|
2046
|
-
if (participantMachine) {
|
|
2047
|
-
const apm_Payload = { participantId, payload };
|
|
2048
|
-
participantMachine.send({ type: 'ACTION', ...apm_Payload });
|
|
2049
|
-
}
|
|
1917
|
+
// AEM no longer forwards ACTION to participant child machines.
|
|
2050
1918
|
},
|
|
2051
1919
|
storeCancelActionForAllParticipants: assign(({ context, event }) => {
|
|
2052
1920
|
const participantId = event.data.participantId
|
|
@@ -2075,31 +1943,7 @@ const AnearEventMachineFunctions = ({
|
|
|
2075
1943
|
consecutiveTimeoutCount: newCount
|
|
2076
1944
|
}
|
|
2077
1945
|
}),
|
|
2078
|
-
processParticipantTimeout: (
|
|
2079
|
-
const participantId = event.participantId || event.data?.participantId
|
|
2080
|
-
// Suppress PARTICIPANT_TIMEOUT events after cancelAllParticipantTimeouts() until new timers
|
|
2081
|
-
// are set up via render. This handles race conditions where an APM's timer fires just
|
|
2082
|
-
// before receiving CANCEL_TIMEOUT. The flag is cleared when renders complete (new timers set up).
|
|
2083
|
-
if (context.suppressParticipantTimeouts) {
|
|
2084
|
-
logger.debug(`[AEM] Suppressing PARTICIPANT_TIMEOUT from ${participantId} (cancelled timers, waiting for new render)`)
|
|
2085
|
-
return
|
|
2086
|
-
}
|
|
2087
|
-
|
|
2088
|
-
const participantName = getParticipantName(context, participantId)
|
|
2089
|
-
logger.info(`[AEM] Event ${context.anearEvent.id} PARTICIPANT_TIMEOUT participantId=${participantId} name=${participantName}`)
|
|
2090
|
-
const timeoutCount = context.consecutiveTimeoutCount || 0
|
|
2091
|
-
const maxConsecutiveTimeouts = C.DEAD_MAN_SWITCH.MAX_CONSECUTIVE_PARTICIPANT_TIMEOUTS
|
|
2092
|
-
|
|
2093
|
-
// Check dead-man switch: if global consecutive timeout threshold reached, cancel event
|
|
2094
|
-
// This detects when the game is "dead" (no actions happening) across all participants
|
|
2095
|
-
if (timeoutCount >= maxConsecutiveTimeouts) {
|
|
2096
|
-
logger.warn(`[AEM] Dead-man switch triggered: ${timeoutCount} consecutive timeouts across all participants (threshold: ${maxConsecutiveTimeouts}). Auto-canceling event.`)
|
|
2097
|
-
self.send({ type: 'CANCEL' })
|
|
2098
|
-
return
|
|
2099
|
-
}
|
|
2100
|
-
|
|
2101
|
-
context.appEventMachine.send({ type: 'PARTICIPANT_TIMEOUT', participantId })
|
|
2102
|
-
},
|
|
1946
|
+
processParticipantTimeout: () => {},
|
|
2103
1947
|
setupParticipantsTimeout: assign(({ context, event }) => {
|
|
2104
1948
|
// Only set up a new timeout if one is provided by the display render workflow
|
|
2105
1949
|
// AND we don't already have an active timeout (i.e., we're entering from waitingForActions,
|
|
@@ -2251,12 +2095,6 @@ const AnearEventMachineFunctions = ({
|
|
|
2251
2095
|
// Forward to AppM
|
|
2252
2096
|
context.appEventMachine.send({ type: appEventName, ...actionEventPayload })
|
|
2253
2097
|
|
|
2254
|
-
// Forward to APM
|
|
2255
|
-
const participantMachine = context.participantMachines[participantId]
|
|
2256
|
-
if (participantMachine) {
|
|
2257
|
-
participantMachine.send({ type: 'ACTION', ...actionEventPayload })
|
|
2258
|
-
}
|
|
2259
|
-
|
|
2260
2098
|
const participantName = getParticipantName(context, participantId)
|
|
2261
2099
|
logger.info(`[AEM] Event ${context.anearEvent.id} forwarded cancel ACTION ${appEventName} from participantId=${participantId} name=${participantName} after allParticipants cancellation`)
|
|
2262
2100
|
|
|
@@ -2267,54 +2105,41 @@ const AnearEventMachineFunctions = ({
|
|
|
2267
2105
|
participantsActionTimeout: null
|
|
2268
2106
|
}),
|
|
2269
2107
|
cleanupExitingParticipant: assign(({ context, event }) => {
|
|
2270
|
-
const
|
|
2108
|
+
const participantId = event?.participantId || event?.data?.id
|
|
2109
|
+
if (!participantId) return {}
|
|
2271
2110
|
const participant = context.participants[participantId]
|
|
2111
|
+
const {
|
|
2112
|
+
[participantId]: _removedChannel,
|
|
2113
|
+
...remainingChannels
|
|
2114
|
+
} = context.participantPrivateChannels
|
|
2272
2115
|
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
const isOpenHouse = context.anearEvent.openHouse || false
|
|
2277
|
-
|
|
2278
|
-
if (isOpenHouse) {
|
|
2279
|
-
// For open house events: preserve participant with active: false, only remove machine
|
|
2280
|
-
const {
|
|
2281
|
-
[participantId]: removedMachine,
|
|
2282
|
-
...remainingMachines
|
|
2283
|
-
} = context.participantMachines
|
|
2284
|
-
|
|
2285
|
-
const now = Date.now()
|
|
2286
|
-
const updatedParticipant = {
|
|
2287
|
-
...participant,
|
|
2288
|
-
active: false,
|
|
2289
|
-
idleAt: now
|
|
2290
|
-
}
|
|
2291
|
-
|
|
2292
|
-
return {
|
|
2293
|
-
participants: {
|
|
2294
|
-
...context.participants,
|
|
2295
|
-
[participantId]: updatedParticipant
|
|
2296
|
-
},
|
|
2297
|
-
participantMachines: remainingMachines
|
|
2298
|
-
}
|
|
2299
|
-
} else {
|
|
2300
|
-
// For non-open-house events: remove both participant and machine (existing behavior)
|
|
2301
|
-
const {
|
|
2302
|
-
[participantId]: removedParticipant,
|
|
2303
|
-
...remainingParticipants
|
|
2304
|
-
} = context.participants
|
|
2305
|
-
|
|
2306
|
-
const {
|
|
2307
|
-
[participantId]: removedMachine,
|
|
2308
|
-
...remainingMachines
|
|
2309
|
-
} = context.participantMachines
|
|
2116
|
+
if (!participant) {
|
|
2117
|
+
return { participantPrivateChannels: remainingChannels }
|
|
2118
|
+
}
|
|
2310
2119
|
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2120
|
+
const isOpenHouse = context.anearEvent.openHouse || false
|
|
2121
|
+
if (isOpenHouse) {
|
|
2122
|
+
const now = Date.now()
|
|
2123
|
+
return {
|
|
2124
|
+
participants: {
|
|
2125
|
+
...context.participants,
|
|
2126
|
+
[participantId]: {
|
|
2127
|
+
...participant,
|
|
2128
|
+
active: false,
|
|
2129
|
+
idleAt: now
|
|
2130
|
+
}
|
|
2131
|
+
},
|
|
2132
|
+
participantPrivateChannels: remainingChannels
|
|
2315
2133
|
}
|
|
2316
|
-
}
|
|
2317
|
-
|
|
2134
|
+
}
|
|
2135
|
+
|
|
2136
|
+
const {
|
|
2137
|
+
[participantId]: _removedParticipant,
|
|
2138
|
+
...remainingParticipants
|
|
2139
|
+
} = context.participants
|
|
2140
|
+
return {
|
|
2141
|
+
participants: remainingParticipants,
|
|
2142
|
+
participantPrivateChannels: remainingChannels
|
|
2318
2143
|
}
|
|
2319
2144
|
}),
|
|
2320
2145
|
notifyCoreServiceMachineExit: ({ context }) => {
|
|
@@ -2322,7 +2147,8 @@ const AnearEventMachineFunctions = ({
|
|
|
2322
2147
|
context.coreServiceMachine.send({ type: 'EVENT_MACHINE_EXIT', eventId: context.anearEvent.id })
|
|
2323
2148
|
},
|
|
2324
2149
|
logCreatorEnter: ({ event }) => logger.debug("[AEM] got creator PARTICIPANT_ENTER: ", event.data.id),
|
|
2325
|
-
|
|
2150
|
+
logWaitingForCreatorPresence: ({ context }) => logger.debug(`[AEM] Event ${context.anearEvent.id} creator/host not found via actions presence.get(); waiting for PARTICIPANT_ENTER`),
|
|
2151
|
+
logAPMReady: () => logger.debug('[AEM] participant session registered'),
|
|
2326
2152
|
logInvalidParticipantEnter: ({ context, event }) => logger.info(`[AEM] Event ${context.anearEvent.id} unexpected PARTICIPANT_ENTER participantId=${event.data.id}`),
|
|
2327
2153
|
logDeferringAppmFinal: ({ context }) => logger.info(`[AEM] Event ${context.anearEvent.id} deferring APPM_FINAL (already transitioning to terminated)`),
|
|
2328
2154
|
logImplicitShutdownWarning: (_c, _e) => {
|
|
@@ -2599,7 +2425,25 @@ const AnearEventMachineFunctions = ({
|
|
|
2599
2425
|
context.participantsDisplayChannel,
|
|
2600
2426
|
]
|
|
2601
2427
|
if (context.spectatorsDisplayChannel) channels.push(context.spectatorsDisplayChannel)
|
|
2602
|
-
|
|
2428
|
+
Object.values(context.participantPrivateChannels || {}).forEach(ch => {
|
|
2429
|
+
if (ch) channels.push(ch)
|
|
2430
|
+
})
|
|
2431
|
+
Object.values(context.exitedParticipantPrivateChannels || {}).forEach(ch => {
|
|
2432
|
+
if (ch) channels.push(ch)
|
|
2433
|
+
})
|
|
2434
|
+
|
|
2435
|
+
const uniqueChannels = Array.from(new Set(channels.filter(Boolean)))
|
|
2436
|
+
const attachedChannels = uniqueChannels.filter((ch) => ch?.state === 'attached')
|
|
2437
|
+
const nonAttachedChannels = uniqueChannels.filter((ch) => ch?.state !== 'attached')
|
|
2438
|
+
|
|
2439
|
+
logger.debug(
|
|
2440
|
+
`[AEM] detachChannels total=${uniqueChannels.length} attached=${attachedChannels.length} skipped=${nonAttachedChannels.length}`
|
|
2441
|
+
)
|
|
2442
|
+
nonAttachedChannels.forEach((ch) => {
|
|
2443
|
+
logger.debug(`[AEM] detachChannels skipping channel ${ch.name} state=${(ch?.state || 'unknown').toUpperCase()}`)
|
|
2444
|
+
})
|
|
2445
|
+
|
|
2446
|
+
await RealtimeMessaging.detachAll(attachedChannels)
|
|
2603
2447
|
return 'done'
|
|
2604
2448
|
}),
|
|
2605
2449
|
fetchParticipantData: fromPromise(async ({ input }) => {
|
|
@@ -2694,6 +2538,10 @@ const AnearEventMachineFunctions = ({
|
|
|
2694
2538
|
})
|
|
2695
2539
|
},
|
|
2696
2540
|
guards: {
|
|
2541
|
+
hasAttachedCreatorOrHost: ({ event }) => {
|
|
2542
|
+
const anearParticipant = event?.output?.anearParticipant ?? event?.data?.anearParticipant
|
|
2543
|
+
return !!anearParticipant
|
|
2544
|
+
},
|
|
2697
2545
|
isReconnectUpdate: ({ context, event }) => {
|
|
2698
2546
|
// participant exists and event.data.type is undefined
|
|
2699
2547
|
return context.participants[event.data.id] && !event.data.type
|
|
@@ -2719,9 +2567,9 @@ const AnearEventMachineFunctions = ({
|
|
|
2719
2567
|
const participantId = event.data.id
|
|
2720
2568
|
const isOpenHouse = context.anearEvent.openHouse || false
|
|
2721
2569
|
const participantExists = !!context.participants[participantId]
|
|
2722
|
-
const
|
|
2723
|
-
// For open house events: participant exists but
|
|
2724
|
-
return isOpenHouse && participantExists && !
|
|
2570
|
+
const channelExists = !!context.participantPrivateChannels[participantId]
|
|
2571
|
+
// For open house events: participant exists but private channel was removed (idle state)
|
|
2572
|
+
return isOpenHouse && participantExists && !channelExists
|
|
2725
2573
|
},
|
|
2726
2574
|
eventCreatorIsHost: ({ context }) => context.anearEvent.hosted,
|
|
2727
2575
|
isOpenHouseEvent: ({ context }) => context.anearEvent.openHouse || false,
|
|
@@ -2747,9 +2595,7 @@ const AnearEventMachineFunctions = ({
|
|
|
2747
2595
|
allParticipantsResponded: ({ context }) => {
|
|
2748
2596
|
return context.participantsActionTimeout && context.participantsActionTimeout.nonResponders.size === 0
|
|
2749
2597
|
},
|
|
2750
|
-
allCancelConfirmationsReceived: (
|
|
2751
|
-
return !context.pendingCancelConfirmations || context.pendingCancelConfirmations.size === 0
|
|
2752
|
-
}
|
|
2598
|
+
allCancelConfirmationsReceived: () => true
|
|
2753
2599
|
},
|
|
2754
2600
|
delays: {
|
|
2755
2601
|
// in the future, these delays should be goverened by the type of App and
|
|
@@ -2767,7 +2613,6 @@ const AnearEventMachine = (anearEvent, {
|
|
|
2767
2613
|
coreServiceMachine,
|
|
2768
2614
|
pugTemplates,
|
|
2769
2615
|
appEventMachineFactory,
|
|
2770
|
-
appParticipantMachineFactory,
|
|
2771
2616
|
imageAssetsUrl,
|
|
2772
2617
|
rehydrate = false
|
|
2773
2618
|
}) => {
|
|
@@ -2787,7 +2632,6 @@ const AnearEventMachine = (anearEvent, {
|
|
|
2787
2632
|
pugTemplates,
|
|
2788
2633
|
pugHelpers,
|
|
2789
2634
|
appEventMachineFactory,
|
|
2790
|
-
appParticipantMachineFactory,
|
|
2791
2635
|
rehydrate
|
|
2792
2636
|
)
|
|
2793
2637
|
|