anear-js-api 0.3.4 → 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
 
@@ -425,9 +462,8 @@ class AnearMessaging {
425
462
  }
426
463
  }
427
464
 
428
- async detachParticipantPrivateChannel(anearEvent, participant) {
465
+ async detachParticipantPrivateChannel(eventId, participant) {
429
466
  const userId = participant.userId
430
- const eventId = anearEvent.id
431
467
  const channel = this.eventChannels[eventId].privates[userId]
432
468
 
433
469
  if (channel) {
@@ -446,12 +482,12 @@ class AnearMessaging {
446
482
  // actionEventName => "reviewResponse"
447
483
  // actionPayload => {questionId: "ab88373ccf", decision:"approved"}
448
484
  //
449
- const payload = message.data.payload
450
485
  const participantId = message.data.participantId
486
+ const payload = message.data.payload
451
487
 
452
- logger.debug(`participantActionMessagingCallback(${anearEvent.id}, ${participantId})`)
488
+ this.resetParticipantTimer(participantId) // participant responded in time, reset any running timer
453
489
 
454
- this.clearParticipantTimer(participantId)
490
+ logger.debug(`participantActionMessagingCallback(${anearEvent.id}, ${participantId})`)
455
491
 
456
492
  const actionJSON = JSON.parse(payload)
457
493
  const [actionEventName, actionPayload] = Object.entries(actionJSON)[0]
@@ -528,39 +564,46 @@ class AnearMessaging {
528
564
  }
529
565
  }
530
566
 
531
- async publishEventParticipantsMessage(anearEvent, participants, css, message, timeoutMilliseconds=0, timeoutCallback=null) {
532
- const eventId = anearEvent.id
533
- const channel = this.eventChannels[eventId].participants
534
-
535
- 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
+ }
536
573
 
537
- await this.publishChannelMessageWithTimeout(
538
- channel,
539
- PublicDisplayMessageType,
540
- css,
541
- message,
542
- timeoutMilliseconds,
543
- setTimerFunction,
544
- timeoutCallback
545
- )
574
+ await this.publishChannelMessage(channel, messageType, payload)
546
575
  }
547
576
 
548
- setMultipleParticipantTimers(anearEvent, participants, timeoutMilliseconds) {
549
- if (timeoutMilliseconds === 0) return
577
+ setMultipleParticipantTimers(anearEvent, participants, timeoutMsecs) {
578
+ if (timeoutMsecs === 0) return [() => {}, 0]
579
+
580
+ const participantTimers = []
550
581
 
551
582
  participants.forEach(
552
- participant => this.setParticipantTimer(anearEvent, participant, timeoutMilliseconds)
583
+ participant => {
584
+ const [startTimer, _timeRemaining] = this.ensureParticipantTimer(anearEvent, participant, timeoutMsecs)
585
+ participantTimers.push(startTimer)
586
+ }
553
587
  )
588
+ const startTimers = () => participantTimers.forEach(startTimer => startTimer())
589
+ return [startTimers, timeoutMsecs]
554
590
  }
555
591
 
556
- async publishEventSpectatorsMessage(anearEvent, css, message, messageType = PublicDisplayMessageType) {
557
- const channel = this.eventChannels[anearEvent.id].spectators
558
- const payload = {
559
- css: css,
560
- content: message
561
- }
592
+ async publishEventParticipantsMessage(anearEvent, participants, css, message, timeoutMsecs=0) {
593
+ const eventId = anearEvent.id
594
+ const channel = this.eventChannels[eventId].participants
562
595
 
563
- 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()
564
607
  }
565
608
 
566
609
  async publishEventPrivateMessage(
@@ -569,44 +612,37 @@ class AnearMessaging {
569
612
  messageType,
570
613
  css,
571
614
  message,
572
- timeoutMilliseconds=0,
573
- timeoutCallback=null) {
615
+ timeoutMsecs=0) {
574
616
 
575
617
  const userId = participant.userId
576
618
  const channel = this.eventChannels[anearEvent.id].privates[userId]
577
619
  if (!channel) throw new Error(`private channel not found. invalid user id ${userId}`)
578
620
 
579
- const setTimerFunction = () => this.setParticipantTimer(anearEvent, participant, timeoutMilliseconds)
621
+ const [startTimer, timeRemaining] = this.ensureParticipantTimer(anearEvent, participant, timeoutMsecs)
580
622
 
581
- await this.publishChannelMessageWithTimeout(
623
+ await this.publishMessage(
582
624
  channel,
583
625
  messageType,
584
626
  css,
585
627
  message,
586
- timeoutMilliseconds,
587
- setTimerFunction,
588
- timeoutCallback
628
+ timeoutMsecs,
629
+ timeRemaining
589
630
  )
631
+ startTimer()
590
632
  }
591
633
 
592
- async publishChannelMessageWithTimeout(
634
+ async publishMessage(
593
635
  channel,
594
636
  messageType,
595
637
  css,
596
638
  message,
597
- timeoutMilliseconds=0,
598
- setTimerFunction,
599
- timeoutCallback=null) {
600
-
601
- const timerCallback = async () => {
602
- if (timeoutMilliseconds > 0) setTimerFunction()
603
- if (timeoutCallback) await timeoutCallback()
604
- }
639
+ timeoutMsecs,
640
+ timeRemaining) {
605
641
 
606
642
  const payload = {
607
643
  css: css,
608
644
  content: message,
609
- timeout: timeoutMilliseconds
645
+ timeout: { timeoutMsecs, timeRemaining }
610
646
  }
611
647
 
612
648
  await this.publishChannelMessage(
@@ -614,7 +650,6 @@ class AnearMessaging {
614
650
  messageType,
615
651
  payload
616
652
  )
617
- await timerCallback()
618
653
  }
619
654
 
620
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
 
@@ -252,7 +250,7 @@ class AnearEvent extends JsonApiResource {
252
250
  logger.debug(`AnearEvent: transitionEvent(${eventName})`)
253
251
 
254
252
  try {
255
- const responseAttributes = await this.messaging.api.transitionEvent(this, eventName)
253
+ const responseAttributes = await this.messaging.api.transitionEvent(this.id, eventName)
256
254
  const newState = responseAttributes.state
257
255
  await this.messaging.publishEventTransitionMessage(this, newState)
258
256
  } catch(err) {
@@ -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.4",
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
+ })