anear-js-api 1.1.5 → 1.1.6

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.
@@ -1312,18 +1312,6 @@ const AnearEventMachineFunctions = ({
1312
1312
 
1313
1313
  const spectatorEvent = { type: 'SPECTATOR_ENTER', data: userJSON }
1314
1314
  context.appEventMachine.send(spectatorEvent)
1315
-
1316
- // v5: AppM snapshots often don't include the triggering event, so the
1317
- // AppMachineTransition subscriber can't reliably "refresh render" on
1318
- // presence events. Explicitly invoke the transition hook here so
1319
- // spectators always get the current view.
1320
- if (typeof context.appMachineTransition === 'function') {
1321
- try {
1322
- context.appMachineTransition(context.appEventMachine.getSnapshot(), spectatorEvent)
1323
- } catch (e) {
1324
- logger.warn('[AEM] AppMachineTransition failed during SPECTATOR_ENTER refresh', e)
1325
- }
1326
- }
1327
1315
  }
1328
1316
  )
1329
1317
  },
@@ -1372,15 +1360,6 @@ const AnearEventMachineFunctions = ({
1372
1360
  if (context.appEventMachine) {
1373
1361
  const reconnectEvent = { type: eventName, participantId }
1374
1362
  context.appEventMachine.send(reconnectEvent)
1375
-
1376
- // Explicitly refresh meta-driven displays on reconnect (browser refresh)
1377
- if (typeof context.appMachineTransition === 'function') {
1378
- try {
1379
- context.appMachineTransition(context.appEventMachine.getSnapshot(), reconnectEvent)
1380
- } catch (e) {
1381
- logger.warn('[AEM] AppMachineTransition failed during RECONNECT refresh', e)
1382
- }
1383
- }
1384
1363
  }
1385
1364
  },
1386
1365
  updateParticipantGeoLocation: assign({
@@ -1425,15 +1404,6 @@ const AnearEventMachineFunctions = ({
1425
1404
  }
1426
1405
  if (context.appEventMachine) {
1427
1406
  context.appEventMachine.send(appMPayload);
1428
-
1429
- // Explicitly refresh meta-driven displays on UPDATE (e.g. geo changes)
1430
- if (typeof context.appMachineTransition === 'function') {
1431
- try {
1432
- context.appMachineTransition(context.appEventMachine.getSnapshot(), appMPayload)
1433
- } catch (e) {
1434
- logger.warn('[AEM] AppMachineTransition failed during UPDATE refresh', e)
1435
- }
1436
- }
1437
1407
  }
1438
1408
  },
1439
1409
  sendExitToAppMachine: ({ context, event }) => {
@@ -1699,19 +1669,118 @@ const AnearEventMachineFunctions = ({
1699
1669
  }
1700
1670
  }
1701
1671
 
1702
- const appActor = createActor(machineToStart, actorInput ? { input: actorInput } : undefined)
1703
- appActor.start()
1704
-
1705
- // Observe transitions to drive rendering (meta parsing, etc.)
1672
+ // Observe AppM transitions with full event payloads.
1673
+ // v5 note: `subscribe()` only provides snapshots, which often do not include
1674
+ // the triggering event. We use `inspect` so AppMachineTransition always gets
1675
+ // (snapshot, event) for meta.eachParticipant(ctx, event) and timeout payloads.
1706
1676
  const appMachineTransition = AppMachineTransition(context.anearEvent)
1707
- // v5: subscribing does not emit the current snapshot immediately, so we
1708
- // manually process the initial snapshot to match v4 onTransition behavior.
1709
- try {
1710
- appMachineTransition(appActor.getSnapshot())
1711
- } catch (e) {
1712
- logger.warn('[AEM] AppMachineTransition failed on initial snapshot', e)
1677
+
1678
+ const REFRESH_EVENT_TYPES = AppMachineTransition.REFRESH_EVENT_TYPES
1679
+
1680
+ const defer =
1681
+ (typeof queueMicrotask === 'function')
1682
+ ? queueMicrotask
1683
+ : (fn) => Promise.resolve().then(fn)
1684
+
1685
+ let appActor
1686
+ let lastRefreshEvent = null
1687
+ let lastRefreshHandledBySnapshot = false
1688
+ let sawAnySnapshot = false
1689
+
1690
+ const inspect = (inspectionEvent) => {
1691
+ if (!inspectionEvent || !appActor) return
1692
+
1693
+ // Only observe the root AppM actor; ignore child actors.
1694
+ if (inspectionEvent.actorRef !== appActor) return
1695
+
1696
+ if (inspectionEvent.type === '@xstate.snapshot') {
1697
+ const { snapshot, event } = inspectionEvent
1698
+ sawAnySnapshot = true
1699
+ try {
1700
+ logger.debug('[AEM][inspect] @xstate.snapshot', {
1701
+ eventType: event?.type,
1702
+ participantId: event?.participantId,
1703
+ nonResponderIds: event?.nonResponderIds,
1704
+ value: snapshot?.value,
1705
+ status: snapshot?.status
1706
+ })
1707
+ } catch (_e) {
1708
+ // logging should never interrupt state processing
1709
+ }
1710
+ try {
1711
+ appMachineTransition(snapshot, event)
1712
+ } catch (e) {
1713
+ logger.warn('[AEM] AppMachineTransition failed during inspect snapshot', e)
1714
+ }
1715
+
1716
+ // If we deferred a "refresh render" for this exact event, mark it as handled
1717
+ // so the fallback won't run and double-render.
1718
+ if (event && lastRefreshEvent && event === lastRefreshEvent) {
1719
+ lastRefreshHandledBySnapshot = true
1720
+ logger.debug('[AEM][inspect] refresh event handled by snapshot (suppress fallback)', {
1721
+ eventType: event?.type,
1722
+ participantId: event?.participantId
1723
+ })
1724
+ }
1725
+ }
1726
+
1727
+ if (inspectionEvent.type === '@xstate.event') {
1728
+ const ev = inspectionEvent.event
1729
+ if (!ev || !ev.type) return
1730
+
1731
+ // Some presence refresh events are internal/no-op transitions, which may not
1732
+ // produce a snapshot update. Defer a refresh render and cancel it if a
1733
+ // snapshot update for the same event happens in the same tick.
1734
+ if (REFRESH_EVENT_TYPES.has(ev.type)) {
1735
+ try {
1736
+ logger.debug('[AEM][inspect] @xstate.event (refresh candidate)', {
1737
+ eventType: ev?.type,
1738
+ participantId: ev?.participantId,
1739
+ nonResponderIds: ev?.nonResponderIds
1740
+ })
1741
+ } catch (_e) {
1742
+ // logging should never interrupt state processing
1743
+ }
1744
+ lastRefreshEvent = ev
1745
+ lastRefreshHandledBySnapshot = false
1746
+
1747
+ defer(() => {
1748
+ if (!appActor) return
1749
+ if (lastRefreshEvent !== ev) return
1750
+ if (lastRefreshHandledBySnapshot) return
1751
+
1752
+ logger.debug('[AEM][inspect] refresh fallback firing (no snapshot update)', {
1753
+ eventType: ev?.type,
1754
+ participantId: ev?.participantId
1755
+ })
1756
+ try {
1757
+ appMachineTransition(appActor.getSnapshot(), ev)
1758
+ } catch (e) {
1759
+ logger.warn('[AEM] AppMachineTransition failed during inspect refresh fallback', e)
1760
+ }
1761
+ })
1762
+ }
1763
+ }
1713
1764
  }
1714
- appActor.subscribe(appMachineTransition)
1765
+
1766
+ const actorOptions = actorInput
1767
+ ? { input: actorInput, inspect }
1768
+ : { inspect }
1769
+
1770
+ appActor = createActor(machineToStart, actorOptions)
1771
+ appActor.start()
1772
+
1773
+ // v5: inspection generally emits an init snapshot, but keep a safe fallback
1774
+ // to match v4 onTransition behavior in case an init snapshot isn't observed.
1775
+ defer(() => {
1776
+ if (sawAnySnapshot) return
1777
+ try {
1778
+ logger.debug('[AEM][inspect] init snapshot fallback (no @xstate.snapshot observed yet)')
1779
+ appMachineTransition(appActor.getSnapshot(), { type: 'xstate.init' })
1780
+ } catch (e) {
1781
+ logger.warn('[AEM] AppMachineTransition failed on initial snapshot fallback', e)
1782
+ }
1783
+ })
1715
1784
 
1716
1785
  // Auto-cleanup when AppM reaches done.
1717
1786
  appActor.subscribe((snapshot) => {
@@ -3,6 +3,23 @@
3
3
  const logger = require('./Logger')
4
4
  const RenderContextBuilder = require('./RenderContextBuilder')
5
5
 
6
+ // Events that should force a "refresh render" even if the AppM did not
7
+ // transition (no-op/internal transitions, presence refreshes, etc.).
8
+ const REFRESH_EVENT_TYPES = new Set([
9
+ 'SPECTATOR_ENTER',
10
+ 'PARTICIPANT_ENTER',
11
+ 'HOST_ENTER',
12
+ // Browser refresh / quick leave+rejoin cases:
13
+ 'PARTICIPANT_RECONNECT',
14
+ 'HOST_RECONNECT',
15
+ // Temporary disconnects (useful for "halt/pause" and UI indicators):
16
+ 'PARTICIPANT_DISCONNECT',
17
+ 'HOST_DISCONNECT',
18
+ // Location/heartbeat updates can also want a "refresh current view":
19
+ 'PARTICIPANT_UPDATE',
20
+ 'HOST_UPDATE'
21
+ ])
22
+
6
23
  /**
7
24
  * AppMachineTransition:
8
25
  * - Runs inside your appEventMachine onTransition.
@@ -19,17 +36,6 @@ const AppMachineTransition = (anearEvent) => {
19
36
  // - Never re-render on the RENDERED ack (prevents ping-pong loops)
20
37
  let lastBaseSignature = null
21
38
 
22
- const REFRESH_EVENT_TYPES = new Set([
23
- 'SPECTATOR_ENTER',
24
- 'PARTICIPANT_ENTER',
25
- // Browser refresh / quick leave+rejoin cases:
26
- 'PARTICIPANT_RECONNECT',
27
- 'HOST_RECONNECT',
28
- // Location/heartbeat updates can also want a "refresh current view":
29
- 'PARTICIPANT_UPDATE',
30
- 'HOST_UPDATE'
31
- ])
32
-
33
39
  return (appEventMachineState, triggeringEvent = null) => {
34
40
  // Handle potential XState version differences and missing properties
35
41
  const stateObj = appEventMachineState || {}
@@ -156,7 +162,7 @@ const AppMachineTransition = (anearEvent) => {
156
162
  const { viewPath, props } = _extractViewAndProps(meta.eachParticipant)
157
163
  const timeoutFn = RenderContextBuilder.buildTimeoutFn('participant', meta.eachParticipant.timeout)
158
164
 
159
- const renderContext = RenderContextBuilder.buildAppRenderContext(appContext, appStateName, event.type, 'eachParticipant', timeoutFn),
165
+ const renderContext = RenderContextBuilder.buildAppRenderContext(appContext, appStateName, event.type, 'eachParticipant', timeoutFn)
160
166
  displayEvent = RenderContextBuilder.buildDisplayEvent(
161
167
  viewPath,
162
168
  renderContext,
@@ -237,4 +243,7 @@ const _stringifiedState = (stateValue) => {
237
243
  return Object.entries(stateValue).map(([k, v]) => `${k}.${_stringifiedState(v)}`).join('.')
238
244
  }
239
245
 
246
+ // Export the refresh-event allowlist so AEM can share it (prevents drift).
247
+ AppMachineTransition.REFRESH_EVENT_TYPES = REFRESH_EVENT_TYPES
248
+
240
249
  module.exports = AppMachineTransition
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "anear-js-api",
3
- "version": "1.1.5",
3
+ "version": "1.1.6",
4
4
  "description": "Javascript Developer API for Anear Apps",
5
5
  "main": "lib/index.js",
6
6
  "scripts": {