anear-js-api 2.0.1 → 2.2.0

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.
@@ -1,5 +1,7 @@
1
1
  # The `eachParticipant` Display Lifecycle
2
2
 
3
+ > Breaking vNext note: `AnearParticipantMachine` has been removed. `AnearEventMachine` now owns participant private channel lifecycle and `DisplayEventProcessor` publishes `PRIVATE_DISPLAY` directly to participant private channels.
4
+
3
5
  This document provides a detailed, step-by-by-step breakdown of how a display targeted at `eachParticipant` travels from the application's state machine (AppM) to an individual participant's browser client.
4
6
 
5
7
  ### The Goal
@@ -10,7 +12,7 @@ We want to render a specific view (`QuestionScreen.pug`) for a single user (`par
10
12
 
11
13
  The core idea is to translate a declarative `meta` block in your application's state machine (AppM) into concrete HTML content that gets delivered to a specific participant's device. This process involves a chain of components:
12
14
 
13
- `AppMachineTransition` -> `AnearEventMachine` -> `DisplayEventProcessor` -> `AnearParticipantMachine` -> **Ably Message**
15
+ `AppMachineTransition` -> `AnearEventMachine` -> `DisplayEventProcessor` -> **Ably Message**
14
16
 
15
17
  Let's break down each step.
16
18
 
package/README.md CHANGED
@@ -18,7 +18,7 @@ The anear-js-api abstracts away:
18
18
  - Participant presence tracking and coordination
19
19
  - Display rendering and template compilation
20
20
  - Asset management (CSS, images) and CDN uploads
21
- - Timeout management for participant actions
21
+ - Timeout orchestration for `allParticipants` action windows
22
22
  - Reconnection handling and error recovery
23
23
 
24
24
  ## Architecture
@@ -28,9 +28,7 @@ The anear-js-api abstracts away:
28
28
  ```
29
29
  AnearCoreServiceMachine (Root)
30
30
  ├── AnearEventMachine (Per Event)
31
- │ ├── AnearParticipantMachine (Per Participant)
32
31
  │ └── AppEventMachine (Developer's App Logic)
33
- └── AppParticipantMachine (Optional, Per Participant)
34
32
  ```
35
33
 
36
34
  ### Core Components
@@ -52,30 +50,26 @@ AnearCoreServiceMachine (Root)
52
50
  - Route messages between participants and app logic
53
51
  - Manage participant presence (enter/leave/exit)
54
52
  - Coordinate display rendering for all channels
55
- - Handle individual participant timeouts
53
+ - Handle allParticipants timeout orchestration
56
54
  - **Key States**: `registerCreator` → `eventCreated` → `announce` → `live` → `closeEvent`
57
55
 
58
- #### AnearParticipantMachine (APM)
59
- - **Purpose**: Manages individual participant state and private communications
60
- - **Responsibilities**:
61
- - Handle participant private channel communications
62
- - Manage participant timeouts and reconnection logic
63
- - Track participant activity and idle states
64
- - Route participant-specific actions to app logic
65
- - **Key States**: `setup` → `live` → `waitReconnect` → `cleanupAndExit`
56
+ #### Breaking vNext: APM Removed
57
+ - Participant-machine orchestration is removed from JSAPI.
58
+ - AEM now manages participant private channel lifecycle and private display publishing directly.
59
+ - JSAPI no longer emits `PARTICIPANT_TIMEOUT`; participant-local timeout UX is owned by browser runtime (`<anear-timeout>`).
66
60
 
67
61
  ### Separation of Concerns: JSAPI vs. AppM
68
62
 
69
63
  A key architectural principle is the clear separation of concerns between the Anear JSAPI (the "engine") and the App Event Machine (the "application").
70
64
 
71
- #### JSAPI (AEM & APM): The Engine Room
65
+ #### JSAPI (AEM): The Engine Room
72
66
 
73
67
  The JSAPI is responsible for all the underlying infrastructure and communication mechanics. Its concerns are purely technical:
74
68
 
75
69
  - **Event Lifecycle & State:** Manages the low-level state transitions of an event (`created` → `announce` → `live` → `closed`) and the lifecycle of participants.
76
70
  - **Real-time Communication:** Handles all Ably channel setup, messaging, presence events, and connection state.
77
71
  - **Orderly Operations:** Ensures smooth, reliable startup and shutdown of all services and participant connections.
78
- - **Notifier:** The JSAPI acts as a notifier. It informs the AppM of important events (`PARTICIPANT_ENTER`, `PARTICIPANT_DISCONNECT`, `ACTION`, `PARTICIPANT_TIMEOUT`) but does not decide what these events mean for the application.
72
+ - **Notifier:** The JSAPI acts as a notifier. It informs the AppM of important events (`PARTICIPANT_ENTER`, `PARTICIPANT_DISCONNECT`, `ACTION`, `ACTIONS_TIMEOUT`) but does not decide what these events mean for the application.
79
73
 
80
74
  #### AppM: The Application & User Experience
81
75
 
@@ -185,7 +179,6 @@ The JSAPI handles participant lifecycle through presence events:
185
179
  | `PARTICIPANT_RECONNECT` | Participant rejoins after disconnect | `HOST_RECONNECT` or `PARTICIPANT_RECONNECT` |
186
180
  | `PARTICIPANT_EXIT` | Participant permanently leaves | `HOST_EXIT` or `PARTICIPANT_EXIT` |
187
181
  | `PARTICIPANT_DISCONNECT` | Temporary connection loss | `HOST_DISCONNECT` or `PARTICIPANT_DISCONNECT` |
188
- | `PARTICIPANT_TIMEOUT` | Participant fails to respond in time | `PARTICIPANT_TIMEOUT` |
189
182
  | `ACTION` | Participant performs an action | Custom event (e.g., `MOVE`, `ANSWER`) |
190
183
 
191
184
  **Key Point**: The AEM uses role-specific events (`HOST_*` vs `PARTICIPANT_*`) based on whether the user is the event creator/host. Your AppM never needs to check `isHost` - it just reacts to the specific events it receives.
@@ -236,9 +229,9 @@ PUG templates receive rich context including:
236
229
 
237
230
  ## Timeout Management
238
231
 
239
- ### Individual Timeouts
232
+ ### Participant-local Timeouts
240
233
 
241
- For turn-based actions where each participant has their own timer:
234
+ Participant-local timeout UX is handled by browser runtime (`<anear-timeout>`). JSAPI does not emit `PARTICIPANT_TIMEOUT`.
242
235
 
243
236
  ```javascript
244
237
  meta: {
@@ -251,8 +244,7 @@ meta: {
251
244
  }
252
245
  },
253
246
  on: {
254
- MOVE: 'nextTurn',
255
- PARTICIPANT_TIMEOUT: 'handleTimeout'
247
+ MOVE: 'nextTurn'
256
248
  }
257
249
  ```
258
250
 
@@ -287,6 +279,7 @@ The JSAPI automatically handles asset management:
287
279
  1. **CSS Upload**: All CSS files in `assets/css/` are uploaded to CloudFront
288
280
  2. **Image Upload**: All images in `assets/images/` are uploaded to CloudFront
289
281
  3. **Template Preloading**: All PUG templates are preloaded and cached
282
+ 4. **Hash-based dedupe**: Asset uploads are decided by SHA-256 content hash comparison against S3 metadata (not file timestamps)
290
283
 
291
284
  ### CI-Managed Assets Mode
292
285
 
@@ -1,12 +1,12 @@
1
1
  const AnearCoreServiceMachine = require('./state_machines/AnearCoreServiceMachine')
2
2
 
3
- const AnearService = (appEventMachineFactory, appParticipantMachineFactory = null) => {
3
+ const AnearService = (appEventMachineFactory) => {
4
4
  //
5
5
  // developer provides appEventMachineFactory:
6
6
  //
7
7
  // appEventMachineFactory = anearEvent => { returns XState Machine) }
8
- // optional appParticipantMachineFactory = anearParticipant => { returns XState Machine) }
8
+ // participant-level machines are no longer managed by JSAPI.
9
9
  //
10
- return AnearCoreServiceMachine(appEventMachineFactory, appParticipantMachineFactory)
10
+ return AnearCoreServiceMachine(appEventMachineFactory)
11
11
  }
12
12
  module.exports = AnearService
@@ -8,6 +8,9 @@ async function registerAppVersionFromCi({
8
8
  gitSha = process.env.GITHUB_SHA,
9
9
  imageUri = process.env.IMAGE_URI,
10
10
  assetManifestHash = process.env.ASSET_MANIFEST_HASH,
11
+ imageAssetsUrl = process.env.IMAGE_ASSETS_URL,
12
+ fontAssetsUrl = process.env.FONT_ASSETS_URL,
13
+ cssUrl = process.env.CSS_URL,
11
14
  status = 'building'
12
15
  } = {}) {
13
16
  if (!appId || !version) {
@@ -19,7 +22,10 @@ async function registerAppVersionFromCi({
19
22
  status,
20
23
  git_sha: gitSha,
21
24
  image_uri: imageUri,
22
- asset_manifest_hash: assetManifestHash
25
+ asset_manifest_hash: assetManifestHash,
26
+ image_assets_url: imageAssetsUrl,
27
+ font_assets_url: fontAssetsUrl,
28
+ css_url: cssUrl
23
29
  })
24
30
 
25
31
  return data
@@ -111,10 +111,8 @@ class AnearEvent extends JsonApiResource {
111
111
  }
112
112
 
113
113
  cancelAllParticipantTimeouts() {
114
- // Cancel all active participant-level timeouts. This sends CANCEL_TIMEOUT
115
- // to all APMs, which will benignly clear any active timeout timers.
116
- // Useful for scenarios where a game event (e.g., LIAR call) should immediately
117
- // cancel all individual participant timeouts.
114
+ // Cancels active allParticipants timeout tracking in AEM.
115
+ // Participant-local timeout ownership now lives in the browser runtime.
118
116
  this.send({ type: "CANCEL_ALL_PARTICIPANT_TIMEOUTS" })
119
117
  }
120
118
 
@@ -61,6 +61,8 @@ const C = require('../utils/Constants')
61
61
  const CreateVersionedEventChannelNameTemplate = (appId, appVersion) => `anear:${appId}:v:${normalizeVersionToken(appVersion)}:e`
62
62
  const DefaultTemplatesRootDir = "./views"
63
63
  const SkipRuntimeAssetSync = process.env.ANEARAPP_SKIP_RUNTIME_ASSET_SYNC === 'true'
64
+ const FallbackImageAssetsUrl = process.env.ANEARAPP_IMAGE_ASSETS_URL || null
65
+ const FallbackFontAssetsUrl = process.env.ANEARAPP_FONT_ASSETS_URL || null
64
66
 
65
67
  // Process-wide signal handling.
66
68
  // We install exactly once per process to avoid duplicate handlers in reconnect/restart scenarios.
@@ -80,11 +82,10 @@ function scheduleExit(code, message) {
80
82
  setTimeout(() => process.exit(code), delayMs)
81
83
  }
82
84
 
83
- const AnearCoreServiceMachineContext = (appId, appEventMachineFactory, appParticipantMachineFactory) => ({
85
+ const AnearCoreServiceMachineContext = (appId, appEventMachineFactory) => ({
84
86
  appId,
85
87
  appData: null,
86
88
  appEventMachineFactory,
87
- appParticipantMachineFactory,
88
89
  anearEventMachines: {}, // All concurrent anear events handled by this core service
89
90
  pugTemplates: {},
90
91
  imageAssetsUrl: null,
@@ -161,13 +162,27 @@ const AnearCoreServiceMachineConfig = appId => ({
161
162
  {
162
163
  guard: 'runtimeAssetSyncDisabled',
163
164
  actions: () => logger.info('[ACSM] Runtime asset sync disabled; assuming CI has already published assets'),
164
- target: 'loadAndCompilePugTemplates'
165
+ target: 'resolveVersionAssetUrls'
165
166
  },
166
167
  {
167
168
  target: 'uploadNewImageAssets'
168
169
  }
169
170
  ]
170
171
  },
172
+ resolveVersionAssetUrls: {
173
+ invoke: {
174
+ src: 'fetchLatestAppVersionAssets',
175
+ input: ({ context }) => ({ appId: context.appId }),
176
+ onDone: {
177
+ actions: assign({
178
+ imageAssetsUrl: ({ event }) => event.output.imageAssetsUrl,
179
+ fontAssetsUrl: ({ event }) => event.output.fontAssetsUrl
180
+ }),
181
+ target: 'loadAndCompilePugTemplates'
182
+ },
183
+ onError: '#failure'
184
+ }
185
+ },
171
186
  uploadNewImageAssets: {
172
187
  invoke: {
173
188
  src: 'uploadNewImageAssets',
@@ -255,6 +270,19 @@ const AnearCoreServiceMachineFunctions = {
255
270
  // v5 pattern: async "services" become Promise Actors via `fromPromise`.
256
271
  // The invoked actor receives `input` from the invoking state's `invoke.input`.
257
272
  fetchAppData: fromPromise(({ input }) => AnearApi.getApp(input.appId)),
273
+ fetchLatestAppVersionAssets: fromPromise(async ({ input }) => {
274
+ const latestVersion = await AnearApi.getLatestAppVersion(input.appId)
275
+ const attrs = latestVersion?.attributes || {}
276
+ const imageAssetsUrl = attrs['image-assets-url'] || FallbackImageAssetsUrl
277
+ const fontAssetsUrl = attrs['font-assets-url'] || FallbackFontAssetsUrl
278
+
279
+ if (!imageAssetsUrl) {
280
+ throw new Error('[ACSM] Missing image-assets-url for latest app version; set app_version image_assets_url in ANAPI or ANEARAPP_IMAGE_ASSETS_URL env var')
281
+ }
282
+
283
+ logger.info(`[ACSM] Using pre-published version assets image_base=${imageAssetsUrl}`)
284
+ return { imageAssetsUrl, fontAssetsUrl }
285
+ }),
258
286
  uploadNewImageAssets: fromPromise(({ input }) => {
259
287
  const uploader = new ImageAssetsUploader(C.ImagesDirPath, input.appId)
260
288
  return uploader.uploadAssets()
@@ -464,14 +492,13 @@ function normalizeVersionToken(value) {
464
492
  return String(value).replace(/[^A-Za-z0-9_.-]/g, '_')
465
493
  }
466
494
 
467
- const AnearCoreServiceMachine = (appEventMachineFactory, appParticipantMachineFactory = null) => {
495
+ const AnearCoreServiceMachine = (appEventMachineFactory) => {
468
496
  const appId = process.env.ANEARAPP_APP_ID
469
497
  const machineConfig = AnearCoreServiceMachineConfig(appId)
470
498
 
471
499
  const anearCoreServiceMachineContext = AnearCoreServiceMachineContext(
472
500
  appId,
473
- appEventMachineFactory,
474
- appParticipantMachineFactory
501
+ appEventMachineFactory
475
502
  )
476
503
 
477
504
  const coreServiceMachine = createMachine(machineConfig, AnearCoreServiceMachineFunctions)