anear-js-api 1.0.1 → 1.1.2
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 +6 -0
- package/lib/AnearService.js +1 -1
- package/lib/api/AnearApi.js +6 -5
- package/lib/api/ApiService.js +70 -3
- package/lib/models/AnearEvent.js +5 -2
- package/lib/state_machines/AnearEventMachine.js +2 -2
- package/lib/state_machines/AnearParticipantMachine.js +4 -1
- package/package.json +1 -1
- package/tests/AnearEvent.test.js +8 -13
- package/tests/AnearParticipant.test.js +2 -2
- package/tests/AnearService.test.js +2 -1
- package/tests/RealtimeMessaging.test.js +6 -6
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
|
|
package/lib/AnearService.js
CHANGED
|
@@ -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
|
package/lib/api/AnearApi.js
CHANGED
|
@@ -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("
|
|
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
|
package/lib/api/ApiService.js
CHANGED
|
@@ -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}`)
|
package/lib/models/AnearEvent.js
CHANGED
|
@@ -158,12 +158,15 @@ class AnearEvent extends JsonApiResource {
|
|
|
158
158
|
return this.attributes.flags.includes(flagName)
|
|
159
159
|
}
|
|
160
160
|
|
|
161
|
-
|
|
161
|
+
spectatorsAllowed() {
|
|
162
162
|
//
|
|
163
163
|
// Canonical spectators decision uses the ANAPI-provided boolean when present.
|
|
164
164
|
// Fallback to legacy negative flag for older payloads.
|
|
165
165
|
//
|
|
166
|
-
|
|
166
|
+
const allowedDasherized = this.attributes['spectators-allowed']
|
|
167
|
+
if (typeof allowedDasherized === 'boolean') return allowedDasherized
|
|
168
|
+
|
|
169
|
+
return !this.hasFlag('no_spectators')
|
|
167
170
|
}
|
|
168
171
|
|
|
169
172
|
isParticipantEventCreator(participant) {
|
|
@@ -1850,8 +1850,8 @@ const AnearEventMachineFunctions = ({
|
|
|
1850
1850
|
eventSupportsSpectators: ({ context }) => {
|
|
1851
1851
|
try {
|
|
1852
1852
|
// True only when ANAPI has indicated spectators are allowed for this event.
|
|
1853
|
-
// JSAPI AnearEvent#
|
|
1854
|
-
return !!(context.anearEvent && context.anearEvent.
|
|
1853
|
+
// JSAPI AnearEvent#spectatorsAllowed reads the canonical spectators-allowed flag.
|
|
1854
|
+
return !!(context.anearEvent && context.anearEvent.spectatorsAllowed && context.anearEvent.spectatorsAllowed())
|
|
1855
1855
|
} catch (_e) {
|
|
1856
1856
|
return false
|
|
1857
1857
|
}
|
|
@@ -234,7 +234,10 @@ const AnearParticipantMachineConfig = participantId => ({
|
|
|
234
234
|
input: ({ context, event }) => ({ context, event }),
|
|
235
235
|
onDone: [
|
|
236
236
|
{ guard: 'hasActionTimeout', actions: 'updateActionTimeout', target: 'waitParticipantResponse' },
|
|
237
|
-
|
|
237
|
+
// If this display does not configure a timeout, explicitly clear any
|
|
238
|
+
// previously-running action timeout. Otherwise, a stale actionTimeoutStart
|
|
239
|
+
// from an older prompt can be "resumed" later and immediately fire.
|
|
240
|
+
{ actions: 'nullActionTimeout', target: 'idle' }
|
|
238
241
|
],
|
|
239
242
|
onError: {
|
|
240
243
|
target: '#error'
|
package/package.json
CHANGED
package/tests/AnearEvent.test.js
CHANGED
|
@@ -29,7 +29,7 @@ describe('AnearEvent', () => {
|
|
|
29
29
|
const mockService = { send: jest.fn() }
|
|
30
30
|
anearEvent.setMachine(mockService)
|
|
31
31
|
anearEvent.announceEvent()
|
|
32
|
-
expect(mockService.send).toHaveBeenCalledWith('ANNOUNCE')
|
|
32
|
+
expect(mockService.send).toHaveBeenCalledWith({ type: 'ANNOUNCE' })
|
|
33
33
|
})
|
|
34
34
|
|
|
35
35
|
test('can be hosted', () => {
|
|
@@ -46,8 +46,8 @@ describe('AnearEvent', () => {
|
|
|
46
46
|
expect(anearEvent.hasFlag('no_spectators')).toBe(true)
|
|
47
47
|
})
|
|
48
48
|
|
|
49
|
-
test('
|
|
50
|
-
expect(anearEvent.
|
|
49
|
+
test('spectatorsAllowed is false when no_spectators flag is present', () => {
|
|
50
|
+
expect(anearEvent.spectatorsAllowed()).toBe(false)
|
|
51
51
|
})
|
|
52
52
|
|
|
53
53
|
test('isPlayable returns true for playable states', () => {
|
|
@@ -94,32 +94,27 @@ describe('AnearEvent', () => {
|
|
|
94
94
|
|
|
95
95
|
test('announceEvent sends ANNOUNCE', () => {
|
|
96
96
|
anearEvent.announceEvent()
|
|
97
|
-
expect(mockService.send).toHaveBeenCalledWith('ANNOUNCE')
|
|
97
|
+
expect(mockService.send).toHaveBeenCalledWith({ type: 'ANNOUNCE' })
|
|
98
98
|
})
|
|
99
99
|
|
|
100
100
|
test('startEvent sends START', () => {
|
|
101
101
|
anearEvent.startEvent()
|
|
102
|
-
expect(mockService.send).toHaveBeenCalledWith('START')
|
|
102
|
+
expect(mockService.send).toHaveBeenCalledWith({ type: 'START' })
|
|
103
103
|
})
|
|
104
104
|
|
|
105
105
|
test('cancelEvent sends CANCEL', () => {
|
|
106
106
|
anearEvent.cancelEvent()
|
|
107
|
-
expect(mockService.send).toHaveBeenCalledWith('CANCEL')
|
|
107
|
+
expect(mockService.send).toHaveBeenCalledWith({ type: 'CANCEL' })
|
|
108
108
|
})
|
|
109
109
|
|
|
110
110
|
test('closeEvent sends CLOSE', () => {
|
|
111
111
|
anearEvent.closeEvent()
|
|
112
|
-
expect(mockService.send).toHaveBeenCalledWith('CLOSE')
|
|
112
|
+
expect(mockService.send).toHaveBeenCalledWith({ type: 'CLOSE' })
|
|
113
113
|
})
|
|
114
114
|
|
|
115
115
|
test('pauseEvent sends PAUSE', () => {
|
|
116
116
|
anearEvent.pauseEvent()
|
|
117
|
-
expect(mockService.send).toHaveBeenCalledWith('PAUSE')
|
|
118
|
-
})
|
|
119
|
-
|
|
120
|
-
test('resumeEvent sends RESUME', () => {
|
|
121
|
-
anearEvent.resumeEvent()
|
|
122
|
-
expect(mockService.send).toHaveBeenCalledWith('RESUME')
|
|
117
|
+
expect(mockService.send).toHaveBeenCalledWith({ type: 'PAUSE', appmContext: { context: undefined, resumeEvent: { type: 'RESUME' } } })
|
|
123
118
|
})
|
|
124
119
|
})
|
|
125
120
|
})
|
|
@@ -43,12 +43,12 @@ describe('AnearParticipant', () => {
|
|
|
43
43
|
})
|
|
44
44
|
|
|
45
45
|
test('isHost returns false for participants', () => {
|
|
46
|
-
expect(participant.isHost
|
|
46
|
+
expect(participant.isHost).toBe(false)
|
|
47
47
|
})
|
|
48
48
|
|
|
49
49
|
test('isHost returns true for hosts', () => {
|
|
50
50
|
participant.attributes['user-type'] = 'host'
|
|
51
|
-
expect(participant.isHost
|
|
51
|
+
expect(participant.isHost).toBe(true)
|
|
52
52
|
})
|
|
53
53
|
|
|
54
54
|
test('eventId returns the correct event ID', () => {
|
|
@@ -12,5 +12,6 @@ const MockParticipantClass = class ParticipantMachine {}
|
|
|
12
12
|
test('test happy path', () => {
|
|
13
13
|
const mockEventMachineFactory = (anearEvent) => new MockAppClass(anearEvent)
|
|
14
14
|
const mockParticipantMachineFactory = (anearParticipant) => new MockParticipantClass(anearParticipant)
|
|
15
|
-
AnearService(mockEventMachineFactory, mockParticipantMachineFactory)
|
|
15
|
+
const service = AnearService(mockEventMachineFactory, mockParticipantMachineFactory)
|
|
16
|
+
service.stop()
|
|
16
17
|
})
|
|
@@ -74,10 +74,10 @@ describe('RealtimeMessaging', () => {
|
|
|
74
74
|
const stateChangeCallback = mockRealtimeInstance.connection.on.mock.calls[0][0]
|
|
75
75
|
|
|
76
76
|
stateChangeCallback({ current: 'connected' })
|
|
77
|
-
expect(mockActor.send).toHaveBeenCalledWith('CONNECTED')
|
|
77
|
+
expect(mockActor.send).toHaveBeenCalledWith({ type: 'CONNECTED' })
|
|
78
78
|
|
|
79
79
|
stateChangeCallback({ current: 'suspended' })
|
|
80
|
-
expect(mockActor.send).toHaveBeenCalledWith('SUSPENDED')
|
|
80
|
+
expect(mockActor.send).toHaveBeenCalledWith({ type: 'SUSPENDED' })
|
|
81
81
|
})
|
|
82
82
|
})
|
|
83
83
|
|
|
@@ -105,10 +105,10 @@ describe('RealtimeMessaging', () => {
|
|
|
105
105
|
const stateChangeCallback = mockChannel.on.mock.calls[0][1]
|
|
106
106
|
|
|
107
107
|
stateChangeCallback({ current: 'attached' })
|
|
108
|
-
expect(mockActor.send).toHaveBeenCalledWith('ATTACHED', {
|
|
108
|
+
expect(mockActor.send).toHaveBeenCalledWith({ type: 'ATTACHED', data: { channelName: 'mock-channel' } })
|
|
109
109
|
|
|
110
110
|
stateChangeCallback({ current: 'failed' })
|
|
111
|
-
expect(mockActor.send).toHaveBeenCalledWith('FAILED', {
|
|
111
|
+
expect(mockActor.send).toHaveBeenCalledWith({ type: 'FAILED', data: { channelName: 'mock-channel' } })
|
|
112
112
|
})
|
|
113
113
|
})
|
|
114
114
|
|
|
@@ -132,7 +132,7 @@ describe('RealtimeMessaging', () => {
|
|
|
132
132
|
const message = { data: { id: 'user-1' } }
|
|
133
133
|
presenceCallback(message)
|
|
134
134
|
|
|
135
|
-
expect(mockActor.send).toHaveBeenCalledWith('PARTICIPANT_ENTER',
|
|
135
|
+
expect(mockActor.send).toHaveBeenCalledWith({ type: 'PARTICIPANT_ENTER', data: message.data })
|
|
136
136
|
})
|
|
137
137
|
})
|
|
138
138
|
|
|
@@ -193,7 +193,7 @@ describe('RealtimeMessaging', () => {
|
|
|
193
193
|
const messageCallback = mockChannel.subscribe.mock.calls[0][1]
|
|
194
194
|
const message = { name: 'incoming-event', data: { info: 'abc' } }
|
|
195
195
|
messageCallback(message)
|
|
196
|
-
expect(mockActor.send).toHaveBeenCalledWith(message.name,
|
|
196
|
+
expect(mockActor.send).toHaveBeenCalledWith({ type: message.name, data: message.data })
|
|
197
197
|
})
|
|
198
198
|
})
|
|
199
199
|
|