anear-js-api 0.5.2 → 0.5.4
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 +5 -91
- package/RENDER_USAGE_EXAMPLES.md +6 -2
- package/lib/api/AnearApi.js +22 -0
- package/lib/api/ApiService.js +12 -0
- package/lib/models/AnearEvent.js +19 -8
- package/lib/state_machines/AnearCoreServiceMachine.js +12 -3
- package/lib/state_machines/AnearEventMachine.js +332 -101
- package/lib/state_machines/AnearParticipantMachine.js +55 -1
- package/lib/utils/AppMachineTransition.js +45 -29
- package/lib/utils/Constants.js +2 -1
- package/lib/utils/DisplayEventProcessor.js +114 -12
- package/lib/utils/RealtimeMessaging.js +9 -0
- package/lib/utils/RenderContextBuilder.js +7 -5
- package/package.json +1 -1
|
@@ -34,6 +34,11 @@ const getAllParticipantIds = (context) => {
|
|
|
34
34
|
return Object.keys(context.participants);
|
|
35
35
|
};
|
|
36
36
|
|
|
37
|
+
const getPresenceEventName = (participant, presenceAction) => {
|
|
38
|
+
const role = participant && participant.isHost ? 'HOST' : 'PARTICIPANT';
|
|
39
|
+
return `${role}_${presenceAction}`;
|
|
40
|
+
};
|
|
41
|
+
|
|
37
42
|
const RealtimeMessaging = require('../utils/RealtimeMessaging')
|
|
38
43
|
const AppMachineTransition = require('../utils/AppMachineTransition')
|
|
39
44
|
const DisplayEventProcessor = require('../utils/DisplayEventProcessor')
|
|
@@ -51,7 +56,8 @@ const AnearEventMachineContext = (
|
|
|
51
56
|
pugTemplates,
|
|
52
57
|
pugHelpers,
|
|
53
58
|
appEventMachineFactory,
|
|
54
|
-
appParticipantMachineFactory
|
|
59
|
+
appParticipantMachineFactory,
|
|
60
|
+
rehydrate = false
|
|
55
61
|
) => ({
|
|
56
62
|
anearEvent,
|
|
57
63
|
coreServiceMachine,
|
|
@@ -59,6 +65,7 @@ const AnearEventMachineContext = (
|
|
|
59
65
|
pugHelpers,
|
|
60
66
|
appEventMachineFactory,
|
|
61
67
|
appParticipantMachineFactory,
|
|
68
|
+
rehydrate,
|
|
62
69
|
appEventMachine: null,
|
|
63
70
|
eventChannel: null, // event control messages
|
|
64
71
|
actionsChannel: null, // participant presence/live actions
|
|
@@ -72,11 +79,14 @@ const AnearEventMachineContext = (
|
|
|
72
79
|
const DeferredStates = [
|
|
73
80
|
'PARTICIPANT_ENTER',
|
|
74
81
|
'PARTICIPANT_LEAVE',
|
|
82
|
+
'PARTICIPANT_UPDATE',
|
|
75
83
|
'SPECTATOR_ENTER',
|
|
76
84
|
'PARTICIPANT_TIMEOUT',
|
|
77
85
|
'ACTION',
|
|
78
86
|
'CANCEL',
|
|
79
|
-
'CLOSE'
|
|
87
|
+
'CLOSE',
|
|
88
|
+
'SAVE',
|
|
89
|
+
'PAUSE'
|
|
80
90
|
]
|
|
81
91
|
|
|
82
92
|
const DeferredStatesPlus = (...additionalStates) => DeferredStates.concat(additionalStates)
|
|
@@ -94,6 +104,10 @@ const ActiveEventGlobalEvents = {
|
|
|
94
104
|
CANCEL: {
|
|
95
105
|
// appM does an abrupt shutdown of the event
|
|
96
106
|
target: '#canceled'
|
|
107
|
+
},
|
|
108
|
+
APPM_FINAL: {
|
|
109
|
+
// AppM reached a root-level final → cleanup only (no ANAPI transition)
|
|
110
|
+
target: '#activeEvent.shutdownOnly'
|
|
97
111
|
}
|
|
98
112
|
}
|
|
99
113
|
|
|
@@ -186,10 +200,6 @@ const ActiveEventStatesConfig = {
|
|
|
186
200
|
target: '#createdRendering'
|
|
187
201
|
},
|
|
188
202
|
PARTICIPANT_LEAVE: [
|
|
189
|
-
{
|
|
190
|
-
cond: 'isHostLeaving',
|
|
191
|
-
actions: ['sendHostExitToAppMachine', 'sendExitToParticipantMachine']
|
|
192
|
-
},
|
|
193
203
|
{
|
|
194
204
|
cond: 'isPermanentLeave',
|
|
195
205
|
actions: ['sendExitToAppMachine', 'sendExitToParticipantMachine']
|
|
@@ -198,6 +208,25 @@ const ActiveEventStatesConfig = {
|
|
|
198
208
|
actions: ['processDisconnectEvents']
|
|
199
209
|
}
|
|
200
210
|
],
|
|
211
|
+
PARTICIPANT_UPDATE: [
|
|
212
|
+
// In the future, the AnearBrowser may periodically send presence updates to the AEM
|
|
213
|
+
// which will include latitude/longitude direction, etc. for Apps that require this
|
|
214
|
+
// level of approved user location tracking. These types of explicit presence.updates
|
|
215
|
+
// will include a type field in the event payload to indicate the type of update.
|
|
216
|
+
// Otherwise, the Realtime messaging system in the mobile client browser may be
|
|
217
|
+
// sending this presence updated implicityly without a type because it has detected
|
|
218
|
+
// that the user left the event and came back. If we receive this and the participant exists,
|
|
219
|
+
// this is a PARTICIPANT_RECONNECT scenario. The User likely navigated away from the
|
|
220
|
+
// event and came back. We need to update the AppM with a PARTICIPANT_RECONNECT event
|
|
221
|
+
// so they can refresh the view for the returning participant.
|
|
222
|
+
{
|
|
223
|
+
cond: 'isReconnectUpdate',
|
|
224
|
+
actions: 'processReconnectEvents'
|
|
225
|
+
},
|
|
226
|
+
{
|
|
227
|
+
actions: ['updateParticipantGeoLocation', 'processUpdateEvents']
|
|
228
|
+
}
|
|
229
|
+
],
|
|
201
230
|
PARTICIPANT_ENTER: [
|
|
202
231
|
{
|
|
203
232
|
cond: 'participantExists',
|
|
@@ -224,6 +253,42 @@ const ActiveEventStatesConfig = {
|
|
|
224
253
|
},
|
|
225
254
|
START: {
|
|
226
255
|
target: '#activeEvent.live'
|
|
256
|
+
},
|
|
257
|
+
PAUSE: {
|
|
258
|
+
target: '#pausingEvent'
|
|
259
|
+
},
|
|
260
|
+
SAVE: {
|
|
261
|
+
target: 'savingAppEventContext'
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
},
|
|
265
|
+
pausingEvent: {
|
|
266
|
+
id: 'pausingEvent',
|
|
267
|
+
deferred: DeferredStates,
|
|
268
|
+
invoke: {
|
|
269
|
+
src: 'saveAppEventContext',
|
|
270
|
+
onDone: {
|
|
271
|
+
actions: ['sendPausedAckToAppMachine'],
|
|
272
|
+
target: '#waitingAnnounce',
|
|
273
|
+
internal: true
|
|
274
|
+
},
|
|
275
|
+
onError: {
|
|
276
|
+
target: '#activeEvent.failure'
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
},
|
|
280
|
+
savingAppEventContext: {
|
|
281
|
+
id: 'savingAppEventContext',
|
|
282
|
+
invoke: {
|
|
283
|
+
src: 'saveAppEventContext',
|
|
284
|
+
onDone: {
|
|
285
|
+
actions: ['sendSavedAckToAppMachine'],
|
|
286
|
+
target: '#waitingAnnounce',
|
|
287
|
+
internal: true
|
|
288
|
+
},
|
|
289
|
+
onError: {
|
|
290
|
+
// If save fails, log and remain in waitingAnnounce; AppM may retry/handle error UI
|
|
291
|
+
target: '#waitingAnnounce'
|
|
227
292
|
}
|
|
228
293
|
}
|
|
229
294
|
},
|
|
@@ -265,10 +330,6 @@ const ActiveEventStatesConfig = {
|
|
|
265
330
|
target: '#announceRendering'
|
|
266
331
|
},
|
|
267
332
|
PARTICIPANT_LEAVE: [
|
|
268
|
-
{
|
|
269
|
-
cond: 'isHostLeaving',
|
|
270
|
-
actions: ['sendHostExitToAppMachine', 'sendExitToParticipantMachine']
|
|
271
|
-
},
|
|
272
333
|
{
|
|
273
334
|
cond: 'isPermanentLeave',
|
|
274
335
|
actions: ['sendExitToAppMachine', 'sendExitToParticipantMachine']
|
|
@@ -289,6 +350,9 @@ const ActiveEventStatesConfig = {
|
|
|
289
350
|
target: '#newParticipantJoining'
|
|
290
351
|
}
|
|
291
352
|
],
|
|
353
|
+
BOOT_PARTICIPANT: {
|
|
354
|
+
actions: 'sendBootEventToParticipantMachine'
|
|
355
|
+
},
|
|
292
356
|
SPECTATOR_ENTER: {
|
|
293
357
|
actions: 'sendSpectatorEnterToAppEventMachine'
|
|
294
358
|
}
|
|
@@ -385,10 +449,6 @@ const ActiveEventStatesConfig = {
|
|
|
385
449
|
target: '#liveRendering'
|
|
386
450
|
},
|
|
387
451
|
PARTICIPANT_LEAVE: [
|
|
388
|
-
{
|
|
389
|
-
cond: 'isHostLeaving',
|
|
390
|
-
actions: ['sendHostExitToAppMachine', 'sendExitToParticipantMachine']
|
|
391
|
-
},
|
|
392
452
|
{
|
|
393
453
|
cond: 'isPermanentLeave',
|
|
394
454
|
actions: ['sendExitToAppMachine', 'sendExitToParticipantMachine']
|
|
@@ -407,8 +467,8 @@ const ActiveEventStatesConfig = {
|
|
|
407
467
|
target: '.participantEntering'
|
|
408
468
|
}
|
|
409
469
|
],
|
|
410
|
-
|
|
411
|
-
|
|
470
|
+
BOOT_PARTICIPANT: {
|
|
471
|
+
actions: 'sendBootEventToParticipantMachine'
|
|
412
472
|
}
|
|
413
473
|
},
|
|
414
474
|
states: {
|
|
@@ -491,8 +551,8 @@ const ActiveEventStatesConfig = {
|
|
|
491
551
|
},
|
|
492
552
|
PARTICIPANT_LEAVE: [
|
|
493
553
|
{
|
|
494
|
-
cond: '
|
|
495
|
-
actions: ['removeLeavingParticipantFromTimeout', '
|
|
554
|
+
cond: 'isBooting',
|
|
555
|
+
actions: ['removeLeavingParticipantFromTimeout', 'sendExitToParticipantMachine']
|
|
496
556
|
},
|
|
497
557
|
{
|
|
498
558
|
cond: 'isPermanentLeave',
|
|
@@ -539,29 +599,6 @@ const ActiveEventStatesConfig = {
|
|
|
539
599
|
}
|
|
540
600
|
}
|
|
541
601
|
},
|
|
542
|
-
paused: {
|
|
543
|
-
initial: 'transitioning',
|
|
544
|
-
states: {
|
|
545
|
-
transitioning: {
|
|
546
|
-
deferred: DeferredStatesPlus('RESUME', 'CLOSE'),
|
|
547
|
-
invoke: {
|
|
548
|
-
src: 'transitionToPaused',
|
|
549
|
-
onDone: {
|
|
550
|
-
target: 'waitingForResume'
|
|
551
|
-
},
|
|
552
|
-
onError: {
|
|
553
|
-
target: '#activeEvent.failure'
|
|
554
|
-
}
|
|
555
|
-
}
|
|
556
|
-
},
|
|
557
|
-
waitingForResume: {
|
|
558
|
-
on: {
|
|
559
|
-
RESUME: '#activeEvent.live',
|
|
560
|
-
CLOSE: '#activeEvent.closeEvent'
|
|
561
|
-
}
|
|
562
|
-
}
|
|
563
|
-
}
|
|
564
|
-
},
|
|
565
602
|
closeEvent: {
|
|
566
603
|
id: 'closeEvent',
|
|
567
604
|
initial: 'notifyingParticipants',
|
|
@@ -631,7 +668,7 @@ const ActiveEventStatesConfig = {
|
|
|
631
668
|
}
|
|
632
669
|
},
|
|
633
670
|
finalizing: {
|
|
634
|
-
|
|
671
|
+
// canceled path does ANAPI transition to canceled, then detaches
|
|
635
672
|
invoke: {
|
|
636
673
|
src: 'eventTransitionCanceled',
|
|
637
674
|
onDone: 'detaching',
|
|
@@ -652,6 +689,42 @@ const ActiveEventStatesConfig = {
|
|
|
652
689
|
}
|
|
653
690
|
}
|
|
654
691
|
},
|
|
692
|
+
shutdownOnly: {
|
|
693
|
+
id: 'shutdownOnly',
|
|
694
|
+
initial: 'notifyingParticipants',
|
|
695
|
+
states: {
|
|
696
|
+
notifyingParticipants: {
|
|
697
|
+
entry: 'sendParticipantExitEvents',
|
|
698
|
+
always: 'waitForParticipantsToExit'
|
|
699
|
+
},
|
|
700
|
+
waitForParticipantsToExit: {
|
|
701
|
+
entry: context => logger.debug(`[AEM] Entering waitForParticipantsToExit with ${getAllParticipantIds(context).length} participants`),
|
|
702
|
+
always: [
|
|
703
|
+
{
|
|
704
|
+
cond: context => getAllParticipantIds(context).length === 0,
|
|
705
|
+
target: 'detaching'
|
|
706
|
+
}
|
|
707
|
+
],
|
|
708
|
+
on: {
|
|
709
|
+
PARTICIPANT_LEAVE: {
|
|
710
|
+
actions: () => logger.debug('[AEM] Ignoring PARTICIPANT_LEAVE during orchestrated shutdown.')
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
},
|
|
714
|
+
detaching: {
|
|
715
|
+
deferred: DeferredStates,
|
|
716
|
+
invoke: {
|
|
717
|
+
src: 'detachChannels',
|
|
718
|
+
onDone: {
|
|
719
|
+
target: '#activeEvent.doneExit'
|
|
720
|
+
},
|
|
721
|
+
onError: {
|
|
722
|
+
target: '#activeEvent.failure'
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
},
|
|
655
728
|
review: {
|
|
656
729
|
on: {
|
|
657
730
|
NEXT: 'reward'
|
|
@@ -708,7 +781,10 @@ const CreateEventChannelsAndAppMachineConfig = {
|
|
|
708
781
|
states: {
|
|
709
782
|
setupEventChannel: {
|
|
710
783
|
// get(eventChannelName) and setup state-change callbacks
|
|
711
|
-
entry: [(c,e) =>
|
|
784
|
+
entry: [(c,e) => {
|
|
785
|
+
const eventType = c.rehydrate ? 'LOAD_EVENT' : 'CREATE_EVENT'
|
|
786
|
+
logger.debug(`[AEM] === ${eventType} ${c.anearEvent.id} ===`)
|
|
787
|
+
}, 'createEventChannel'],
|
|
712
788
|
invoke: {
|
|
713
789
|
src: 'attachToEventChannel',
|
|
714
790
|
onDone: {
|
|
@@ -720,7 +796,18 @@ const CreateEventChannelsAndAppMachineConfig = {
|
|
|
720
796
|
}
|
|
721
797
|
},
|
|
722
798
|
on: {
|
|
723
|
-
ATTACHED:
|
|
799
|
+
ATTACHED: 'enterEventPresence'
|
|
800
|
+
}
|
|
801
|
+
},
|
|
802
|
+
enterEventPresence: {
|
|
803
|
+
invoke: {
|
|
804
|
+
src: 'enterEventPresence',
|
|
805
|
+
onDone: {
|
|
806
|
+
actions: ['subscribeToEventMessages'],
|
|
807
|
+
target: 'setupParticipantsDisplayChannel',
|
|
808
|
+
internal: true
|
|
809
|
+
},
|
|
810
|
+
onError: {
|
|
724
811
|
actions: ['subscribeToEventMessages'],
|
|
725
812
|
target: 'setupParticipantsDisplayChannel'
|
|
726
813
|
}
|
|
@@ -782,9 +869,16 @@ const CreateEventChannelsAndAppMachineConfig = {
|
|
|
782
869
|
}
|
|
783
870
|
},
|
|
784
871
|
createAppEventMachine: {
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
872
|
+
invoke: {
|
|
873
|
+
src: 'createAppEventMachine',
|
|
874
|
+
onDone: {
|
|
875
|
+
actions: ['setAppEventMachine'],
|
|
876
|
+
target: '#activeEvent',
|
|
877
|
+
internal: true
|
|
878
|
+
},
|
|
879
|
+
onError: {
|
|
880
|
+
target: '#activeEvent.failure'
|
|
881
|
+
}
|
|
788
882
|
}
|
|
789
883
|
}
|
|
790
884
|
}
|
|
@@ -806,12 +900,20 @@ const AnearEventMachineStatesConfig = eventId => ({
|
|
|
806
900
|
|
|
807
901
|
const AnearEventMachineFunctions = ({
|
|
808
902
|
actions: {
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
903
|
+
sendPausedAckToAppMachine: (context, _event) => {
|
|
904
|
+
if (context.appEventMachine) {
|
|
905
|
+
context.appEventMachine.send('PAUSED')
|
|
906
|
+
}
|
|
907
|
+
},
|
|
908
|
+
sendSavedAckToAppMachine: (context, _event) => {
|
|
909
|
+
if (context.appEventMachine) {
|
|
910
|
+
context.appEventMachine.send('SAVED')
|
|
911
|
+
}
|
|
912
|
+
},
|
|
913
|
+
setAppEventMachine: assign({
|
|
914
|
+
appEventMachine: (context, event) => {
|
|
915
|
+
const service = event.data.service
|
|
916
|
+
return service
|
|
815
917
|
}
|
|
816
918
|
}),
|
|
817
919
|
notifyAppMachineRendered: (context, event) => {
|
|
@@ -924,8 +1026,7 @@ const AnearEventMachineFunctions = ({
|
|
|
924
1026
|
return;
|
|
925
1027
|
}
|
|
926
1028
|
|
|
927
|
-
const
|
|
928
|
-
const eventName = isHost ? 'HOST_ENTER' : 'PARTICIPANT_ENTER';
|
|
1029
|
+
const eventName = getPresenceEventName(anearParticipant, 'ENTER');
|
|
929
1030
|
const eventPayload = { participant: participantInfo };
|
|
930
1031
|
|
|
931
1032
|
logger.debug(`[AEM] Sending ${eventName} for ${anearParticipant.id}`);
|
|
@@ -934,11 +1035,10 @@ const AnearEventMachineFunctions = ({
|
|
|
934
1035
|
processDisconnectEvents: (context, event) => {
|
|
935
1036
|
const participantId = event.data.id;
|
|
936
1037
|
const participant = context.participants[participantId];
|
|
937
|
-
const
|
|
938
|
-
const eventName = isHost ? 'HOST_DISCONNECT' : 'PARTICIPANT_DISCONNECT';
|
|
1038
|
+
const eventName = getPresenceEventName(participant, 'DISCONNECT');
|
|
939
1039
|
const participantMachine = context.participantMachines[participantId];
|
|
940
1040
|
|
|
941
|
-
logger.debug(`[AEM] processing
|
|
1041
|
+
logger.debug(`[AEM] processing ${eventName} for ${participantId}`);
|
|
942
1042
|
if (participantMachine) {
|
|
943
1043
|
participantMachine.send('PARTICIPANT_DISCONNECT');
|
|
944
1044
|
}
|
|
@@ -947,31 +1047,79 @@ const AnearEventMachineFunctions = ({
|
|
|
947
1047
|
processReconnectEvents: (context, event) => {
|
|
948
1048
|
const participantId = event.data.id;
|
|
949
1049
|
const participant = context.participants[participantId];
|
|
950
|
-
const
|
|
951
|
-
const eventName = isHost ? 'HOST_RECONNECT' : 'PARTICIPANT_RECONNECT';
|
|
1050
|
+
const eventName = getPresenceEventName(participant, 'RECONNECT');
|
|
952
1051
|
const participantMachine = context.participantMachines[participantId];
|
|
953
1052
|
|
|
954
|
-
logger.debug(`[AEM] processing
|
|
1053
|
+
logger.debug(`[AEM] processing ${eventName} for ${participantId}`);
|
|
955
1054
|
if (participantMachine) {
|
|
956
1055
|
participantMachine.send('PARTICIPANT_RECONNECT');
|
|
957
1056
|
}
|
|
958
1057
|
context.appEventMachine.send(eventName, { participantId });
|
|
959
1058
|
},
|
|
1059
|
+
updateParticipantGeoLocation: assign({
|
|
1060
|
+
participants: (context, event) => {
|
|
1061
|
+
const { id, geoLocation } = event.data;
|
|
1062
|
+
const participantInfoToUpdate = context.participants[id];
|
|
1063
|
+
|
|
1064
|
+
if (!participantInfoToUpdate) return context.participants;
|
|
1065
|
+
|
|
1066
|
+
// Create a new, updated info object
|
|
1067
|
+
const updatedParticipantInfo = {
|
|
1068
|
+
...participantInfoToUpdate,
|
|
1069
|
+
geoLocation
|
|
1070
|
+
};
|
|
1071
|
+
|
|
1072
|
+
// Return a new participants object with the updated participant info
|
|
1073
|
+
return {
|
|
1074
|
+
...context.participants,
|
|
1075
|
+
[id]: updatedParticipantInfo
|
|
1076
|
+
};
|
|
1077
|
+
}
|
|
1078
|
+
}),
|
|
1079
|
+
processUpdateEvents: (context, event) => {
|
|
1080
|
+
const { id } = event.data;
|
|
1081
|
+
// NOTE: get the full AnearParticipant object from the AEM instance,
|
|
1082
|
+
// NOT the plain info object from the context.
|
|
1083
|
+
const participant = context.participants[id];
|
|
1084
|
+
|
|
1085
|
+
if (!participant) return;
|
|
1086
|
+
|
|
1087
|
+
const eventName = getPresenceEventName(participant, 'UPDATE');
|
|
1088
|
+
const participantMachine = context.participantMachines[id];
|
|
1089
|
+
|
|
1090
|
+
// AppM gets the role-specific event
|
|
1091
|
+
const appMPayload = { type: eventName, participant };
|
|
1092
|
+
|
|
1093
|
+
logger.debug(`[AEM] processing ${eventName} for ${id}`);
|
|
1094
|
+
if (participantMachine) {
|
|
1095
|
+
// APM always gets the generic event
|
|
1096
|
+
const apmPayload = { type: 'PARTICIPANT_UPDATE', participant };
|
|
1097
|
+
participantMachine.send(apmPayload);
|
|
1098
|
+
}
|
|
1099
|
+
context.appEventMachine.send(appMPayload);
|
|
1100
|
+
},
|
|
960
1101
|
sendExitToAppMachine: (context, event) => {
|
|
961
|
-
// coming from an action channel message, event.data.id
|
|
962
1102
|
const participantId = event.data.id;
|
|
963
|
-
const
|
|
964
|
-
if (
|
|
965
|
-
|
|
966
|
-
|
|
1103
|
+
const participant = context.participants[participantId];
|
|
1104
|
+
if (participant) {
|
|
1105
|
+
const eventName = getPresenceEventName(participant, 'EXIT');
|
|
1106
|
+
logger.debug(`[AEM] sending ${eventName} to AppM for participant ${participantId}`);
|
|
1107
|
+
context.appEventMachine.send(eventName, { participantId });
|
|
967
1108
|
} else {
|
|
968
1109
|
logger.warn(`[AEM] Participant info not found for id ${participantId} during sendExitToAppMachine`);
|
|
969
1110
|
}
|
|
970
1111
|
},
|
|
971
|
-
|
|
972
|
-
const participantId = event.data
|
|
973
|
-
|
|
974
|
-
|
|
1112
|
+
sendBootEventToParticipantMachine: (context, event) => {
|
|
1113
|
+
const { participantId, reason } = event.data;
|
|
1114
|
+
const participantMachine = context.participantMachines[participantId];
|
|
1115
|
+
if (participantMachine) {
|
|
1116
|
+
participantMachine.send({
|
|
1117
|
+
type: 'BOOT_PARTICIPANT',
|
|
1118
|
+
data: { reason }
|
|
1119
|
+
})
|
|
1120
|
+
} else {
|
|
1121
|
+
logger.warn(`[AEM] Participant machine not found for id ${participantId} during sendBootEventToParticipantMachine`);
|
|
1122
|
+
}
|
|
975
1123
|
},
|
|
976
1124
|
sendExitToParticipantMachine: (context, event) => {
|
|
977
1125
|
// coming from an action channel message, event.data.id
|
|
@@ -986,18 +1134,27 @@ const AnearEventMachineFunctions = ({
|
|
|
986
1134
|
sendParticipantExitEvents: context => {
|
|
987
1135
|
Object.values(context.participantMachines).forEach(pm => pm.send('PARTICIPANT_EXIT'))
|
|
988
1136
|
},
|
|
1137
|
+
updateParticipantPresence: (context, event) => {
|
|
1138
|
+
const participantId = event.data.id;
|
|
1139
|
+
const participant = context.participants[participantId];
|
|
1140
|
+
const participantMachine = context.participantMachines[participantId];
|
|
989
1141
|
|
|
1142
|
+
if (!participant) {
|
|
1143
|
+
logger.warn(`[AEM] Participant info not found for id ${participantId} during updateParticipantPresence`);
|
|
1144
|
+
return;
|
|
1145
|
+
}
|
|
990
1146
|
|
|
991
|
-
|
|
992
|
-
// lookup the participantMachine and update its context
|
|
993
|
-
const participantMachine = context.participantMachines[event.data.id]
|
|
1147
|
+
// APM always gets the generic event
|
|
994
1148
|
if (participantMachine) {
|
|
995
1149
|
// opportunity to send presence data update like geoLocation, and
|
|
996
1150
|
// to inform app that a participant still has interest in the possibly long
|
|
997
1151
|
// running, light-participation event
|
|
998
|
-
participantMachine.send('PARTICIPANT_UPDATE', event.data)
|
|
1152
|
+
participantMachine.send('PARTICIPANT_UPDATE', event.data);
|
|
999
1153
|
}
|
|
1000
|
-
|
|
1154
|
+
|
|
1155
|
+
// AppM gets the role-specific event
|
|
1156
|
+
const eventName = getPresenceEventName(participant, 'UPDATE');
|
|
1157
|
+
context.appEventMachine.send(eventName, event.data);
|
|
1001
1158
|
},
|
|
1002
1159
|
processParticipantAction: (context, event) => {
|
|
1003
1160
|
// event.data.participantId,
|
|
@@ -1053,16 +1210,23 @@ const AnearEventMachineFunctions = ({
|
|
|
1053
1210
|
{ participantId: event.participantId }
|
|
1054
1211
|
),
|
|
1055
1212
|
setupParticipantsTimeout: assign((context, event) => {
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1213
|
+
// Only set up a new timeout if one is provided in the event data.
|
|
1214
|
+
// This prevents overwriting an existing timeout during a simple re-render.
|
|
1215
|
+
if (event.data && event.data.participantsTimeout) {
|
|
1216
|
+
const timeoutMsecs = event.data.participantsTimeout.msecs
|
|
1217
|
+
const allParticipantIds = getPlayingParticipantIds(context)
|
|
1218
|
+
logger.debug(`[AEM] Starting participants action timeout for ${timeoutMsecs}ms. Responders: ${allParticipantIds.join(', ')}`)
|
|
1059
1219
|
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1220
|
+
return {
|
|
1221
|
+
participantsActionTimeout: {
|
|
1222
|
+
msecs: timeoutMsecs,
|
|
1223
|
+
startedAt: Date.now(),
|
|
1224
|
+
nonResponders: new Set(allParticipantIds)
|
|
1225
|
+
}
|
|
1064
1226
|
}
|
|
1065
1227
|
}
|
|
1228
|
+
// If no timeout data, return empty object to not change context
|
|
1229
|
+
return {};
|
|
1066
1230
|
}),
|
|
1067
1231
|
processParticipantResponse: assign((context, event) => {
|
|
1068
1232
|
const participantId = event.data.participantId
|
|
@@ -1078,17 +1242,24 @@ const AnearEventMachineFunctions = ({
|
|
|
1078
1242
|
}
|
|
1079
1243
|
}),
|
|
1080
1244
|
removeLeavingParticipantFromTimeout: assign((context, event) => {
|
|
1081
|
-
const participantId = event.data.id
|
|
1082
|
-
const
|
|
1083
|
-
|
|
1084
|
-
|
|
1245
|
+
const participantId = event.data.id;
|
|
1246
|
+
const participant = context.participants[participantId];
|
|
1247
|
+
|
|
1248
|
+
// If there's no active timeout, or if the leaving participant is the host, do nothing.
|
|
1249
|
+
if (!context.participantsActionTimeout || (participant && participant.isHost)) {
|
|
1250
|
+
return {};
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
const { nonResponders, ...rest } = context.participantsActionTimeout;
|
|
1254
|
+
const newNonResponders = new Set(nonResponders);
|
|
1255
|
+
newNonResponders.delete(participantId);
|
|
1085
1256
|
|
|
1086
1257
|
return {
|
|
1087
1258
|
participantsActionTimeout: {
|
|
1088
1259
|
...rest,
|
|
1089
1260
|
nonResponders: newNonResponders
|
|
1090
1261
|
}
|
|
1091
|
-
}
|
|
1262
|
+
};
|
|
1092
1263
|
}),
|
|
1093
1264
|
sendActionsTimeoutToAppM: (context, _event) => {
|
|
1094
1265
|
const { nonResponders, msecs } = context.participantsActionTimeout
|
|
@@ -1139,6 +1310,63 @@ const AnearEventMachineFunctions = ({
|
|
|
1139
1310
|
logInvalidParticipantEnter: (c, e) => logger.info("[AEM] Error: Unexepected PARTICIPANT_ENTER with id: ", e.data.id),
|
|
1140
1311
|
},
|
|
1141
1312
|
services: {
|
|
1313
|
+
saveAppEventContext: async (context, event) => {
|
|
1314
|
+
// Events like PAUSE/SAVE are sent as send('PAUSE', { appmContext: {...} })
|
|
1315
|
+
// event.appmContext -> { context, resumeEvent }
|
|
1316
|
+
const appmContext = event?.appmContext || {}
|
|
1317
|
+
const payload = {
|
|
1318
|
+
eventId: context.anearEvent.id,
|
|
1319
|
+
savedAt: new Date().toISOString(),
|
|
1320
|
+
...appmContext
|
|
1321
|
+
}
|
|
1322
|
+
await AnearApi.saveAppEventContext(context.anearEvent.id, payload)
|
|
1323
|
+
return 'done'
|
|
1324
|
+
},
|
|
1325
|
+
enterEventPresence: async (context, _event) => {
|
|
1326
|
+
const data = { actor: 'AEM', eventId: context.anearEvent.id, start: Date.now() }
|
|
1327
|
+
await RealtimeMessaging.setPresence(context.eventChannel, data)
|
|
1328
|
+
return { service: started }
|
|
1329
|
+
},
|
|
1330
|
+
createAppEventMachine: async (context, _event) => {
|
|
1331
|
+
// Build the AppM, optionally rehydrating from saved app_event_context
|
|
1332
|
+
const baseMachine = context.appEventMachineFactory(context.anearEvent)
|
|
1333
|
+
|
|
1334
|
+
let machineToStart = baseMachine
|
|
1335
|
+
let resumeEvent = null
|
|
1336
|
+
|
|
1337
|
+
if (context.rehydrate) {
|
|
1338
|
+
try {
|
|
1339
|
+
const { appmContext } = await AnearApi.getLatestAppEventContext(context.anearEvent.id)
|
|
1340
|
+
if (appmContext && typeof appmContext === 'object') {
|
|
1341
|
+
const savedContext = appmContext.context
|
|
1342
|
+
resumeEvent = appmContext.resumeEvent
|
|
1343
|
+
if (savedContext && typeof savedContext === 'object') {
|
|
1344
|
+
machineToStart = baseMachine.withContext(savedContext)
|
|
1345
|
+
}
|
|
1346
|
+
}
|
|
1347
|
+
} catch (e) {
|
|
1348
|
+
// Log and proceed without rehydration
|
|
1349
|
+
logger.warn('[AEM] Failed to fetch or parse app_event_context. Starting clean.', e)
|
|
1350
|
+
}
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1353
|
+
const service = interpret(machineToStart)
|
|
1354
|
+
service.subscribe(AppMachineTransition(context.anearEvent))
|
|
1355
|
+
// Auto-cleanup when AppM final: notify AEM
|
|
1356
|
+
try {
|
|
1357
|
+
service.onDone(() => {
|
|
1358
|
+
logger.debug('[AEM] AppM reached final state, sending APPM_FINAL for cleanup-only shutdown')
|
|
1359
|
+
context.anearEvent.send('APPM_FINAL')
|
|
1360
|
+
})
|
|
1361
|
+
} catch (_e) {}
|
|
1362
|
+
const started = service.start()
|
|
1363
|
+
|
|
1364
|
+
if (resumeEvent && resumeEvent.type) {
|
|
1365
|
+
started.send(resumeEvent)
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1368
|
+
return { service: started }
|
|
1369
|
+
},
|
|
1142
1370
|
renderDisplay: async (context, event) => {
|
|
1143
1371
|
const displayEventProcessor = new DisplayEventProcessor(context)
|
|
1144
1372
|
|
|
@@ -1201,9 +1429,6 @@ const AnearEventMachineFunctions = ({
|
|
|
1201
1429
|
transitionToCanceled: (context, event) => {
|
|
1202
1430
|
return AnearApi.transitionEvent(context.anearEvent.id, 'canceled')
|
|
1203
1431
|
},
|
|
1204
|
-
transitionToPaused: (context, event) => {
|
|
1205
|
-
return AnearApi.transitionEvent(context.anearEvent.id, 'paused')
|
|
1206
|
-
},
|
|
1207
1432
|
eventTransitionClosed: async (context, event) => {
|
|
1208
1433
|
// This service handles the transition of the event to 'closed' via AnearApi
|
|
1209
1434
|
// and the publishing of the 'EVENT_TRANSITION' message to ABRs.
|
|
@@ -1224,6 +1449,10 @@ const AnearEventMachineFunctions = ({
|
|
|
1224
1449
|
}
|
|
1225
1450
|
},
|
|
1226
1451
|
guards: {
|
|
1452
|
+
isReconnectUpdate: (context, event) => {
|
|
1453
|
+
// participant exists and event.data.type is undefined
|
|
1454
|
+
return context.participants[event.data.id] && !event.data.type
|
|
1455
|
+
},
|
|
1227
1456
|
isPermanentLeave: (context, event) => {
|
|
1228
1457
|
// The remote client has left the event. This is a permanent exit
|
|
1229
1458
|
// from the event
|
|
@@ -1237,18 +1466,18 @@ const AnearEventMachineFunctions = ({
|
|
|
1237
1466
|
return false
|
|
1238
1467
|
}
|
|
1239
1468
|
},
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
const participant = context.participants[participantId];
|
|
1243
|
-
return participant && participant.isHost;
|
|
1469
|
+
isBooting: (_c, event) => {
|
|
1470
|
+
return event.data.type === 'BOOTED'
|
|
1244
1471
|
},
|
|
1245
1472
|
participantExists: (context, event) => !!context.participants[event.data.id],
|
|
1246
|
-
eventCreatorIsHost: (context,
|
|
1247
|
-
isOpenHouseEvent: (context,
|
|
1473
|
+
eventCreatorIsHost: (context, _e) => context.anearEvent.hosted,
|
|
1474
|
+
isOpenHouseEvent: (context, _e) => context.anearEvent.openHouse || false,
|
|
1248
1475
|
isParticipantsTimeoutActive: (context, event) => {
|
|
1249
|
-
|
|
1476
|
+
const isStartingNewTimeout = event.data && event.data.participantsTimeout && event.data.participantsTimeout.msecs > 0;
|
|
1477
|
+
const isTimeoutAlreadyRunning = context.participantsActionTimeout !== null;
|
|
1478
|
+
return isStartingNewTimeout || isTimeoutAlreadyRunning;
|
|
1250
1479
|
},
|
|
1251
|
-
allParticipantsResponded: (context,
|
|
1480
|
+
allParticipantsResponded: (context, _e) => {
|
|
1252
1481
|
return context.participantsActionTimeout && context.participantsActionTimeout.nonResponders.size === 0
|
|
1253
1482
|
}
|
|
1254
1483
|
},
|
|
@@ -1258,7 +1487,7 @@ const AnearEventMachineFunctions = ({
|
|
|
1258
1487
|
timeoutEventAnnounce: context => C.TIMEOUT_MSECS.ANNOUNCE,
|
|
1259
1488
|
timeoutEventStart: context => C.TIMEOUT_MSECS.START,
|
|
1260
1489
|
timeoutRendered: context => C.TIMEOUT_MSECS.RENDERED_EVENT_DELAY,
|
|
1261
|
-
participantsActionTimeout: (context,
|
|
1490
|
+
participantsActionTimeout: (context, _e) => {
|
|
1262
1491
|
return context.participantsActionTimeout.msecs
|
|
1263
1492
|
}
|
|
1264
1493
|
}
|
|
@@ -1269,7 +1498,8 @@ const AnearEventMachine = (anearEvent, {
|
|
|
1269
1498
|
pugTemplates,
|
|
1270
1499
|
appEventMachineFactory,
|
|
1271
1500
|
appParticipantMachineFactory,
|
|
1272
|
-
imageAssetsUrl
|
|
1501
|
+
imageAssetsUrl,
|
|
1502
|
+
rehydrate = false
|
|
1273
1503
|
}) => {
|
|
1274
1504
|
const expandedConfig = {predictableActionArguments: true, ...AnearEventMachineStatesConfig(anearEvent.id)}
|
|
1275
1505
|
|
|
@@ -1282,7 +1512,8 @@ const AnearEventMachine = (anearEvent, {
|
|
|
1282
1512
|
pugTemplates,
|
|
1283
1513
|
pugHelpers,
|
|
1284
1514
|
appEventMachineFactory,
|
|
1285
|
-
appParticipantMachineFactory
|
|
1515
|
+
appParticipantMachineFactory,
|
|
1516
|
+
rehydrate
|
|
1286
1517
|
)
|
|
1287
1518
|
|
|
1288
1519
|
const service = interpret(eventMachine.withContext(anearEventMachineContext))
|