anear-js-api 0.3.6 → 0.3.7
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,6 +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')
|
|
4
5
|
const logger = require('../utils/Logger')
|
|
5
6
|
const { Mutex } = require('async-mutex')
|
|
6
7
|
|
|
@@ -25,7 +26,6 @@ const AblyLogLevel = process.env.ANEARAPP_ABLY_LOG_LEVEL || 0 // 0 - no logging,
|
|
|
25
26
|
const AnearCreateEventChannelName = `anear:${AppId}:e`
|
|
26
27
|
|
|
27
28
|
class AnearMessaging {
|
|
28
|
-
|
|
29
29
|
constructor(AnearEventClass, AnearParticipantClass) {
|
|
30
30
|
this.api = new AnearApi()
|
|
31
31
|
this.AnearEventClass = AnearEventClass
|
|
@@ -86,31 +86,64 @@ class AnearMessaging {
|
|
|
86
86
|
return this.realtime.channels.get(channelName, channelParams)
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
89
|
+
ensureParticipantTimer(anearEvent, participant, timeoutMsecs) {
|
|
90
|
+
// this is called when a new timer is being started for a privateMessage
|
|
91
|
+
// sent to a participant, or public message to all participants
|
|
92
|
+
// If the timer already exists and is paused, it is resumed with the timeRemaining
|
|
93
|
+
// [a starter function, timeRemaining] is returned
|
|
94
|
+
let timeRemaining = timeoutMsecs
|
|
95
|
+
let timerStarter = () => {}
|
|
96
|
+
|
|
97
|
+
if (timeoutMsecs > 0) {
|
|
98
|
+
let timer = this.participantTimers[participant.id] || createTimer(anearEvent, participant, timeoutMsecs)
|
|
99
|
+
|
|
100
|
+
if (timer.isPaused) {
|
|
101
|
+
timerRemaining = this.timeRemaining
|
|
102
|
+
timerStarter = () => timer.resume()
|
|
103
|
+
} else {
|
|
104
|
+
timerStarter = () => timer.start(timeoutMsecs)
|
|
105
|
+
}
|
|
94
106
|
}
|
|
107
|
+
return [timerStarter, timeRemaining]
|
|
95
108
|
}
|
|
96
109
|
|
|
97
|
-
|
|
98
|
-
const
|
|
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
|
|
99
116
|
|
|
100
|
-
|
|
117
|
+
return timer
|
|
118
|
+
}
|
|
101
119
|
|
|
102
|
-
|
|
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]
|
|
103
124
|
|
|
104
|
-
|
|
125
|
+
if (timer) {
|
|
126
|
+
timer.reset()
|
|
127
|
+
delete this.participantTimers[participantId]
|
|
128
|
+
}
|
|
129
|
+
}
|
|
105
130
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
)
|
|
131
|
+
pauseParticipantTimer(participantId) {
|
|
132
|
+
const timer = this.participantTimers[particpantId]
|
|
133
|
+
|
|
134
|
+
if (timer && timer.isRunning) timer.pause()
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
resetParticipantTimer(participantId) {
|
|
138
|
+
// called after participant takes Action before timer expires
|
|
139
|
+
const timer = this.participantTimers[particpantId]
|
|
140
|
+
if (timer) timer.reset()
|
|
110
141
|
}
|
|
111
142
|
|
|
112
|
-
async timerExpired(anearEvent, participant,
|
|
113
|
-
logger.debug(`participant (${anearEvent.id}, ${participant.id}) TIMED OUT after ${
|
|
143
|
+
async timerExpired(anearEvent, participant, timeoutMsecs) {
|
|
144
|
+
logger.debug(`participant (${anearEvent.id}, ${participant.id}) TIMED OUT after ${timeoutMsecs} msecs`)
|
|
145
|
+
|
|
146
|
+
delete this.participantTimers[participant.id]
|
|
114
147
|
|
|
115
148
|
await anearEvent.participantTimedOut(participant)
|
|
116
149
|
await anearEvent.update()
|
|
@@ -222,7 +255,7 @@ class AnearMessaging {
|
|
|
222
255
|
}
|
|
223
256
|
|
|
224
257
|
async refreshActiveParticipants(anearEvent) {
|
|
225
|
-
const allParticipants = anearEvent.participants.active
|
|
258
|
+
const allParticipants = anearEvent.participants.active
|
|
226
259
|
|
|
227
260
|
return Promise.all(
|
|
228
261
|
allParticipants.map(
|
|
@@ -406,12 +439,16 @@ class AnearMessaging {
|
|
|
406
439
|
const participantId = message.data.participantId
|
|
407
440
|
|
|
408
441
|
logger.debug(`**** LEAVE PARTICIPANT **** participantLeaveMessagingCallback(user: ${participantId})`)
|
|
442
|
+
|
|
443
|
+
// pause the participant timer if one was active. The participant may return shortly
|
|
444
|
+
// and we can resume this timer
|
|
445
|
+
this.pauseParticipantTimer(participantId)
|
|
409
446
|
}
|
|
410
447
|
|
|
411
448
|
async closeParticipant(anearEvent, participantId, callback) {
|
|
412
449
|
logger.debug(`closeParticipant(${participantId})`)
|
|
413
450
|
|
|
414
|
-
this.
|
|
451
|
+
this.destroyParticipantTimer(participantId)
|
|
415
452
|
|
|
416
453
|
const participant = await this.getAnearParticipantFromStorage(participantId)
|
|
417
454
|
|
|
@@ -445,12 +482,12 @@ class AnearMessaging {
|
|
|
445
482
|
// actionEventName => "reviewResponse"
|
|
446
483
|
// actionPayload => {questionId: "ab88373ccf", decision:"approved"}
|
|
447
484
|
//
|
|
448
|
-
const payload = message.data.payload
|
|
449
485
|
const participantId = message.data.participantId
|
|
486
|
+
const payload = message.data.payload
|
|
450
487
|
|
|
451
|
-
|
|
488
|
+
this.resetParticipantTimer(participantId) // participant responded in time, reset any running timer
|
|
452
489
|
|
|
453
|
-
|
|
490
|
+
logger.debug(`participantActionMessagingCallback(${anearEvent.id}, ${participantId})`)
|
|
454
491
|
|
|
455
492
|
const actionJSON = JSON.parse(payload)
|
|
456
493
|
const [actionEventName, actionPayload] = Object.entries(actionJSON)[0]
|
|
@@ -527,39 +564,46 @@ class AnearMessaging {
|
|
|
527
564
|
}
|
|
528
565
|
}
|
|
529
566
|
|
|
530
|
-
async
|
|
531
|
-
const
|
|
532
|
-
const
|
|
533
|
-
|
|
534
|
-
|
|
567
|
+
async publishEventSpectatorsMessage(anearEvent, css, message, messageType = PublicDisplayMessageType) {
|
|
568
|
+
const channel = this.eventChannels[anearEvent.id].spectators
|
|
569
|
+
const payload = {
|
|
570
|
+
css: css,
|
|
571
|
+
content: message
|
|
572
|
+
}
|
|
535
573
|
|
|
536
|
-
await this.
|
|
537
|
-
channel,
|
|
538
|
-
PublicDisplayMessageType,
|
|
539
|
-
css,
|
|
540
|
-
message,
|
|
541
|
-
timeoutMilliseconds,
|
|
542
|
-
setTimerFunction,
|
|
543
|
-
timeoutCallback
|
|
544
|
-
)
|
|
574
|
+
await this.publishChannelMessage(channel, messageType, payload)
|
|
545
575
|
}
|
|
546
576
|
|
|
547
|
-
setMultipleParticipantTimers(anearEvent, participants,
|
|
548
|
-
if (
|
|
577
|
+
setMultipleParticipantTimers(anearEvent, participants, timeoutMsecs) {
|
|
578
|
+
if (timeoutMsecs === 0) return [() => {}, 0]
|
|
579
|
+
|
|
580
|
+
const participantTimers = []
|
|
549
581
|
|
|
550
582
|
participants.forEach(
|
|
551
|
-
participant =>
|
|
583
|
+
participant => {
|
|
584
|
+
const [startTimer, _timeRemaining] = this.ensureParticipantTimer(anearEvent, participant, timeoutMsecs)
|
|
585
|
+
participantTimers.push(startTimer)
|
|
586
|
+
}
|
|
552
587
|
)
|
|
588
|
+
const startTimers = () => participantTimers.forEach(startTimer => startTimer())
|
|
589
|
+
return [startTimers, timeoutMsecs]
|
|
553
590
|
}
|
|
554
591
|
|
|
555
|
-
async
|
|
556
|
-
const
|
|
557
|
-
const
|
|
558
|
-
css: css,
|
|
559
|
-
content: message
|
|
560
|
-
}
|
|
592
|
+
async publishEventParticipantsMessage(anearEvent, participants, css, message, timeoutMsecs=0) {
|
|
593
|
+
const eventId = anearEvent.id
|
|
594
|
+
const channel = this.eventChannels[eventId].participants
|
|
561
595
|
|
|
562
|
-
|
|
596
|
+
const [startTimers, timeRemaining] = this.setMultipleParticipantTimers(eventId, participants, timeoutMsecs)
|
|
597
|
+
|
|
598
|
+
await this.publishMessage(
|
|
599
|
+
channel,
|
|
600
|
+
PublicDisplayMessageType,
|
|
601
|
+
css,
|
|
602
|
+
message,
|
|
603
|
+
timeoutMsecs,
|
|
604
|
+
timeRemaining
|
|
605
|
+
)
|
|
606
|
+
startTimers()
|
|
563
607
|
}
|
|
564
608
|
|
|
565
609
|
async publishEventPrivateMessage(
|
|
@@ -568,44 +612,37 @@ class AnearMessaging {
|
|
|
568
612
|
messageType,
|
|
569
613
|
css,
|
|
570
614
|
message,
|
|
571
|
-
|
|
572
|
-
timeoutCallback=null) {
|
|
615
|
+
timeoutMsecs=0) {
|
|
573
616
|
|
|
574
617
|
const userId = participant.userId
|
|
575
618
|
const channel = this.eventChannels[anearEvent.id].privates[userId]
|
|
576
619
|
if (!channel) throw new Error(`private channel not found. invalid user id ${userId}`)
|
|
577
620
|
|
|
578
|
-
const
|
|
621
|
+
const [startTimer, timeRemaining] = this.ensureParticipantTimer(anearEvent, participant, timeoutMsecs)
|
|
579
622
|
|
|
580
|
-
await this.
|
|
623
|
+
await this.publishMessage(
|
|
581
624
|
channel,
|
|
582
625
|
messageType,
|
|
583
626
|
css,
|
|
584
627
|
message,
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
timeoutCallback
|
|
628
|
+
timeoutMsecs,
|
|
629
|
+
timeRemaining
|
|
588
630
|
)
|
|
631
|
+
startTimer()
|
|
589
632
|
}
|
|
590
633
|
|
|
591
|
-
async
|
|
634
|
+
async publishMessage(
|
|
592
635
|
channel,
|
|
593
636
|
messageType,
|
|
594
637
|
css,
|
|
595
638
|
message,
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
timeoutCallback=null) {
|
|
599
|
-
|
|
600
|
-
const timerCallback = async () => {
|
|
601
|
-
if (timeoutMilliseconds > 0) setTimerFunction()
|
|
602
|
-
if (timeoutCallback) await timeoutCallback()
|
|
603
|
-
}
|
|
639
|
+
timeoutMsecs,
|
|
640
|
+
timeRemaining) {
|
|
604
641
|
|
|
605
642
|
const payload = {
|
|
606
643
|
css: css,
|
|
607
644
|
content: message,
|
|
608
|
-
timeout:
|
|
645
|
+
timeout: { timeoutMsecs, timeRemaining }
|
|
609
646
|
}
|
|
610
647
|
|
|
611
648
|
await this.publishChannelMessage(
|
|
@@ -613,7 +650,6 @@ class AnearMessaging {
|
|
|
613
650
|
messageType,
|
|
614
651
|
payload
|
|
615
652
|
)
|
|
616
|
-
await timerCallback()
|
|
617
653
|
}
|
|
618
654
|
|
|
619
655
|
async publishEventTransitionMessage(anearEvent, newState) {
|
package/lib/models/AnearEvent.js
CHANGED
|
@@ -128,7 +128,7 @@ class AnearEvent extends JsonApiResource {
|
|
|
128
128
|
throw new Error('You must implement an async participantEnterEventCallback() in your AnearEvent sub-class');
|
|
129
129
|
}
|
|
130
130
|
|
|
131
|
-
async participantRefreshEventCallback(participant) {
|
|
131
|
+
async participantRefreshEventCallback(participant, remainingTimeout = null) {
|
|
132
132
|
// You may implement an async participantRefreshEventCallback() in your AnearEvent sub-class
|
|
133
133
|
}
|
|
134
134
|
|
|
@@ -163,14 +163,13 @@ class AnearEvent extends JsonApiResource {
|
|
|
163
163
|
await this.spectatorRefreshEventCallback()
|
|
164
164
|
}
|
|
165
165
|
|
|
166
|
-
async publishEventParticipantsMessage(message, timeoutMilliseconds=0
|
|
166
|
+
async publishEventParticipantsMessage(message, timeoutMilliseconds=0) {
|
|
167
167
|
await this.messaging.publishEventParticipantsMessage(
|
|
168
168
|
this,
|
|
169
|
-
this.
|
|
169
|
+
this.participants.active,
|
|
170
170
|
this.css,
|
|
171
171
|
message,
|
|
172
|
-
timeoutMilliseconds
|
|
173
|
-
timeoutCallback
|
|
172
|
+
timeoutMilliseconds
|
|
174
173
|
)
|
|
175
174
|
}
|
|
176
175
|
|
|
@@ -178,15 +177,14 @@ class AnearEvent extends JsonApiResource {
|
|
|
178
177
|
await this.messaging.publishEventSpectatorsMessage(this, this.css, message)
|
|
179
178
|
}
|
|
180
179
|
|
|
181
|
-
async publishEventPrivateMessage(participant, message, timeoutMilliseconds=0
|
|
180
|
+
async publishEventPrivateMessage(participant, message, timeoutMilliseconds=0) {
|
|
182
181
|
await this.messaging.publishEventPrivateMessage(
|
|
183
182
|
this,
|
|
184
183
|
participant,
|
|
185
184
|
PrivateDisplayMessageType,
|
|
186
185
|
this.css,
|
|
187
186
|
message,
|
|
188
|
-
timeoutMilliseconds
|
|
189
|
-
timeoutCallback
|
|
187
|
+
timeoutMilliseconds
|
|
190
188
|
)
|
|
191
189
|
}
|
|
192
190
|
|
|
@@ -94,7 +94,7 @@ const DefaultOptionsFunc = anearEvent => {
|
|
|
94
94
|
},
|
|
95
95
|
services: {
|
|
96
96
|
joinEventHandler: (context, event) => anearEvent.participantEnterEventCallback(event.participant),
|
|
97
|
-
refreshEventHandler: (context, event) => anearEvent.participantRefreshEventCallback(event.participant),
|
|
97
|
+
refreshEventHandler: (context, event) => anearEvent.participantRefreshEventCallback(event.participant, event.remainingTimeout),
|
|
98
98
|
closeEventHandler: (context, event) => anearEvent.participantCloseEventCallback(event.participant),
|
|
99
99
|
timeoutEventHandler: (context, event) => anearEvent.participantTimedOutEventCallback(event.participant),
|
|
100
100
|
actionEventHandler: (context, event) => anearEvent.participantActionEventCallback(event.participant, event.type, event.payload)
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"use strict"
|
|
2
|
+
|
|
3
|
+
const logger = require('./Logger')
|
|
4
|
+
|
|
5
|
+
const Off = "off"
|
|
6
|
+
const Running = "running"
|
|
7
|
+
const Paused = "paused"
|
|
8
|
+
const Expired = "expired"
|
|
9
|
+
|
|
10
|
+
class ParticipantTimer {
|
|
11
|
+
constructor(participantId, expireCallback) {
|
|
12
|
+
this.participantId = participantId
|
|
13
|
+
this.expireCallback = expireCallback
|
|
14
|
+
|
|
15
|
+
this.turnOff()
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
start(timeoutMsecs, now = Date.now()) {
|
|
19
|
+
this.startedAt = now
|
|
20
|
+
logger.debug(`starting ${timeoutMsecs} msec timer for participant ${this.participantId}`)
|
|
21
|
+
this.runTimer(timeoutMsecs)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
runTimer(timeoutMsecs) {
|
|
25
|
+
const timerExpired = () => {
|
|
26
|
+
this.state = Expired
|
|
27
|
+
this.expireCallback()
|
|
28
|
+
}
|
|
29
|
+
this.state = Running
|
|
30
|
+
this.id = setTimeout(timerExpired, timeoutMsecs)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
pause(now = Date.now()) {
|
|
34
|
+
// if running, stop the timer
|
|
35
|
+
if (!this.isRunning) throw new Error("timer not running")
|
|
36
|
+
|
|
37
|
+
clearTimeout(this.id)
|
|
38
|
+
this.id = null
|
|
39
|
+
this.timeRemaining = now - this.startedAt
|
|
40
|
+
|
|
41
|
+
logger.debug(`pausing timer for participant ${this.participantId}. Time remaining: ${this.timeRemaining}`)
|
|
42
|
+
this.state = Paused
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
resume() {
|
|
46
|
+
// if paused, restarts the timer with the timeRemaining
|
|
47
|
+
if (!this.isPaused) throw new Error("timer not paused")
|
|
48
|
+
|
|
49
|
+
if (this.timeRemaining > 0) {
|
|
50
|
+
logger.debug(`resuming ${this.timeRemaining} msec timer for participant ${this.participantId}`)
|
|
51
|
+
this.runTimer(this.timeRemaining)
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
reset() {
|
|
56
|
+
// if running, stops the timer and/or sets the timer state to Off
|
|
57
|
+
logger.debug(`resetting timer for participant ${this.participantId}`)
|
|
58
|
+
|
|
59
|
+
if (this.id) clearTimeout(this.id)
|
|
60
|
+
|
|
61
|
+
this.turnOff()
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
turnOff() {
|
|
65
|
+
this.id = null
|
|
66
|
+
this.state = Off
|
|
67
|
+
this.startedAt = null
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
get isRunning() {
|
|
71
|
+
return this.state === Running
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
get isPaused() {
|
|
75
|
+
return this.state === Paused
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
get isOff() {
|
|
79
|
+
return this.state === Off
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
get isExpired() {
|
|
83
|
+
return this.state === Expired
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
module.exports = ParticipantTimer
|
package/package.json
CHANGED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"use strict"
|
|
2
|
+
|
|
3
|
+
const ParticipantTimer = require('../lib/utils/ParticipantTimer')
|
|
4
|
+
const ParticipantId = "machvee"
|
|
5
|
+
const Now = Date.now()
|
|
6
|
+
|
|
7
|
+
jest.useFakeTimers();
|
|
8
|
+
|
|
9
|
+
afterEach(() => jest.clearAllTimers)
|
|
10
|
+
|
|
11
|
+
test('constructor with callback basic usage', () => {
|
|
12
|
+
const callback = jest.fn()
|
|
13
|
+
const t = new ParticipantTimer(ParticipantId, callback)
|
|
14
|
+
expect(t).toBeDefined()
|
|
15
|
+
expect(t.isRunning).toBe(false)
|
|
16
|
+
expect(t.isPaused).toBe(false)
|
|
17
|
+
t.start(500, Now)
|
|
18
|
+
expect(t.isRunning).toBe(true)
|
|
19
|
+
jest.runAllTimers()
|
|
20
|
+
expect(callback).toHaveBeenCalledTimes(1)
|
|
21
|
+
expect(t.isExpired).toBe(true)
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
test('start and then pause does not invoke callback', () => {
|
|
25
|
+
const callback = jest.fn()
|
|
26
|
+
const t = new ParticipantTimer(ParticipantId, callback)
|
|
27
|
+
t.start(1000, Now)
|
|
28
|
+
jest.advanceTimersByTime(500);
|
|
29
|
+
t.pause(Now + 500)
|
|
30
|
+
expect(t.isPaused).toBe(true)
|
|
31
|
+
expect(t.timeRemaining).toEqual(500)
|
|
32
|
+
jest.advanceTimersByTime(501);
|
|
33
|
+
expect(callback).toHaveBeenCalledTimes(0)
|
|
34
|
+
expect(t.isPaused).toBe(true)
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
test('start, pause, then resume', () => {
|
|
38
|
+
const callback = jest.fn()
|
|
39
|
+
const t = new ParticipantTimer(ParticipantId, callback)
|
|
40
|
+
t.start(1000, Now)
|
|
41
|
+
jest.advanceTimersByTime(500);
|
|
42
|
+
t.pause(Now + 500)
|
|
43
|
+
expect(t.isPaused).toBe(true)
|
|
44
|
+
expect(t.timeRemaining).toEqual(500)
|
|
45
|
+
|
|
46
|
+
t.resume(Now + 1001)
|
|
47
|
+
jest.advanceTimersByTime(501);
|
|
48
|
+
expect(callback).toHaveBeenCalledTimes(1)
|
|
49
|
+
})
|