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
|
-
|
|
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
|
-
|
|
41
|
-
|
|
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
|
@@ -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(
|
|
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(
|
|
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
|
|