anear-js-api 1.3.2 → 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.
@@ -80,7 +80,10 @@ const AnearCoreServiceMachineConfig = appId => ({
80
80
  context: ({ input }) => input,
81
81
  states: {
82
82
  fetchAppDataWithRetry: {
83
- entry: _ => logger.info('anear-js-api version: ', anearJsApiVersion),
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 }) => logger.debug(`Waiting on ${context.appData.data.attributes['short-name']} lifecycle command`),
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
- logger.debug(`fetched ${output.data.attributes["short-name"]} app data`)
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
- logger.debug(`ACSM ${event.eventId} is ${dropped ? "done → cleaning up" : "NOT FOUND"}`)
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.debug("[AEM] announce state...waiting for event START"),
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: () => logger.debug('[AEM] live state...waiting for actions'),
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.debug(`[AEM] === ${eventType} ${context.anearEvent.id} ===`)
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.debug(`[AEM] Sending ${eventName} for ${anearParticipant.id}`);
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.debug(`[AEM] processing ${eventName} for ${participantId}`);
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.debug(`[AEM] processing ${eventName} for ${participantId}`);
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
- logger.debug(`[AEM] sending ${eventName} to AppM for participant ${participantId}`);
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] Cancelling all participant timeouts (manual) for ${Object.keys(context.participantMachines).length} participants`)
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
- logger.info(`[AEM] Sending CANCEL_TIMEOUT to all ${participantCount} APMs for declarative cancel ACTION`)
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
- logger.info(`[AEM] Starting cancellation sequence for ACTION ${event.data.appEventName}. Waiting for ${pendingConfirmations.size} APM confirmations: ${[...pendingConfirmations].join(', ')}`)
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] Starting manual cancellation sequence (cancelAllParticipantTimeouts). Waiting for ${pendingConfirmations.size} APM confirmations: ${[...pendingConfirmations].join(', ')}`)
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
- logger.info(`[AEM] All cancellation confirmations received. Forwarding cancel ACTION ${appEventName} from ${participantId} to AppM and APM`)
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.debug(`[AEM] got Event ${appEventName} from payload from participant ${participantId}`)
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] Intercepting cancel ACTION ${appEventName} from ${participantId} (eachParticipant timeout cancellation)`)
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] Intercepting cancel ACTION ${appEventName} for allParticipants timeout from ${participantId}`)
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 ${event.participantId} (cancelled timers, waiting for new render)`)
2002
+ logger.debug(`[AEM] Suppressing PARTICIPANT_TIMEOUT from ${participantId} (cancelled timers, waiting for new render)`)
1976
2003
  return
1977
2004
  }
1978
2005
 
1979
- const participantId = event.participantId
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. Non-responders: ${nonResponderIds.join(', ')}`)
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] Sending ACTIONS_TIMEOUT (timeout expired)`)
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. Non-responders at cancellation: ${nonResponderIds.join(', ')}`)
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
- logger.info(`[AEM] Forwarded cancel ACTION ${appEventName} from ${participantId} to AppM and APM after allParticipants cancellation`)
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("[AEM] Error: Unexepected PARTICIPANT_ENTER with id: ", event.data.id),
2216
- logDeferringAppmFinal: (_c, _e) => logger.info("[AEM] Deferring APPM_FINAL event because AEM is already transitioning to terminated state."),
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
- logger.debug('─'.repeat(40))
2637
- logger.debug(`[AEM] NEXT STATE → ${JSON.stringify(state.value)}`)
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
@@ -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
 
@@ -262,7 +263,7 @@ const AnearParticipantMachineConfig = participantId => ({
262
263
  // XState v5 handles this correctly - the timer fires from the parent state
263
264
  // and can transition even during invoke processing.
264
265
  actionTimeout: {
265
- actions: 'nullActionTimeout',
266
+ actions: ['captureTimeoutForLogging', 'nullActionTimeout'],
266
267
  target: '#participantTimedOut'
267
268
  }
268
269
  },
@@ -303,13 +304,22 @@ const AnearParticipantMachineConfig = participantId => ({
303
304
  }
304
305
  },
305
306
  on: {
306
- RENDER_DISPLAY: {
307
- // BLOCKED: RENDER_DISPLAY during active timeout wait is not allowed.
308
- // The AppM must first cancel all participant timers (via cancelAllParticipantTimeouts()
309
- // or cancel: "ACTION" in meta) and wait for ACK before issuing new RENDER_DISPLAY
310
- // with timeouts. This prevents timer conflicts and ensures clean state transitions.
311
- actions: 'logBlockedRenderDuringWait'
312
- },
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
+ ],
313
323
  PARTICIPANT_EXIT: {
314
324
  actions: 'logExit',
315
325
  target: '#cleanupAndExit'
@@ -342,7 +352,7 @@ const AnearParticipantMachineConfig = participantId => ({
342
352
  },
343
353
  participantTimedOut: {
344
354
  id: 'participantTimedOut',
345
- entry: ['logParticipantTimedOut', 'sendTimeoutEvents', 'nullActionTimeout'],
355
+ entry: ['logParticipantTimedOut', 'sendTimeoutEvents'],
346
356
  always: 'idle'
347
357
  },
348
358
  cleanupAndExit: {
@@ -454,8 +464,15 @@ const AnearParticipantMachineFunctions = {
454
464
  return actor
455
465
  }
456
466
  }),
467
+ captureTimeoutForLogging: assign(({ context }) => {
468
+ // Capture timeout value before it's cleared by nullActionTimeout
469
+ return {
470
+ capturedTimeoutMsecs: context.actionTimeoutMsecs
471
+ }
472
+ }),
457
473
  sendTimeoutEvents: ({ context }) => {
458
- logger.info(`[APM] Timer expired for ${context.anearParticipant.id}, sending PARTICIPANT_TIMEOUT to AEM`)
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`)
459
476
  // AEM currently expects `event.participantId` (not nested under `data`).
460
477
  context.anearEvent.send({
461
478
  type: 'PARTICIPANT_TIMEOUT',
@@ -488,7 +505,7 @@ const AnearParticipantMachineFunctions = {
488
505
  if (existingMsecs != null || existingStart != null) {
489
506
  logger.debug(`[APM] WARNING: Starting new timer for ${context.anearParticipant.id} but existing timer state found: msecs=${existingMsecs}, start=${existingStart}`)
490
507
  }
491
- logger.debug(`[APM] Starting new timer for ${context.anearParticipant.id} with ${timeoutFromEvent}ms`)
508
+ logger.info(`[APM] Timer started participantId=${context.anearParticipant.id} timeout=${timeoutFromEvent}ms`)
492
509
  return {
493
510
  actionTimeoutMsecs: timeoutFromEvent,
494
511
  actionTimeoutStart: now
@@ -518,7 +535,7 @@ const AnearParticipantMachineFunctions = {
518
535
  }
519
536
 
520
537
  // No existing timer - start a new one (shouldn't happen in waitParticipantResponse, but handle gracefully)
521
- logger.debug(`[APM] Starting new timer for ${context.anearParticipant.id} with ${timeoutFromEvent}ms`)
538
+ logger.info(`[APM] Timer started participantId=${context.anearParticipant.id} timeout=${timeoutFromEvent}ms`)
522
539
  return {
523
540
  actionTimeoutMsecs: timeoutFromEvent,
524
541
  actionTimeoutStart: now
@@ -638,6 +655,12 @@ const AnearParticipantMachineFunctions = {
638
655
  },
639
656
  guards: {
640
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
+ },
641
664
  hasActionTimeout: ({ event }) => {
642
665
  const timeout = event?.output?.timeout ?? event?.data?.timeout
643
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.debug(`[AppMachineTransition] onTransition to state '${stateName}'. Meta detected: ${hasMeta}`)
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) : []
@@ -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
- logger.debug(`[DisplayEventProcessor] Publishing ${viewPath} to PARTICIPANTS_DISPLAY`)
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
- logger.debug(`[DisplayEventProcessor] Publishing ${viewPath} to SPECTATORS_DISPLAY`)
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.debug(`[DisplayEventProcessor] Processing RENDER_DISPLAY ${viewPath} for all participants`)
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.debug(`[DisplayEventProcessor] Processing RENDER_DISPLAY ${viewPath} for participant ${participantId}`)
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
- logger.debug(`[DisplayEventProcessor] Processing RENDER_DISPLAY ${viewPath} for host`)
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 if (timeout !== null) {
406
- // Fallback: no global timeout; if this participant has a real timer,
407
- // show a bar based on their own APM state.
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
- let remainingMsecs = null
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
- visualTimeout = {
418
- msecs: timeout,
419
- remainingMsecs: remainingMsecs ?? timeout
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
- logger.debug(`[RTM] rcvd presence ${eventName}, type: ${type} from ${data.id} on ${channel.name}`)
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
- logger.debug(`[RTM] presence.enter on ${channel.name} with`, data)
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
- logger.debug(`message rcvd on channel ${channel.name}. sending ${message.name}`)
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
  )
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "anear-js-api",
3
- "version": "1.3.2",
3
+ "version": "1.3.3",
4
4
  "description": "Javascript Developer API for Anear Apps",
5
5
  "main": "lib/index.js",
6
6
  "scripts": {