anear-js-api 0.5.2 → 0.5.4

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.
@@ -109,6 +109,10 @@ const AnearParticipantMachineConfig = participantId => ({
109
109
  PARTICIPANT_RECONNECT: {
110
110
  actions: 'logReconnected',
111
111
  internal: true
112
+ },
113
+ BOOT_PARTICIPANT: {
114
+ actions: 'logBooted',
115
+ target: '#cleanupAndExit'
112
116
  }
113
117
  }
114
118
  },
@@ -151,11 +155,17 @@ const AnearParticipantMachineConfig = participantId => ({
151
155
  src: 'publishPrivateDisplay',
152
156
  onDone: [
153
157
  { cond: 'hasActionTimeout', actions: 'updateActionTimeout', target: 'waitParticipantResponse' },
154
- { target: 'idle', internal: true }
158
+ { target: 'idle' }
155
159
  ],
156
160
  onError: {
157
161
  target: '#error'
158
162
  }
163
+ },
164
+ on: {
165
+ PARTICIPANT_EXIT: {
166
+ actions: 'logExit',
167
+ target: '#cleanupAndExit'
168
+ }
159
169
  }
160
170
  },
161
171
  waitParticipantResponse: {
@@ -223,6 +233,35 @@ const AnearParticipantMachineConfig = participantId => ({
223
233
  }
224
234
  }
225
235
  },
236
+ booting: {
237
+ initial: 'publishingShutdown',
238
+ states: {
239
+ publishingShutdown: {
240
+ invoke: {
241
+ id: 'publishBootMessage',
242
+ src: 'publishBootMessageService',
243
+ onDone: 'waitingForClientExit',
244
+ onError: '#cleanupAndExit'
245
+ }
246
+ },
247
+ waitingForClientExit: {
248
+ after: {
249
+ bootCleanupTimeout: {
250
+ target: '#cleanupAndExit'
251
+ }
252
+ },
253
+ on: {
254
+ PARTICIPANT_EXIT: '#cleanupAndExit',
255
+ ACTION: {
256
+ internal: true
257
+ },
258
+ PARTICIPANT_DISCONNECT: {
259
+ internal: true
260
+ }
261
+ }
262
+ }
263
+ }
264
+ },
226
265
  done: {
227
266
  id: 'done',
228
267
  entry: ['notifyEventMachineExit'],
@@ -356,6 +395,20 @@ const AnearParticipantMachineFunctions = {
356
395
 
357
396
  return { timeout: event.timeout }
358
397
  },
398
+ publishBootMessageService: async (context, event) => {
399
+ const { reason } = event.data;
400
+ const payload = {
401
+ content: {
402
+ reason: reason || 'You have been removed from the event.'
403
+ }
404
+ }
405
+
406
+ await RealtimeMessaging.publish(
407
+ context.privateChannel,
408
+ 'FORCE_SHUTDOWN',
409
+ payload
410
+ )
411
+ },
359
412
  attachToPrivateChannel: (context, event) => RealtimeMessaging.attachTo(context.privateChannel),
360
413
  detachPrivateChannel: async (context, event) => {
361
414
  return context.privateChannel ? RealtimeMessaging.detachAll([context.privateChannel]) : Promise.resolve()
@@ -368,6 +421,7 @@ const AnearParticipantMachineFunctions = {
368
421
  wasMidTurnOnDisconnect: context => context.actionTimeoutMsecs !== null && context.actionTimeoutMsecs > 0
369
422
  },
370
423
  delays: {
424
+ bootCleanupTimeout: () => C.TIMEOUT_MSECS.BOOT_EXIT,
371
425
  actionTimeout: context => context.actionTimeoutMsecs,
372
426
  dynamicReconnectTimeout: context => {
373
427
  // If an action timeout is active, use its remaining time.
@@ -37,7 +37,6 @@ const AppMachineTransition = (anearEvent) => {
37
37
  // Process all meta objects to handle unpredictable AppM structures
38
38
  metaObjects.forEach(meta => {
39
39
  let viewer
40
- let viewPath
41
40
  let timeoutFn
42
41
  let displayEvent
43
42
 
@@ -49,18 +48,23 @@ const AppMachineTransition = (anearEvent) => {
49
48
 
50
49
  if (meta.allParticipants) {
51
50
  viewer = 'allParticipants'
52
- viewPath = _getViewPath(meta.allParticipants)
51
+ const { viewPath, props } = _extractViewAndProps(meta.allParticipants)
53
52
  timeoutFn = RenderContextBuilder.buildTimeoutFn(viewer, meta.allParticipants.timeout)
54
53
 
55
54
  displayEvent = RenderContextBuilder.buildDisplayEvent(
56
55
  viewPath,
57
- RenderContextBuilder.buildAppRenderContext(appContext, appStateName, event.type, viewer, timeoutFn)
56
+ RenderContextBuilder.buildAppRenderContext(appContext, appStateName, event.type, viewer, timeoutFn),
57
+ viewer,
58
+ null,
59
+ null,
60
+ props
58
61
  )
59
62
  displayEvents.push(displayEvent)
60
63
  }
61
64
 
62
65
  if (meta.eachParticipant) {
63
66
  // Check if participant is a function (new selective rendering format)
67
+ viewer = 'eachParticipant'
64
68
  if (typeof meta.eachParticipant === 'function') {
65
69
  // New selective participant rendering
66
70
  const participantRenderFunc = meta.eachParticipant
@@ -73,7 +77,7 @@ const AppMachineTransition = (anearEvent) => {
73
77
  appContext,
74
78
  appStateName,
75
79
  event.type,
76
- 'eachParticipant',
80
+ viewer,
77
81
  null
78
82
  )
79
83
 
@@ -81,13 +85,15 @@ const AppMachineTransition = (anearEvent) => {
81
85
  if (participantDisplay && participantDisplay.participantId && participantDisplay.view) {
82
86
  // For selective rendering, timeout is handled directly in the participant display object
83
87
  const timeout = participantDisplay.timeout || null
88
+ const props = participantDisplay.props || {}
84
89
 
85
90
  displayEvent = RenderContextBuilder.buildDisplayEvent(
86
91
  participantDisplay.view,
87
92
  baseAppRenderContext, // Reuse the same base context
88
- 'eachParticipant',
93
+ viewer,
89
94
  participantDisplay.participantId,
90
- timeout
95
+ timeout,
96
+ props
91
97
  )
92
98
  displayEvents.push(displayEvent)
93
99
  }
@@ -95,15 +101,17 @@ const AppMachineTransition = (anearEvent) => {
95
101
  }
96
102
  } else {
97
103
  // Legacy participant rendering - normalize to selective format
98
- const viewPath = _getViewPath(meta.eachParticipant)
104
+ const { viewPath, props } = _extractViewAndProps(meta.eachParticipant)
99
105
  const timeoutFn = RenderContextBuilder.buildTimeoutFn('participant', meta.eachParticipant.timeout)
100
106
 
101
107
  const renderContext = RenderContextBuilder.buildAppRenderContext(appContext, appStateName, event.type, 'eachParticipant', timeoutFn),
102
108
  displayEvent = RenderContextBuilder.buildDisplayEvent(
103
109
  viewPath,
104
110
  renderContext,
105
- 'eachParticipant',
106
- 'ALL_PARTICIPANTS' // Special marker for "all participants"
111
+ viewer,
112
+ 'ALL_PARTICIPANTS', // Special marker for "all participants"
113
+ null,
114
+ props
107
115
  )
108
116
  displayEvents.push(displayEvent)
109
117
  }
@@ -111,24 +119,32 @@ const AppMachineTransition = (anearEvent) => {
111
119
 
112
120
  if (meta.host) {
113
121
  viewer = 'host'
114
- viewPath = _getViewPath(meta.host)
122
+ const { viewPath, props } = _extractViewAndProps(meta.host)
115
123
  timeoutFn = RenderContextBuilder.buildTimeoutFn(viewer, meta.host.timeout)
116
124
 
117
125
  displayEvent = RenderContextBuilder.buildDisplayEvent(
118
126
  viewPath,
119
127
  RenderContextBuilder.buildAppRenderContext(appContext, appStateName, event.type, viewer, timeoutFn),
120
- 'host'
128
+ viewer,
129
+ null,
130
+ null,
131
+ props
121
132
  )
122
133
  displayEvents.push(displayEvent)
123
134
  }
124
135
 
125
136
  if (meta.spectators) {
126
137
  viewer = 'spectators'
127
- viewPath = _getViewPath(meta.spectators)
138
+ const { viewPath, props } = _extractViewAndProps(meta.spectators)
139
+ timeoutFn = RenderContextBuilder.buildTimeoutFn(viewer, meta.spectators.timeout)
128
140
 
129
141
  displayEvent = RenderContextBuilder.buildDisplayEvent(
130
142
  viewPath,
131
- RenderContextBuilder.buildAppRenderContext(appContext, appStateName, event.type, viewer)
143
+ RenderContextBuilder.buildAppRenderContext(appContext, appStateName, event.type, viewer, timeoutFn),
144
+ viewer,
145
+ null,
146
+ null,
147
+ props
132
148
  )
133
149
  displayEvents.push(displayEvent)
134
150
  }
@@ -140,28 +156,28 @@ const AppMachineTransition = (anearEvent) => {
140
156
  }
141
157
  }
142
158
 
143
- // Send CLOSE AFTER processing meta (including exit displays)
144
- if (isDone) {
145
- logger.debug('[AppMachineTransition] AppM reached final state, sending CLOSE')
146
- anearEvent.send('CLOSE')
147
- }
159
+ // Note: When AppM reaches a final state, the service.onDone() callback
160
+ // in AnearEventMachine.js will fire and send APPM_FINAL to trigger cleanup.
161
+ // Apps should explicitly call anearEvent.closeEvent() or anearEvent.cancelEvent()
162
+ // as entry actions in final states if they need API state transitions.
148
163
  }
149
164
  }
150
165
 
151
- const _getViewPath = (config) => {
152
- if (!config) return null
153
- if (typeof config === 'string') return config
166
+ const _extractViewAndProps = (config) => {
167
+ if (!config) return { viewPath: null, props: {} }
168
+
169
+ if (typeof config === 'string') {
170
+ return { viewPath: config, props: {} }
171
+ }
172
+
154
173
  if (typeof config === 'object') {
155
- // Handle various possible view path formats
156
- if (config.view) return config.view
157
- if (config.template) return config.template
158
- if (config.path) return config.path
159
- // If no view path found, log warning but don't throw
160
- logger.warn(`[AppMachineTransition] No view path found in config: ${JSON.stringify(config)}`)
161
- return null
174
+ const viewPath = config.view
175
+ const props = config.props || {}
176
+ return { viewPath, props }
162
177
  }
178
+
163
179
  logger.warn(`[AppMachineTransition] Unknown meta format: ${JSON.stringify(config)}`)
164
- return null
180
+ return { viewPath: null, props: {} }
165
181
  }
166
182
 
167
183
  const _stringifiedState = (stateValue) => {
@@ -13,7 +13,8 @@ module.exports = {
13
13
  ANNOUNCE: 5 * 60 * 1000, // 5 minutes
14
14
  START: 5 * 60 * 1000, // 5 minutes
15
15
  RENDERED_EVENT_DELAY: 100, // 100 milliseconds
16
- RECONNECT: 30 * 1000 // 30 seconds
16
+ RECONNECT: 30 * 1000, // 30 seconds
17
+ BOOT_EXIT: 2 * 1000 // 5 seconds
17
18
  },
18
19
  EventStates: {
19
20
  CREATED: 'created',
@@ -6,10 +6,17 @@
6
6
  * {
7
7
  * app: application XState context
8
8
  * meta: {
9
- * viewer: // 'eachParticipant', 'allParticipants', 'spectators'
9
+ * viewer: // 'eachParticipant', 'allParticipants', 'spectators', 'host'
10
10
  * state: // Stringified state name (e.g., 'live.registration.waitForOpponentToJoin')
11
- * event: // The triggering event for this transition ('PARTICIPANT_ENTER')
12
- * timeout: // null|| func}
11
+ * event: // The triggering event for this transition ('PARTIPANT_ENTER')
12
+ * // timeout exposed to views for display purposes only; actual timers are attached
13
+ * // only to the viewer that configured a timeout in AppM meta.
14
+ * // Shape: { msecs: number, remainingMsecs: number }
15
+ * timeout: // optional
16
+ * }
17
+ * props: {
18
+ * // All properties from the `props` object in the AppM's meta block.
19
+ * // e.g., meta: { view: 'foo', props: { title: 'Hello' } } -> `props.title`
13
20
  * }
14
21
  *
15
22
  * // When 'eachParticipant' displayType
@@ -38,6 +45,8 @@ const C = require('./Constants')
38
45
  class DisplayEventProcessor {
39
46
  constructor(anearEventMachineContext) {
40
47
  this.anearEvent = anearEventMachineContext.anearEvent
48
+ // Snapshot of current AEM timeout context for this render
49
+ this.participantsActionTimeout = anearEventMachineContext.participantsActionTimeout
41
50
  this.pugTemplates = anearEventMachineContext.pugTemplates
42
51
  this.pugHelpers = anearEventMachineContext.pugHelpers
43
52
  this.participantMachines = anearEventMachineContext.participantMachines
@@ -50,6 +59,26 @@ class DisplayEventProcessor {
50
59
  processAndPublish(displayEvents) {
51
60
  let participantsTimeout = null
52
61
 
62
+ // Pre-compute allParticipants timeout for this render batch so other viewers
63
+ // (host/spectators) can mirror it visually even before AEM context is updated
64
+ try {
65
+ const allEvent = displayEvents.find(e => (e.viewer || e.appRenderContext?.meta?.viewer) === 'allParticipants')
66
+ if (allEvent) {
67
+ const { timeout: displayTimeout, appRenderContext } = allEvent
68
+ const timeoutFn = appRenderContext?.meta?.timeoutFn
69
+ if (displayTimeout !== null && displayTimeout > 0) {
70
+ this.precomputedParticipantsTimeout = { msecs: displayTimeout, startedAt: Date.now() }
71
+ } else if (timeoutFn) {
72
+ const msecs = timeoutFn(appRenderContext.app)
73
+ if (typeof msecs === 'number' && msecs > 0) {
74
+ this.precomputedParticipantsTimeout = { msecs, startedAt: Date.now() }
75
+ }
76
+ }
77
+ }
78
+ } catch (_e) {
79
+ // ignore precompute errors; fall back to runtime values
80
+ }
81
+
53
82
  const publishPromises = displayEvents.map(event => {
54
83
  const { publishPromise, timeout } = this._processSingle(event)
55
84
  if (timeout) {
@@ -77,7 +106,7 @@ class DisplayEventProcessor {
77
106
  }
78
107
 
79
108
  _processSingle(displayEvent) {
80
- const { viewPath, appRenderContext, target, participantId, timeout: displayTimeout } = displayEvent
109
+ const { viewPath, appRenderContext, viewer, participantId, timeout: displayTimeout, props } = displayEvent
81
110
  const timeoutFn = appRenderContext.meta.timeoutFn
82
111
 
83
112
  const normalizedPath = viewPath.endsWith(C.PugSuffix) ? viewPath : `${viewPath}${C.PugSuffix}`
@@ -93,7 +122,8 @@ class DisplayEventProcessor {
93
122
  ...appRenderContext,
94
123
  anearEvent: this.anearEvent,
95
124
  participants: this.participantsIndex,
96
- ...this.pugHelpers
125
+ ...this.pugHelpers,
126
+ props
97
127
  }
98
128
 
99
129
  const formattedDisplayMessage = () => {
@@ -104,11 +134,11 @@ class DisplayEventProcessor {
104
134
  return displayMessage
105
135
  }
106
136
 
107
- // The target determines who sees the view. It defaults to the 'viewer'
108
- // from the meta block, but can be overridden (e.g., for 'host').
109
- const displayTarget = target || appRenderContext.meta.viewer
137
+ // The viewer determines who sees the view. It can be set directly on the
138
+ // display event, but if not, it falls back to the viewer from the meta block.
139
+ const displayViewer = viewer || appRenderContext.meta.viewer
110
140
 
111
- switch (displayTarget) {
141
+ switch (displayViewer) {
112
142
  case 'allParticipants':
113
143
  logger.debug(`[DisplayEventProcessor] Publishing ${viewPath} to PARTICIPANTS_DISPLAY`)
114
144
 
@@ -122,6 +152,20 @@ class DisplayEventProcessor {
122
152
  }
123
153
  }
124
154
 
155
+ // Expose timeout to template context for countdown bars
156
+ if (timeout) {
157
+ const startedAt = (this.participantsActionTimeout && this.participantsActionTimeout.startedAt) || (this.precomputedParticipantsTimeout && this.precomputedParticipantsTimeout.startedAt)
158
+ const now = Date.now()
159
+ const remainingMsecs = startedAt ? Math.max(0, timeout.msecs - (now - startedAt)) : timeout.msecs
160
+ templateRenderContext.meta = {
161
+ ...templateRenderContext.meta,
162
+ timeout: { msecs: timeout.msecs, remainingMsecs }
163
+ }
164
+ logger.debug(`[DisplayEventProcessor] allParticipants timeout resolved to ${timeout.msecs}ms (remaining ${remainingMsecs}ms)`)
165
+ } else {
166
+ logger.debug(`[DisplayEventProcessor] no allParticipants timeout resolved`)
167
+ }
168
+
125
169
  publishPromise = RealtimeMessaging.publish(
126
170
  this.participantsDisplayChannel,
127
171
  'PARTICIPANTS_DISPLAY',
@@ -132,6 +176,30 @@ class DisplayEventProcessor {
132
176
  case 'spectators':
133
177
  logger.debug(`[DisplayEventProcessor] Publishing ${viewPath} to SPECTATORS_DISPLAY`)
134
178
 
179
+ // Spectators never have action timeouts; they can display a visual bar only.
180
+ // Prefer their own configured timeout for visuals, otherwise mirror the
181
+ // allParticipants collective timeout (remaining time) if one is running.
182
+ let specTimeout = null
183
+ if (timeoutFn) {
184
+ const msecs = timeoutFn(appRenderContext.app)
185
+ if (typeof msecs === 'number' && msecs > 0) {
186
+ specTimeout = { msecs, remainingMsecs: msecs }
187
+ }
188
+ }
189
+ if (!specTimeout) {
190
+ const pat = this.precomputedParticipantsTimeout || this.participantsActionTimeout
191
+ if (pat && typeof pat.msecs === 'number') {
192
+ const now = Date.now()
193
+ const start = pat.startedAt || Date.now()
194
+ const remainingMsecs = Math.max(0, pat.msecs - (now - start))
195
+ specTimeout = { msecs: pat.msecs, remainingMsecs }
196
+ }
197
+ }
198
+ if (specTimeout) {
199
+ templateRenderContext.meta = { ...templateRenderContext.meta, timeout: specTimeout }
200
+ logger.debug(`[DisplayEventProcessor] spectators visual timeout set to msecs=${specTimeout.msecs}, remaining=${specTimeout.remainingMsecs}`)
201
+ }
202
+
135
203
  publishPromise = RealtimeMessaging.publish(
136
204
  this.spectatorsDisplayChannel,
137
205
  'SPECTATORS_DISPLAY',
@@ -157,12 +225,45 @@ class DisplayEventProcessor {
157
225
 
158
226
  case 'host':
159
227
  logger.debug(`[DisplayEventProcessor] Processing RENDER_DISPLAY ${viewPath} for host`)
228
+ // Host may optionally have a real timeout (if configured). Otherwise, host
229
+ // mirrors the allParticipants visual timer without starting a host timeout.
230
+ let hostOwnMsecs = null
231
+ if (timeoutFn) {
232
+ const msecs = timeoutFn(appRenderContext.app)
233
+ if (typeof msecs === 'number' && msecs > 0) hostOwnMsecs = msecs
234
+ }
235
+ if (hostOwnMsecs !== null) {
236
+ templateRenderContext.meta = {
237
+ ...templateRenderContext.meta,
238
+ timeout: { msecs: hostOwnMsecs, remainingMsecs: hostOwnMsecs }
239
+ }
240
+ logger.debug(`[DisplayEventProcessor] host timeout resolved to ${hostOwnMsecs}ms`)
241
+ } else {
242
+ // Visual-only: mirror participants timeout if it exists
243
+ const pat = this.precomputedParticipantsTimeout || this.participantsActionTimeout
244
+ if (pat && typeof pat.msecs === 'number') {
245
+ const now = Date.now()
246
+ const start = pat.startedAt || Date.now()
247
+ const remainingMsecs = Math.max(0, pat.msecs - (now - start))
248
+ templateRenderContext.meta = {
249
+ ...templateRenderContext.meta,
250
+ timeout: { msecs: pat.msecs, remainingMsecs }
251
+ }
252
+ logger.debug(`[DisplayEventProcessor] host visual timeout from allParticipants msecs=${pat.msecs}, remaining=${remainingMsecs}`)
253
+ } else {
254
+ logger.debug(`[DisplayEventProcessor] host timeout not set`)
255
+ }
256
+ }
160
257
 
161
- publishPromise = this._processHostDisplay(template, templateRenderContext, timeoutFn)
258
+ publishPromise = this._processHostDisplay(
259
+ template,
260
+ templateRenderContext,
261
+ hostOwnMsecs !== null ? (() => hostOwnMsecs) : null
262
+ )
162
263
  break
163
264
 
164
265
  default:
165
- throw new Error(`Unknown display target: ${displayTarget}`)
266
+ throw new Error(`Unknown display viewer: ${displayViewer}`)
166
267
  }
167
268
  return { publishPromise, timeout }
168
269
  }
@@ -245,7 +346,8 @@ class DisplayEventProcessor {
245
346
  }
246
347
 
247
348
  if (timeout !== null) {
248
- privateRenderContext.timeout = timeout
349
+ // APM will compute remaining on its own; start remaining at full msecs here
350
+ privateRenderContext.timeout = { msecs: timeout, remainingMsecs: timeout }
249
351
  }
250
352
 
251
353
  const privateHtml = template(privateRenderContext)
@@ -85,6 +85,15 @@ class RealtimeMessaging {
85
85
  return channel.publish(msgType, message)
86
86
  }
87
87
 
88
+ async setPresence(channel, data) {
89
+ try {
90
+ await channel.presence.enter(data)
91
+ logger.debug(`[RTM] presence.enter on ${channel.name} with`, data)
92
+ } catch (e) {
93
+ logger.warn(`[RTM] presence.enter failed on ${channel.name}`, e)
94
+ }
95
+ }
96
+
88
97
  /**
89
98
  * Detach a list of Ably channels.
90
99
  * If any detach fails, Promise.all will reject.
@@ -59,19 +59,21 @@ class RenderContextBuilder {
59
59
  * Build display event object
60
60
  * @param {string} viewPath - Template/view path
61
61
  * @param {Object} appRenderContext - Context object
62
- * @param {string} target - Optional target (host, participant, etc.)
62
+ * @param {string} viewer - Optional viewer type (host, participant, etc.)
63
63
  * @param {string} participantId - Optional participant ID for selective rendering
64
64
  * @param {number} timeout - Optional timeout in milliseconds
65
+ * @param {Object} props - Optional arguments for the view
65
66
  * @returns {Object} Display event object
66
67
  */
67
- static buildDisplayEvent(viewPath, appRenderContext, target = null, participantId = null, timeout = null) {
68
+ static buildDisplayEvent(viewPath, appRenderContext, viewer = null, participantId = null, timeout = null, props = {}) {
68
69
  const displayEvent = {
69
70
  viewPath,
70
- appRenderContext
71
+ appRenderContext,
72
+ props
71
73
  }
72
74
 
73
- if (target) {
74
- displayEvent.target = target
75
+ if (viewer) {
76
+ displayEvent.viewer = viewer
75
77
  }
76
78
 
77
79
  if (participantId) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "anear-js-api",
3
- "version": "0.5.2",
3
+ "version": "0.5.4",
4
4
  "description": "Javascript Developer API for Anear Apps",
5
5
  "main": "lib/index.js",
6
6
  "scripts": {