anear-js-api 0.4.20 → 0.4.22

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.
Files changed (44) hide show
  1. package/.env.test +8 -0
  2. package/__mocks__/api/AnearApi.js +30 -0
  3. package/__mocks__/utils/RealtimeMessaging.js +17 -0
  4. package/jest.config.js +9 -0
  5. package/lib/AnearService.js +12 -0
  6. package/lib/api/AnearApi.js +41 -21
  7. package/lib/api/ApiService.js +1 -1
  8. package/lib/index.js +4 -8
  9. package/lib/models/AnearEvent.js +35 -254
  10. package/lib/models/AnearParticipant.js +19 -85
  11. package/lib/models/JsonApiResource.js +0 -49
  12. package/lib/state_machines/AnearCoreServiceMachine.js +264 -0
  13. package/lib/state_machines/AnearEventMachine.js +940 -0
  14. package/lib/state_machines/AnearParticipantMachine.js +286 -0
  15. package/lib/utils/AssetFileCollector.js +63 -0
  16. package/lib/utils/Constants.js +10 -0
  17. package/lib/utils/CssProcessing.js +65 -0
  18. package/lib/utils/CssUploader.js +82 -0
  19. package/lib/utils/ImageAssetsUploader.js +101 -0
  20. package/lib/utils/MetaProcessing.js +147 -0
  21. package/lib/utils/MetaViewPathParticipantProcessing.js +53 -0
  22. package/lib/utils/Participants.js +0 -13
  23. package/lib/utils/PugHelpers.js +14 -0
  24. package/lib/utils/PugLoader.js +48 -0
  25. package/lib/utils/RealtimeMessaging.js +159 -0
  26. package/package.json +7 -3
  27. package/tests/AnearEvent.test.js +65 -349
  28. package/tests/AnearParticipant.test.js +52 -51
  29. package/tests/AnearService.test.js +11 -0
  30. package/tests/PugLoader.test.js +24 -0
  31. package/tests/fixtures/AnearAppFixture.js +28 -0
  32. package/tests/fixtures/AnearEventFixture.js +1 -1
  33. package/tests/fixtures/test_pug_templates/subdir/template_3.pug +1 -0
  34. package/tests/fixtures/test_pug_templates/template_1.pug +1 -0
  35. package/tests/fixtures/test_pug_templates/template_2.pug +1 -0
  36. package/lib/api/__mocks__/AnearApi.js +0 -17
  37. package/lib/messaging/AnearMessaging.js +0 -645
  38. package/lib/messaging/__mocks__/AnearMessaging.js +0 -22
  39. package/lib/utils/AnearXstate.js +0 -86
  40. package/lib/utils/ParticipantTimer.js +0 -117
  41. package/lib/utils/Persist.js +0 -52
  42. package/tests/ParticipantTimer.test.js +0 -66
  43. package/tests/Participants.test.js +0 -234
  44. package/tests/Persist.test.js +0 -102
package/.env.test ADDED
@@ -0,0 +1,8 @@
1
+ # connects with the machtest developer account
2
+ ANEARAPP_API_KEY=a197d1e4da2bfd4bb5d85ca82553f14f4fad2c5c650c25f49c8514eb80ef4fe1
3
+ ANEARAPP_API_VERSION=v1
4
+ ANEARAPP_APP_ID=e64a78d8-48f5-44f7-8b89-af2d6977a8c2
5
+ ANEARAPP_LOGGER_FILE=logs/development.log
6
+ ANEARAPP_LOGGER_LEVEL=debug
7
+ ANEARAPP_ABLY_LOG_LEVEL=2
8
+ ANEARAPP_PRESENCE_TIMEOUT_SECONDS=3600
@@ -0,0 +1,30 @@
1
+ "use strict"
2
+
3
+ const logger = require('../../lib/utils/Logger')
4
+ const AnearAppData = require('../../tests/fixtures/AnearAppFixture')
5
+
6
+ class AnearApi {
7
+ constructor(apiKey, apiVersion) {
8
+ this.apiKey = apiKey
9
+ this.apiVersion = apiVersion
10
+ }
11
+
12
+ getAccount() {
13
+ }
14
+
15
+ getApp() {
16
+ return Promise.resolve(AnearAppData)
17
+ }
18
+
19
+ async transitionEvent(eventId, eventName='next') {
20
+ return {state: "announce"}
21
+ }
22
+
23
+ getEventParticipant(participantId, geoLocation) {
24
+ return Promise.resolve(new AnearParticipant({}, {}))
25
+ }
26
+ }
27
+
28
+ const anearApiInstance = new AnearApi()
29
+
30
+ module.exports = anearApiInstance
@@ -0,0 +1,17 @@
1
+ "use strict"
2
+
3
+ const logger = require('../../lib/utils/Logger')
4
+
5
+ class RealtimeMessaging {
6
+ initRealtime(appId, appMachine) {
7
+ return this
8
+ }
9
+
10
+ getChannel(channelName) {
11
+ return {}
12
+ }
13
+ }
14
+
15
+ const realtimeMessagingInstance = new RealtimeMessaging()
16
+
17
+ module.exports = realtimeMessagingInstance
package/jest.config.js ADDED
@@ -0,0 +1,9 @@
1
+ require('dotenv').config({ path: '.env.test' })
2
+ module.exports = {
3
+ // Jest configuration options...
4
+ moduleNameMapper: {
5
+ 'AnearApi': '<rootDir>/__mocks__/api/AnearApi.js',
6
+ 'RealtimeMessaging': '<rootDir>/__mocks__/utils/RealtimeMessaging.js'
7
+ }
8
+ }
9
+
@@ -0,0 +1,12 @@
1
+ const AnearCoreServiceMachine = require('./state_machines/AnearCoreServiceMachine')
2
+
3
+ const AnearService = (appEventMachineFactory, appParticipantMachineFactory = null) => {
4
+ //
5
+ // developer provides appEventMachineFactory:
6
+ //
7
+ // appEventMachineFactory = anearEvent => { returns XState Machine) }
8
+ // optional appParticipantMachineFactory = anearParticipant => { returns XState Machine) }
9
+ //
10
+ AnearCoreServiceMachine(appEventMachineFactory, appParticipantMachineFactory)
11
+ }
12
+ module.exports = AnearService
@@ -3,62 +3,82 @@ const logger = require('../utils/Logger')
3
3
  const ApiService = require('./ApiService')
4
4
 
5
5
  class AnearApi extends ApiService {
6
-
7
6
  constructor(apiKey, apiVersion) {
8
7
  super(apiKey, apiVersion)
9
8
  }
10
9
 
11
10
  getAccount() {
12
11
  logger.debug("API: GET /accounts")
13
-
14
12
  return this.get("accounts")
15
13
  }
16
14
 
17
15
  getEvent(eventId) {
18
16
  logger.debug(`API: GET event ${eventId}`)
19
-
20
- return this.get("events", {id: eventId})
17
+ return this.get("events", { id: eventId })
21
18
  }
22
19
 
23
20
  getAppZones(appId) {
24
21
  logger.debug(`API: GET app_zones ${appId}`)
25
-
26
- return this.get("app_zones", {id: appId})
22
+ return this.get("app_zones", { id: appId })
27
23
  }
28
24
 
29
25
  getApp(appId) {
30
26
  logger.debug(`API: GET app ${appId}`)
31
-
32
- return this.get("apps", {id: appId})
27
+ return this.get("apps", { id: appId })
33
28
  }
34
29
 
35
30
  getZoneEvents(zoneId) {
36
31
  logger.debug(`API: GET zone_events ${zoneId}`)
37
-
38
- return this.get("zone_events", {id: zoneId})
32
+ return this.get("zone_events", { id: zoneId })
39
33
  }
40
34
 
41
- async transitionEvent(eventId, eventName='next') {
42
- logger.debug(`API: POST transition event ${eventName}`)
43
-
44
- const relationships = {event: eventId}
45
- const json = await this.post("transitions", {event_name: eventName}, relationships)
35
+ async transitionEvent(eventId, newStateName) {
36
+ logger.debug(`API: POST transition event ${newStateName}`)
37
+ const relationships = { event: eventId }
38
+ const json = await this.post("transitions", { state: newStateName }, relationships)
46
39
  const attrs = json.data.attributes
47
- logger.debug(`API: newState is ${attrs.state}`)
48
40
  return attrs
49
41
  }
50
42
 
51
- getEventParticipantJson(participantId, geoLocation) {
43
+ getEventParticipantJson(participantId) {
52
44
  logger.debug(`API: GET event_participant ${participantId}`)
53
-
54
- return this.get("event_participants", {id: participantId})
45
+ return this.get("event_participants", { id: participantId })
55
46
  }
56
47
 
57
48
  getUser(userId) {
58
49
  logger.debug(`API: GET user ${userId}`)
50
+ return this.get("users", { id: userId })
51
+ }
59
52
 
60
- return this.get("users", {id: userId})
53
+ async getAppCssUploadUrl(appId, contentHash, fileSuffix = ".min.css") {
54
+ logger.debug(`API: POST get app styles upload URL for app ${appId} with suffix ${fileSuffix} and content_hash ${contentHash}`)
55
+ const postAttrs = { content_hash: contentHash, suffix: fileSuffix }
56
+ const relationships = { app: appId }
57
+ const json = await this.post("app_styles", postAttrs, relationships)
58
+ const attrs = json.data.attributes
59
+ logger.debug('getAppCssUploadUrl response:', attrs)
60
+ return attrs
61
+ }
62
+
63
+ async getAppImageAssetsUploadUrls(appId, files) {
64
+ logger.debug(`API: POST get image assets upload URLs for app ${appId}`)
65
+ const postAttrs = { files }
66
+ const relationships = { app: appId }
67
+ const json = await this.post("app_image_assets", postAttrs, relationships)
68
+ const attrs = json.data.attributes
69
+ logger.debug('getAppImageAssetsUploadUrls response:', attrs)
70
+ return attrs
61
71
  }
62
72
  }
63
73
 
64
- module.exports = AnearApi
74
+ // Instantiate and export the API immediately
75
+ const apiKey = process.env.ANEARAPP_API_KEY
76
+ const apiVersion = process.env.ANEARAPP_API_VERSION
77
+
78
+ if (!apiKey || !apiVersion) {
79
+ throw new Error("API_KEY and API_VERSION must be defined in environment variables")
80
+ }
81
+
82
+ const anearApiInstance = new AnearApi(apiKey, apiVersion)
83
+
84
+ module.exports = anearApiInstance
@@ -4,7 +4,7 @@ const ErrorResponse = require('./ErrorResponse')
4
4
  const qs = require('qs')
5
5
  const fetch = require('cross-fetch')
6
6
 
7
- const DeveloperApiURL = 'https://api.anearapp.com/developer/'
7
+ const DeveloperApiURL = 'https://api.anear.me/developer/'
8
8
 
9
9
  class ApiService {
10
10
  constructor(apiKey, apiVersion) {
package/lib/index.js CHANGED
@@ -1,14 +1,12 @@
1
1
  'use strict';
2
2
 
3
+ const logger = require('./utils/Logger')
3
4
  const JsonApiResource = require('./models/JsonApiResource')
4
5
  const JsonApiArrayResource = require('./models/JsonApiArrayResource')
6
+ const AnearService = require('./AnearService')
5
7
  const AnearEvent = require('./models/AnearEvent')
6
- const logger = require('./utils/Logger')
7
8
  const AnearParticipant = require('./models/AnearParticipant')
8
- const AnearMessaging = require('./messaging/AnearMessaging')
9
- const AnearApiService = require('./api/ApiService')
10
9
  const Fixtures = require('../tests/fixtures')
11
- const MockMessaging = require('./messaging/__mocks__/AnearMessaging')
12
10
 
13
11
  module.exports = {
14
12
  JsonApiResource,
@@ -16,9 +14,7 @@ module.exports = {
16
14
  AnearEvent,
17
15
  logger,
18
16
  AnearParticipant,
19
- AnearMessaging,
20
- AnearApiService,
21
- Fixtures,
22
- MockMessaging,
17
+ AnearService,
18
+ Fixtures
23
19
  }
24
20
 
@@ -1,65 +1,57 @@
1
1
  "use strict"
2
-
3
- const { Mutex } = require('async-mutex')
4
-
5
- const AnearXstate = require('../utils/AnearXstate')
2
+ const logger = require('../utils/Logger')
6
3
 
7
4
  const JsonApiResource = require('./JsonApiResource')
8
- const Participants = require('../utils/Participants')
9
- const logger = require('../utils/Logger')
10
5
 
11
- const PlayableStates = ['closing', 'closed', 'canceled']
6
+ const UnplayableStates = ['closing', 'closed', 'canceled']
12
7
 
13
8
  class AnearEvent extends JsonApiResource {
14
9
 
15
- constructor(json, messaging) {
10
+ constructor(json) {
16
11
  super(json)
17
12
  this.zone = this.findIncluded(this.relationships.zone)
18
13
  this.app = this.findIncluded(this.zone.relationships.app)
19
- this.messaging = messaging
20
- this.anearStateMachine = this.initStateMachine(json.previousState)
21
- this.participants = new Participants(this, json.participants)
22
- this.mutex = new Mutex()
14
+ this.send = () => {} // until initialized
23
15
  }
24
16
 
25
- toJSON() {
26
- return {
27
- ...super.toJSON(),
28
- participants: this.participants.toJSON(),
29
- context: this.stateMachineContext,
30
- previousState: this.anearStateMachine.currentState
17
+ get userId() {
18
+ return this.relationships.user.data.id
19
+ }
20
+
21
+ get zoneId() {
22
+ return this.relationships.zone.data.id
23
+ }
24
+
25
+ setMachine(service) {
26
+ if (service) {
27
+ this.send = service.send.bind(service)
28
+ } else {
29
+ this.send = () => {}
31
30
  }
32
31
  }
33
32
 
34
- startStateMachine() {
35
- this.anearStateMachine.startService()
33
+ announceEvent() {
34
+ this.send("ANNOUNCE")
36
35
  }
37
36
 
38
- stateMachineConfig() {
39
- // override in subclass with custom Xstate config
40
- return {}
37
+ startEvent() {
38
+ this.send("START")
41
39
  }
42
40
 
43
- stateMachineOptions() {
44
- // override in subclass with custom Xstate options
45
- return {}
41
+ cancelEvent() {
42
+ this.send("CANCEL")
46
43
  }
47
44
 
48
- initStateMachine(previousState) {
49
- return new AnearXstate(
50
- this.stateMachineConfig(),
51
- this.stateMachineOptions(),
52
- previousState,
53
- this.context
54
- )
45
+ closeEvent() {
46
+ this.send("CLOSE")
55
47
  }
56
48
 
57
- get userId() {
58
- return this.relationships.user.data.id
49
+ pauseEvent() {
50
+ this.send("PAUSE")
59
51
  }
60
52
 
61
- get zoneId() {
62
- return this.relationships.zone.data.id
53
+ resumeEvent() {
54
+ this.send("RESUME")
63
55
  }
64
56
 
65
57
  getClonedFromEvent() {
@@ -77,11 +69,6 @@ class AnearEvent extends JsonApiResource {
77
69
  return clonedFrom ? clonedFrom.context : null
78
70
  }
79
71
 
80
- get stateMachineContext() {
81
- // active XState context
82
- return this.anearStateMachine.context
83
- }
84
-
85
72
  get eventState() {
86
73
  return this.attributes.state
87
74
  }
@@ -109,243 +96,37 @@ class AnearEvent extends JsonApiResource {
109
96
  return this.attributes.flags.includes(flagName)
110
97
  }
111
98
 
112
- get css() {
113
- // override with a String of CSS to be sent with each publishEvent*Message
114
- // this will be compressed out of the initial publishEvent*Message
115
- // and will only be received by the client when the CSS changes.
116
- return null;
117
- }
118
-
119
99
  allowsSpectators() {
120
100
  return !this.hasFlag("no_spectators")
121
101
  }
122
102
 
123
- spectatorCount() {
124
- return this.messaging.getSpectatorCount(this)
125
- }
126
-
127
- isParticipantEventCreator(RParticipant) {
103
+ isParticipantEventCreator(participant) {
128
104
  return participant.userId === this.userId
129
105
  }
130
106
 
131
- publishEventParticipantsMessage(message, timeoutMsecs=0) {
132
- return this.messaging.publishEventParticipantsMessage(
133
- this,
134
- this.participants.active(),
135
- this.css,
136
- message,
137
- timeoutMsecs
138
- )
139
- }
140
-
141
- async runExclusive(name, callback) {
142
- logger.debug(`waiting for ${name} mutex`)
143
-
144
- await this.mutex.runExclusive(
145
- async () => {
146
- logger.debug(`mutex ${name} locked!`)
147
- await callback()
148
- }
149
- )
150
- logger.debug(`mutex ${name} released!`)
151
- }
152
-
153
- publishEventSpectatorsMessage(message, timeoutMsecs = 0) {
154
- return this.messaging.publishEventSpectatorsMessage(this, this.css, message, timeoutMsecs, timeoutMsecs)
155
- }
156
-
157
- publishEventPrivateMessage(participant, message, timeoutMsecs=0) {
158
- return this.messaging.publishEventPrivateMessage(
159
- this,
160
- participant,
161
- this.css,
162
- message,
163
- timeoutMsecs
164
- )
165
- }
166
-
167
- publishEventTransitionMessage(newState) {
168
- return this.messaging.publishEventTransitionMessage(
169
- this,
170
- newState
171
- )
172
- }
173
-
174
- async participantEnter(participant) {
175
- // Called each time a participant ENTERs (attaches to) the Event's Action Channel.
176
- // This could be when joining an event for the first time, rejoining, or after a browser
177
- // refresh/reconnect. If the participant exists in Storage and in this.participants,
178
- // its probably due to a browser refresh, and we call Refresh. Else, its either a brand
179
- // new participant or an anearEvent recovery after crash, and so we invoke new participant enter
180
- //
181
- if (participant.exists()) { // persisted in storage?
182
- if (this.participants.exists(participant)) {
183
- logger.debug(`AnearEvent: participant ${participant.id} exists. Refreshing...`)
184
-
185
- // Likely here due to participant browser refresh
186
- // get the existing participant, turn off his timer and send refresh event to StateMachine
187
- const existingParticipant = this.participants.get(participant)
188
- existingParticipant.interruptTimer()
189
- this.anearStateMachine.sendRefreshEvent({participant: existingParticipant})
190
- await existingParticipant.update()
191
- } else {
192
- // Likely here due to prior AnearEvent error and this is a event reload and restart
193
- // add the participants record back into Participants and update persisted copy
194
- this.participants.add(participant)
195
- this.anearStateMachine.sendJoinEvent({ participant })
196
- await participant.update()
197
- }
198
- } else {
199
- // New Participant first-time join this AnearEvent
200
- this.participants.add(participant) // add the participants record
201
- this.anearStateMachine.sendJoinEvent({ participant })
202
- await participant.persist()
203
- }
204
- }
205
-
206
- participantExit(participant) {
207
- // this informs the state machine that the participant has exited the event
208
- // and removes that participant completely
209
- this.anearStateMachine.sendParticipantExitEvent({ participant })
210
- return this.participantPurge(participant)
211
- }
212
-
213
- spectatorView(userId) {
214
- this.anearStateMachine.sendSpectatorViewEvent({userId})
215
- }
216
-
217
- participantPurge(participant) {
218
- this.participants.purge(participant)
219
- return participant.remove()
220
- }
221
-
222
- participantAction(participant, actionEventName, actionPayload) {
223
- this.anearStateMachine.sendActionEvent(actionEventName, {participant, payload: actionPayload})
224
- }
225
-
226
- participantTimedOut(participant) {
227
- this.anearStateMachine.sendTimeoutEvent({ participant })
228
- }
229
-
230
- async transitionEvent(eventName='next') {
231
- //
232
- // Allows the app/game to transition the remote AnearEvent which can un/hide and event
233
- // and change its discoverability by mobile web users. Apps can also determine when
234
- // and how many mobile-app users can join and event.
235
- //
236
- // 1. send the transition event to the Anear API
237
- // 2. get back the event newState from Anear API response
238
- // 3. publish the new Event State to all subscribers (e.g. participants)
239
- //
240
- logger.debug(`AnearEvent: transitionEvent(${eventName})`)
241
-
242
- const responseAttributes = await this.messaging.api.transitionEvent(this.id, eventName)
243
- const newState = responseAttributes.state
244
- this.attributes.state = newState
245
- await this.messaging.publishEventTransitionMessage(this, newState)
246
- }
247
-
248
107
  isPlayable() {
249
- return !PlayableStates.includes(this.eventState)
250
- }
251
-
252
- async transitionToAnnounce() {
253
- if (this.eventState === 'created') await this.transitionEvent('announce')
108
+ return !UnplayableStates.includes(this.eventState)
254
109
  }
255
110
 
256
- async transitionNextNext() {
257
- await this.transitionEvent('next')
258
- await this.transitionEvent('next')
259
- }
260
-
261
- transitionLive() {
262
- return this.transitionNextNext()
263
- }
264
-
265
- transitionClosed() {
266
- return this.transitionNextNext()
267
- }
268
-
269
- transitionCanceled() {
270
- return this.transitionEvent('cancel')
271
- }
272
-
273
- closeOutParticipants() {
274
- // returns a Promise
275
- // upon exiting the event, this will clean up any participants remaining
276
- // by closing out their messaging channels and purging them
277
- return Promise.all(
278
- this.participants.all.map(
279
- p => {
280
- return this.messaging.closeParticipant(
281
- this,
282
- p.id,
283
- async (anearEvent, participant) => {
284
- await anearEvent.participantPurge(participant)
285
- }
286
- )
287
- }
288
- )
289
- )
290
- }
291
-
292
- purgeParticipants() {
293
- // returns a Promise
294
- // remove participants and host from Participants class and from Storage
295
- const all = this.participants.all
296
- if (this.participants.host) all.push(this.participants.host)
297
-
298
- return Promise.all(
299
- all.map(p => this.participantPurge(p))
300
- )
301
- }
302
-
303
- resetParticipantTimers() {
304
- // turns off any active AnearParticipant timer
305
- this.participants.resetAllTimers()
306
- }
307
-
308
- destroyParticipantTimers() {
309
- // turns off any active and removes all other AnearParticipant timer
310
- this.participants.destroyAllTimers()
311
- }
312
-
313
- eventChannelName () {
111
+ get eventChannelName() {
314
112
  return this.getChannelName('event')
315
113
  }
316
114
 
317
- participantsChannelName () {
115
+ get participantsChannelName () {
318
116
  return this.getChannelName('participants')
319
117
  }
320
118
 
321
- actionsChannelName () {
119
+ get actionsChannelName () {
322
120
  return this.getChannelName('actions')
323
121
  }
324
122
 
325
- spectatorsChannelName () {
123
+ get spectatorsChannelName () {
326
124
  return this.getChannelName('spectators')
327
125
  }
328
126
 
329
127
  getChannelName (key) {
330
128
  return this.attributes[`${key}-channel-name`]
331
129
  }
332
-
333
- async closeEvent() {
334
- await this.closeOutParticipants()
335
- await this.closeMessaging()
336
- }
337
-
338
- closeMessaging () {
339
- return this.messaging.detachAll(this.id)
340
- }
341
-
342
- logDebugMessage(...args) {
343
- logger.debug(...args)
344
- }
345
-
346
- logError(context, event) {
347
- logger.error("XState: ERROR: ", event.data)
348
- }
349
130
  }
350
131
 
351
132
  module.exports = AnearEvent