anear-js-api 0.4.40 → 0.5.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.
@@ -0,0 +1,144 @@
1
+ # The `eachParticipant` Display Lifecycle
2
+
3
+ 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
+
5
+ ### The Goal
6
+
7
+ We want to render a specific view (`QuestionScreen.pug`) for a single user (`participant-123`) and make sure they answer within 10 seconds.
8
+
9
+ ### The Big Picture
10
+
11
+ 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
+
13
+ `AppMachineTransition` -> `AnearEventMachine` -> `DisplayEventProcessor` -> `AnearParticipantMachine` -> **Ably Message**
14
+
15
+ Let's break down each step.
16
+
17
+ ---
18
+
19
+ ### Part 1: The Trigger (Your AppM & `AppMachineTransition.js`)
20
+
21
+ It all starts in your application-specific state machine (e.g., `anear-q-and-a/StateMachine.js`). When your machine enters a state that needs to display something to participants, you define a `meta` object.
22
+
23
+ **Context:** The `meta` object is how your AppM communicates rendering intentions to the Anear platform. The `AppMachineTransition` module is a subscriber that listens for *every* state change in your AppM. Its job is to parse that `meta` object and translate it into a standardized command for the rest of the system.
24
+
25
+ #### Code Example (Your AppM)
26
+ Imagine your Q&A machine enters the `askQuestion` state. The state definition would look like this:
27
+
28
+ ```javascript
29
+ // anear-q-and-a/StateMachine.js
30
+ // ...
31
+ confirmMove: {
32
+ meta: {
33
+ // 'eachParticipant' is a function for selective rendering.
34
+ // This example shows how to target a single participant based on the
35
+ // event that triggered this state transition.
36
+ eachParticipant: (appContext, event) => {
37
+ // 'event' is the event that led to this state, e.g., { type: 'MOVE', participantId: 'p1', ... }
38
+ const movingParticipantId = event.participantId;
39
+
40
+ if (!movingParticipantId) return []; // Always return an array
41
+
42
+ // We only want to send a display to the participant who just moved.
43
+ return [{
44
+ participantId: movingParticipantId,
45
+ view: 'participant/MoveConfirmation', // A view confirming their move was received
46
+ props: {
47
+ message: 'Your move has been recorded. Waiting for opponent...',
48
+ move: event.moveData
49
+ },
50
+ timeout: 2000 // A short timeout for the confirmation display
51
+ }];
52
+ }
53
+ }
54
+ }
55
+ // ...
56
+ ```
57
+
58
+ #### Lifecycle Step 1: Parsing the `meta` block
59
+
60
+ When your AppM transitions to `confirmMove`, the `AppMachineTransition` function is invoked.
61
+
62
+ * **File:** `/Users/machvee/dev/anear-js-api/lib/utils/AppMachineTransition.js`
63
+ * **Description:** It detects the `meta.eachParticipant` property. Since it's a function, it executes it, receiving the array of display instructions we defined above. It then iterates through that array. For each instruction, it creates a standardized "display event" object.
64
+ * **Code Reference (`AppMachineTransition.js` lines 62-95):**
65
+
66
+ ```javascript
67
+ // ... inside AppMachineTransition.js
68
+ if (meta.eachParticipant) {
69
+ if (typeof meta.eachParticipant === 'function') {
70
+ // This is our path: the selective rendering function
71
+ const participantRenderFunc = meta.eachParticipant;
72
+ const participantDisplays = participantRenderFunc(appContext, event); // Executes our function from the AppM
73
+
74
+ // ... loops through the returned array ...
75
+ participantDisplays.forEach(participantDisplay => {
76
+ if (participantDisplay && participantDisplay.participantId && participantDisplay.view) {
77
+ const timeout = participantDisplay.timeout || null;
78
+
79
+ // Packages the info into a standard object
80
+ displayEvent = RenderContextBuilder.buildDisplayEvent(
81
+ participantDisplay.view,
82
+ baseAppRenderContext,
83
+ 'eachParticipant',
84
+ participantDisplay.participantId,
85
+ timeout
86
+ );
87
+ displayEvents.push(displayEvent);
88
+ }
89
+ });
90
+ } // ...
91
+ }
92
+ ```
93
+
94
+ #### Lifecycle Step 2: Sending the Command
95
+
96
+ After processing all `meta` properties, `AppMachineTransition` bundles all the generated `displayEvent` objects into a single event and sends it to the `AnearEventMachine` (AEM).
97
+
98
+ * **File:** `/Users/machvee/dev/anear-js-api/lib/utils/AppMachineTransition.js`
99
+ * **Code Reference (lines 137-140):**
100
+
101
+ ```javascript
102
+ if (displayEvents.length > 0) {
103
+ // Sends one event with a list of all rendering jobs
104
+ anearEvent.send('RENDER_DISPLAY', { displayEvents });
105
+ }
106
+ ```
107
+
108
+ ---
109
+
110
+ ### Part 2: The Router (`AnearEventMachine.js`)
111
+
112
+ **Context:** The `AnearEventMachine` (AEM) is the central orchestrator for an event. When it receives the `RENDER_DISPLAY` event, its role isn't to render anything itself, but to delegate the task to a specialized processor.
113
+
114
+ #### Lifecycle Step 3: Delegation
115
+
116
+ The AEM enters a rendering state (e.g., `announceRendering` or `liveRendering`) and invokes its `renderDisplay` service.
117
+
118
+ * **File:** `/Users/machvee/dev/anear-js-api/lib/state_machines/AnearEventMachine.js`
119
+ * **Description:** This service is simple: it creates an instance of `DisplayEventProcessor` and tells it to handle the `displayEvents` array we sent in the previous step.
120
+ * **Code Reference (lines 1144-1148):**
121
+
122
+ ```javascript
123
+ // ... inside AnearEventMachine.js
124
+ services: {
125
+ renderDisplay: async (context, event) => {
126
+ // The event here contains our { displayEvents } payload
127
+ const displayEventProcessor = new DisplayEventProcessor(context);
128
+ return await displayEventProcessor.processAndPublish(event.displayEvents);
129
+ },
130
+ // ...
131
+ ```
132
+
133
+ ---
134
+
135
+ ### Part 3: The Processor (`DisplayEventProcessor.js`)
136
+
137
+ **Context:** This class is the workhorse. It knows how to handle different display targets (`allParticipants`, `spectators`, `host`, and our target, `eachParticipant`). It's responsible for compiling the Pug templates and figuring out *who* gets the final HTML.
138
+
139
+ #### Lifecycle Step 4: Processing and Routing the Display
140
+
141
+ The `DisplayEventProcessor` loops through each `displayEvent` and, based on the target, decides what to do.
142
+
143
+ * **File:** `/Users/machvee/dev/anear-js-api/lib/utils/DisplayEventProcessor.js`
144
+ * **Description:** For an `eachParticipant`
@@ -18,15 +18,19 @@ const Config = {
18
18
  states: {
19
19
  registration: {
20
20
  meta: {
21
- participants: 'ViewableGameBoard',
21
+ allParticipants: 'ViewableGameBoard',
22
22
  spectators: 'ViewableGameBoard'
23
23
  }
24
24
  },
25
25
  gameInProgress: {
26
26
  meta: {
27
- participant: {
28
- view: 'PlayableGameBoard',
29
- timeout: calcParticipantTimeout
27
+ eachParticipant: {
28
+ view: 'PlayableGameBoard',
29
+ timeout: calcParticipantTimeout,
30
+ props: {
31
+ title: "Your Move!",
32
+ highlightLastMove: true
33
+ }
30
34
  },
31
35
  spectators: 'ViewableGameBoard'
32
36
  }
@@ -51,26 +55,26 @@ const actions = {
51
55
  renderGameOver: (context, event) => {
52
56
  anearEvent.render(
53
57
  'GameOver', // viewPath
54
- 'participants', // displayType
58
+ 'allParticipants', // displayType
55
59
  context, // appContext (AppM's context)
56
60
  event, // event that triggered this render
57
61
  null, // timeout (null for no timeout)
58
62
  { winner: context.winningPlayerId } // additional props
59
63
  )
60
64
  },
61
-
65
+
62
66
  // Render with timeout for participant displays
63
67
  renderWithTimeout: (context, event) => {
64
68
  anearEvent.render(
65
69
  'WaitingForMove',
66
- 'participant',
70
+ 'eachParticipant',
67
71
  context,
68
72
  event,
69
73
  (appContext, participantId) => 30000, // 30 second timeout
70
74
  { currentPlayer: context.currentPlayerToken }
71
75
  )
72
76
  },
73
-
77
+
74
78
  // Render for spectators
75
79
  renderForSpectators: (context, event) => {
76
80
  anearEvent.render(
@@ -95,11 +99,11 @@ const actions = {
95
99
  // Render winner display
96
100
  anearEvent.render(
97
101
  'WinnerDisplay',
98
- 'participants',
102
+ 'allParticipants',
99
103
  context,
100
104
  event,
101
105
  null,
102
- {
106
+ {
103
107
  winner: context.winner,
104
108
  finalScore: context.score,
105
109
  gameDuration: context.gameDuration
@@ -109,14 +113,14 @@ const actions = {
109
113
  // Render tie display
110
114
  anearEvent.render(
111
115
  'TieDisplay',
112
- 'participants',
116
+ 'allParticipants',
113
117
  context,
114
118
  event,
115
119
  null,
116
120
  { finalScore: context.score }
117
121
  )
118
122
  }
119
-
123
+
120
124
  // Now close the event
121
125
  anearEvent.closeEvent()
122
126
  }
@@ -128,14 +132,14 @@ const actions = {
128
132
  ### `anearEvent.render(viewPath, displayType, appContext, event, timeout, props)`
129
133
 
130
134
  - **`viewPath`** (string): Template/view path to render (e.g., 'GameBoard', 'GameOver')
131
- - **`displayType`** (string): One of 'participants', 'participant', or 'spectators'
135
+ - **`displayType`** (string): One of 'allParticipants', 'eachParticipant', or 'spectators'
132
136
  - **`appContext`** (Object): The AppM's context object (available in scope)
133
137
  - **`event`** (Object): The event that triggered this render (available in scope)
134
- - **`timeout`** (Function|number|null):
138
+ - **`timeout`** (Function|number|null):
135
139
  - `null`: No timeout
136
140
  - `number`: Fixed timeout in milliseconds
137
141
  - `Function`: Dynamic timeout function `(appContext, participantId) => msecs`
138
- - **`props`** (Object): Additional properties merged into meta (optional)
142
+ - **`props`** (Object): Additional properties made available at the root of the pug template's render context (optional)
139
143
 
140
144
  ## When to Use Each Approach
141
145
 
@@ -79,6 +79,28 @@ class AnearApi extends ApiService {
79
79
  logger.debug('getAppFontAssetsUploadUrls response:', attrs)
80
80
  return attrs
81
81
  }
82
+
83
+ async saveAppEventContext(eventId, appmContext) {
84
+ logger.debug(`API: POST developer events/${eventId}/app_event_context`)
85
+ const path = `events/${eventId}/app_event_context`
86
+ return this.postRaw(path, { appm_context: appmContext })
87
+ }
88
+
89
+ async getLatestAppEventContext(eventId) {
90
+ logger.debug(`API: GET developer events/${eventId}/app_event_context`)
91
+ const path = `events/${eventId}/app_event_context`
92
+ const json = await this.get(path)
93
+ const attrs = json.data && json.data.attributes ? json.data.attributes : {}
94
+ const eventIdAttr = attrs['event-id']
95
+ const raw = attrs['appm-context']
96
+ let appmContext = null
97
+ try {
98
+ appmContext = typeof raw === 'string' ? JSON.parse(raw) : raw
99
+ } catch (e) {
100
+ // leave appmContext as null if parsing fails
101
+ }
102
+ return { eventId: eventIdAttr, appmContext }
103
+ }
82
104
  }
83
105
 
84
106
  // Instantiate and export the API immediately
@@ -56,6 +56,18 @@ class ApiService {
56
56
  return this.issueRequest(request)
57
57
  }
58
58
 
59
+ postRaw(path, body={}) {
60
+ const urlString = `${this.api_base_url}/${path}`
61
+ const request = new fetch.Request(
62
+ urlString, {
63
+ method: 'POST',
64
+ headers: this.default_headers,
65
+ body: JSON.stringify(body)
66
+ }
67
+ )
68
+ return this.issueRequest(request)
69
+ }
70
+
59
71
  put(resource, id, attributes, relationships={}) {
60
72
  const payload = this.formatPayload(resource, attributes, relationships)
61
73
  const urlString = `${this.api_base_url}/${resource}/${id}`
@@ -1,5 +1,6 @@
1
1
  "use strict"
2
2
  const logger = require('../utils/Logger')
3
+ const anearApi = require('../api/AnearApi')
3
4
 
4
5
  const JsonApiResource = require('./JsonApiResource')
5
6
 
@@ -46,6 +47,20 @@ class AnearEvent extends JsonApiResource {
46
47
  this.send("CLOSE")
47
48
  }
48
49
 
50
+ pauseEvent(context, resumeEvent = { type: 'RESUME' }) {
51
+ // Persist via AEM and acknowledge with PAUSED
52
+ this.send('PAUSE', { appmContext: { context, resumeEvent } })
53
+ }
54
+
55
+ saveEvent(context, resumeEvent = { type: 'RESUME' }) {
56
+ // Delegate save to the AEM; AEM will persist via ANAPI and acknowledge with SAVED
57
+ this.send('SAVE', { appmContext: { context, resumeEvent } })
58
+ }
59
+
60
+ bootParticipant(participantId, reason) {
61
+ this.send("BOOT_PARTICIPANT", { participantId, reason })
62
+ }
63
+
49
64
  render(viewPath, displayType, appContext, event, timeout = null, props = {}) {
50
65
  // Explicit render method for guaranteed rendering control
51
66
  // This complements the meta: {} approach for when you need explicit control
@@ -80,14 +95,6 @@ class AnearEvent extends JsonApiResource {
80
95
  })
81
96
  }
82
97
 
83
- pauseEvent() {
84
- this.send("PAUSE")
85
- }
86
-
87
- resumeEvent() {
88
- this.send("RESUME")
89
- }
90
-
91
98
  getClonedFromEvent() {
92
99
  // if the current event was a clone of previous event, fetch if from
93
100
  // Peristence and return
@@ -111,6 +118,18 @@ class AnearEvent extends JsonApiResource {
111
118
  return this.attributes.hosted
112
119
  }
113
120
 
121
+ get name() {
122
+ return this.attributes.name
123
+ }
124
+
125
+ get description() {
126
+ return this.attributes.description
127
+ }
128
+
129
+ get createdAt() {
130
+ return this.attributes['created-at']
131
+ }
132
+
114
133
  get participantTimeout() {
115
134
  // TODO: This probably should be set for each publishEventPrivateMessage
116
135
  // and then referenceable as an anear-data attribute in the html. That way
@@ -18,7 +18,8 @@ class AnearParticipant extends JsonApiResource {
18
18
  id: this.data.id,
19
19
  name: this.name,
20
20
  avatarUrl: this.avatarUrl,
21
- geoLocation: this.geoLocation
21
+ geoLocation: this.geoLocation,
22
+ isHost: this.isHost
22
23
  }
23
24
  }
24
25
 
@@ -62,6 +63,10 @@ class AnearParticipant extends JsonApiResource {
62
63
  return this.attributes['private-channel-name']
63
64
  }
64
65
 
66
+ get isHost() {
67
+ return this.userType === HostUserType
68
+ }
69
+
65
70
  setMachine(service) {
66
71
  if (service) {
67
72
  this.send = service.send.bind(service)
@@ -69,11 +74,6 @@ class AnearParticipant extends JsonApiResource {
69
74
  this.send = () => {}
70
75
  }
71
76
  }
72
-
73
-
74
- isHost() {
75
- return this.userType === HostUserType
76
- }
77
77
  }
78
78
 
79
79
  module.exports = AnearParticipant
@@ -134,7 +134,7 @@ const AnearCoreServiceMachineConfig = appId => ({
134
134
  }
135
135
  },
136
136
  waitAnearEventLifecycleCommand: {
137
- // The Anear API backend will send CREATE_EVENT messages with the event JSON data
137
+ // The Anear API backend will send CREATE_EVENT or LOAD_EVENT messages with the event JSON data
138
138
  // to this createEventMessages Channel when it needs to create a new instance of an
139
139
  // Event
140
140
  entry: (context) => logger.debug(`Waiting on ${context.appData.data.attributes['short-name']} lifecycle command`),
@@ -142,6 +142,9 @@ const AnearCoreServiceMachineConfig = appId => ({
142
142
  CREATE_EVENT: {
143
143
  actions: ['startNewEventMachine']
144
144
  },
145
+ LOAD_EVENT: {
146
+ actions: ['startNewEventMachine']
147
+ },
145
148
  EVENT_MACHINE_EXIT: {
146
149
  actions: [
147
150
  'cleanupEventMachine'
@@ -234,6 +237,11 @@ const AnearCoreServiceMachineFunctions = {
234
237
  context.coreServiceMachine,
235
238
  'CREATE_EVENT'
236
239
  )
240
+ RealtimeMessaging.subscribe(
241
+ context.newEventCreationChannel,
242
+ context.coreServiceMachine,
243
+ 'LOAD_EVENT'
244
+ )
237
245
  },
238
246
  startNewEventMachine: assign(
239
247
  {
@@ -242,11 +250,12 @@ const AnearCoreServiceMachineFunctions = {
242
250
  const anearEvent = new AnearEvent(eventJSON)
243
251
 
244
252
  if (context.anearEventMachines[anearEvent.id]) {
245
- logger.info(`[ACSM] Event machine for ${anearEvent.id} already exists. Ignoring CREATE_EVENT.`)
253
+ logger.info(`[ACSM] Event machine for ${anearEvent.id} already exists. Ignoring ${event.type}.`)
246
254
  return context.anearEventMachines
247
255
  }
248
256
 
249
- const service = AnearEventMachine(anearEvent, context)
257
+ const isLoadEvent = event.type === 'LOAD_EVENT'
258
+ const service = AnearEventMachine(anearEvent, { ...context, rehydrate: isLoadEvent })
250
259
 
251
260
  return {
252
261
  ...context.anearEventMachines,