anear-js-api 0.3.32 → 0.3.34
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.
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
"use strict"
|
|
2
2
|
const Ably = require('ably/promises')
|
|
3
3
|
const AnearApi = require('../api/AnearApi')
|
|
4
|
-
const ParticipantTimer = require('../utils/ParticipantTimer')
|
|
5
4
|
const logger = require('../utils/Logger')
|
|
6
|
-
const { Mutex } = require('async-mutex')
|
|
7
5
|
|
|
8
6
|
const AppId = process.env.ANEARAPP_APP_ID
|
|
9
7
|
|
|
@@ -29,7 +27,6 @@ class AnearMessaging {
|
|
|
29
27
|
this.api = new AnearApi()
|
|
30
28
|
this.AnearEventClass = AnearEventClass
|
|
31
29
|
this.AnearParticipantClass = AnearParticipantClass
|
|
32
|
-
this.mutex = new Mutex()
|
|
33
30
|
this.anearEvents = {}
|
|
34
31
|
|
|
35
32
|
const baseUrl = this.api.api_base_url
|
|
@@ -53,7 +50,6 @@ class AnearMessaging {
|
|
|
53
50
|
}
|
|
54
51
|
|
|
55
52
|
this.eventChannels = {}
|
|
56
|
-
this.participantTimers = {}
|
|
57
53
|
|
|
58
54
|
this.initRealtime(clientOptions)
|
|
59
55
|
}
|
|
@@ -85,75 +81,6 @@ class AnearMessaging {
|
|
|
85
81
|
return this.realtime.channels.get(channelName, channelParams)
|
|
86
82
|
}
|
|
87
83
|
|
|
88
|
-
ensureParticipantTimer(anearEvent, participant, timeoutMsecs) {
|
|
89
|
-
// this is called when a new timer is being started for a privateMessage
|
|
90
|
-
// sent to a participant, or public message to all participants
|
|
91
|
-
// If the timer already exists and is paused, it is resumed with the timeRemaining
|
|
92
|
-
// [a starter function, timeRemaining] is returned
|
|
93
|
-
let timeRemaining = timeoutMsecs
|
|
94
|
-
let timerStarter = () => {}
|
|
95
|
-
|
|
96
|
-
if (timeoutMsecs > 0) {
|
|
97
|
-
const timer = this.participantTimers[participant.id] || this.createTimer(anearEvent, participant, timeoutMsecs)
|
|
98
|
-
|
|
99
|
-
if (timer.isRunning) {
|
|
100
|
-
timeRemaining = timer.interrupt()
|
|
101
|
-
} else {
|
|
102
|
-
timerStarter = () => timer.start(timeoutMsecs)
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
logger.debug(`ensureParticipantTimer(timeRemaining: ${timeRemaining})`)
|
|
106
|
-
|
|
107
|
-
return [timerStarter, timeRemaining]
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
createTimer(anearEvent, participant, timeoutMsecs) {
|
|
111
|
-
const timer = new ParticipantTimer(
|
|
112
|
-
participant.id,
|
|
113
|
-
async () => await this.timerExpired(anearEvent, participant, timeoutMsecs)
|
|
114
|
-
)
|
|
115
|
-
this.participantTimers[participant.id] = timer
|
|
116
|
-
|
|
117
|
-
return timer
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
destroyParticipantTimer(participantId) {
|
|
121
|
-
// participant will not be receiving any more display messages
|
|
122
|
-
// so we close out and delete the ParticipantTimer
|
|
123
|
-
const timer = this.participantTimers[participantId]
|
|
124
|
-
|
|
125
|
-
if (timer) {
|
|
126
|
-
timer.reset()
|
|
127
|
-
delete this.participantTimers[participantId]
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
resetAllParticipantTimers() {
|
|
132
|
-
// turns off all participant timers
|
|
133
|
-
Object.values(this.participantTimers).forEach(timer => timer.reset())
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
interruptParticipantTimer(participantId) {
|
|
137
|
-
const timer = this.participantTimers[participantId]
|
|
138
|
-
|
|
139
|
-
if (timer && timer.isRunning) timer.interrupt()
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
resetParticipantTimer(participantId) {
|
|
143
|
-
// called after participant takes Action before timer expires
|
|
144
|
-
const timer = this.participantTimers[participantId]
|
|
145
|
-
if (timer) timer.reset()
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
async timerExpired(anearEvent, participant, timeoutMsecs) {
|
|
149
|
-
logger.debug(`participant (${anearEvent.id}, ${participant.id}) TIMED OUT after ${timeoutMsecs} msecs`)
|
|
150
|
-
|
|
151
|
-
delete this.participantTimers[participant.id]
|
|
152
|
-
|
|
153
|
-
await anearEvent.participantTimedOut(participant)
|
|
154
|
-
await anearEvent.update()
|
|
155
|
-
}
|
|
156
|
-
|
|
157
84
|
async getAnearEventFromStorage(eventId) {
|
|
158
85
|
return await this.AnearEventClass.getFromStorage(eventId, this.AnearParticipantClass, this)
|
|
159
86
|
}
|
|
@@ -215,7 +142,7 @@ class AnearMessaging {
|
|
|
215
142
|
let loadedEvent = anearEvent
|
|
216
143
|
|
|
217
144
|
if (!eventExists) {
|
|
218
|
-
await
|
|
145
|
+
await anearEvent.runExclusive("createEventCallback", async () => {
|
|
219
146
|
await anearEvent.createdEventCallback()
|
|
220
147
|
await anearEvent.persist()
|
|
221
148
|
// start the state machine before initialiing Realtime Messaging
|
|
@@ -249,6 +176,8 @@ class AnearMessaging {
|
|
|
249
176
|
const anearEvent = new this.AnearEventClass(eventJson, this.AnearParticipantClass, this)
|
|
250
177
|
// This needs work!!
|
|
251
178
|
// loadedEvent = await this.getAnearEventFromStorage(anearEvent.id)
|
|
179
|
+
// NOTE: there should be existing presence state to read from action channel
|
|
180
|
+
// to drive this...not a reload from storage
|
|
252
181
|
// await this.refreshActiveParticipants(loadedEvent)
|
|
253
182
|
// await this.initEventRealtimeMessaging(loadedEvent)
|
|
254
183
|
// loadedEvent.startStateMachine()
|
|
@@ -262,7 +191,7 @@ class AnearMessaging {
|
|
|
262
191
|
|
|
263
192
|
async refreshActiveParticipants(anearEvent) {
|
|
264
193
|
await this.participants.reloadFromStorage(
|
|
265
|
-
p => this.processParticipantEnter(anearEvent, p.id
|
|
194
|
+
p => this.processParticipantEnter(anearEvent, p.id)
|
|
266
195
|
)
|
|
267
196
|
}
|
|
268
197
|
|
|
@@ -399,7 +328,7 @@ class AnearMessaging {
|
|
|
399
328
|
await this.processParticipantEnter(anearEvent, participantId, geoLocation)
|
|
400
329
|
}
|
|
401
330
|
|
|
402
|
-
async processParticipantEnter(anearEvent, participantId, geoLocation) {
|
|
331
|
+
async processParticipantEnter(anearEvent, participantId, geoLocation = null) {
|
|
403
332
|
|
|
404
333
|
logger.debug(`processing Participant Enter for event: ${anearEvent.id}, participant: ${participantId}`)
|
|
405
334
|
//
|
|
@@ -411,15 +340,15 @@ class AnearMessaging {
|
|
|
411
340
|
const participantJson = await this.api.getEventParticipantJson(participantId)
|
|
412
341
|
const participant = new this.AnearParticipantClass(participantJson, anearEvent)
|
|
413
342
|
|
|
414
|
-
participant.geoLocation = geoLocation
|
|
415
|
-
|
|
416
343
|
const persistedAnearParticipant = await this.getAnearParticipantFromStorage(participantId, anearEvent)
|
|
417
344
|
|
|
418
345
|
if (persistedAnearParticipant) {
|
|
419
346
|
participant.context = persistedAnearParticipant.context
|
|
420
347
|
}
|
|
421
348
|
|
|
422
|
-
await
|
|
349
|
+
await anearEvent.runExclusive("participantEnterCallback", async () => {
|
|
350
|
+
participant.geoLocation = geoLocation
|
|
351
|
+
|
|
423
352
|
await this.setupPrivatePublishingChannel(participant)
|
|
424
353
|
await anearEvent.participantEnter(participant)
|
|
425
354
|
await anearEvent.update()
|
|
@@ -446,13 +375,15 @@ class AnearMessaging {
|
|
|
446
375
|
}
|
|
447
376
|
|
|
448
377
|
async participantLeaveMessagingCallback(anearEvent, message) {
|
|
449
|
-
// this can be just a temporary leave (a participant refreshing their browser for example),
|
|
378
|
+
// this can be just a temporary leave (a participant refreshing their browser for example),
|
|
379
|
+
// so pause any participant timers
|
|
450
380
|
const userId = message.clientId
|
|
451
381
|
const participantId = message.data.id
|
|
382
|
+
const participant = anearEvent.participants.getById(participantId)
|
|
452
383
|
|
|
453
|
-
logger.debug(`**** LEAVE PARTICIPANT **** participantLeaveMessagingCallback(participant: ${
|
|
384
|
+
logger.debug(`**** LEAVE PARTICIPANT **** participantLeaveMessagingCallback(participant: ${participant.id})`)
|
|
454
385
|
|
|
455
|
-
|
|
386
|
+
participant.interruptTimer()
|
|
456
387
|
}
|
|
457
388
|
|
|
458
389
|
async closeParticipant(anearEvent, participantId, callback = null) {
|
|
@@ -462,14 +393,14 @@ class AnearMessaging {
|
|
|
462
393
|
// and cleaning out any remaining participants.
|
|
463
394
|
logger.debug(`closeParticipant(${participantId})`)
|
|
464
395
|
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
const participant = await this.getAnearParticipantFromStorage(participantId, anearEvent)
|
|
396
|
+
const participant = anearEvent.participants.getById(participantId)
|
|
468
397
|
|
|
469
398
|
if (participant) {
|
|
399
|
+
participant.destroyTimer()
|
|
400
|
+
|
|
470
401
|
await this.detachParticipantPrivateChannel(anearEvent.id, participant)
|
|
471
402
|
|
|
472
|
-
await
|
|
403
|
+
await anearEvent.runExclusive("closeParticipant", async () => {
|
|
473
404
|
if (callback) {
|
|
474
405
|
await callback(anearEvent, participant)
|
|
475
406
|
}
|
|
@@ -501,16 +432,16 @@ class AnearMessaging {
|
|
|
501
432
|
const participantId = message.data.participantId
|
|
502
433
|
const payload = message.data.payload
|
|
503
434
|
|
|
504
|
-
|
|
435
|
+
const participant = anearEvent.participants.getById(participantId)
|
|
436
|
+
|
|
437
|
+
participant.resetTimer() // participant responded in time, reset any running timer
|
|
505
438
|
|
|
506
439
|
logger.debug(`participantActionMessagingCallback(${anearEvent.id}, ${participantId})`)
|
|
507
440
|
|
|
508
441
|
const actionJSON = JSON.parse(payload)
|
|
509
442
|
const [actionEventName, actionPayload] = Object.entries(actionJSON)[0]
|
|
510
443
|
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
await this.runExclusive("participantActionCallback", async () => {
|
|
444
|
+
await anearEvent.runExclusive("participantActionCallback", async () => {
|
|
514
445
|
await anearEvent.participantAction(participant, actionEventName, actionPayload)
|
|
515
446
|
await anearEvent.update()
|
|
516
447
|
await participant.update()
|
|
@@ -540,18 +471,6 @@ class AnearMessaging {
|
|
|
540
471
|
}
|
|
541
472
|
}
|
|
542
473
|
|
|
543
|
-
async runExclusive(name, callback) {
|
|
544
|
-
logger.debug(`waiting for ${name} mutex`)
|
|
545
|
-
|
|
546
|
-
await this.mutex.runExclusive(
|
|
547
|
-
async () => {
|
|
548
|
-
logger.debug(`mutex ${name} locked!`)
|
|
549
|
-
await callback()
|
|
550
|
-
}
|
|
551
|
-
)
|
|
552
|
-
logger.debug(`mutex ${name} released!`)
|
|
553
|
-
}
|
|
554
|
-
|
|
555
474
|
subscribeEventMessages(channel, messageType, callback) {
|
|
556
475
|
channel.subscribe(messageType, callback)
|
|
557
476
|
logger.debug(`subscribed to ${messageType} messages on ${channel.name}`)
|
|
@@ -593,14 +512,14 @@ class AnearMessaging {
|
|
|
593
512
|
)
|
|
594
513
|
}
|
|
595
514
|
|
|
596
|
-
setMultipleParticipantTimers(
|
|
515
|
+
setMultipleParticipantTimers(participants, timeoutMsecs) {
|
|
597
516
|
if (timeoutMsecs === 0) return [() => {}, 0]
|
|
598
517
|
|
|
599
518
|
const participantTimers = []
|
|
600
519
|
|
|
601
520
|
participants.forEach(
|
|
602
521
|
participant => {
|
|
603
|
-
const [startTimer, _timeRemaining] =
|
|
522
|
+
const [startTimer, _timeRemaining] = participant.ensureTimer(timeoutMsecs)
|
|
604
523
|
participantTimers.push(startTimer)
|
|
605
524
|
}
|
|
606
525
|
)
|
|
@@ -612,7 +531,7 @@ class AnearMessaging {
|
|
|
612
531
|
const eventId = anearEvent.id
|
|
613
532
|
const channel = this.eventChannels[eventId].participants
|
|
614
533
|
|
|
615
|
-
const [startTimers, timeRemaining] = this.setMultipleParticipantTimers(
|
|
534
|
+
const [startTimers, timeRemaining] = this.setMultipleParticipantTimers(participants, timeoutMsecs)
|
|
616
535
|
|
|
617
536
|
await this.publishMessage(
|
|
618
537
|
channel,
|
|
@@ -637,7 +556,7 @@ class AnearMessaging {
|
|
|
637
556
|
const channel = this.eventChannels[anearEvent.id].privates[userId]
|
|
638
557
|
if (!channel) throw new Error(`private channel not found. invalid user id ${userId}`)
|
|
639
558
|
|
|
640
|
-
const [startTimer, timeRemaining] =
|
|
559
|
+
const [startTimer, timeRemaining] = participant.ensureTimer(timeoutMsecs)
|
|
641
560
|
|
|
642
561
|
await this.publishMessage(
|
|
643
562
|
channel,
|
package/lib/models/AnearEvent.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
"use strict"
|
|
2
2
|
|
|
3
|
+
const { Mutex } = require('async-mutex')
|
|
4
|
+
|
|
3
5
|
const AnearXstate = require('../utils/AnearXstate')
|
|
4
6
|
const { DefaultConfigFunc, DefaultOptionsFunc } = require('../utils/AnearXstateDefaults')
|
|
5
7
|
|
|
@@ -19,6 +21,7 @@ class AnearEvent extends JsonApiResource {
|
|
|
19
21
|
this.messaging = messaging
|
|
20
22
|
this.anearStateMachine = this.initStateMachine(json.previousState)
|
|
21
23
|
this.participants = new Participants(this, json.participants)
|
|
24
|
+
this.mutex = new Mutex()
|
|
22
25
|
}
|
|
23
26
|
|
|
24
27
|
toJSON() {
|
|
@@ -177,6 +180,18 @@ class AnearEvent extends JsonApiResource {
|
|
|
177
180
|
)
|
|
178
181
|
}
|
|
179
182
|
|
|
183
|
+
async runExclusive(name, callback) {
|
|
184
|
+
logger.debug(`waiting for ${name} mutex`)
|
|
185
|
+
|
|
186
|
+
await this.mutex.runExclusive(
|
|
187
|
+
async () => {
|
|
188
|
+
logger.debug(`mutex ${name} locked!`)
|
|
189
|
+
await callback()
|
|
190
|
+
}
|
|
191
|
+
)
|
|
192
|
+
logger.debug(`mutex ${name} released!`)
|
|
193
|
+
}
|
|
194
|
+
|
|
180
195
|
async publishEventSpectatorsMessage(message) {
|
|
181
196
|
await this.messaging.publishEventSpectatorsMessage(this, this.css, message)
|
|
182
197
|
}
|
|
@@ -1,19 +1,23 @@
|
|
|
1
1
|
"use strict"
|
|
2
2
|
const JsonApiResource = require('./JsonApiResource')
|
|
3
|
+
const ParticipantTimer = require('../utils/ParticipantTimer')
|
|
4
|
+
const logger = require('../utils/Logger')
|
|
5
|
+
|
|
3
6
|
const HostUserType = "host"
|
|
4
7
|
|
|
5
8
|
class AnearParticipant extends JsonApiResource {
|
|
6
9
|
constructor(json, anearEvent) {
|
|
7
10
|
super(json)
|
|
8
11
|
this.anearEvent = anearEvent
|
|
12
|
+
this.timer = null
|
|
9
13
|
this._state = json.state
|
|
10
14
|
this._timestamp = json.timestamp
|
|
15
|
+
this._geoLocation = null
|
|
11
16
|
}
|
|
12
17
|
|
|
13
18
|
toJSON() {
|
|
14
19
|
return {
|
|
15
20
|
...super.toJSON(),
|
|
16
|
-
geoLocation: this._geoLocation || null,
|
|
17
21
|
state: this.state,
|
|
18
22
|
timestamp: this.timestamp
|
|
19
23
|
}
|
|
@@ -78,6 +82,59 @@ class AnearParticipant extends JsonApiResource {
|
|
|
78
82
|
get privateChannelName() {
|
|
79
83
|
return this.attributes['private-channel-name']
|
|
80
84
|
}
|
|
85
|
+
|
|
86
|
+
ensureTimer(timeoutMsecs) {
|
|
87
|
+
// this is called when a new timer is being started for a privateMessage
|
|
88
|
+
// sent to a participant, or public message to all participants
|
|
89
|
+
// If the timer already exists and is paused, it is resumed with the timeRemaining
|
|
90
|
+
// [a starter function, timeRemaining] is returned
|
|
91
|
+
let timeRemaining = timeoutMsecs
|
|
92
|
+
let timerStarter = () => {}
|
|
93
|
+
|
|
94
|
+
if (timeoutMsecs > 0) {
|
|
95
|
+
|
|
96
|
+
this.timer ||= new ParticipantTimer(
|
|
97
|
+
this.id,
|
|
98
|
+
async () => await this.timerExpired(timeoutMsecs)
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
if (this.timer.isRunning) {
|
|
102
|
+
timeRemaining = this.timer.interrupt()
|
|
103
|
+
} else {
|
|
104
|
+
timerStarter = () => this.timer.start(timeoutMsecs)
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
logger.debug(`ensureTimer(timeRemaining: ${timeRemaining})`)
|
|
108
|
+
|
|
109
|
+
return [timerStarter, timeRemaining]
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
destroyTimer() {
|
|
113
|
+
// participant will not be receiving any more display messages
|
|
114
|
+
// so we close out and delete the timer
|
|
115
|
+
if (this.timer) {
|
|
116
|
+
this.timer.reset()
|
|
117
|
+
this.timer = null
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async timerExpired(timeoutMsecs) {
|
|
122
|
+
logger.debug(`participant (${this.anearEvent.id}, ${this.id}) TIMED OUT after ${timeoutMsecs} msecs`)
|
|
123
|
+
|
|
124
|
+
this.timer = null
|
|
125
|
+
|
|
126
|
+
await this.anearEvent.participantTimedOut(this)
|
|
127
|
+
await this.anearEvent.update()
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
interruptTimer() {
|
|
131
|
+
if (this.timer && this.timer.isRunning) this.timer.interrupt()
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
resetTimer() {
|
|
135
|
+
// called after participant takes Action before timer expires
|
|
136
|
+
if (this.timer) this.timer.reset()
|
|
137
|
+
}
|
|
81
138
|
}
|
|
82
139
|
|
|
83
140
|
module.exports = AnearParticipant
|
package/package.json
CHANGED
|
@@ -43,3 +43,40 @@ test('participant can be repeatedly rehydrated and updated', async () => {
|
|
|
43
43
|
console.error(error)
|
|
44
44
|
}
|
|
45
45
|
})
|
|
46
|
+
|
|
47
|
+
test('participant does not persist geoLocation', async () => {
|
|
48
|
+
try {
|
|
49
|
+
const someGeoLocation = {
|
|
50
|
+
coords: {
|
|
51
|
+
latitude: 25.7862067,
|
|
52
|
+
longitude: -80.1415718
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
const participant = new TestParticipant(player1, MockEvent)
|
|
56
|
+
|
|
57
|
+
participant.geoLocation = someGeoLocation
|
|
58
|
+
|
|
59
|
+
await participant.persist()
|
|
60
|
+
|
|
61
|
+
expect(participant.geoLocation.coords.latitude).toBe(someGeoLocation.coords.latitude)
|
|
62
|
+
|
|
63
|
+
let p = await TestParticipant.getFromStorage(player1.data.id, MockEvent)
|
|
64
|
+
|
|
65
|
+
expect(p.geoLocation).toBe(null)
|
|
66
|
+
|
|
67
|
+
expect(p.context.responses).toStrictEqual(['A', 'C', 'D', 'A'])
|
|
68
|
+
expect(p.anearEvent).toBe(MockEvent)
|
|
69
|
+
p.context.responses.push('B')
|
|
70
|
+
|
|
71
|
+
await p.update()
|
|
72
|
+
|
|
73
|
+
p = await TestParticipant.getFromStorage(player1.data.id, MockEvent)
|
|
74
|
+
expect(p.context.responses[4]).toBe('B')
|
|
75
|
+
expect(p.anearEvent).toBe(MockEvent)
|
|
76
|
+
|
|
77
|
+
await p.remove()
|
|
78
|
+
|
|
79
|
+
} catch(error) {
|
|
80
|
+
console.error(error)
|
|
81
|
+
}
|
|
82
|
+
})
|