anear-js-api 1.1.3 → 1.1.4

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.
@@ -10,18 +10,86 @@ const NotableChannelEvents = ['attached', 'suspended', 'failed']
10
10
  class RealtimeMessaging {
11
11
  constructor() {
12
12
  this.ablyRealtime = null
13
+ // Prevent accidental duplicate listeners/subscriptions which can happen if
14
+ // getChannel()/subscribe()/enablePresenceCallbacks() are called more than once
15
+ // for the same actor+channel pair (e.g. reconnect/re-init scenarios).
16
+ this._channelStateListenerKeys = new Set()
17
+ this._presenceListenerKeys = new Set()
18
+ this._messageListenerKeys = new Set()
19
+ }
20
+
21
+ logAblyStateChange({ scope, name, current, previous, retryIn, reason }) {
22
+ // Keep normal churn at debug, but surface real problems at info/warn.
23
+ // Ably `reason` is typically an ErrorInfo with { code, message, statusCode }.
24
+ const label = name ? `${scope}:${name}` : scope
25
+ const msg = `[RTM] ${label} state changed ${previous || 'NONE'} → ${current}`
26
+ const details = { retryIn, reason }
27
+
28
+ switch (current) {
29
+ case 'FAILED':
30
+ logger.warn(msg, details)
31
+ break
32
+ case 'SUSPENDED':
33
+ case 'DISCONNECTED':
34
+ logger.info(msg, details)
35
+ break
36
+ default:
37
+ logger.debug(msg, details)
38
+ break
39
+ }
13
40
  }
14
41
 
15
42
  initRealtime(appId, actor) {
43
+ // initRealtime is expected to be called once per process/appId. If it is
44
+ // called again, close the existing connection to avoid duplicate callbacks.
45
+ if (this.ablyRealtime) {
46
+ logger.warn('[RTM] initRealtime called while already initialized; closing previous Ably client')
47
+ try {
48
+ this.ablyRealtime.close()
49
+ } catch (e) {
50
+ logger.warn('[RTM] error closing previous Ably client', e)
51
+ }
52
+ this.ablyRealtime = null
53
+ this._channelStateListenerKeys.clear()
54
+ this._presenceListenerKeys.clear()
55
+ this._messageListenerKeys.clear()
56
+ }
57
+
16
58
  const clientOptions = this.ablyClientOptions(appId)
17
59
 
18
60
  logger.debug("[RTM] Ably Client Options", clientOptions)
19
61
 
20
62
  this.ablyRealtime = new Ably.Realtime(clientOptions)
21
63
 
22
- this.ablyRealtime.connection.on((stateChange) =>
23
- actor.send({ type: stateChange.current.toUpperCase() })
24
- )
64
+ this.ablyRealtime.connection.on((stateChange) => {
65
+ const type = stateChange.current.toUpperCase()
66
+ const previous = stateChange.previous ? stateChange.previous.toUpperCase() : null
67
+ const reason = stateChange.reason
68
+ ? {
69
+ code: stateChange.reason.code,
70
+ message: stateChange.reason.message,
71
+ statusCode: stateChange.reason.statusCode
72
+ }
73
+ : null
74
+
75
+ this.logAblyStateChange({
76
+ scope: 'connection',
77
+ name: null,
78
+ current: type,
79
+ previous,
80
+ retryIn: stateChange.retryIn,
81
+ reason
82
+ })
83
+ actor.send({
84
+ type,
85
+ data: {
86
+ previous,
87
+ current: type,
88
+ retryIn: stateChange.retryIn,
89
+ reason
90
+ }
91
+ })
92
+ })
25
93
  return this
26
94
  }
27
95
 
@@ -35,10 +103,39 @@ class RealtimeMessaging {
35
103
  }
36
104
 
37
105
  enableCallbacks(channel, actor) {
106
+ const key = `${actor.id}::${channel.name}::channel_state`
107
+ if (this._channelStateListenerKeys.has(key)) return
108
+ this._channelStateListenerKeys.add(key)
109
+
38
110
  const stateChangeCallback = stateChange => {
39
111
  const channelState = stateChange.current.toUpperCase()
40
- logger.debug(`[RTM] sending machine event: ${channel.name} state changed to ${channelState}`)
41
- return actor.send({ type: channelState, data: { channelName: channel.name } })
112
+ const previous = stateChange.previous ? stateChange.previous.toUpperCase() : null
113
+ const reason = stateChange.reason
114
+ ? {
115
+ code: stateChange.reason.code,
116
+ message: stateChange.reason.message,
117
+ statusCode: stateChange.reason.statusCode
118
+ }
119
+ : null
120
+
121
+ this.logAblyStateChange({
122
+ scope: 'channel',
123
+ name: channel.name,
124
+ current: channelState,
125
+ previous,
126
+ retryIn: stateChange.retryIn,
127
+ reason
128
+ })
129
+ return actor.send({
130
+ type: channelState,
131
+ data: {
132
+ channelName: channel.name,
133
+ previous,
134
+ current: channelState,
135
+ resumed: stateChange.resumed,
136
+ reason
137
+ }
138
+ })
42
139
  }
43
140
 
44
141
  logger.debug(`[RTM] enabling ${NotableChannelEvents} on ${channel.name}`)
@@ -50,6 +147,10 @@ class RealtimeMessaging {
50
147
  const presenceActionFunc = action => {
51
148
  const eventName = `${presencePrefix}_${action.toUpperCase()}` // e.g. PARTICIPANT_ENTER, PARTICIPANT_LEAVE
52
149
 
150
+ const key = `${actor.id}::${channel.name}::presence::${eventName}`
151
+ if (this._presenceListenerKeys.has(key)) return
152
+ this._presenceListenerKeys.add(key)
153
+
53
154
  logger.debug(`[RTM] ${channel.name} subscribing to ${eventName}`)
54
155
 
55
156
  channel.presence.subscribe(
@@ -113,6 +214,10 @@ class RealtimeMessaging {
113
214
 
114
215
  subscribe(channel, actor, eventName = null) {
115
216
  // Note: subscribing to an Ably channel will implicitly attach()
217
+ const key = `${actor.id}::${channel.name}::subscribe::${eventName || '*'}`
218
+ if (this._messageListenerKeys.has(key)) return
219
+ this._messageListenerKeys.add(key)
220
+
116
221
  const args = []
117
222
 
118
223
  if (eventName) args.push(eventName)
@@ -165,6 +270,9 @@ class RealtimeMessaging {
165
270
  close() {
166
271
  this.ablyRealtime && this.ablyRealtime.close()
167
272
  this.ablyRealtime = null
273
+ this._channelStateListenerKeys.clear()
274
+ this._presenceListenerKeys.clear()
275
+ this._messageListenerKeys.clear()
168
276
  }
169
277
  }
170
278
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "anear-js-api",
3
- "version": "1.1.3",
3
+ "version": "1.1.4",
4
4
  "description": "Javascript Developer API for Anear Apps",
5
5
  "main": "lib/index.js",
6
6
  "scripts": {
@@ -38,6 +38,8 @@ jest.mock('../lib/api/AnearApi', () => ({
38
38
  }))
39
39
  jest.mock('../lib/utils/Logger', () => ({
40
40
  debug: jest.fn(),
41
+ info: jest.fn(),
42
+ warn: jest.fn(),
41
43
  error: jest.fn(),
42
44
  }))
43
45
 
@@ -48,6 +50,9 @@ describe('RealtimeMessaging', () => {
48
50
  // Reset mocks and the singleton's internal state before each test
49
51
  jest.clearAllMocks()
50
52
  RealtimeMessaging.ablyRealtime = null
53
+ RealtimeMessaging._channelStateListenerKeys && RealtimeMessaging._channelStateListenerKeys.clear()
54
+ RealtimeMessaging._presenceListenerKeys && RealtimeMessaging._presenceListenerKeys.clear()
55
+ RealtimeMessaging._messageListenerKeys && RealtimeMessaging._messageListenerKeys.clear()
51
56
  mockActor = {
52
57
  id: 'test-actor',
53
58
  send: jest.fn(),
@@ -74,10 +79,10 @@ describe('RealtimeMessaging', () => {
74
79
  const stateChangeCallback = mockRealtimeInstance.connection.on.mock.calls[0][0]
75
80
 
76
81
  stateChangeCallback({ current: 'connected' })
77
- expect(mockActor.send).toHaveBeenCalledWith({ type: 'CONNECTED' })
82
+ expect(mockActor.send).toHaveBeenCalledWith(expect.objectContaining({ type: 'CONNECTED' }))
78
83
 
79
84
  stateChangeCallback({ current: 'suspended' })
80
- expect(mockActor.send).toHaveBeenCalledWith({ type: 'SUSPENDED' })
85
+ expect(mockActor.send).toHaveBeenCalledWith(expect.objectContaining({ type: 'SUSPENDED' }))
81
86
  })
82
87
  })
83
88
 
@@ -105,10 +110,20 @@ describe('RealtimeMessaging', () => {
105
110
  const stateChangeCallback = mockChannel.on.mock.calls[0][1]
106
111
 
107
112
  stateChangeCallback({ current: 'attached' })
108
- expect(mockActor.send).toHaveBeenCalledWith({ type: 'ATTACHED', data: { channelName: 'mock-channel' } })
113
+ expect(mockActor.send).toHaveBeenCalledWith(
114
+ expect.objectContaining({
115
+ type: 'ATTACHED',
116
+ data: expect.objectContaining({ channelName: 'mock-channel' })
117
+ })
118
+ )
109
119
 
110
120
  stateChangeCallback({ current: 'failed' })
111
- expect(mockActor.send).toHaveBeenCalledWith({ type: 'FAILED', data: { channelName: 'mock-channel' } })
121
+ expect(mockActor.send).toHaveBeenCalledWith(
122
+ expect.objectContaining({
123
+ type: 'FAILED',
124
+ data: expect.objectContaining({ channelName: 'mock-channel' })
125
+ })
126
+ )
112
127
  })
113
128
  })
114
129