anear-js-api 1.3.1 → 1.3.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/state_machines/AnearCoreServiceMachine.js +21 -5
- package/lib/state_machines/AnearEventMachine.js +67 -30
- package/lib/state_machines/AnearParticipantMachine.js +101 -33
- package/lib/utils/AppMachineTransition.js +2 -1
- package/lib/utils/CssUploader.js +3 -1
- package/lib/utils/DisplayEventProcessor.js +23 -17
- package/lib/utils/FontAssetsUploader.js +5 -2
- package/lib/utils/ImageAssetsUploader.js +3 -0
- package/lib/utils/RealtimeMessaging.js +49 -3
- package/package.json +1 -1
|
@@ -80,7 +80,10 @@ const AnearCoreServiceMachineConfig = appId => ({
|
|
|
80
80
|
context: ({ input }) => input,
|
|
81
81
|
states: {
|
|
82
82
|
fetchAppDataWithRetry: {
|
|
83
|
-
entry:
|
|
83
|
+
entry: ({ context }) => {
|
|
84
|
+
const appId = context.appId || process.env.ANEARAPP_APP_ID || 'unknown'
|
|
85
|
+
logger.info(`[ACSM] ===== Booting App (${appId}) on anear-js-api version ${anearJsApiVersion} =====`)
|
|
86
|
+
},
|
|
84
87
|
initial: 'fetchAppData',
|
|
85
88
|
states: {
|
|
86
89
|
fetchAppData: {
|
|
@@ -111,7 +114,7 @@ const AnearCoreServiceMachineConfig = appId => ({
|
|
|
111
114
|
entry: ['initRealtime'],
|
|
112
115
|
on: {
|
|
113
116
|
CONNECTED: {
|
|
114
|
-
actions: ['createEventsCreationChannel', 'subscribeCreateEventMessages']
|
|
117
|
+
actions: ['createEventsCreationChannel', 'subscribeCreateEventMessages', ({ context }) => logger.info(`[ACSM] Realtime messaging connected for app ${context.appId}`)]
|
|
115
118
|
},
|
|
116
119
|
ATTACHED: 'uploadNewImageAssets'
|
|
117
120
|
}
|
|
@@ -168,7 +171,11 @@ const AnearCoreServiceMachineConfig = appId => ({
|
|
|
168
171
|
// The Anear API backend will send CREATE_EVENT or LOAD_EVENT messages with the event JSON data
|
|
169
172
|
// to this createEventMessages Channel when it needs to create a new instance of an
|
|
170
173
|
// Event
|
|
171
|
-
entry: ({ context }) =>
|
|
174
|
+
entry: ({ context }) => {
|
|
175
|
+
const shortName = context.appData.data.attributes['short-name']
|
|
176
|
+
logger.info(`[ACSM] Ready to receive events for app ${shortName}`)
|
|
177
|
+
logger.debug(`Waiting on ${shortName} lifecycle command`)
|
|
178
|
+
},
|
|
172
179
|
on: {
|
|
173
180
|
CREATE_EVENT: {
|
|
174
181
|
actions: ['startNewEventMachine']
|
|
@@ -221,6 +228,8 @@ const AnearCoreServiceMachineFunctions = {
|
|
|
221
228
|
pugTemplates: (_args) => {
|
|
222
229
|
const pugLoader = new PugLoader(DefaultTemplatesRootDir)
|
|
223
230
|
const templates = pugLoader.compiledPugTemplates()
|
|
231
|
+
const templateCount = Object.keys(templates).length
|
|
232
|
+
logger.info(`[ACSM] Loaded ${templateCount} Pug template(s)`)
|
|
224
233
|
logger.debug(`loaded pug templates ${Object.keys(templates)}`)
|
|
225
234
|
return templates
|
|
226
235
|
}
|
|
@@ -240,7 +249,9 @@ const AnearCoreServiceMachineFunctions = {
|
|
|
240
249
|
{
|
|
241
250
|
appData: ({ event }) => {
|
|
242
251
|
const output = event.output
|
|
243
|
-
|
|
252
|
+
const attrs = output.data.attributes
|
|
253
|
+
logger.info(`[ACSM] Loading App from API app.short_name=${attrs["short-name"]} app.slug=${attrs.slug} app.id=${output.data.id}`)
|
|
254
|
+
logger.debug(`fetched ${attrs["short-name"]} app data`)
|
|
244
255
|
return output
|
|
245
256
|
}
|
|
246
257
|
}
|
|
@@ -264,6 +275,7 @@ const AnearCoreServiceMachineFunctions = {
|
|
|
264
275
|
self,
|
|
265
276
|
'LOAD_EVENT'
|
|
266
277
|
)
|
|
278
|
+
logger.info(`[ACSM] Subscribed to CREATE_EVENT and LOAD_EVENT on events channel`)
|
|
267
279
|
},
|
|
268
280
|
startNewEventMachine: assign(
|
|
269
281
|
{
|
|
@@ -294,7 +306,11 @@ const AnearCoreServiceMachineFunctions = {
|
|
|
294
306
|
),
|
|
295
307
|
cleanupEventMachine: assign(({ context, event }) => {
|
|
296
308
|
const { [event.eventId]: dropped, ...remaining } = context.anearEventMachines
|
|
297
|
-
|
|
309
|
+
if (dropped) {
|
|
310
|
+
logger.info(`[ACSM] Event ${event.eventId} closed, freeing event slot`)
|
|
311
|
+
} else {
|
|
312
|
+
logger.debug(`ACSM ${event.eventId} is NOT FOUND`)
|
|
313
|
+
}
|
|
298
314
|
|
|
299
315
|
return {
|
|
300
316
|
anearEventMachines: remaining
|
|
@@ -78,6 +78,12 @@ const getAllParticipantIds = (context) => {
|
|
|
78
78
|
return Object.keys(context.participants);
|
|
79
79
|
};
|
|
80
80
|
|
|
81
|
+
// Helper to get participant name from ID, fallback to ID if not found
|
|
82
|
+
const getParticipantName = (context, participantId) => {
|
|
83
|
+
const participant = context.participants[participantId];
|
|
84
|
+
return participant?.name || participantId;
|
|
85
|
+
};
|
|
86
|
+
|
|
81
87
|
// Participant status helper functions for active/idle state management
|
|
82
88
|
const getActiveParticipantIds = (context) => {
|
|
83
89
|
return Object.keys(context.participants).filter(id => {
|
|
@@ -187,6 +193,7 @@ const ActiveEventGlobalEvents = {
|
|
|
187
193
|
},
|
|
188
194
|
CANCEL: {
|
|
189
195
|
// appM does an abrupt shutdown of the event
|
|
196
|
+
actions: ({ context }) => logger.info(`[AEM] Event ${context.anearEvent.id} CANCEL received`),
|
|
190
197
|
target: '#canceled'
|
|
191
198
|
},
|
|
192
199
|
APPM_FINAL: {
|
|
@@ -518,7 +525,7 @@ const ActiveEventStatesConfig = {
|
|
|
518
525
|
},
|
|
519
526
|
waitingToStart: {
|
|
520
527
|
id: 'waitingToStart',
|
|
521
|
-
entry: () => logger.
|
|
528
|
+
entry: ({ context }) => logger.info(`[AEM] Event ${context.anearEvent.id} created → announce`),
|
|
522
529
|
after: {
|
|
523
530
|
timeoutEventStart: {
|
|
524
531
|
actions: ({ context }) => logger.info(`[AEM] Event ${context.anearEvent.id} TIMED OUT waiting for START`),
|
|
@@ -527,6 +534,7 @@ const ActiveEventStatesConfig = {
|
|
|
527
534
|
},
|
|
528
535
|
on: {
|
|
529
536
|
START: {
|
|
537
|
+
actions: ({ context }) => logger.info(`[AEM] Event ${context.anearEvent.id} announce → live`),
|
|
530
538
|
target: '#activeEvent.live'
|
|
531
539
|
}
|
|
532
540
|
}
|
|
@@ -653,7 +661,13 @@ const ActiveEventStatesConfig = {
|
|
|
653
661
|
},
|
|
654
662
|
waitingForActions: {
|
|
655
663
|
id: 'waitingForActions',
|
|
656
|
-
entry: () =>
|
|
664
|
+
entry: ({ context }) => {
|
|
665
|
+
// Only log transition on initial entry to live state, not on nested state changes
|
|
666
|
+
const currentState = context.appEventMachine?.getSnapshot?.()?.value
|
|
667
|
+
if (!currentState || typeof currentState === 'string' || Object.keys(currentState).length === 0) {
|
|
668
|
+
logger.debug('[AEM] live state...waiting for actions')
|
|
669
|
+
}
|
|
670
|
+
},
|
|
657
671
|
initial: 'idle',
|
|
658
672
|
states: {
|
|
659
673
|
idle: {},
|
|
@@ -887,6 +901,7 @@ const ActiveEventStatesConfig = {
|
|
|
887
901
|
},
|
|
888
902
|
closeEvent: {
|
|
889
903
|
id: 'closeEvent',
|
|
904
|
+
entry: ({ context }) => logger.info(`[AEM] Event ${context.anearEvent.id} live → closed`),
|
|
890
905
|
initial: 'notifyingParticipants',
|
|
891
906
|
on: {
|
|
892
907
|
APPM_FINAL: {
|
|
@@ -954,6 +969,7 @@ const ActiveEventStatesConfig = {
|
|
|
954
969
|
},
|
|
955
970
|
canceled: {
|
|
956
971
|
id: 'canceled',
|
|
972
|
+
entry: ({ context }) => logger.info(`[AEM] Event ${context.anearEvent.id} → canceled`),
|
|
957
973
|
initial: 'notifyingParticipants',
|
|
958
974
|
on: {
|
|
959
975
|
CLOSE: {
|
|
@@ -1231,7 +1247,7 @@ const CreateEventChannelsAndAppMachineConfig = {
|
|
|
1231
1247
|
// get(eventChannelName) and setup state-change callbacks
|
|
1232
1248
|
entry: [({ context }) => {
|
|
1233
1249
|
const eventType = context.rehydrate ? 'LOAD_EVENT' : 'CREATE_EVENT'
|
|
1234
|
-
logger.
|
|
1250
|
+
logger.info(`[AEM] ${eventType} Event ${context.anearEvent.id}`)
|
|
1235
1251
|
}, 'createEventChannel'],
|
|
1236
1252
|
invoke: {
|
|
1237
1253
|
src: 'attachToEventChannel',
|
|
@@ -1526,6 +1542,7 @@ const AnearEventMachineFunctions = ({
|
|
|
1526
1542
|
userJSON => {
|
|
1527
1543
|
if (!context.appEventMachine) return
|
|
1528
1544
|
|
|
1545
|
+
logger.info(`[AEM] Event ${context.anearEvent.id} SPECTATOR_ENTER spectatorId=${userJSON.id}`)
|
|
1529
1546
|
const spectatorEvent = { type: 'SPECTATOR_ENTER', data: userJSON }
|
|
1530
1547
|
context.appEventMachine.send(spectatorEvent)
|
|
1531
1548
|
}
|
|
@@ -1548,7 +1565,7 @@ const AnearEventMachineFunctions = ({
|
|
|
1548
1565
|
const eventName = getPresenceEventName(anearParticipant, 'ENTER');
|
|
1549
1566
|
const eventPayload = { type: eventName, participant: participantInfo };
|
|
1550
1567
|
|
|
1551
|
-
logger.
|
|
1568
|
+
logger.info(`[AEM] Event ${context.anearEvent.id} ${eventName} participantId=${anearParticipant.id} name=${participantInfo.name}`);
|
|
1552
1569
|
context.appEventMachine.send(eventPayload);
|
|
1553
1570
|
},
|
|
1554
1571
|
processDisconnectEvents: ({ context, event }) => {
|
|
@@ -1556,8 +1573,9 @@ const AnearEventMachineFunctions = ({
|
|
|
1556
1573
|
const participant = context.participants[participantId];
|
|
1557
1574
|
const eventName = getPresenceEventName(participant, 'DISCONNECT');
|
|
1558
1575
|
const participantMachine = context.participantMachines[participantId];
|
|
1576
|
+
const participantName = getParticipantName(context, participantId);
|
|
1559
1577
|
|
|
1560
|
-
logger.
|
|
1578
|
+
logger.info(`[AEM] Event ${context.anearEvent.id} ${eventName} participantId=${participantId} name=${participantName}`);
|
|
1561
1579
|
if (participantMachine) {
|
|
1562
1580
|
participantMachine.send({ type: 'PARTICIPANT_DISCONNECT' });
|
|
1563
1581
|
}
|
|
@@ -1569,7 +1587,7 @@ const AnearEventMachineFunctions = ({
|
|
|
1569
1587
|
const eventName = getPresenceEventName(participant, 'RECONNECT');
|
|
1570
1588
|
const participantMachine = context.participantMachines[participantId];
|
|
1571
1589
|
|
|
1572
|
-
logger.
|
|
1590
|
+
logger.info(`[AEM] Event ${context.anearEvent.id} ${eventName} participantId=${participantId}`);
|
|
1573
1591
|
if (participantMachine) {
|
|
1574
1592
|
participantMachine.send({ type: 'PARTICIPANT_RECONNECT' });
|
|
1575
1593
|
}
|
|
@@ -1683,7 +1701,8 @@ const AnearEventMachineFunctions = ({
|
|
|
1683
1701
|
const participant = context.participants[participantId];
|
|
1684
1702
|
if (participant) {
|
|
1685
1703
|
const eventName = getPresenceEventName(participant, 'EXIT');
|
|
1686
|
-
|
|
1704
|
+
const participantName = getParticipantName(context, participantId);
|
|
1705
|
+
logger.info(`[AEM] Event ${context.anearEvent.id} ${eventName} participantId=${participantId} name=${participantName}`);
|
|
1687
1706
|
context.appEventMachine.send({ type: eventName, participantId });
|
|
1688
1707
|
} else {
|
|
1689
1708
|
logger.warn(`[AEM] Participant info not found for id ${participantId} during sendExitToAppMachine`);
|
|
@@ -1719,7 +1738,7 @@ const AnearEventMachineFunctions = ({
|
|
|
1719
1738
|
// participant-level timeouts. This is useful when a game event (e.g., LIAR call)
|
|
1720
1739
|
// should immediately cancel all individual participant timeouts.
|
|
1721
1740
|
// APMs that are not in waitParticipantResponse state will benignly ignore this event.
|
|
1722
|
-
logger.info(`[AEM]
|
|
1741
|
+
logger.info(`[AEM] Event ${context.anearEvent.id} cancelling all participant timeouts (manual) for ${Object.keys(context.participantMachines).length} participants`)
|
|
1723
1742
|
Object.values(context.participantMachines).forEach(pm => pm.send({ type: 'CANCEL_TIMEOUT' }))
|
|
1724
1743
|
|
|
1725
1744
|
// Note: If we're in waitAllParticipantsResponse state, the nested state's handler
|
|
@@ -1733,7 +1752,8 @@ const AnearEventMachineFunctions = ({
|
|
|
1733
1752
|
}),
|
|
1734
1753
|
sendCancelTimeoutToAllAPMs: ({ context }) => {
|
|
1735
1754
|
const participantCount = Object.keys(context.participantMachines).length
|
|
1736
|
-
|
|
1755
|
+
const actionName = context.eachParticipantCancelActionType || 'unknown'
|
|
1756
|
+
logger.info(`[AEM] Event ${context.anearEvent.id} sending CANCEL_TIMEOUT to all ${participantCount} APMs for ACTION ${actionName}`)
|
|
1737
1757
|
Object.values(context.participantMachines).forEach(pm => {
|
|
1738
1758
|
pm.send({ type: 'CANCEL_TIMEOUT' })
|
|
1739
1759
|
})
|
|
@@ -1743,7 +1763,8 @@ const AnearEventMachineFunctions = ({
|
|
|
1743
1763
|
const activeParticipantIds = getActiveParticipantIds(context)
|
|
1744
1764
|
const pendingConfirmations = new Set(activeParticipantIds)
|
|
1745
1765
|
|
|
1746
|
-
|
|
1766
|
+
const participantName = getParticipantName(context, event.data.participantId)
|
|
1767
|
+
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`)
|
|
1747
1768
|
|
|
1748
1769
|
return {
|
|
1749
1770
|
pendingCancelConfirmations: pendingConfirmations,
|
|
@@ -1760,7 +1781,7 @@ const AnearEventMachineFunctions = ({
|
|
|
1760
1781
|
const activeParticipantIds = getActiveParticipantIds(context)
|
|
1761
1782
|
const pendingConfirmations = new Set(activeParticipantIds)
|
|
1762
1783
|
|
|
1763
|
-
logger.info(`[AEM]
|
|
1784
|
+
logger.info(`[AEM] Event ${context.anearEvent.id} starting manual cancellation sequence, waiting for ${pendingConfirmations.size} confirmations`)
|
|
1764
1785
|
|
|
1765
1786
|
return {
|
|
1766
1787
|
pendingCancelConfirmations: pendingConfirmations,
|
|
@@ -1795,7 +1816,8 @@ const AnearEventMachineFunctions = ({
|
|
|
1795
1816
|
payload
|
|
1796
1817
|
}
|
|
1797
1818
|
|
|
1798
|
-
|
|
1819
|
+
const participantName = getParticipantName(context, participantId)
|
|
1820
|
+
logger.info(`[AEM] Event ${context.anearEvent.id} all cancellation confirmations received, forwarding ACTION ${appEventName} from participantId=${participantId} name=${participantName}`)
|
|
1799
1821
|
|
|
1800
1822
|
// Forward to AppM
|
|
1801
1823
|
context.appEventMachine.send({ type: appEventName, ...actionEventPayload })
|
|
@@ -1879,13 +1901,14 @@ const AnearEventMachineFunctions = ({
|
|
|
1879
1901
|
const participantId = event.data.participantId
|
|
1880
1902
|
const eventMessagePayload = JSON.parse(event.data.payload) // { eventName: {eventObject} }
|
|
1881
1903
|
const [appEventName, payload] = Object.entries(eventMessagePayload)[0]
|
|
1904
|
+
const participantName = getParticipantName(context, participantId)
|
|
1882
1905
|
|
|
1883
|
-
logger.
|
|
1906
|
+
logger.info(`[AEM] Event ${context.anearEvent.id} ACTION participantId=${participantId} name=${participantName} action=${appEventName}`)
|
|
1884
1907
|
logger.debug(`[AEM] eachParticipantCancelActionType is: ${context.eachParticipantCancelActionType}`)
|
|
1885
1908
|
|
|
1886
1909
|
// Check if this ACTION should trigger cancellation for eachParticipant timeout
|
|
1887
1910
|
if (context.eachParticipantCancelActionType === appEventName) {
|
|
1888
|
-
logger.info(`[AEM]
|
|
1911
|
+
logger.info(`[AEM] Event ${context.anearEvent.id} intercepting cancel ACTION ${appEventName} from participantId=${participantId} name=${participantName} (eachParticipant timeout)`)
|
|
1889
1912
|
// Trigger cancellation sequence via self-send
|
|
1890
1913
|
self.send({
|
|
1891
1914
|
type: 'TRIGGER_CANCEL_SEQUENCE',
|
|
@@ -1915,6 +1938,13 @@ const AnearEventMachineFunctions = ({
|
|
|
1915
1938
|
const participantId = event.data.participantId;
|
|
1916
1939
|
const { nonResponders } = context.participantsActionTimeout;
|
|
1917
1940
|
|
|
1941
|
+
// Forward to AppM with the finalAction flag
|
|
1942
|
+
const eventMessagePayload = JSON.parse(event.data.payload);
|
|
1943
|
+
const [appEventName, payload] = Object.entries(eventMessagePayload)[0];
|
|
1944
|
+
const participantName = getParticipantName(context, participantId);
|
|
1945
|
+
|
|
1946
|
+
logger.info(`[AEM] Event ${context.anearEvent.id} ACTION participantId=${participantId} name=${participantName} action=${appEventName}`)
|
|
1947
|
+
|
|
1918
1948
|
// Check if this is the last responder before the context is updated.
|
|
1919
1949
|
// This assumes the current participant IS a non-responder.
|
|
1920
1950
|
const isFinalAction = nonResponders.size === 1 && nonResponders.has(participantId);
|
|
@@ -1922,11 +1952,6 @@ const AnearEventMachineFunctions = ({
|
|
|
1922
1952
|
if (isFinalAction) {
|
|
1923
1953
|
logger.debug(`[AEM] Final ACTION received from ${participantId}, allParticipants timeout cancelled (all responded)`)
|
|
1924
1954
|
}
|
|
1925
|
-
logger.info(`[AEM] Participants FINAL ACTION is ${isFinalAction}`)
|
|
1926
|
-
|
|
1927
|
-
// Forward to AppM with the finalAction flag
|
|
1928
|
-
const eventMessagePayload = JSON.parse(event.data.payload);
|
|
1929
|
-
const [appEventName, payload] = Object.entries(eventMessagePayload)[0];
|
|
1930
1955
|
const appM_Payload = {
|
|
1931
1956
|
participantId,
|
|
1932
1957
|
payload,
|
|
@@ -1945,8 +1970,9 @@ const AnearEventMachineFunctions = ({
|
|
|
1945
1970
|
const participantId = event.data.participantId
|
|
1946
1971
|
const eventMessagePayload = JSON.parse(event.data.payload)
|
|
1947
1972
|
const [appEventName, payload] = Object.entries(eventMessagePayload)[0]
|
|
1973
|
+
const participantName = getParticipantName(context, participantId)
|
|
1948
1974
|
|
|
1949
|
-
logger.info(`[AEM]
|
|
1975
|
+
logger.info(`[AEM] Event ${context.anearEvent.id} intercepting cancel ACTION ${appEventName} from participantId=${participantId} name=${participantName} (allParticipants timeout)`)
|
|
1950
1976
|
|
|
1951
1977
|
return {
|
|
1952
1978
|
pendingCancelAction: {
|
|
@@ -1968,15 +1994,17 @@ const AnearEventMachineFunctions = ({
|
|
|
1968
1994
|
}
|
|
1969
1995
|
}),
|
|
1970
1996
|
processParticipantTimeout: ({ context, event, self }) => {
|
|
1997
|
+
const participantId = event.participantId || event.data?.participantId
|
|
1971
1998
|
// Suppress PARTICIPANT_TIMEOUT events after cancelAllParticipantTimeouts() until new timers
|
|
1972
1999
|
// are set up via render. This handles race conditions where an APM's timer fires just
|
|
1973
2000
|
// before receiving CANCEL_TIMEOUT. The flag is cleared when renders complete (new timers set up).
|
|
1974
2001
|
if (context.suppressParticipantTimeouts) {
|
|
1975
|
-
logger.debug(`[AEM] Suppressing PARTICIPANT_TIMEOUT from ${
|
|
2002
|
+
logger.debug(`[AEM] Suppressing PARTICIPANT_TIMEOUT from ${participantId} (cancelled timers, waiting for new render)`)
|
|
1976
2003
|
return
|
|
1977
2004
|
}
|
|
1978
2005
|
|
|
1979
|
-
const
|
|
2006
|
+
const participantName = getParticipantName(context, participantId)
|
|
2007
|
+
logger.info(`[AEM] Event ${context.anearEvent.id} PARTICIPANT_TIMEOUT participantId=${participantId} name=${participantName}`)
|
|
1980
2008
|
const timeoutCount = context.consecutiveTimeoutCount || 0
|
|
1981
2009
|
const maxConsecutiveTimeouts = C.DEAD_MAN_SWITCH.MAX_CONSECUTIVE_PARTICIPANT_TIMEOUTS
|
|
1982
2010
|
|
|
@@ -2080,7 +2108,7 @@ const AnearEventMachineFunctions = ({
|
|
|
2080
2108
|
const nonResponderIds = [...nonResponders]
|
|
2081
2109
|
const allActiveParticipantIds = getActiveParticipantIds(context)
|
|
2082
2110
|
|
|
2083
|
-
logger.info(`[AEM] allParticipants timeout expired after ${msecs}ms
|
|
2111
|
+
logger.info(`[AEM] Event ${context.anearEvent.id} allParticipants timeout expired after ${msecs}ms, nonResponderIds=[${nonResponderIds.join(',')}]`)
|
|
2084
2112
|
|
|
2085
2113
|
// Track consecutive allParticipants timeouts where ALL participants timed out
|
|
2086
2114
|
const allTimedOut = nonResponders.size === allActiveParticipantIds.length && allActiveParticipantIds.length > 0
|
|
@@ -2109,7 +2137,7 @@ const AnearEventMachineFunctions = ({
|
|
|
2109
2137
|
|
|
2110
2138
|
if (context.appEventMachine) {
|
|
2111
2139
|
// Send ACTIONS_TIMEOUT when timeout expires
|
|
2112
|
-
logger.info(`[AEM]
|
|
2140
|
+
logger.info(`[AEM] Event ${context.anearEvent.id} sending ACTIONS_TIMEOUT, nonResponderIds=[${nonResponderIds.join(',')}]`)
|
|
2113
2141
|
context.appEventMachine.send({ type: 'ACTIONS_TIMEOUT', timeout: msecs, nonResponderIds })
|
|
2114
2142
|
}
|
|
2115
2143
|
|
|
@@ -2118,7 +2146,7 @@ const AnearEventMachineFunctions = ({
|
|
|
2118
2146
|
sendActionsTimeoutToAppMCanceled: ({ context }) => {
|
|
2119
2147
|
const { nonResponders, msecs } = context.participantsActionTimeout
|
|
2120
2148
|
const nonResponderIds = [...nonResponders]
|
|
2121
|
-
logger.info(`[AEM] allParticipants timeout cancelled after ${msecs}ms
|
|
2149
|
+
logger.info(`[AEM] Event ${context.anearEvent.id} allParticipants timeout cancelled after ${msecs}ms, nonResponderIds=[${nonResponderIds.join(',')}]`)
|
|
2122
2150
|
|
|
2123
2151
|
if (context.appEventMachine) {
|
|
2124
2152
|
// Send ACTIONS_TIMEOUT when timeout is canceled (backward compatibility)
|
|
@@ -2147,7 +2175,8 @@ const AnearEventMachineFunctions = ({
|
|
|
2147
2175
|
participantMachine.send({ type: 'ACTION', ...actionEventPayload })
|
|
2148
2176
|
}
|
|
2149
2177
|
|
|
2150
|
-
|
|
2178
|
+
const participantName = getParticipantName(context, participantId)
|
|
2179
|
+
logger.info(`[AEM] Event ${context.anearEvent.id} forwarded cancel ACTION ${appEventName} from participantId=${participantId} name=${participantName} after allParticipants cancellation`)
|
|
2151
2180
|
|
|
2152
2181
|
// Clear the pending action
|
|
2153
2182
|
context.pendingCancelAction = null
|
|
@@ -2212,8 +2241,8 @@ const AnearEventMachineFunctions = ({
|
|
|
2212
2241
|
},
|
|
2213
2242
|
logCreatorEnter: ({ event }) => logger.debug("[AEM] got creator PARTICIPANT_ENTER: ", event.data.id),
|
|
2214
2243
|
logAPMReady: ({ event }) => logger.debug("[AEM] PARTICIPANT_MACHINE_READY for: ", event.data.anearParticipant.id),
|
|
2215
|
-
logInvalidParticipantEnter: ({ event }) => logger.info(
|
|
2216
|
-
logDeferringAppmFinal: (
|
|
2244
|
+
logInvalidParticipantEnter: ({ context, event }) => logger.info(`[AEM] Event ${context.anearEvent.id} unexpected PARTICIPANT_ENTER participantId=${event.data.id}`),
|
|
2245
|
+
logDeferringAppmFinal: ({ context }) => logger.info(`[AEM] Event ${context.anearEvent.id} deferring APPM_FINAL (already transitioning to terminated)`),
|
|
2217
2246
|
logImplicitShutdownWarning: (_c, _e) => {
|
|
2218
2247
|
logger.error("********************************************************************************");
|
|
2219
2248
|
logger.error("[AEM] WARNING: AppMachine reached a 'final' state without explicitly calling anearEvent.closeEvent() or anearEvent.cancelEvent().");
|
|
@@ -2633,8 +2662,16 @@ const AnearEventMachine = (anearEvent, {
|
|
|
2633
2662
|
// v5 note: subscriptions do not emit the current snapshot immediately;
|
|
2634
2663
|
// use getSnapshot() if you want the current value right away.
|
|
2635
2664
|
service.subscribe(state => {
|
|
2636
|
-
|
|
2637
|
-
|
|
2665
|
+
// Log major state transitions at INFO level, keep detailed state dumps at DEBUG
|
|
2666
|
+
const stateValue = state.value
|
|
2667
|
+
const stateStr = typeof stateValue === 'string' ? stateValue : JSON.stringify(stateValue)
|
|
2668
|
+
// Only log if it's a major state change (not nested state transitions)
|
|
2669
|
+
if (typeof stateValue === 'string' || (typeof stateValue === 'object' && Object.keys(stateValue).length === 1)) {
|
|
2670
|
+
logger.debug(`[AEM] State: ${stateStr}`)
|
|
2671
|
+
} else {
|
|
2672
|
+
logger.debug('─'.repeat(40))
|
|
2673
|
+
logger.debug(`[AEM] NEXT STATE → ${stateStr}`)
|
|
2674
|
+
}
|
|
2638
2675
|
})
|
|
2639
2676
|
|
|
2640
2677
|
return service
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
*
|
|
24
24
|
* - Enforce participant-level timeouts and activity rules:
|
|
25
25
|
* - Action timeouts:
|
|
26
|
-
* - After a
|
|
26
|
+
* - After a RENDER_DISPLAY that expects input,
|
|
27
27
|
* starts a per-participant response timer.
|
|
28
28
|
* - If the timer fires before an ACTION arrives, emits PARTICIPANT_TIMEOUT
|
|
29
29
|
* back to the AEM and the appParticipantMachine.
|
|
@@ -64,7 +64,7 @@
|
|
|
64
64
|
* Ably wiring or lifecycle plumbing.
|
|
65
65
|
*
|
|
66
66
|
* Incoming events (from AEM / system) – high level:
|
|
67
|
-
* - RENDER_DISPLAY
|
|
67
|
+
* - RENDER_DISPLAY:
|
|
68
68
|
* - Prompt the participant with a new view or interaction on their private channel.
|
|
69
69
|
* - Optionally include an action timeout; APM transitions into a wait state.
|
|
70
70
|
*
|
|
@@ -87,7 +87,7 @@
|
|
|
87
87
|
* 2. It creates and attaches to the participant’s private Ably channel, then
|
|
88
88
|
* signals PARTICIPANT_MACHINE_READY back to the AEM.
|
|
89
89
|
* 3. It enters the live state, handling:
|
|
90
|
-
* -
|
|
90
|
+
* - RENDER_DISPLAY → publish UI, optionally start timeout.
|
|
91
91
|
* - ACTION → forward to appParticipantMachine, clear timeout, go idle.
|
|
92
92
|
* - DISCONNECT / RECONNECT → manage reconnect windows and possible timeouts.
|
|
93
93
|
* - EXIT / BOOT → send shutdown messaging and clean up.
|
|
@@ -110,6 +110,7 @@ const AnearParticipantMachineContext = (anearParticipant, anearEvent, appPartici
|
|
|
110
110
|
appParticipantMachine: null,
|
|
111
111
|
actionTimeoutMsecs: null,
|
|
112
112
|
actionTimeoutStart: null,
|
|
113
|
+
capturedTimeoutMsecs: null, // Captured timeout value for logging before clearing
|
|
113
114
|
lastSeen: CurrentDateTimestamp()
|
|
114
115
|
})
|
|
115
116
|
|
|
@@ -166,9 +167,6 @@ const AnearParticipantMachineConfig = participantId => ({
|
|
|
166
167
|
RENDER_DISPLAY: {
|
|
167
168
|
target: '#renderDisplay'
|
|
168
169
|
},
|
|
169
|
-
PRIVATE_DISPLAY: {
|
|
170
|
-
target: '#renderDisplay'
|
|
171
|
-
},
|
|
172
170
|
CANCEL_TIMEOUT: {
|
|
173
171
|
// Acknowledge CANCEL_TIMEOUT even when idle (no active timer).
|
|
174
172
|
// This ensures the AEM's cancellation sequence completes for all participants.
|
|
@@ -215,9 +213,6 @@ const AnearParticipantMachineConfig = participantId => ({
|
|
|
215
213
|
RENDER_DISPLAY: {
|
|
216
214
|
target: '#renderDisplay'
|
|
217
215
|
},
|
|
218
|
-
PRIVATE_DISPLAY: {
|
|
219
|
-
target: '#renderDisplay'
|
|
220
|
-
},
|
|
221
216
|
PARTICIPANT_EXIT: {
|
|
222
217
|
target: '#cleanupAndExit'
|
|
223
218
|
},
|
|
@@ -264,25 +259,38 @@ const AnearParticipantMachineConfig = participantId => ({
|
|
|
264
259
|
target: '#participantTimedOut'
|
|
265
260
|
},
|
|
266
261
|
after: {
|
|
262
|
+
// Note: This timer continues running even when in nested .rendering state.
|
|
263
|
+
// XState v5 handles this correctly - the timer fires from the parent state
|
|
264
|
+
// and can transition even during invoke processing.
|
|
267
265
|
actionTimeout: {
|
|
268
|
-
actions: 'nullActionTimeout',
|
|
266
|
+
actions: ['captureTimeoutForLogging', 'nullActionTimeout'],
|
|
269
267
|
target: '#participantTimedOut'
|
|
270
268
|
}
|
|
271
269
|
},
|
|
272
270
|
initial: 'waiting',
|
|
273
271
|
states: {
|
|
274
272
|
waiting: {
|
|
275
|
-
// The actual waiting state.
|
|
273
|
+
// The actual waiting state. RENDER_DISPLAY is blocked here
|
|
274
|
+
// to prevent timer conflicts. AppM must cancel timers first.
|
|
276
275
|
},
|
|
277
276
|
rendering: {
|
|
277
|
+
// NOTE: This nested state is currently unreachable as RENDER_DISPLAY
|
|
278
|
+
// is blocked during waitParticipantResponse. Kept for potential future use or
|
|
279
|
+
// other edge cases. The parent's `after: actionTimeout` timer continues running
|
|
280
|
+
// during invoke if this state were to be entered.
|
|
278
281
|
invoke: {
|
|
279
282
|
src: 'publishPrivateDisplay',
|
|
280
283
|
input: ({ context, event }) => ({ context, event }),
|
|
281
|
-
onDone:
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
284
|
+
onDone: [
|
|
285
|
+
{
|
|
286
|
+
guard: 'hasActionTimeout',
|
|
287
|
+
actions: 'updateActionTimeoutIfChanged',
|
|
288
|
+
target: 'waiting'
|
|
289
|
+
},
|
|
290
|
+
{
|
|
291
|
+
target: 'waiting'
|
|
292
|
+
}
|
|
293
|
+
],
|
|
286
294
|
onError: {
|
|
287
295
|
target: '#error'
|
|
288
296
|
}
|
|
@@ -296,15 +304,22 @@ const AnearParticipantMachineConfig = participantId => ({
|
|
|
296
304
|
}
|
|
297
305
|
},
|
|
298
306
|
on: {
|
|
299
|
-
RENDER_DISPLAY:
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
307
|
+
RENDER_DISPLAY: [
|
|
308
|
+
{
|
|
309
|
+
// BLOCKED: RENDER_DISPLAY that carries a NEW timeout while we're already
|
|
310
|
+
// waiting on an ACTION is not allowed (it would reset / conflict timers).
|
|
311
|
+
// The AppM must first cancel timers (via cancelAllParticipantTimeouts()
|
|
312
|
+
// or cancel: "ACTION" in meta) and wait for ACK before issuing new
|
|
313
|
+
// RENDER_DISPLAY with timeouts.
|
|
314
|
+
guard: 'incomingRenderHasTimeout',
|
|
315
|
+
actions: 'logBlockedRenderDuringWait'
|
|
316
|
+
},
|
|
317
|
+
{
|
|
318
|
+
// Allowed: visual-only refresh (no timeout). We publish the display while
|
|
319
|
+
// staying inside waitParticipantResponse so the parent after: timer continues.
|
|
320
|
+
target: '.rendering'
|
|
321
|
+
}
|
|
322
|
+
],
|
|
308
323
|
PARTICIPANT_EXIT: {
|
|
309
324
|
actions: 'logExit',
|
|
310
325
|
target: '#cleanupAndExit'
|
|
@@ -337,7 +352,7 @@ const AnearParticipantMachineConfig = participantId => ({
|
|
|
337
352
|
},
|
|
338
353
|
participantTimedOut: {
|
|
339
354
|
id: 'participantTimedOut',
|
|
340
|
-
entry: ['logParticipantTimedOut', 'sendTimeoutEvents'
|
|
355
|
+
entry: ['logParticipantTimedOut', 'sendTimeoutEvents'],
|
|
341
356
|
always: 'idle'
|
|
342
357
|
},
|
|
343
358
|
cleanupAndExit: {
|
|
@@ -346,7 +361,6 @@ const AnearParticipantMachineConfig = participantId => ({
|
|
|
346
361
|
// Ignore most events during cleanup, but allow exit displays
|
|
347
362
|
on: {
|
|
348
363
|
RENDER_DISPLAY: { actions: 'logIgnoringRenderDisplayCleanup' },
|
|
349
|
-
PRIVATE_DISPLAY: { actions: 'logIgnoringPrivateDisplayCleanup' },
|
|
350
364
|
PARTICIPANT_EXIT: {
|
|
351
365
|
actions: 'logIgnoringRedundantExit'
|
|
352
366
|
},
|
|
@@ -407,7 +421,6 @@ const AnearParticipantMachineConfig = participantId => ({
|
|
|
407
421
|
// Final state - ignore all events except exit displays
|
|
408
422
|
on: {
|
|
409
423
|
RENDER_DISPLAY: { actions: 'logIgnoringRenderDisplayDone' },
|
|
410
|
-
PRIVATE_DISPLAY: { actions: 'logIgnoringPrivateDisplayDone' },
|
|
411
424
|
'*': {
|
|
412
425
|
actions: 'logIgnoringEventDone'
|
|
413
426
|
}
|
|
@@ -451,8 +464,15 @@ const AnearParticipantMachineFunctions = {
|
|
|
451
464
|
return actor
|
|
452
465
|
}
|
|
453
466
|
}),
|
|
467
|
+
captureTimeoutForLogging: assign(({ context }) => {
|
|
468
|
+
// Capture timeout value before it's cleared by nullActionTimeout
|
|
469
|
+
return {
|
|
470
|
+
capturedTimeoutMsecs: context.actionTimeoutMsecs
|
|
471
|
+
}
|
|
472
|
+
}),
|
|
454
473
|
sendTimeoutEvents: ({ context }) => {
|
|
455
|
-
|
|
474
|
+
const timeoutMsecs = context.capturedTimeoutMsecs ?? context.actionTimeoutMsecs ?? 'unknown'
|
|
475
|
+
logger.info(`[APM] Timer expired participantId=${context.anearParticipant.id} timeout=${timeoutMsecs}ms, sending PARTICIPANT_TIMEOUT to AEM`)
|
|
456
476
|
// AEM currently expects `event.participantId` (not nested under `data`).
|
|
457
477
|
context.anearEvent.send({
|
|
458
478
|
type: 'PARTICIPANT_TIMEOUT',
|
|
@@ -485,7 +505,37 @@ const AnearParticipantMachineFunctions = {
|
|
|
485
505
|
if (existingMsecs != null || existingStart != null) {
|
|
486
506
|
logger.debug(`[APM] WARNING: Starting new timer for ${context.anearParticipant.id} but existing timer state found: msecs=${existingMsecs}, start=${existingStart}`)
|
|
487
507
|
}
|
|
488
|
-
logger.
|
|
508
|
+
logger.info(`[APM] Timer started participantId=${context.anearParticipant.id} timeout=${timeoutFromEvent}ms`)
|
|
509
|
+
return {
|
|
510
|
+
actionTimeoutMsecs: timeoutFromEvent,
|
|
511
|
+
actionTimeoutStart: now
|
|
512
|
+
}
|
|
513
|
+
}),
|
|
514
|
+
updateActionTimeoutIfChanged: assign(({ context, event }) => {
|
|
515
|
+
const timeoutFromEvent = event?.output?.timeout ?? event?.data?.timeout
|
|
516
|
+
const now = CurrentDateTimestamp()
|
|
517
|
+
|
|
518
|
+
// When RENDER_DISPLAY arrives during waitParticipantResponse, check if the timeout has changed.
|
|
519
|
+
// If it has, update the timer. This handles state transitions where a new timeout is needed.
|
|
520
|
+
const existingMsecs = context.actionTimeoutMsecs
|
|
521
|
+
const existingStart = context.actionTimeoutStart
|
|
522
|
+
|
|
523
|
+
if (timeoutFromEvent == null || timeoutFromEvent <= 0) {
|
|
524
|
+
// New display has no timeout - preserve existing timer (this is a visual status update)
|
|
525
|
+
// Don't clear it, as we're still waiting for the original ACTION
|
|
526
|
+
logger.debug(`[APM] Preserving existing timer for ${context.anearParticipant.id} (new display has no timeout, visual status update)`)
|
|
527
|
+
return {}
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// If we already have an active timer and receive a new timeout, this is a bug (detected above).
|
|
531
|
+
// Preserve the existing timer to avoid conflicts. The AppM should cancel timers first.
|
|
532
|
+
if (existingMsecs != null && existingStart != null) {
|
|
533
|
+
logger.warn(`[APM] AppM BUG: Received timeout=${timeoutFromEvent}ms while timer already active (${existingMsecs}ms) for ${context.anearParticipant.id}. Preserving existing timer. AppM should cancel timers first.`)
|
|
534
|
+
return {}
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
// No existing timer - start a new one (shouldn't happen in waitParticipantResponse, but handle gracefully)
|
|
538
|
+
logger.info(`[APM] Timer started participantId=${context.anearParticipant.id} timeout=${timeoutFromEvent}ms`)
|
|
489
539
|
return {
|
|
490
540
|
actionTimeoutMsecs: timeoutFromEvent,
|
|
491
541
|
actionTimeoutStart: now
|
|
@@ -556,13 +606,25 @@ const AnearParticipantMachineFunctions = {
|
|
|
556
606
|
logReconnected: ({ context }) => logger.info(`[APM] Participant ${context.anearParticipant.id} has RECONNECTED`),
|
|
557
607
|
logImmediateTimeout: ({ context }) => logger.debug(`[APM] Timeout of ${context.actionTimeoutMsecs}ms is immediate for ${context.anearParticipant.id}.`),
|
|
558
608
|
logIgnoringRenderDisplayCleanup: (_args) => logger.debug('[APM] ignoring RENDER_DISPLAY during cleanup'),
|
|
559
|
-
logIgnoringPrivateDisplayCleanup: (_args) => logger.debug('[APM] ignoring PRIVATE_DISPLAY during cleanup'),
|
|
560
609
|
logIgnoringRedundantExit: () => logger.debug('[APM] ignoring redundant PARTICIPIPANT_EXIT during cleanup'),
|
|
561
610
|
logIgnoringReconnectCleanup: () => logger.debug('[APM] ignoring PARTICIPANT_RECONNECT during cleanup - already timed out'),
|
|
562
611
|
logIgnoringActionCleanup: () => logger.debug('[APM] ignoring ACTION during cleanup'),
|
|
563
612
|
logIgnoringRenderDisplayDone: () => logger.debug('[APM] ignoring RENDER_DISPLAY in final state'),
|
|
564
|
-
|
|
565
|
-
|
|
613
|
+
logIgnoringEventDone: ({ event }) => logger.debug('[APM] ignoring event in final state: ', event.type),
|
|
614
|
+
logBlockedRenderDuringWait: ({ context, event }) => {
|
|
615
|
+
const eventType = event?.type ?? 'RENDER_DISPLAY'
|
|
616
|
+
const timeout = event?.timeout ?? event?.data?.timeout
|
|
617
|
+
const participantId = context.anearParticipant.id
|
|
618
|
+
const remainingMsecs = context.actionTimeoutMsecs
|
|
619
|
+
const timeoutInfo = timeout ? ` (new timeout: ${timeout}ms)` : ''
|
|
620
|
+
|
|
621
|
+
logger.error(`[APM] ⚠️ BLOCKED: ${eventType} received during active timeout wait for participant ${participantId}. Current timer: ${remainingMsecs}ms remaining.${timeoutInfo}`)
|
|
622
|
+
logger.error(`[APM] ⚠️ The AppM must first cancel all participant timers using one of these methods:`)
|
|
623
|
+
logger.error(`[APM] ⚠️ 1. Declarative: Add cancel: "ACTION_NAME" to meta.eachParticipant or meta.allParticipants`)
|
|
624
|
+
logger.error(`[APM] ⚠️ 2. Manual: Call anearEvent.cancelAllParticipantTimeouts() and wait for ACK`)
|
|
625
|
+
logger.error(`[APM] ⚠️ Only after timers are cancelled (APM transitions to idle) can new RENDER_DISPLAY with timeouts be issued.`)
|
|
626
|
+
logger.error(`[APM] ⚠️ This ${eventType} event is being IGNORED to prevent timer conflicts.`)
|
|
627
|
+
}
|
|
566
628
|
},
|
|
567
629
|
actors: {
|
|
568
630
|
publishPrivateDisplay: fromPromise(async ({ input }) => {
|
|
@@ -593,6 +655,12 @@ const AnearParticipantMachineFunctions = {
|
|
|
593
655
|
},
|
|
594
656
|
guards: {
|
|
595
657
|
hasAppParticipantMachine: ({ context }) => context.appParticipantMachineFactory !== null,
|
|
658
|
+
incomingRenderHasTimeout: ({ event }) => {
|
|
659
|
+
// RENDER_DISPLAY events are sent directly as { type, content, timeout? }
|
|
660
|
+
// We only block when a *new* timeout is being asserted during an active wait.
|
|
661
|
+
const timeout = event?.timeout ?? event?.data?.timeout
|
|
662
|
+
return timeout != null && timeout > 0
|
|
663
|
+
},
|
|
596
664
|
hasActionTimeout: ({ event }) => {
|
|
597
665
|
const timeout = event?.output?.timeout ?? event?.data?.timeout
|
|
598
666
|
return timeout != null && timeout > 0
|
|
@@ -55,7 +55,8 @@ const AppMachineTransition = (anearEvent) => {
|
|
|
55
55
|
const stateName = _stringifiedState(value)
|
|
56
56
|
const hasMeta = rawMeta && Object.keys(rawMeta).length > 0;
|
|
57
57
|
|
|
58
|
-
logger.
|
|
58
|
+
logger.info(`[AppMachineTransition] AppM state transition to '${stateName}' event=${event.type}`)
|
|
59
|
+
logger.debug(`[AppMachineTransition] Meta detected: ${hasMeta}`)
|
|
59
60
|
|
|
60
61
|
// Handle unpredictable meta structure
|
|
61
62
|
const metaObjects = rawMeta ? Object.values(rawMeta) : []
|
package/lib/utils/CssUploader.js
CHANGED
|
@@ -27,7 +27,7 @@ class CSSUploader {
|
|
|
27
27
|
*/
|
|
28
28
|
async uploadCss() {
|
|
29
29
|
if (!fs.existsSync(this.cssDirPath)) {
|
|
30
|
-
logger.info(`No css directory found at ${this.cssDirPath}. Skipping CSS upload.`)
|
|
30
|
+
logger.info(`[CssUploader] No css directory found at ${this.cssDirPath}. Skipping CSS upload.`)
|
|
31
31
|
return
|
|
32
32
|
}
|
|
33
33
|
|
|
@@ -38,8 +38,10 @@ class CSSUploader {
|
|
|
38
38
|
|
|
39
39
|
if (presignedUrl) {
|
|
40
40
|
await this.uploadToS3(presignedUrl, css, 'text/css')
|
|
41
|
+
logger.info(`[CssUploader] CSS uploaded successfully`)
|
|
41
42
|
logger.debug('Uploaded CSS file to S3.')
|
|
42
43
|
} else {
|
|
44
|
+
logger.info(`[CssUploader] CSS up to date`)
|
|
43
45
|
logger.debug('CSS file is up to date; no upload necessary.')
|
|
44
46
|
}
|
|
45
47
|
|
|
@@ -141,7 +141,8 @@ class DisplayEventProcessor {
|
|
|
141
141
|
|
|
142
142
|
switch (displayViewer) {
|
|
143
143
|
case 'allParticipants':
|
|
144
|
-
|
|
144
|
+
const templateName = normalizedPath.replace(C.PugSuffix, '')
|
|
145
|
+
logger.info(`[DisplayEventProcessor] RENDER_DISPLAY template=${templateName} viewer=allParticipants`)
|
|
145
146
|
|
|
146
147
|
// Use display timeout if available, otherwise fall back to timeoutFn
|
|
147
148
|
if (displayTimeout !== null && displayTimeout > 0) {
|
|
@@ -175,7 +176,8 @@ class DisplayEventProcessor {
|
|
|
175
176
|
break
|
|
176
177
|
|
|
177
178
|
case 'spectators':
|
|
178
|
-
|
|
179
|
+
const templateNameSpectators = normalizedPath.replace(C.PugSuffix, '')
|
|
180
|
+
logger.info(`[DisplayEventProcessor] RENDER_DISPLAY template=${templateNameSpectators} viewer=spectators`)
|
|
179
181
|
|
|
180
182
|
// If this event/app does not support spectators, the spectatorsDisplayChannel
|
|
181
183
|
// will be null. In that case, skip publishing instead of throwing.
|
|
@@ -217,13 +219,14 @@ class DisplayEventProcessor {
|
|
|
217
219
|
break
|
|
218
220
|
|
|
219
221
|
case 'eachParticipant':
|
|
222
|
+
const templateNameEach = normalizedPath.replace(C.PugSuffix, '')
|
|
220
223
|
if (participantId === 'ALL_PARTICIPANTS') {
|
|
221
224
|
// Legacy behavior - send to all participants (uses timeoutFn)
|
|
222
|
-
logger.
|
|
225
|
+
logger.info(`[DisplayEventProcessor] RENDER_DISPLAY template=${templateNameEach} viewer=eachParticipant participantId=ALL_PARTICIPANTS`)
|
|
223
226
|
publishPromise = this._processPrivateParticipantDisplays(template, templateRenderContext, timeoutFn)
|
|
224
227
|
} else if (participantId) {
|
|
225
228
|
// Selective participant rendering - send to specific participant only (uses displayTimeout)
|
|
226
|
-
logger.
|
|
229
|
+
logger.info(`[DisplayEventProcessor] RENDER_DISPLAY template=${templateNameEach} viewer=eachParticipant participantId=${participantId}`)
|
|
227
230
|
publishPromise = this._processSelectiveParticipantDisplay(template, templateRenderContext, null, participantId, displayTimeout)
|
|
228
231
|
} else {
|
|
229
232
|
// Fallback - should not happen with unified approach
|
|
@@ -233,7 +236,8 @@ class DisplayEventProcessor {
|
|
|
233
236
|
break
|
|
234
237
|
|
|
235
238
|
case 'host':
|
|
236
|
-
|
|
239
|
+
const templateNameHost = normalizedPath.replace(C.PugSuffix, '')
|
|
240
|
+
logger.info(`[DisplayEventProcessor] RENDER_DISPLAY template=${templateNameHost} viewer=host`)
|
|
237
241
|
// Host may optionally have a real timeout (if configured). Otherwise, host
|
|
238
242
|
// mirrors the allParticipants visual timer without starting a host timeout.
|
|
239
243
|
let hostOwnMsecs = null
|
|
@@ -402,21 +406,25 @@ class DisplayEventProcessor {
|
|
|
402
406
|
const start = pat.startedAt || Date.now()
|
|
403
407
|
const remainingMsecs = Math.max(0, pat.msecs - (now - start))
|
|
404
408
|
visualTimeout = { msecs: pat.msecs, remainingMsecs }
|
|
405
|
-
} else
|
|
406
|
-
// Fallback: no global timeout
|
|
407
|
-
// show a bar based on
|
|
409
|
+
} else {
|
|
410
|
+
// Fallback: no global timeout.
|
|
411
|
+
// If this participant currently has an active APM timer, show a bar based on that.
|
|
412
|
+
// This enables "visual-only" re-renders (timeout=null) during an active wait without
|
|
413
|
+
// attempting to restart/reset the timer.
|
|
408
414
|
const state = participantMachine?.state
|
|
409
415
|
const ctx = state?.context
|
|
410
|
-
|
|
411
|
-
if (ctx && ctx.actionTimeoutStart && ctx.actionTimeoutMsecs != null) {
|
|
416
|
+
if (ctx && ctx.actionTimeoutStart && ctx.actionTimeoutMsecs != null && ctx.actionTimeoutMsecs > 0) {
|
|
412
417
|
const now = Date.now()
|
|
413
418
|
const elapsed = now - ctx.actionTimeoutStart
|
|
414
419
|
const remaining = ctx.actionTimeoutMsecs - elapsed
|
|
415
|
-
remainingMsecs = remaining > 0 ? remaining : 0
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
+
const remainingMsecs = remaining > 0 ? remaining : 0
|
|
421
|
+
visualTimeout = {
|
|
422
|
+
msecs: ctx.actionTimeoutMsecs,
|
|
423
|
+
remainingMsecs
|
|
424
|
+
}
|
|
425
|
+
} else if (timeout !== null) {
|
|
426
|
+
// Backward compatibility: if we have an explicit timeout, mirror it.
|
|
427
|
+
visualTimeout = { msecs: timeout, remainingMsecs: timeout }
|
|
420
428
|
}
|
|
421
429
|
}
|
|
422
430
|
} catch (_e) {
|
|
@@ -438,8 +446,6 @@ class DisplayEventProcessor {
|
|
|
438
446
|
renderMessage.timeout = timeout
|
|
439
447
|
}
|
|
440
448
|
|
|
441
|
-
logger.debug(`Calculated a timeout of ${renderMessage.timeout} for ${participantId}`)
|
|
442
|
-
|
|
443
449
|
// v5: send events as objects
|
|
444
450
|
participantMachine.send({ type: 'RENDER_DISPLAY', ...renderMessage })
|
|
445
451
|
}
|
|
@@ -19,7 +19,7 @@ class FontAssetsUploader {
|
|
|
19
19
|
await fs.stat(this.fontsDirPath)
|
|
20
20
|
} catch (e) {
|
|
21
21
|
if (e.code === 'ENOENT') {
|
|
22
|
-
logger.info(`assets/fonts directory not found at ${this.fontsDirPath}. Skipping font upload.`)
|
|
22
|
+
logger.info(`[FontAssetsUploader] assets/fonts directory not found at ${this.fontsDirPath}. Skipping font upload.`)
|
|
23
23
|
return null
|
|
24
24
|
}
|
|
25
25
|
throw e
|
|
@@ -29,7 +29,7 @@ class FontAssetsUploader {
|
|
|
29
29
|
const filesToUpload = await collector.collectFiles()
|
|
30
30
|
|
|
31
31
|
if (filesToUpload.length === 0) {
|
|
32
|
-
logger.info("No font files to upload.")
|
|
32
|
+
logger.info("[FontAssetsUploader] No font files to upload.")
|
|
33
33
|
return null
|
|
34
34
|
}
|
|
35
35
|
|
|
@@ -42,6 +42,7 @@ class FontAssetsUploader {
|
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
if (presignedUrls && presignedUrls.length > 0) {
|
|
45
|
+
logger.info(`[FontAssetsUploader] Uploading ${presignedUrls.length} font asset(s)`)
|
|
45
46
|
await Promise.all(presignedUrls.map(async (uploadInfo) => {
|
|
46
47
|
const { path: relativePath, 'presigned-url': presignedUrl } = uploadInfo
|
|
47
48
|
const fileData = filesToUpload.find(file => file.path === relativePath)
|
|
@@ -55,7 +56,9 @@ class FontAssetsUploader {
|
|
|
55
56
|
throw new Error(`File data not found for path: ${relativePath}`)
|
|
56
57
|
}
|
|
57
58
|
}))
|
|
59
|
+
logger.info(`[FontAssetsUploader] Font assets uploaded successfully`)
|
|
58
60
|
} else {
|
|
61
|
+
logger.info(`[FontAssetsUploader] Font assets up to date`)
|
|
59
62
|
logger.debug("No new or updated font files to upload.")
|
|
60
63
|
}
|
|
61
64
|
|
|
@@ -46,6 +46,7 @@ class ImageAssetsUploader {
|
|
|
46
46
|
|
|
47
47
|
// Step 3: For each presigned URL, upload image file content to S3
|
|
48
48
|
if (presignedUrls && presignedUrls.length > 0) {
|
|
49
|
+
logger.info(`[ImageAssetsUploader] Uploading ${presignedUrls.length} image asset(s)`)
|
|
49
50
|
await Promise.all(presignedUrls.map(async (uploadInfo) => {
|
|
50
51
|
const { path: filePath, 'presigned-url': presignedUrl } = uploadInfo
|
|
51
52
|
|
|
@@ -64,7 +65,9 @@ class ImageAssetsUploader {
|
|
|
64
65
|
throw new Error(`File data not found for path: ${filePath}`)
|
|
65
66
|
}
|
|
66
67
|
}))
|
|
68
|
+
logger.info(`[ImageAssetsUploader] Image assets uploaded successfully`)
|
|
67
69
|
} else {
|
|
70
|
+
logger.info(`[ImageAssetsUploader] Image assets up to date`)
|
|
68
71
|
logger.debug('No files to upload; all files are up to date.')
|
|
69
72
|
}
|
|
70
73
|
|
|
@@ -7,6 +7,42 @@ const AnearApi = require('../api/AnearApi')
|
|
|
7
7
|
const DefaultHeartbeatIntervalMsecs = 15000
|
|
8
8
|
const NotableChannelEvents = ['attached', 'suspended', 'failed']
|
|
9
9
|
|
|
10
|
+
/**
|
|
11
|
+
* Extracts a short channel name abbreviation from the full channel name
|
|
12
|
+
* @param {string} channelName - Full channel name (e.g., "anear:appId:e:eventId:actions")
|
|
13
|
+
* @returns {string} - Short abbreviation (e.g., "actions", "private", "participants", "spectators", "events")
|
|
14
|
+
*/
|
|
15
|
+
function getChannelShortName(channelName) {
|
|
16
|
+
if (!channelName) return 'unknown'
|
|
17
|
+
|
|
18
|
+
// Pattern: anear:appId:e:eventId:actions → actions
|
|
19
|
+
// Pattern: anear:appId:e:eventId:private:participantId → private
|
|
20
|
+
// Pattern: anear:appId:e:eventId:participants → participants
|
|
21
|
+
// Pattern: anear:appId:e:eventId:spectators → spectators
|
|
22
|
+
// Pattern: anear:appId:e → events
|
|
23
|
+
|
|
24
|
+
const parts = channelName.split(':')
|
|
25
|
+
|
|
26
|
+
// Check for event-scoped channels (anear:appId:e:eventId:...)
|
|
27
|
+
if (parts.length >= 4 && parts[2] === 'e') {
|
|
28
|
+
if (parts.length === 5) {
|
|
29
|
+
// anear:appId:e:eventId:channelType
|
|
30
|
+
return parts[4] // actions, participants, spectators
|
|
31
|
+
} else if (parts.length === 6 && parts[4] === 'private') {
|
|
32
|
+
// anear:appId:e:eventId:private:participantId
|
|
33
|
+
return 'private'
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Check for event creation channel (anear:appId:e)
|
|
38
|
+
if (parts.length === 3 && parts[2] === 'e') {
|
|
39
|
+
return 'events'
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Fallback: return last part or full name if pattern doesn't match
|
|
43
|
+
return parts[parts.length - 1] || channelName
|
|
44
|
+
}
|
|
45
|
+
|
|
10
46
|
class RealtimeMessaging {
|
|
11
47
|
constructor() {
|
|
12
48
|
this.ablyRealtime = null
|
|
@@ -160,7 +196,9 @@ class RealtimeMessaging {
|
|
|
160
196
|
// Actions reference event.data to get at participantId, etc
|
|
161
197
|
const { data } = message // ably presence event message from browser contains message.data.id
|
|
162
198
|
const type = data.type ? data.type : 'NONE'
|
|
163
|
-
|
|
199
|
+
const channelShortName = getChannelShortName(channel.name)
|
|
200
|
+
const channelLabel = channelShortName === 'actions' ? 'actions:presence' : channelShortName
|
|
201
|
+
logger.info(`[RTM] received presence ${eventName} participantId=${data.id} on channel ${channelLabel}`)
|
|
164
202
|
actor.send({ type: eventName, data })
|
|
165
203
|
}
|
|
166
204
|
)
|
|
@@ -183,13 +221,18 @@ class RealtimeMessaging {
|
|
|
183
221
|
}
|
|
184
222
|
|
|
185
223
|
async publish(channel, msgType, message) {
|
|
224
|
+
const participantId = message?.data?.participantId || message?.participantId || null
|
|
225
|
+
const participantIdStr = participantId ? ` participantId=${participantId}` : ''
|
|
226
|
+
const channelShortName = getChannelShortName(channel.name)
|
|
227
|
+
logger.info(`[RTM] publishing ${msgType}${participantIdStr} on channel ${channelShortName}`)
|
|
186
228
|
return channel.publish(msgType, message)
|
|
187
229
|
}
|
|
188
230
|
|
|
189
231
|
async setPresence(channel, data) {
|
|
190
232
|
try {
|
|
191
233
|
await channel.presence.enter(data)
|
|
192
|
-
|
|
234
|
+
const channelShortName = getChannelShortName(channel.name)
|
|
235
|
+
logger.info(`[RTM] presence.enter participantId=${data.id} on channel ${channelShortName}`)
|
|
193
236
|
} catch (e) {
|
|
194
237
|
logger.warn(`[RTM] presence.enter failed on ${channel.name}`, e)
|
|
195
238
|
}
|
|
@@ -223,7 +266,10 @@ class RealtimeMessaging {
|
|
|
223
266
|
if (eventName) args.push(eventName)
|
|
224
267
|
args.push(
|
|
225
268
|
message => {
|
|
226
|
-
|
|
269
|
+
const participantId = message.data?.participantId || null
|
|
270
|
+
const participantIdStr = participantId ? ` participantId=${participantId}` : ''
|
|
271
|
+
const channelShortName = getChannelShortName(channel.name)
|
|
272
|
+
logger.info(`[RTM] received message ${message.name}${participantIdStr} on channel ${channelShortName}`)
|
|
227
273
|
actor.send({ type: message.name, data: message.data })
|
|
228
274
|
}
|
|
229
275
|
)
|