anear-js-api 0.6.5 → 1.1.1

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/jest.config.js CHANGED
@@ -1,4 +1,10 @@
1
1
  require('dotenv').config({ path: '.env.test' })
2
+
3
+ // Provide sane defaults so unit tests don't depend on local machine env.
4
+ process.env.ANEARAPP_API_KEY = process.env.ANEARAPP_API_KEY || 'test-api-key'
5
+ process.env.ANEARAPP_API_SECRET = process.env.ANEARAPP_API_SECRET || 'test-api-secret'
6
+ process.env.ANEARAPP_API_VERSION = process.env.ANEARAPP_API_VERSION || 'v1'
7
+
2
8
  module.exports = {
3
9
  }
4
10
 
@@ -7,6 +7,6 @@ const AnearService = (appEventMachineFactory, appParticipantMachineFactory = nul
7
7
  // appEventMachineFactory = anearEvent => { returns XState Machine) }
8
8
  // optional appParticipantMachineFactory = anearParticipant => { returns XState Machine) }
9
9
  //
10
- AnearCoreServiceMachine(appEventMachineFactory, appParticipantMachineFactory)
10
+ return AnearCoreServiceMachine(appEventMachineFactory, appParticipantMachineFactory)
11
11
  }
12
12
  module.exports = AnearService
@@ -3,8 +3,8 @@ const logger = require('../utils/Logger')
3
3
  const ApiService = require('./ApiService')
4
4
 
5
5
  class AnearApi extends ApiService {
6
- constructor(apiKey, apiVersion) {
7
- super(apiKey, apiVersion)
6
+ constructor(apiKey, apiSecret, apiVersion) {
7
+ super(apiKey, apiSecret, apiVersion)
8
8
  }
9
9
 
10
10
  getAccount() {
@@ -105,12 +105,13 @@ class AnearApi extends ApiService {
105
105
 
106
106
  // Instantiate and export the API immediately
107
107
  const apiKey = process.env.ANEARAPP_API_KEY
108
+ const apiSecret = process.env.ANEARAPP_API_SECRET
108
109
  const apiVersion = process.env.ANEARAPP_API_VERSION
109
110
 
110
- if (!apiKey || !apiVersion) {
111
- throw new Error("API_KEY and API_VERSION must be defined in environment variables")
111
+ if (!apiKey || !apiSecret || !apiVersion) {
112
+ throw new Error("ANEARAPP_API_KEY, ANEARAPP_API_SECRET, and ANEARAPP_API_VERSION must be defined in environment variables")
112
113
  }
113
114
 
114
- const anearApiInstance = new AnearApi(apiKey, apiVersion)
115
+ const anearApiInstance = new AnearApi(apiKey, apiSecret, apiVersion)
115
116
 
116
117
  module.exports = anearApiInstance
@@ -13,19 +13,81 @@ const logger = require('../utils/Logger')
13
13
  const DEFAULT_DEVELOPER_API_URL = 'https://api.anear.me/developer'
14
14
 
15
15
  class ApiService {
16
- constructor(apiKey, apiVersion) {
16
+ constructor(apiKey, apiSecret, apiVersion) {
17
17
  const baseUrl = (process.env.ANEARAPP_API_URL || DEFAULT_DEVELOPER_API_URL).replace(/\/+$/, '')
18
18
  const versionSegment = apiVersion ? `/${apiVersion}` : ''
19
19
  this.api_base_url = `${baseUrl}${versionSegment}`
20
20
  logger.debug(`ApiService configured api_base_url=${this.api_base_url}`)
21
+
22
+ this.api_key = apiKey
23
+ this.api_secret = apiSecret
24
+ this.developer_auth = null
25
+ this._authPromise = null
26
+
21
27
  this.defaultHeaderObject = {
22
28
  'Accept': 'application/json',
23
- 'Content-Type': 'application/json',
24
- 'X-Api-Key': apiKey
29
+ 'Content-Type': 'application/json'
25
30
  }
26
31
  this.default_headers = new fetch.Headers(this.defaultHeaderObject)
27
32
  }
28
33
 
34
+ async ensureDeveloperAuth() {
35
+ const now = Math.floor(Date.now() / 1000)
36
+ if (this.developer_auth && this.developer_auth.expires_at && this.developer_auth.expires_at > now) {
37
+ return this.developer_auth
38
+ }
39
+
40
+ if (this._authPromise) return this._authPromise
41
+
42
+ this._authPromise = this.loginDeveloper()
43
+ .finally(() => {
44
+ this._authPromise = null
45
+ })
46
+
47
+ return this._authPromise
48
+ }
49
+
50
+ async loginDeveloper() {
51
+ if (!this.api_key || !this.api_secret) {
52
+ throw new Error("Developer API credentials must be provided (api_key + secret)")
53
+ }
54
+
55
+ const payload = {
56
+ data: {
57
+ type: "sessions",
58
+ attributes: {
59
+ api_key: this.api_key,
60
+ secret: this.api_secret
61
+ }
62
+ }
63
+ }
64
+
65
+ const request = new fetch.Request(
66
+ `${this.api_base_url}/sessions`, {
67
+ method: 'POST',
68
+ headers: this.default_headers,
69
+ body: JSON.stringify(payload)
70
+ }
71
+ )
72
+
73
+ logger.debug(`HTTP ${request.method} ${request.url} (developer session)`)
74
+ const resp = await fetch(request)
75
+ logger.debug(`HTTP response status=${resp.status} url=${request.url}`)
76
+ const json = await this.checkStatus(resp)
77
+
78
+ this.developer_auth = {
79
+ auth_token: json.auth_token,
80
+ expires_at: json.expires_at,
81
+ developer_id: json.developer_id
82
+ }
83
+
84
+ const bearer = `Bearer ${this.developer_auth.auth_token}`
85
+ this.default_headers.set('Authorization', bearer)
86
+ // Keep the plain-object form in sync for consumers like Ably authHeaders.
87
+ this.defaultHeaderObject['Authorization'] = bearer
88
+ return this.developer_auth
89
+ }
90
+
29
91
  idParamUrlString(resource, params) {
30
92
  let idParam = ''
31
93
  if (params.id) {
@@ -103,6 +165,11 @@ class ApiService {
103
165
  }
104
166
 
105
167
  async issueRequest(request) {
168
+ await this.ensureDeveloperAuth()
169
+ // Ensure each request has the latest Authorization header
170
+ if (this.developer_auth && this.developer_auth.auth_token) {
171
+ request.headers.set('Authorization', `Bearer ${this.developer_auth.auth_token}`)
172
+ }
106
173
  logger.debug(`HTTP ${request.method} ${request.url}`)
107
174
  const resp = await fetch(request)
108
175
  logger.debug(`HTTP response status=${resp.status} url=${request.url}`)
@@ -32,37 +32,37 @@ class AnearEvent extends JsonApiResource {
32
32
  }
33
33
 
34
34
  announceEvent() {
35
- this.send("ANNOUNCE")
35
+ this.send({ type: "ANNOUNCE" })
36
36
  }
37
37
 
38
38
  startEvent() {
39
- this.send("START")
39
+ this.send({ type: "START" })
40
40
  }
41
41
 
42
42
  cancelEvent() {
43
- this.send("CANCEL")
43
+ this.send({ type: "CANCEL" })
44
44
  }
45
45
 
46
46
  closeEvent() {
47
- this.send("CLOSE")
47
+ this.send({ type: "CLOSE" })
48
48
  }
49
49
 
50
50
  restartEvent() {
51
- this.send("RESTART")
51
+ this.send({ type: "RESTART" })
52
52
  }
53
53
 
54
54
  pauseEvent(context, resumeEvent = { type: 'RESUME' }) {
55
55
  // Persist via AEM and acknowledge with PAUSED
56
- this.send('PAUSE', { appmContext: { context, resumeEvent } })
56
+ this.send({ type: 'PAUSE', appmContext: { context, resumeEvent } })
57
57
  }
58
58
 
59
59
  saveEvent(context, resumeEvent = { type: 'RESUME' }) {
60
60
  // Delegate save to the AEM; AEM will persist via ANAPI and acknowledge with SAVED
61
- this.send('SAVE', { appmContext: { context, resumeEvent } })
61
+ this.send({ type: 'SAVE', appmContext: { context, resumeEvent } })
62
62
  }
63
63
 
64
64
  bootParticipant(participantId, reason) {
65
- this.send("BOOT_PARTICIPANT", { participantId, reason })
65
+ this.send({ type: "BOOT_PARTICIPANT", data: { participantId, reason } })
66
66
  }
67
67
 
68
68
  render(viewPath, displayType, appContext, event, timeout = null, props = {}) {
@@ -91,7 +91,8 @@ class AnearEvent extends JsonApiResource {
91
91
  Object.assign(appRenderContext.meta, props)
92
92
  }
93
93
 
94
- this.send("RENDER_DISPLAY", {
94
+ this.send({
95
+ type: "RENDER_DISPLAY",
95
96
  displayEvents: [{
96
97
  viewPath,
97
98
  appRenderContext
@@ -157,12 +158,15 @@ class AnearEvent extends JsonApiResource {
157
158
  return this.attributes.flags.includes(flagName)
158
159
  }
159
160
 
160
- allowsSpectators() {
161
+ spectatorsAllowed() {
161
162
  //
162
163
  // Canonical spectators decision uses the ANAPI-provided boolean when present.
163
164
  // Fallback to legacy negative flag for older payloads.
164
165
  //
165
- return this.attributes['spectators-allowed']
166
+ const allowedDasherized = this.attributes['spectators-allowed']
167
+ if (typeof allowedDasherized === 'boolean') return allowedDasherized
168
+
169
+ return !this.hasFlag('no_spectators')
166
170
  }
167
171
 
168
172
  isParticipantEventCreator(participant) {
@@ -44,7 +44,7 @@
44
44
  * app developer’s AppEventMachine (AppM) and any participant machines.
45
45
  */
46
46
 
47
- const { assign, createMachine, interpret } = require('xstate')
47
+ const { assign, createMachine, createActor, fromPromise } = require('xstate')
48
48
  const logger = require('../utils/Logger')
49
49
  const { version: anearJsApiVersion } = require('../../package.json')
50
50
 
@@ -63,7 +63,6 @@ const DefaultTemplatesRootDir = "./views"
63
63
 
64
64
  const AnearCoreServiceMachineContext = (appId, appEventMachineFactory, appParticipantMachineFactory) => ({
65
65
  appId,
66
- coreServiceMachine: null,
67
66
  appData: null,
68
67
  appEventMachineFactory,
69
68
  appParticipantMachineFactory,
@@ -75,33 +74,19 @@ const AnearCoreServiceMachineContext = (appId, appEventMachineFactory, appPartic
75
74
  retryDelay: 0
76
75
  })
77
76
 
78
- const GlobalEventConfig = {
79
- // This wildcard will catch done.invoke.anearEventMachine_<id>
80
- 'done.invoke.anearEventMachine_*': {
81
- }
82
- }
83
-
84
77
  const AnearCoreServiceMachineConfig = appId => ({
85
78
  id: `AnearCoreServiceMachine_${appId}`,
86
- initial: 'waitForContextUpdate',
87
- on: GlobalEventConfig,
79
+ initial: 'fetchAppDataWithRetry',
80
+ context: ({ input }) => input,
88
81
  states: {
89
- waitForContextUpdate: {
90
- id: 'waitForContextUpdate',
91
- on: {
92
- UPDATE_CONTEXT: {
93
- actions: ['updateContextWithMachineRef'],
94
- target: 'fetchAppDataWithRetry'
95
- }
96
- }
97
- },
98
82
  fetchAppDataWithRetry: {
99
- entry: (c, e) => logger.info('anear-js-api version: ', anearJsApiVersion),
83
+ entry: _ => logger.info('anear-js-api version: ', anearJsApiVersion),
100
84
  initial: 'fetchAppData',
101
85
  states: {
102
86
  fetchAppData: {
103
87
  invoke: {
104
88
  src: 'fetchAppData',
89
+ input: ({ context }) => ({ appId: context.appId }),
105
90
  onDone: {
106
91
  actions: ['setAppData'],
107
92
  target: '#initRealtimeMessaging'
@@ -134,10 +119,11 @@ const AnearCoreServiceMachineConfig = appId => ({
134
119
  uploadNewImageAssets: {
135
120
  invoke: {
136
121
  src: 'uploadNewImageAssets',
122
+ input: ({ context }) => ({ appId: context.appId }),
137
123
  onDone: {
138
124
  actions: assign({
139
- // event.data has imageAssetsUrl returned by src service
140
- imageAssetsUrl: (_context, event) => event.data
125
+ // v5: `event.output` is the resolved promise value
126
+ imageAssetsUrl: ({ event }) => event.output
141
127
  }),
142
128
  target: 'uploadNewFontAssets'
143
129
  },
@@ -147,9 +133,10 @@ const AnearCoreServiceMachineConfig = appId => ({
147
133
  uploadNewFontAssets: {
148
134
  invoke: {
149
135
  src: 'uploadNewFontAssets',
136
+ input: ({ context }) => ({ appId: context.appId }),
150
137
  onDone: {
151
138
  actions: assign({
152
- fontAssetsUrl: (_context, event) => event.data
139
+ fontAssetsUrl: ({ event }) => event.output
153
140
  }),
154
141
  target: 'minifyCssAndUpload'
155
142
  },
@@ -160,6 +147,11 @@ const AnearCoreServiceMachineConfig = appId => ({
160
147
  invoke: {
161
148
  id: 'minifyCssAndUpload',
162
149
  src: 'minifyCssAndUpload',
150
+ input: ({ context }) => ({
151
+ appId: context.appId,
152
+ imageAssetsUrl: context.imageAssetsUrl,
153
+ fontAssetsUrl: context.fontAssetsUrl
154
+ }),
163
155
  onDone: {
164
156
  target: 'loadAndCompilePugTemplates'
165
157
  },
@@ -176,7 +168,7 @@ const AnearCoreServiceMachineConfig = appId => ({
176
168
  // The Anear API backend will send CREATE_EVENT or LOAD_EVENT messages with the event JSON data
177
169
  // to this createEventMessages Channel when it needs to create a new instance of an
178
170
  // Event
179
- entry: (context) => logger.debug(`Waiting on ${context.appData.data.attributes['short-name']} lifecycle command`),
171
+ entry: ({ context }) => logger.debug(`Waiting on ${context.appData.data.attributes['short-name']} lifecycle command`),
180
172
  on: {
181
173
  CREATE_EVENT: {
182
174
  actions: ['startNewEventMachine']
@@ -193,50 +185,40 @@ const AnearCoreServiceMachineConfig = appId => ({
193
185
  },
194
186
  failure: {
195
187
  id: 'failure',
196
- entry: (_c, event) => logger.debug("Failure! ", event.data),
188
+ entry: ({ event }) => logger.debug("Failure! ", event.error),
197
189
  type: 'final'
198
190
  }
199
191
  }
200
192
  })
201
193
 
202
194
  const AnearCoreServiceMachineFunctions = {
203
- services: {
204
- fetchAppData: context => AnearApi.getApp(context.appId),
205
- uploadNewImageAssets: context => {
206
- const uploader = new ImageAssetsUploader(
207
- C.ImagesDirPath,
208
- context.appId
209
- )
195
+ actors: {
196
+ // v5 pattern: async "services" become Promise Actors via `fromPromise`.
197
+ // The invoked actor receives `input` from the invoking state's `invoke.input`.
198
+ fetchAppData: fromPromise(({ input }) => AnearApi.getApp(input.appId)),
199
+ uploadNewImageAssets: fromPromise(({ input }) => {
200
+ const uploader = new ImageAssetsUploader(C.ImagesDirPath, input.appId)
210
201
  return uploader.uploadAssets()
211
- },
212
- uploadNewFontAssets: context => {
213
- const uploader = new FontAssetsUploader(
214
- C.FontsDirPath,
215
- context.appId
216
- )
202
+ }),
203
+ uploadNewFontAssets: fromPromise(({ input }) => {
204
+ const uploader = new FontAssetsUploader(C.FontsDirPath, input.appId)
217
205
  return uploader.uploadAssets()
218
- },
219
- minifyCssAndUpload: (context) => {
206
+ }),
207
+ minifyCssAndUpload: fromPromise(({ input }) => {
220
208
  const uploader = new CssUploader(
221
209
  C.CssDirPath,
222
- context.imageAssetsUrl,
223
- context.fontAssetsUrl,
224
- context.appId
210
+ input.imageAssetsUrl,
211
+ input.fontAssetsUrl,
212
+ input.appId
225
213
  )
226
-
227
214
  return uploader.uploadCss()
228
- }
215
+ })
229
216
  },
230
217
  actions: {
231
- updateContextWithMachineRef: assign(
232
- {
233
- coreServiceMachine: (_, event) => event.coreServiceMachine
234
- }
235
- ),
236
- initRealtime: context => RealtimeMessaging.initRealtime(context.appId, context.coreServiceMachine),
218
+ initRealtime: ({ context, self }) => RealtimeMessaging.initRealtime(context.appId, self),
237
219
  loadPugFiles: assign(
238
220
  {
239
- pugTemplates: () => {
221
+ pugTemplates: (_args) => {
240
222
  const pugLoader = new PugLoader(DefaultTemplatesRootDir)
241
223
  const templates = pugLoader.compiledPugTemplates()
242
224
  logger.debug(`loaded pug templates ${Object.keys(templates)}`)
@@ -245,7 +227,7 @@ const AnearCoreServiceMachineFunctions = {
245
227
  }
246
228
  ),
247
229
  incrementRetryDelay: assign({
248
- retryDelay: (context, _e) => {
230
+ retryDelay: ({ context }, _params) => {
249
231
  const retryTimesBeforeReset = 6
250
232
  const increment = 5000
251
233
  const start = increment
@@ -256,35 +238,36 @@ const AnearCoreServiceMachineFunctions = {
256
238
  }),
257
239
  setAppData: assign(
258
240
  {
259
- appData: (_, event) => {
260
- logger.debug(`fetched ${event.data.data.attributes["short-name"]} app data`)
261
- return event.data
241
+ appData: ({ event }) => {
242
+ const output = event.output
243
+ logger.debug(`fetched ${output.data.attributes["short-name"]} app data`)
244
+ return output
262
245
  }
263
246
  }
264
247
  ),
265
248
  createEventsCreationChannel: assign(
266
249
  {
267
- newEventCreationChannel: context => {
250
+ newEventCreationChannel: ({ context, self }) => {
268
251
  const channelName = CreateEventChannelNameTemplate(context.appId)
269
- return RealtimeMessaging.getChannel(channelName, context.coreServiceMachine)
252
+ return RealtimeMessaging.getChannel(channelName, self)
270
253
  }
271
254
  }
272
255
  ),
273
- subscribeCreateEventMessages: context => {
256
+ subscribeCreateEventMessages: ({ context, self }) => {
274
257
  RealtimeMessaging.subscribe(
275
258
  context.newEventCreationChannel,
276
- context.coreServiceMachine,
259
+ self,
277
260
  'CREATE_EVENT'
278
261
  )
279
262
  RealtimeMessaging.subscribe(
280
263
  context.newEventCreationChannel,
281
- context.coreServiceMachine,
264
+ self,
282
265
  'LOAD_EVENT'
283
266
  )
284
267
  },
285
268
  startNewEventMachine: assign(
286
269
  {
287
- anearEventMachines: (context, event) => {
270
+ anearEventMachines: ({ context, event, self }) => {
288
271
  const eventJSON = JSON.parse(event.data)
289
272
  const anearEvent = new AnearEvent(eventJSON)
290
273
 
@@ -294,16 +277,22 @@ const AnearCoreServiceMachineFunctions = {
294
277
  }
295
278
 
296
279
  const isLoadEvent = event.type === 'LOAD_EVENT'
297
- const service = AnearEventMachine(anearEvent, { ...context, rehydrate: isLoadEvent })
280
+ const actor = AnearEventMachine(anearEvent, {
281
+ ...context,
282
+ // AEM expects a reference back to its supervisor; in v5 we can pass `self`
283
+ // directly rather than storing a "coreServiceMachine" reference in context.
284
+ coreServiceMachine: self,
285
+ rehydrate: isLoadEvent
286
+ })
298
287
 
299
288
  return {
300
289
  ...context.anearEventMachines,
301
- [anearEvent.id]: service.start()
290
+ [anearEvent.id]: actor.start()
302
291
  }
303
292
  }
304
293
  }
305
294
  ),
306
- cleanupEventMachine: assign((context, event) => {
295
+ cleanupEventMachine: assign(({ context, event }) => {
307
296
  const { [event.eventId]: dropped, ...remaining } = context.anearEventMachines
308
297
  logger.debug(`ACSM ${event.eventId} is ${dropped ? "done → cleaning up" : "NOT FOUND"}`)
309
298
 
@@ -313,16 +302,16 @@ const AnearCoreServiceMachineFunctions = {
313
302
  })
314
303
  },
315
304
  delays: {
316
- retry_with_backoff_delay: (context, event) => context.retryDelay
305
+ retry_with_backoff_delay: ({ context }) => context.retryDelay
317
306
  },
318
307
  guards: {
319
- noImageAssetFilesFound: (_, event) => event.data === null
308
+ noImageAssetFilesFound: ({ event }) => event.output === null
320
309
  }
321
310
  }
322
311
 
323
312
  const AnearCoreServiceMachine = (appEventMachineFactory, appParticipantMachineFactory = null) => {
324
313
  const appId = process.env.ANEARAPP_APP_ID
325
- const expandedConfig = {predictableActionArguments: true, ...AnearCoreServiceMachineConfig(appId)}
314
+ const machineConfig = AnearCoreServiceMachineConfig(appId)
326
315
 
327
316
  const anearCoreServiceMachineContext = AnearCoreServiceMachineContext(
328
317
  appId,
@@ -330,16 +319,9 @@ const AnearCoreServiceMachine = (appEventMachineFactory, appParticipantMachineFa
330
319
  appParticipantMachineFactory
331
320
  )
332
321
 
333
- const coreServiceMachine = createMachine(
334
- expandedConfig,
335
- AnearCoreServiceMachineFunctions
336
- ).withContext(anearCoreServiceMachineContext)
337
-
338
- const coreServiceMachineStarted = interpret(coreServiceMachine).start()
339
-
340
- coreServiceMachineStarted.send('UPDATE_CONTEXT', { coreServiceMachine: coreServiceMachineStarted })
322
+ const coreServiceMachine = createMachine(machineConfig, AnearCoreServiceMachineFunctions)
341
323
 
342
- return coreServiceMachineStarted
324
+ return createActor(coreServiceMachine, { input: anearCoreServiceMachineContext }).start()
343
325
  }
344
326
 
345
327
  module.exports = AnearCoreServiceMachine