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
- clearParticipantTimer(participantId) {
90
- const timerId = this.participantTimers[participantId]
91
- if (timerId) {
92
- clearTimeout(timerId)
93
- this.participantTimers[participantId] = null
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
- setParticipantTimer(anearEvent, participant, timeoutMilliseconds) {
98
- const participantId = participant.id
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
- this.clearParticipantTimer(participantId)
117
+ return timer
118
+ }
101
119
 
102
- if (timeoutMilliseconds === 0) return
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
- logger.debug(`setting ${timeoutMilliseconds} msec timer for event ${anearEvent.id}, participant ${participant.id}`)
125
+ if (timer) {
126
+ timer.reset()
127
+ delete this.participantTimers[participantId]
128
+ }
129
+ }
105
130
 
106
- this.participantTimers[participantId] = setTimeout(
107
- async () => await this.timerExpired(anearEvent, participant, timeoutMilliseconds),
108
- timeoutMilliseconds
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, timeoutMilliseconds) {
113
- logger.debug(`participant (${anearEvent.id}, ${participant.id}) TIMED OUT after ${timeoutMilliseconds} msecs`)
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(false)
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.clearParticipantTimer(participantId)
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
- logger.debug(`participantActionMessagingCallback(${anearEvent.id}, ${participantId})`)
488
+ this.resetParticipantTimer(participantId) // participant responded in time, reset any running timer
452
489
 
453
- this.clearParticipantTimer(participantId)
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 publishEventParticipantsMessage(anearEvent, participants, css, message, timeoutMilliseconds=0, timeoutCallback=null) {
531
- const eventId = anearEvent.id
532
- const channel = this.eventChannels[eventId].participants
533
-
534
- const setTimerFunction = () => this.setMultipleParticipantTimers(eventId, participants, timeoutMilliseconds)
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.publishChannelMessageWithTimeout(
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, timeoutMilliseconds) {
548
- if (timeoutMilliseconds === 0) return
577
+ setMultipleParticipantTimers(anearEvent, participants, timeoutMsecs) {
578
+ if (timeoutMsecs === 0) return [() => {}, 0]
579
+
580
+ const participantTimers = []
549
581
 
550
582
  participants.forEach(
551
- participant => this.setParticipantTimer(anearEvent, participant, timeoutMilliseconds)
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 publishEventSpectatorsMessage(anearEvent, css, message, messageType = PublicDisplayMessageType) {
556
- const channel = this.eventChannels[anearEvent.id].spectators
557
- const payload = {
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
- await this.publishChannelMessage(channel, messageType, payload)
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
- timeoutMilliseconds=0,
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 setTimerFunction = () => this.setParticipantTimer(anearEvent, participant, timeoutMilliseconds)
621
+ const [startTimer, timeRemaining] = this.ensureParticipantTimer(anearEvent, participant, timeoutMsecs)
579
622
 
580
- await this.publishChannelMessageWithTimeout(
623
+ await this.publishMessage(
581
624
  channel,
582
625
  messageType,
583
626
  css,
584
627
  message,
585
- timeoutMilliseconds,
586
- setTimerFunction,
587
- timeoutCallback
628
+ timeoutMsecs,
629
+ timeRemaining
588
630
  )
631
+ startTimer()
589
632
  }
590
633
 
591
- async publishChannelMessageWithTimeout(
634
+ async publishMessage(
592
635
  channel,
593
636
  messageType,
594
637
  css,
595
638
  message,
596
- timeoutMilliseconds=0,
597
- setTimerFunction,
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: timeoutMilliseconds
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) {
@@ -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, timeoutCallback=null) {
166
+ async publishEventParticipantsMessage(message, timeoutMilliseconds=0) {
167
167
  await this.messaging.publishEventParticipantsMessage(
168
168
  this,
169
- this.activeParticipants,
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, timeoutCallback=null) {
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
@@ -132,6 +132,8 @@ class Participants {
132
132
  this._host = rec
133
133
  } else {
134
134
  this._participants[anearParticipant.id] = rec
135
+ anearParticipant.timestamp = this.currentTimestamp
136
+ anearParticipant.state = ActiveState
135
137
  }
136
138
  return rec
137
139
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "anear-js-api",
3
- "version": "0.3.6",
3
+ "version": "0.3.7",
4
4
  "description": "Javascript Developer API for Anear Apps",
5
5
  "main": "lib/index.js",
6
6
  "scripts": {
@@ -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
+ })