@hellotext/hellotext 2.4.3 → 2.4.5
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.
- package/dist/hellotext.js +1 -1
- package/lib/api/webchat/messages.cjs +8 -0
- package/lib/api/webchat/messages.js +8 -0
- package/lib/channels/application_channel.cjs +143 -10
- package/lib/channels/application_channel.js +143 -10
- package/lib/channels/webchat_channel.cjs +57 -0
- package/lib/channels/webchat_channel.js +57 -0
- package/lib/controllers/webchat_controller.cjs +167 -22
- package/lib/controllers/webchat_controller.js +172 -18
- package/package.json +1 -1
- package/src/api/webchat/messages.js +7 -0
- package/src/channels/application_channel.js +152 -9
- package/src/channels/webchat_channel.js +61 -1
- package/src/controllers/webchat_controller.js +193 -21
|
@@ -30,6 +30,14 @@ let WebchatMessagesAPI = /*#__PURE__*/function () {
|
|
|
30
30
|
headers: _hellotext.default.headers
|
|
31
31
|
});
|
|
32
32
|
}
|
|
33
|
+
}, {
|
|
34
|
+
key: "catchUp",
|
|
35
|
+
value: function catchUp(afterId) {
|
|
36
|
+
return this.index({
|
|
37
|
+
after_id: afterId,
|
|
38
|
+
session: _hellotext.default.session
|
|
39
|
+
});
|
|
40
|
+
}
|
|
33
41
|
}, {
|
|
34
42
|
key: "create",
|
|
35
43
|
value: async function create(formData) {
|
|
@@ -32,6 +32,14 @@ var WebchatMessagesAPI = /*#__PURE__*/function () {
|
|
|
32
32
|
}
|
|
33
33
|
return index;
|
|
34
34
|
}()
|
|
35
|
+
}, {
|
|
36
|
+
key: "catchUp",
|
|
37
|
+
value: function catchUp(afterId) {
|
|
38
|
+
return this.index({
|
|
39
|
+
after_id: afterId,
|
|
40
|
+
session: Hellotext.session
|
|
41
|
+
});
|
|
42
|
+
}
|
|
35
43
|
}, {
|
|
36
44
|
key: "create",
|
|
37
45
|
value: function () {
|
|
@@ -13,6 +13,7 @@ function _toPrimitive(input, hint) { if (typeof input !== "object" || input ===
|
|
|
13
13
|
let ApplicationChannel = /*#__PURE__*/function () {
|
|
14
14
|
function ApplicationChannel() {
|
|
15
15
|
_classCallCheck(this, ApplicationChannel);
|
|
16
|
+
ApplicationChannel.channels.add(this);
|
|
16
17
|
}
|
|
17
18
|
_createClass(ApplicationChannel, [{
|
|
18
19
|
key: "send",
|
|
@@ -26,18 +27,20 @@ let ApplicationChannel = /*#__PURE__*/function () {
|
|
|
26
27
|
identifier: JSON.stringify(identifier),
|
|
27
28
|
data: JSON.stringify(data || {})
|
|
28
29
|
};
|
|
29
|
-
|
|
30
|
-
|
|
30
|
+
const socket = ApplicationChannel.ensureWebSocket();
|
|
31
|
+
const message = JSON.stringify(payload);
|
|
32
|
+
if (socket.readyState === WebSocket.OPEN) {
|
|
33
|
+
socket.send(message);
|
|
31
34
|
} else {
|
|
32
|
-
|
|
33
|
-
|
|
35
|
+
socket.addEventListener('open', () => {
|
|
36
|
+
socket.send(message);
|
|
34
37
|
});
|
|
35
38
|
}
|
|
36
39
|
}
|
|
37
40
|
}, {
|
|
38
41
|
key: "onMessage",
|
|
39
42
|
value: function onMessage(callback) {
|
|
40
|
-
|
|
43
|
+
const handler = event => {
|
|
41
44
|
const data = JSON.parse(event.data);
|
|
42
45
|
const {
|
|
43
46
|
type,
|
|
@@ -47,24 +50,154 @@ let ApplicationChannel = /*#__PURE__*/function () {
|
|
|
47
50
|
return;
|
|
48
51
|
}
|
|
49
52
|
callback(message);
|
|
50
|
-
}
|
|
53
|
+
};
|
|
54
|
+
ApplicationChannel.messageHandlers.add(handler);
|
|
55
|
+
ApplicationChannel.ensureWebSocket().addEventListener('message', handler);
|
|
56
|
+
}
|
|
57
|
+
}, {
|
|
58
|
+
key: "onDisconnect",
|
|
59
|
+
value: function onDisconnect(callback) {
|
|
60
|
+
ApplicationChannel.disconnectHandlers.add(callback);
|
|
61
|
+
}
|
|
62
|
+
}, {
|
|
63
|
+
key: "onSubscriptionConfirmed",
|
|
64
|
+
value: function onSubscriptionConfirmed(callback) {
|
|
65
|
+
ApplicationChannel.subscriptionConfirmHandlers.add(callback);
|
|
51
66
|
}
|
|
52
67
|
}, {
|
|
53
68
|
key: "webSocket",
|
|
54
69
|
get: function () {
|
|
55
|
-
|
|
56
|
-
return ApplicationChannel.webSocket = new WebSocket(_core.Configuration.actionCableUrl);
|
|
57
|
-
}
|
|
58
|
-
return ApplicationChannel.webSocket;
|
|
70
|
+
return ApplicationChannel.ensureWebSocket();
|
|
59
71
|
}
|
|
60
72
|
}, {
|
|
61
73
|
key: "ignoredEvents",
|
|
62
74
|
get: function () {
|
|
63
75
|
return ['ping', 'confirm_subscription', 'welcome'];
|
|
64
76
|
}
|
|
77
|
+
}], [{
|
|
78
|
+
key: "ensureWebSocket",
|
|
79
|
+
value: function ensureWebSocket() {
|
|
80
|
+
if (this.webSocket && !this.closedWebSocket(this.webSocket)) {
|
|
81
|
+
return this.webSocket;
|
|
82
|
+
}
|
|
83
|
+
if (this.webSocket) {
|
|
84
|
+
this.needsResubscribe = true;
|
|
85
|
+
}
|
|
86
|
+
return this.openWebSocket();
|
|
87
|
+
}
|
|
88
|
+
}, {
|
|
89
|
+
key: "openWebSocket",
|
|
90
|
+
value: function openWebSocket() {
|
|
91
|
+
this.clearReconnectTimeout();
|
|
92
|
+
const socket = new WebSocket(_core.Configuration.actionCableUrl);
|
|
93
|
+
this.webSocket = socket;
|
|
94
|
+
this.installWebSocketHandlers(socket);
|
|
95
|
+
return socket;
|
|
96
|
+
}
|
|
97
|
+
}, {
|
|
98
|
+
key: "installWebSocketHandlers",
|
|
99
|
+
value: function installWebSocketHandlers(socket) {
|
|
100
|
+
socket.addEventListener('open', () => this.handleOpen(socket));
|
|
101
|
+
socket.addEventListener('close', () => this.handleDisconnect(socket));
|
|
102
|
+
socket.addEventListener('error', () => this.handleDisconnect(socket));
|
|
103
|
+
socket.addEventListener('message', event => this.handleControlMessage(event));
|
|
104
|
+
this.messageHandlers.forEach(handler => {
|
|
105
|
+
socket.addEventListener('message', handler);
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
}, {
|
|
109
|
+
key: "handleOpen",
|
|
110
|
+
value: function handleOpen(socket) {
|
|
111
|
+
if (socket !== this.webSocket) {
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
this.reconnectAttempts = 0;
|
|
115
|
+
if (!this.needsResubscribe) {
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
this.needsResubscribe = false;
|
|
119
|
+
this.resubscribeChannels();
|
|
120
|
+
}
|
|
121
|
+
}, {
|
|
122
|
+
key: "handleControlMessage",
|
|
123
|
+
value: function handleControlMessage(event) {
|
|
124
|
+
let data;
|
|
125
|
+
try {
|
|
126
|
+
data = JSON.parse(event.data);
|
|
127
|
+
} catch {
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
if (data.type !== 'confirm_subscription') {
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
this.subscriptionConfirmHandlers.forEach(callback => callback(data.identifier));
|
|
134
|
+
}
|
|
135
|
+
}, {
|
|
136
|
+
key: "handleDisconnect",
|
|
137
|
+
value: function handleDisconnect(socket) {
|
|
138
|
+
if (socket !== this.webSocket) {
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
this.disconnectHandlers.forEach(callback => callback());
|
|
142
|
+
this.webSocket = null;
|
|
143
|
+
this.needsResubscribe = true;
|
|
144
|
+
this.scheduleReconnect();
|
|
145
|
+
}
|
|
146
|
+
}, {
|
|
147
|
+
key: "scheduleReconnect",
|
|
148
|
+
value: function scheduleReconnect() {
|
|
149
|
+
if (this.reconnectTimeout) {
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
this.reconnectTimeout = setTimeout(() => {
|
|
153
|
+
this.reconnectTimeout = null;
|
|
154
|
+
this.reconnectAttempts += 1;
|
|
155
|
+
this.openWebSocket();
|
|
156
|
+
}, this.reconnectDelay);
|
|
157
|
+
}
|
|
158
|
+
}, {
|
|
159
|
+
key: "clearReconnectTimeout",
|
|
160
|
+
value: function clearReconnectTimeout() {
|
|
161
|
+
if (this.reconnectTimeout) {
|
|
162
|
+
clearTimeout(this.reconnectTimeout);
|
|
163
|
+
this.reconnectTimeout = null;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}, {
|
|
167
|
+
key: "resubscribeChannels",
|
|
168
|
+
value: function resubscribeChannels() {
|
|
169
|
+
this.channels.forEach(channel => {
|
|
170
|
+
const resubscribe = channel.resubscribe || channel.subscribe;
|
|
171
|
+
if (typeof resubscribe === 'function') {
|
|
172
|
+
resubscribe.call(channel);
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
}, {
|
|
177
|
+
key: "closedWebSocket",
|
|
178
|
+
value: function closedWebSocket(socket) {
|
|
179
|
+
return socket.readyState === WebSocket.CLOSED || socket.readyState === WebSocket.CLOSING;
|
|
180
|
+
}
|
|
181
|
+
}, {
|
|
182
|
+
key: "reconnectDelay",
|
|
183
|
+
get: function () {
|
|
184
|
+
const delay = Math.min(this.reconnectMaxDelay, this.reconnectBaseDelay * 2 ** this.reconnectAttempts);
|
|
185
|
+
const jitter = Math.round(delay * this.reconnectJitter * Math.random());
|
|
186
|
+
return delay + jitter;
|
|
187
|
+
}
|
|
65
188
|
}]);
|
|
66
189
|
return ApplicationChannel;
|
|
67
190
|
}();
|
|
68
191
|
ApplicationChannel.webSocket = void 0;
|
|
192
|
+
ApplicationChannel.channels = new Set();
|
|
193
|
+
ApplicationChannel.messageHandlers = new Set();
|
|
194
|
+
ApplicationChannel.disconnectHandlers = new Set();
|
|
195
|
+
ApplicationChannel.subscriptionConfirmHandlers = new Set();
|
|
196
|
+
ApplicationChannel.reconnectTimeout = null;
|
|
197
|
+
ApplicationChannel.reconnectAttempts = 0;
|
|
198
|
+
ApplicationChannel.reconnectBaseDelay = 500;
|
|
199
|
+
ApplicationChannel.reconnectMaxDelay = 10000;
|
|
200
|
+
ApplicationChannel.reconnectJitter = 0.3;
|
|
201
|
+
ApplicationChannel.needsResubscribe = false;
|
|
69
202
|
var _default = ApplicationChannel;
|
|
70
203
|
exports.default = _default;
|
|
@@ -7,6 +7,7 @@ import { Configuration } from '../core';
|
|
|
7
7
|
var ApplicationChannel = /*#__PURE__*/function () {
|
|
8
8
|
function ApplicationChannel() {
|
|
9
9
|
_classCallCheck(this, ApplicationChannel);
|
|
10
|
+
ApplicationChannel.channels.add(this);
|
|
10
11
|
}
|
|
11
12
|
_createClass(ApplicationChannel, [{
|
|
12
13
|
key: "send",
|
|
@@ -21,18 +22,20 @@ var ApplicationChannel = /*#__PURE__*/function () {
|
|
|
21
22
|
identifier: JSON.stringify(identifier),
|
|
22
23
|
data: JSON.stringify(data || {})
|
|
23
24
|
};
|
|
24
|
-
|
|
25
|
-
|
|
25
|
+
var socket = ApplicationChannel.ensureWebSocket();
|
|
26
|
+
var message = JSON.stringify(payload);
|
|
27
|
+
if (socket.readyState === WebSocket.OPEN) {
|
|
28
|
+
socket.send(message);
|
|
26
29
|
} else {
|
|
27
|
-
|
|
28
|
-
|
|
30
|
+
socket.addEventListener('open', () => {
|
|
31
|
+
socket.send(message);
|
|
29
32
|
});
|
|
30
33
|
}
|
|
31
34
|
}
|
|
32
35
|
}, {
|
|
33
36
|
key: "onMessage",
|
|
34
37
|
value: function onMessage(callback) {
|
|
35
|
-
|
|
38
|
+
var handler = event => {
|
|
36
39
|
var data = JSON.parse(event.data);
|
|
37
40
|
var {
|
|
38
41
|
type,
|
|
@@ -42,23 +45,153 @@ var ApplicationChannel = /*#__PURE__*/function () {
|
|
|
42
45
|
return;
|
|
43
46
|
}
|
|
44
47
|
callback(message);
|
|
45
|
-
}
|
|
48
|
+
};
|
|
49
|
+
ApplicationChannel.messageHandlers.add(handler);
|
|
50
|
+
ApplicationChannel.ensureWebSocket().addEventListener('message', handler);
|
|
51
|
+
}
|
|
52
|
+
}, {
|
|
53
|
+
key: "onDisconnect",
|
|
54
|
+
value: function onDisconnect(callback) {
|
|
55
|
+
ApplicationChannel.disconnectHandlers.add(callback);
|
|
56
|
+
}
|
|
57
|
+
}, {
|
|
58
|
+
key: "onSubscriptionConfirmed",
|
|
59
|
+
value: function onSubscriptionConfirmed(callback) {
|
|
60
|
+
ApplicationChannel.subscriptionConfirmHandlers.add(callback);
|
|
46
61
|
}
|
|
47
62
|
}, {
|
|
48
63
|
key: "webSocket",
|
|
49
64
|
get: function get() {
|
|
50
|
-
|
|
51
|
-
return ApplicationChannel.webSocket = new WebSocket(Configuration.actionCableUrl);
|
|
52
|
-
}
|
|
53
|
-
return ApplicationChannel.webSocket;
|
|
65
|
+
return ApplicationChannel.ensureWebSocket();
|
|
54
66
|
}
|
|
55
67
|
}, {
|
|
56
68
|
key: "ignoredEvents",
|
|
57
69
|
get: function get() {
|
|
58
70
|
return ['ping', 'confirm_subscription', 'welcome'];
|
|
59
71
|
}
|
|
72
|
+
}], [{
|
|
73
|
+
key: "ensureWebSocket",
|
|
74
|
+
value: function ensureWebSocket() {
|
|
75
|
+
if (this.webSocket && !this.closedWebSocket(this.webSocket)) {
|
|
76
|
+
return this.webSocket;
|
|
77
|
+
}
|
|
78
|
+
if (this.webSocket) {
|
|
79
|
+
this.needsResubscribe = true;
|
|
80
|
+
}
|
|
81
|
+
return this.openWebSocket();
|
|
82
|
+
}
|
|
83
|
+
}, {
|
|
84
|
+
key: "openWebSocket",
|
|
85
|
+
value: function openWebSocket() {
|
|
86
|
+
this.clearReconnectTimeout();
|
|
87
|
+
var socket = new WebSocket(Configuration.actionCableUrl);
|
|
88
|
+
this.webSocket = socket;
|
|
89
|
+
this.installWebSocketHandlers(socket);
|
|
90
|
+
return socket;
|
|
91
|
+
}
|
|
92
|
+
}, {
|
|
93
|
+
key: "installWebSocketHandlers",
|
|
94
|
+
value: function installWebSocketHandlers(socket) {
|
|
95
|
+
socket.addEventListener('open', () => this.handleOpen(socket));
|
|
96
|
+
socket.addEventListener('close', () => this.handleDisconnect(socket));
|
|
97
|
+
socket.addEventListener('error', () => this.handleDisconnect(socket));
|
|
98
|
+
socket.addEventListener('message', event => this.handleControlMessage(event));
|
|
99
|
+
this.messageHandlers.forEach(handler => {
|
|
100
|
+
socket.addEventListener('message', handler);
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
}, {
|
|
104
|
+
key: "handleOpen",
|
|
105
|
+
value: function handleOpen(socket) {
|
|
106
|
+
if (socket !== this.webSocket) {
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
this.reconnectAttempts = 0;
|
|
110
|
+
if (!this.needsResubscribe) {
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
this.needsResubscribe = false;
|
|
114
|
+
this.resubscribeChannels();
|
|
115
|
+
}
|
|
116
|
+
}, {
|
|
117
|
+
key: "handleControlMessage",
|
|
118
|
+
value: function handleControlMessage(event) {
|
|
119
|
+
var data;
|
|
120
|
+
try {
|
|
121
|
+
data = JSON.parse(event.data);
|
|
122
|
+
} catch (_unused) {
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
if (data.type !== 'confirm_subscription') {
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
this.subscriptionConfirmHandlers.forEach(callback => callback(data.identifier));
|
|
129
|
+
}
|
|
130
|
+
}, {
|
|
131
|
+
key: "handleDisconnect",
|
|
132
|
+
value: function handleDisconnect(socket) {
|
|
133
|
+
if (socket !== this.webSocket) {
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
this.disconnectHandlers.forEach(callback => callback());
|
|
137
|
+
this.webSocket = null;
|
|
138
|
+
this.needsResubscribe = true;
|
|
139
|
+
this.scheduleReconnect();
|
|
140
|
+
}
|
|
141
|
+
}, {
|
|
142
|
+
key: "scheduleReconnect",
|
|
143
|
+
value: function scheduleReconnect() {
|
|
144
|
+
if (this.reconnectTimeout) {
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
this.reconnectTimeout = setTimeout(() => {
|
|
148
|
+
this.reconnectTimeout = null;
|
|
149
|
+
this.reconnectAttempts += 1;
|
|
150
|
+
this.openWebSocket();
|
|
151
|
+
}, this.reconnectDelay);
|
|
152
|
+
}
|
|
153
|
+
}, {
|
|
154
|
+
key: "clearReconnectTimeout",
|
|
155
|
+
value: function clearReconnectTimeout() {
|
|
156
|
+
if (this.reconnectTimeout) {
|
|
157
|
+
clearTimeout(this.reconnectTimeout);
|
|
158
|
+
this.reconnectTimeout = null;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}, {
|
|
162
|
+
key: "resubscribeChannels",
|
|
163
|
+
value: function resubscribeChannels() {
|
|
164
|
+
this.channels.forEach(channel => {
|
|
165
|
+
var resubscribe = channel.resubscribe || channel.subscribe;
|
|
166
|
+
if (typeof resubscribe === 'function') {
|
|
167
|
+
resubscribe.call(channel);
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
}, {
|
|
172
|
+
key: "closedWebSocket",
|
|
173
|
+
value: function closedWebSocket(socket) {
|
|
174
|
+
return socket.readyState === WebSocket.CLOSED || socket.readyState === WebSocket.CLOSING;
|
|
175
|
+
}
|
|
176
|
+
}, {
|
|
177
|
+
key: "reconnectDelay",
|
|
178
|
+
get: function get() {
|
|
179
|
+
var delay = Math.min(this.reconnectMaxDelay, this.reconnectBaseDelay * 2 ** this.reconnectAttempts);
|
|
180
|
+
var jitter = Math.round(delay * this.reconnectJitter * Math.random());
|
|
181
|
+
return delay + jitter;
|
|
182
|
+
}
|
|
60
183
|
}]);
|
|
61
184
|
return ApplicationChannel;
|
|
62
185
|
}();
|
|
63
186
|
ApplicationChannel.webSocket = void 0;
|
|
187
|
+
ApplicationChannel.channels = new Set();
|
|
188
|
+
ApplicationChannel.messageHandlers = new Set();
|
|
189
|
+
ApplicationChannel.disconnectHandlers = new Set();
|
|
190
|
+
ApplicationChannel.subscriptionConfirmHandlers = new Set();
|
|
191
|
+
ApplicationChannel.reconnectTimeout = null;
|
|
192
|
+
ApplicationChannel.reconnectAttempts = 0;
|
|
193
|
+
ApplicationChannel.reconnectBaseDelay = 500;
|
|
194
|
+
ApplicationChannel.reconnectMaxDelay = 10000;
|
|
195
|
+
ApplicationChannel.reconnectJitter = 0.3;
|
|
196
|
+
ApplicationChannel.needsResubscribe = false;
|
|
64
197
|
export default ApplicationChannel;
|
|
@@ -30,12 +30,25 @@ let WebchatChannel = /*#__PURE__*/function (_ApplicationChannel) {
|
|
|
30
30
|
_this.id = id;
|
|
31
31
|
_this.session = session;
|
|
32
32
|
_this.conversation = conversation;
|
|
33
|
+
// Keep our own subscription intent instead of trusting the socket state.
|
|
34
|
+
// The shared WebSocket can reconnect independently, but an explicit
|
|
35
|
+
// unsubscribe means this channel should not silently join again.
|
|
36
|
+
_this.subscribed = false;
|
|
37
|
+
_this.awaitingReconnectConfirmation = false;
|
|
38
|
+
_this.reconnectCallbacks = new Set();
|
|
39
|
+
|
|
40
|
+
// ActionCable confirms subscriptions at the socket level. This channel
|
|
41
|
+
// listens for those confirmations so the controller can wait until Rails has
|
|
42
|
+
// accepted this exact WebchatChannel subscription before fetching missed
|
|
43
|
+
// messages from the REST catch-up endpoint.
|
|
44
|
+
_this.onSubscriptionConfirmed(identifier => _this.handleSubscriptionConfirmed(identifier));
|
|
33
45
|
_this.subscribe();
|
|
34
46
|
return _this;
|
|
35
47
|
}
|
|
36
48
|
_createClass(WebchatChannel, [{
|
|
37
49
|
key: "subscribe",
|
|
38
50
|
value: function subscribe() {
|
|
51
|
+
this.subscribed = true;
|
|
39
52
|
const params = {
|
|
40
53
|
channel: 'WebchatChannel',
|
|
41
54
|
id: this.id,
|
|
@@ -50,6 +63,7 @@ let WebchatChannel = /*#__PURE__*/function (_ApplicationChannel) {
|
|
|
50
63
|
}, {
|
|
51
64
|
key: "unsubscribe",
|
|
52
65
|
value: function unsubscribe() {
|
|
66
|
+
this.subscribed = false;
|
|
53
67
|
const params = {
|
|
54
68
|
channel: 'WebchatChannel',
|
|
55
69
|
id: this.id,
|
|
@@ -61,6 +75,49 @@ let WebchatChannel = /*#__PURE__*/function (_ApplicationChannel) {
|
|
|
61
75
|
identifier: params
|
|
62
76
|
});
|
|
63
77
|
}
|
|
78
|
+
}, {
|
|
79
|
+
key: "resubscribe",
|
|
80
|
+
value: function resubscribe() {
|
|
81
|
+
if (this.subscribed === false) return;
|
|
82
|
+
|
|
83
|
+
// Reconnect recovery has two phases: send the subscription command first,
|
|
84
|
+
// then wait for ActionCable to confirm it. Catch-up work should run after
|
|
85
|
+
// that confirmation so broadcasts and REST backfill are pointed at the same
|
|
86
|
+
// live conversation subscription.
|
|
87
|
+
this.awaitingReconnectConfirmation = true;
|
|
88
|
+
this.subscribe();
|
|
89
|
+
}
|
|
90
|
+
}, {
|
|
91
|
+
key: "onReconnect",
|
|
92
|
+
value: function onReconnect(callback) {
|
|
93
|
+
this.reconnectCallbacks.add(callback);
|
|
94
|
+
}
|
|
95
|
+
}, {
|
|
96
|
+
key: "handleSubscriptionConfirmed",
|
|
97
|
+
value: function handleSubscriptionConfirmed(identifier) {
|
|
98
|
+
if (!this.awaitingReconnectConfirmation || !this.matchesIdentifier(identifier)) return;
|
|
99
|
+
|
|
100
|
+
// Only the first matching confirmation completes the reconnect cycle. This
|
|
101
|
+
// prevents unrelated subscription confirmations on the shared socket from
|
|
102
|
+
// triggering duplicate catch-up requests.
|
|
103
|
+
this.awaitingReconnectConfirmation = false;
|
|
104
|
+
this.reconnectCallbacks.forEach(callback => callback());
|
|
105
|
+
}
|
|
106
|
+
}, {
|
|
107
|
+
key: "matchesIdentifier",
|
|
108
|
+
value: function matchesIdentifier(identifier) {
|
|
109
|
+
let params;
|
|
110
|
+
try {
|
|
111
|
+
params = typeof identifier === 'string' ? JSON.parse(identifier) : identifier;
|
|
112
|
+
} catch {
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// ActionCable sends the same identifier payload we used when subscribing.
|
|
117
|
+
// Matching every routing key keeps a confirmation for another webchat,
|
|
118
|
+
// session, or conversation from being treated as this channel's reconnect.
|
|
119
|
+
return params.channel === 'WebchatChannel' && params.id === this.id && params.session === this.session && params.conversation === this.conversation;
|
|
120
|
+
}
|
|
64
121
|
}, {
|
|
65
122
|
key: "startTypingIndicator",
|
|
66
123
|
value: function startTypingIndicator() {
|
|
@@ -23,12 +23,25 @@ var WebchatChannel = /*#__PURE__*/function (_ApplicationChannel) {
|
|
|
23
23
|
_this.id = id;
|
|
24
24
|
_this.session = session;
|
|
25
25
|
_this.conversation = conversation;
|
|
26
|
+
// Keep our own subscription intent instead of trusting the socket state.
|
|
27
|
+
// The shared WebSocket can reconnect independently, but an explicit
|
|
28
|
+
// unsubscribe means this channel should not silently join again.
|
|
29
|
+
_this.subscribed = false;
|
|
30
|
+
_this.awaitingReconnectConfirmation = false;
|
|
31
|
+
_this.reconnectCallbacks = new Set();
|
|
32
|
+
|
|
33
|
+
// ActionCable confirms subscriptions at the socket level. This channel
|
|
34
|
+
// listens for those confirmations so the controller can wait until Rails has
|
|
35
|
+
// accepted this exact WebchatChannel subscription before fetching missed
|
|
36
|
+
// messages from the REST catch-up endpoint.
|
|
37
|
+
_this.onSubscriptionConfirmed(identifier => _this.handleSubscriptionConfirmed(identifier));
|
|
26
38
|
_this.subscribe();
|
|
27
39
|
return _this;
|
|
28
40
|
}
|
|
29
41
|
_createClass(WebchatChannel, [{
|
|
30
42
|
key: "subscribe",
|
|
31
43
|
value: function subscribe() {
|
|
44
|
+
this.subscribed = true;
|
|
32
45
|
var params = {
|
|
33
46
|
channel: 'WebchatChannel',
|
|
34
47
|
id: this.id,
|
|
@@ -43,6 +56,7 @@ var WebchatChannel = /*#__PURE__*/function (_ApplicationChannel) {
|
|
|
43
56
|
}, {
|
|
44
57
|
key: "unsubscribe",
|
|
45
58
|
value: function unsubscribe() {
|
|
59
|
+
this.subscribed = false;
|
|
46
60
|
var params = {
|
|
47
61
|
channel: 'WebchatChannel',
|
|
48
62
|
id: this.id,
|
|
@@ -54,6 +68,49 @@ var WebchatChannel = /*#__PURE__*/function (_ApplicationChannel) {
|
|
|
54
68
|
identifier: params
|
|
55
69
|
});
|
|
56
70
|
}
|
|
71
|
+
}, {
|
|
72
|
+
key: "resubscribe",
|
|
73
|
+
value: function resubscribe() {
|
|
74
|
+
if (this.subscribed === false) return;
|
|
75
|
+
|
|
76
|
+
// Reconnect recovery has two phases: send the subscription command first,
|
|
77
|
+
// then wait for ActionCable to confirm it. Catch-up work should run after
|
|
78
|
+
// that confirmation so broadcasts and REST backfill are pointed at the same
|
|
79
|
+
// live conversation subscription.
|
|
80
|
+
this.awaitingReconnectConfirmation = true;
|
|
81
|
+
this.subscribe();
|
|
82
|
+
}
|
|
83
|
+
}, {
|
|
84
|
+
key: "onReconnect",
|
|
85
|
+
value: function onReconnect(callback) {
|
|
86
|
+
this.reconnectCallbacks.add(callback);
|
|
87
|
+
}
|
|
88
|
+
}, {
|
|
89
|
+
key: "handleSubscriptionConfirmed",
|
|
90
|
+
value: function handleSubscriptionConfirmed(identifier) {
|
|
91
|
+
if (!this.awaitingReconnectConfirmation || !this.matchesIdentifier(identifier)) return;
|
|
92
|
+
|
|
93
|
+
// Only the first matching confirmation completes the reconnect cycle. This
|
|
94
|
+
// prevents unrelated subscription confirmations on the shared socket from
|
|
95
|
+
// triggering duplicate catch-up requests.
|
|
96
|
+
this.awaitingReconnectConfirmation = false;
|
|
97
|
+
this.reconnectCallbacks.forEach(callback => callback());
|
|
98
|
+
}
|
|
99
|
+
}, {
|
|
100
|
+
key: "matchesIdentifier",
|
|
101
|
+
value: function matchesIdentifier(identifier) {
|
|
102
|
+
var params;
|
|
103
|
+
try {
|
|
104
|
+
params = typeof identifier === 'string' ? JSON.parse(identifier) : identifier;
|
|
105
|
+
} catch (_unused) {
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// ActionCable sends the same identifier payload we used when subscribing.
|
|
110
|
+
// Matching every routing key keeps a confirmation for another webchat,
|
|
111
|
+
// session, or conversation from being treated as this channel's reconnect.
|
|
112
|
+
return params.channel === 'WebchatChannel' && params.id === this.id && params.session === this.session && params.conversation === this.conversation;
|
|
113
|
+
}
|
|
57
114
|
}, {
|
|
58
115
|
key: "startTypingIndicator",
|
|
59
116
|
value: function startTypingIndicator() {
|