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.
@@ -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 and optional participant machines.
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
- participantMachines: {},
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
- // v5 note: onDone of a fromPromise actor provides the resolved value on `event.output`.
254
- // event.output.anearParticipant will be null if no presence enter
255
- // was available on actions channel via get(). We must wait for
256
- // the undeferred PARTICIPANT_ENTER instead. But if get() DID
257
- // return the creator presence, then an APM will be created
258
- // and we goto waiting for the PARTICIPANT_MACHINE_READY
259
- actions: ['startNewParticipantMachine'],
260
- target: 'waiting'
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: ['startNewParticipantMachine'],
293
- target: '#waiting.creatorStatus'
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', 'sendExitToParticipantMachine']
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', 'sendExitToParticipantMachine']
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: 'sendBootEventToParticipantMachine'
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
- // a PARTICIPANT_ENTER received from a new user JOIN click. create an AnearParticipantMachine instance.
594
- // This machine tracks presence, geo-location (when approved by mobile participant),
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: ['startNewParticipantMachine'],
602
- target: '#waitParticipantReady'
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', 'sendExitToParticipantMachine']
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: 'sendBootEventToParticipantMachine'
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
- // Forward per-participant timeouts (from APM) to the AppM
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', 'clearParticipantTimeoutSuppression']
698
+ actions: ['setLastRenderResult', 'setupParticipantsTimeout', 'setupCancelActionType']
768
699
  },
769
700
  {
770
701
  target: 'notifyingRenderComplete',
771
- actions: ['setLastRenderResult', 'setupCancelActionType', 'clearParticipantTimeoutSuppression']
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', 'clearParticipantTimeoutSuppression']
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', 'clearParticipantTimeoutSuppression', 'notifyAppMachineRendered']
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', 'sendExitToParticipantMachine']
806
+ actions: ['removeLeavingParticipantFromTimeout', 'handleParticipantExit']
876
807
  },
877
808
  {
878
809
  guard: 'isPermanentLeave',
879
- actions: ['removeLeavingParticipantFromTimeout', 'sendExitToAppMachine', 'sendExitToParticipantMachine']
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. Unless already exists,
897
- // create an AnearParticipantMachine instance.
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: ['startNewParticipantMachine'],
906
- target: '#waitParticipantJoined',
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: ['cleanupExitingParticipant', (c, e) => logger.debug(`[AEM] Participant ${e.data.id} permanently left during closeEvent shutdown.`)]
912
+ actions: ['handleParticipantExit', (c, e) => logger.debug(`[AEM] Participant ${e.data.id} permanently left during closeEvent shutdown.`)]
994
913
  },
995
914
  {
996
- actions: ['cleanupExitingParticipant', (c, e) => logger.debug(`[AEM] Participant ${e.data.id} left during closeEvent shutdown.`)]
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: ['cleanupExitingParticipant', (c, e) => logger.debug(`[AEM] Participant ${e.data.id} permanently left during canceled shutdown.`)]
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: ['cleanupExitingParticipant', (c, e) => logger.debug(`[AEM] Participant ${e.data.id} left during canceled shutdown.`)]
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: ['cleanupExitingParticipant', (c, e) => logger.debug(`[AEM] Participant ${e.data.id} permanently left during shutdownOnly.`)]
1052
+ actions: ['handleParticipantExit', (c, e) => logger.debug(`[AEM] Participant ${e.data.id} permanently left during shutdownOnly.`)]
1134
1053
  },
1135
1054
  {
1136
- actions: ['cleanupExitingParticipant', (c, e) => logger.debug(`[AEM] Participant ${e.data.id} left during shutdownOnly.`)]
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: ['cleanupExitingParticipant', (c, e) => logger.debug(`[AEM] Participant ${e.data.id} permanently left during suspending.`)]
1119
+ actions: ['handleParticipantExit', (c, e) => logger.debug(`[AEM] Participant ${e.data.id} permanently left during suspending.`)]
1201
1120
  },
1202
1121
  {
1203
- actions: ['cleanupExitingParticipant', (c, e) => logger.debug(`[AEM] Participant ${e.data.id} left during suspending.`)]
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
- startNewParticipantMachine: assign(({ context, event }) => {
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.participantMachines[anearParticipant.id]) {
1570
- // Open house reconnect: participant exists but machine was removed, recreate it
1571
- logger.debug(`[AEM] recreating participant machine for reconnect: ${anearParticipant.id}`)
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
- participantMachines: {
1578
- ...context.participantMachines,
1579
- [anearParticipant.id]: service.start()
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("[AEM] starting new participant machine for: ", anearParticipant)
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
- participantMachines: {
1607
- ...context.participantMachines,
1608
- [anearParticipant.id]: service.start()
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 = event.data?.anearParticipant ?? event.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
- sendBootEventToParticipantMachine: ({ context, event }) => {
1697
+ sendBootEventToParticipant: ({ context, event }) => {
1794
1698
  const { participantId, reason } = event.data;
1795
- const participantMachine = context.participantMachines[participantId];
1796
- if (participantMachine) {
1797
- participantMachine.send({
1798
- type: 'BOOT_PARTICIPANT',
1799
- data: { reason }
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 machine not found for id ${participantId} during sendBootEventToParticipantMachine`);
1709
+ logger.warn(`[AEM] Participant private channel not found for id ${participantId} during BOOT_PARTICIPANT`)
1803
1710
  }
1804
1711
  },
1805
- sendExitToParticipantMachine: ({ context, event }) => {
1806
- // coming from an action channel message, event.data.id
1807
- const participantMachine = context.participantMachines[event.data.id]
1808
- if (participantMachine) {
1809
- logger.debug("[AEM] sending PARTICIPANT_EXIT to ", participantMachine.id)
1810
- participantMachine.send({ type: 'PARTICIPANT_EXIT' })
1811
- } else {
1812
- logger.warn(`[AEM] Participant machine not found for id ${event.data.id} during sendExitToParticipantMachine`)
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
- sendParticipantExitEvents: ({ context }) => {
1816
- Object.values(context.participantMachines).forEach(pm => pm.send({ type: 'PARTICIPANT_EXIT' }))
1817
- },
1818
- cancelAllParticipantTimeouts: assign(({ context }) => {
1819
- // Send CANCEL_TIMEOUT to all participant machines to cancel any active
1820
- // participant-level timeouts. This is useful when a game event (e.g., LIAR call)
1821
- // should immediately cancel all individual participant timeouts.
1822
- // APMs that are not in waitParticipantResponse state will benignly ignore this event.
1823
- logger.info(`[AEM] Event ${context.anearEvent.id} cancelling all participant timeouts (manual) for ${Object.keys(context.participantMachines).length} participants`)
1824
- Object.values(context.participantMachines).forEach(pm => pm.send({ type: 'CANCEL_TIMEOUT' }))
1825
-
1826
- // Note: If we're in waitAllParticipantsResponse state, the nested state's handler
1827
- // will also handle this event and cancel the allParticipants timeout.
1828
-
1829
- // Set suppression flag to ignore stale PARTICIPANT_TIMEOUT events from cancelled timers.
1830
- // This flag will be cleared when new timers are set up (via render completion).
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
- suppressParticipantTimeouts: true
1759
+ participants: remainingParticipants,
1760
+ participantPrivateChannels: remainingChannels,
1761
+ exitedParticipantPrivateChannels: participantChannel
1762
+ ? [...(context.exitedParticipantPrivateChannels || []), participantChannel]
1763
+ : (context.exitedParticipantPrivateChannels || [])
1833
1764
  }
1834
1765
  }),
1835
- sendCancelTimeoutToAllAPMs: ({ context }) => {
1836
- const participantCount = Object.keys(context.participantMachines).length
1837
- const actionName = context.eachParticipantCancelActionType || 'unknown'
1838
- logger.info(`[AEM] Event ${context.anearEvent.id} sending CANCEL_TIMEOUT to all ${participantCount} APMs for ACTION ${actionName}`)
1839
- Object.values(context.participantMachines).forEach(pm => {
1840
- pm.send({ type: 'CANCEL_TIMEOUT' })
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} starting cancellation sequence for ACTION ${event.data.appEventName} from participantId=${event.data.participantId} name=${participantName}, waiting for ${pendingConfirmations.size} confirmations`)
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
- const participantId = event.participantId || event.data?.id
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
- pendingCancelConfirmations: null,
1917
- pendingCancelAction: null,
1918
- suppressParticipantTimeouts: false
1824
+ pendingCancelAction: null
1919
1825
  }),
1920
1826
  dropEventDuringCancellation: ({ context, event }) => {
1921
- // Drop/ignore ACTION and PARTICIPANT_TIMEOUT events received during cancellation wait
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, self }) => {
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
- // Forward to APM (without the flag)
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: ({ context, event, self }) => {
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 { participantId } = event
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
- logger.debug(`[AEM] cleaning up exiting participant ${participantId}`)
2274
-
2275
- if (participant) {
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
- return {
2312
- participants: remainingParticipants,
2313
- participantMachines: remainingMachines
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
- } else {
2317
- return {}
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
- logAPMReady: ({ event }) => logger.debug("[AEM] PARTICIPANT_MACHINE_READY for: ", event.data.anearParticipant.id),
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
- await RealtimeMessaging.detachAll(channels)
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 machineExists = !!context.participantMachines[participantId]
2723
- // For open house events: participant exists but machine was removed (idle state)
2724
- return isOpenHouse && participantExists && !machineExists
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: ({ context }) => {
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