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.
- package/EACH_PARTICIPANT_LIFECYCLE.md +3 -1
- package/README.md +12 -19
- package/lib/AnearService.js +3 -3
- package/lib/ci/registerAppVersion.js +7 -1
- package/lib/models/AnearEvent.js +2 -4
- package/lib/state_machines/AnearCoreServiceMachine.js +33 -6
- package/lib/state_machines/AnearEventMachine.js +195 -360
- package/lib/utils/AssetFileCollector.js +0 -2
- package/lib/utils/DisplayEventProcessor.js +21 -40
- package/package.json +1 -1
- package/tests/DisplayEventProcessor.test.js +39 -26
- package/lib/state_machines/AnearParticipantMachine.js +0 -765
|
@@ -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` ->
|
|
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
|
|
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
|
|
53
|
+
- Handle allParticipants timeout orchestration
|
|
56
54
|
- **Key States**: `registerCreator` → `eventCreated` → `announce` → `live` → `closeEvent`
|
|
57
55
|
|
|
58
|
-
####
|
|
59
|
-
-
|
|
60
|
-
-
|
|
61
|
-
|
|
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
|
|
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`, `
|
|
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
|
-
###
|
|
232
|
+
### Participant-local Timeouts
|
|
240
233
|
|
|
241
|
-
|
|
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
|
|
package/lib/AnearService.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
const AnearCoreServiceMachine = require('./state_machines/AnearCoreServiceMachine')
|
|
2
2
|
|
|
3
|
-
const AnearService = (appEventMachineFactory
|
|
3
|
+
const AnearService = (appEventMachineFactory) => {
|
|
4
4
|
//
|
|
5
5
|
// developer provides appEventMachineFactory:
|
|
6
6
|
//
|
|
7
7
|
// appEventMachineFactory = anearEvent => { returns XState Machine) }
|
|
8
|
-
//
|
|
8
|
+
// participant-level machines are no longer managed by JSAPI.
|
|
9
9
|
//
|
|
10
|
-
return AnearCoreServiceMachine(appEventMachineFactory
|
|
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
|
package/lib/models/AnearEvent.js
CHANGED
|
@@ -111,10 +111,8 @@ class AnearEvent extends JsonApiResource {
|
|
|
111
111
|
}
|
|
112
112
|
|
|
113
113
|
cancelAllParticipantTimeouts() {
|
|
114
|
-
//
|
|
115
|
-
//
|
|
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
|
|
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: '
|
|
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
|
|
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)
|