@rails/actioncable 6.0.2 → 7.1.2
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/README.md +6 -6
- package/app/assets/javascripts/action_cable.js +254 -260
- package/app/assets/javascripts/actioncable.esm.js +512 -0
- package/app/assets/javascripts/actioncable.js +510 -0
- package/package.json +8 -11
- package/src/connection.js +19 -4
- package/src/connection_monitor.js +15 -16
- package/src/consumer.js +6 -1
- package/src/index.js +2 -0
- package/src/index_with_name_deprecation.js +2 -0
- package/src/internal.js +20 -0
- package/src/logger.js +12 -0
- package/src/subscription_guarantor.js +50 -0
- package/src/subscriptions.js +21 -4
- package/CHANGELOG.md +0 -179
@@ -0,0 +1,510 @@
|
|
1
|
+
(function(global, factory) {
|
2
|
+
typeof exports === "object" && typeof module !== "undefined" ? factory(exports) : typeof define === "function" && define.amd ? define([ "exports" ], factory) : (global = typeof globalThis !== "undefined" ? globalThis : global || self,
|
3
|
+
factory(global.ActionCable = {}));
|
4
|
+
})(this, (function(exports) {
|
5
|
+
"use strict";
|
6
|
+
var adapters = {
|
7
|
+
logger: self.console,
|
8
|
+
WebSocket: self.WebSocket
|
9
|
+
};
|
10
|
+
var logger = {
|
11
|
+
log(...messages) {
|
12
|
+
if (this.enabled) {
|
13
|
+
messages.push(Date.now());
|
14
|
+
adapters.logger.log("[ActionCable]", ...messages);
|
15
|
+
}
|
16
|
+
}
|
17
|
+
};
|
18
|
+
const now = () => (new Date).getTime();
|
19
|
+
const secondsSince = time => (now() - time) / 1e3;
|
20
|
+
class ConnectionMonitor {
|
21
|
+
constructor(connection) {
|
22
|
+
this.visibilityDidChange = this.visibilityDidChange.bind(this);
|
23
|
+
this.connection = connection;
|
24
|
+
this.reconnectAttempts = 0;
|
25
|
+
}
|
26
|
+
start() {
|
27
|
+
if (!this.isRunning()) {
|
28
|
+
this.startedAt = now();
|
29
|
+
delete this.stoppedAt;
|
30
|
+
this.startPolling();
|
31
|
+
addEventListener("visibilitychange", this.visibilityDidChange);
|
32
|
+
logger.log(`ConnectionMonitor started. stale threshold = ${this.constructor.staleThreshold} s`);
|
33
|
+
}
|
34
|
+
}
|
35
|
+
stop() {
|
36
|
+
if (this.isRunning()) {
|
37
|
+
this.stoppedAt = now();
|
38
|
+
this.stopPolling();
|
39
|
+
removeEventListener("visibilitychange", this.visibilityDidChange);
|
40
|
+
logger.log("ConnectionMonitor stopped");
|
41
|
+
}
|
42
|
+
}
|
43
|
+
isRunning() {
|
44
|
+
return this.startedAt && !this.stoppedAt;
|
45
|
+
}
|
46
|
+
recordPing() {
|
47
|
+
this.pingedAt = now();
|
48
|
+
}
|
49
|
+
recordConnect() {
|
50
|
+
this.reconnectAttempts = 0;
|
51
|
+
this.recordPing();
|
52
|
+
delete this.disconnectedAt;
|
53
|
+
logger.log("ConnectionMonitor recorded connect");
|
54
|
+
}
|
55
|
+
recordDisconnect() {
|
56
|
+
this.disconnectedAt = now();
|
57
|
+
logger.log("ConnectionMonitor recorded disconnect");
|
58
|
+
}
|
59
|
+
startPolling() {
|
60
|
+
this.stopPolling();
|
61
|
+
this.poll();
|
62
|
+
}
|
63
|
+
stopPolling() {
|
64
|
+
clearTimeout(this.pollTimeout);
|
65
|
+
}
|
66
|
+
poll() {
|
67
|
+
this.pollTimeout = setTimeout((() => {
|
68
|
+
this.reconnectIfStale();
|
69
|
+
this.poll();
|
70
|
+
}), this.getPollInterval());
|
71
|
+
}
|
72
|
+
getPollInterval() {
|
73
|
+
const {staleThreshold: staleThreshold, reconnectionBackoffRate: reconnectionBackoffRate} = this.constructor;
|
74
|
+
const backoff = Math.pow(1 + reconnectionBackoffRate, Math.min(this.reconnectAttempts, 10));
|
75
|
+
const jitterMax = this.reconnectAttempts === 0 ? 1 : reconnectionBackoffRate;
|
76
|
+
const jitter = jitterMax * Math.random();
|
77
|
+
return staleThreshold * 1e3 * backoff * (1 + jitter);
|
78
|
+
}
|
79
|
+
reconnectIfStale() {
|
80
|
+
if (this.connectionIsStale()) {
|
81
|
+
logger.log(`ConnectionMonitor detected stale connection. reconnectAttempts = ${this.reconnectAttempts}, time stale = ${secondsSince(this.refreshedAt)} s, stale threshold = ${this.constructor.staleThreshold} s`);
|
82
|
+
this.reconnectAttempts++;
|
83
|
+
if (this.disconnectedRecently()) {
|
84
|
+
logger.log(`ConnectionMonitor skipping reopening recent disconnect. time disconnected = ${secondsSince(this.disconnectedAt)} s`);
|
85
|
+
} else {
|
86
|
+
logger.log("ConnectionMonitor reopening");
|
87
|
+
this.connection.reopen();
|
88
|
+
}
|
89
|
+
}
|
90
|
+
}
|
91
|
+
get refreshedAt() {
|
92
|
+
return this.pingedAt ? this.pingedAt : this.startedAt;
|
93
|
+
}
|
94
|
+
connectionIsStale() {
|
95
|
+
return secondsSince(this.refreshedAt) > this.constructor.staleThreshold;
|
96
|
+
}
|
97
|
+
disconnectedRecently() {
|
98
|
+
return this.disconnectedAt && secondsSince(this.disconnectedAt) < this.constructor.staleThreshold;
|
99
|
+
}
|
100
|
+
visibilityDidChange() {
|
101
|
+
if (document.visibilityState === "visible") {
|
102
|
+
setTimeout((() => {
|
103
|
+
if (this.connectionIsStale() || !this.connection.isOpen()) {
|
104
|
+
logger.log(`ConnectionMonitor reopening stale connection on visibilitychange. visibilityState = ${document.visibilityState}`);
|
105
|
+
this.connection.reopen();
|
106
|
+
}
|
107
|
+
}), 200);
|
108
|
+
}
|
109
|
+
}
|
110
|
+
}
|
111
|
+
ConnectionMonitor.staleThreshold = 6;
|
112
|
+
ConnectionMonitor.reconnectionBackoffRate = .15;
|
113
|
+
var INTERNAL = {
|
114
|
+
message_types: {
|
115
|
+
welcome: "welcome",
|
116
|
+
disconnect: "disconnect",
|
117
|
+
ping: "ping",
|
118
|
+
confirmation: "confirm_subscription",
|
119
|
+
rejection: "reject_subscription"
|
120
|
+
},
|
121
|
+
disconnect_reasons: {
|
122
|
+
unauthorized: "unauthorized",
|
123
|
+
invalid_request: "invalid_request",
|
124
|
+
server_restart: "server_restart",
|
125
|
+
remote: "remote"
|
126
|
+
},
|
127
|
+
default_mount_path: "/cable",
|
128
|
+
protocols: [ "actioncable-v1-json", "actioncable-unsupported" ]
|
129
|
+
};
|
130
|
+
const {message_types: message_types, protocols: protocols} = INTERNAL;
|
131
|
+
const supportedProtocols = protocols.slice(0, protocols.length - 1);
|
132
|
+
const indexOf = [].indexOf;
|
133
|
+
class Connection {
|
134
|
+
constructor(consumer) {
|
135
|
+
this.open = this.open.bind(this);
|
136
|
+
this.consumer = consumer;
|
137
|
+
this.subscriptions = this.consumer.subscriptions;
|
138
|
+
this.monitor = new ConnectionMonitor(this);
|
139
|
+
this.disconnected = true;
|
140
|
+
}
|
141
|
+
send(data) {
|
142
|
+
if (this.isOpen()) {
|
143
|
+
this.webSocket.send(JSON.stringify(data));
|
144
|
+
return true;
|
145
|
+
} else {
|
146
|
+
return false;
|
147
|
+
}
|
148
|
+
}
|
149
|
+
open() {
|
150
|
+
if (this.isActive()) {
|
151
|
+
logger.log(`Attempted to open WebSocket, but existing socket is ${this.getState()}`);
|
152
|
+
return false;
|
153
|
+
} else {
|
154
|
+
const socketProtocols = [ ...protocols, ...this.consumer.subprotocols || [] ];
|
155
|
+
logger.log(`Opening WebSocket, current state is ${this.getState()}, subprotocols: ${socketProtocols}`);
|
156
|
+
if (this.webSocket) {
|
157
|
+
this.uninstallEventHandlers();
|
158
|
+
}
|
159
|
+
this.webSocket = new adapters.WebSocket(this.consumer.url, socketProtocols);
|
160
|
+
this.installEventHandlers();
|
161
|
+
this.monitor.start();
|
162
|
+
return true;
|
163
|
+
}
|
164
|
+
}
|
165
|
+
close({allowReconnect: allowReconnect} = {
|
166
|
+
allowReconnect: true
|
167
|
+
}) {
|
168
|
+
if (!allowReconnect) {
|
169
|
+
this.monitor.stop();
|
170
|
+
}
|
171
|
+
if (this.isOpen()) {
|
172
|
+
return this.webSocket.close();
|
173
|
+
}
|
174
|
+
}
|
175
|
+
reopen() {
|
176
|
+
logger.log(`Reopening WebSocket, current state is ${this.getState()}`);
|
177
|
+
if (this.isActive()) {
|
178
|
+
try {
|
179
|
+
return this.close();
|
180
|
+
} catch (error) {
|
181
|
+
logger.log("Failed to reopen WebSocket", error);
|
182
|
+
} finally {
|
183
|
+
logger.log(`Reopening WebSocket in ${this.constructor.reopenDelay}ms`);
|
184
|
+
setTimeout(this.open, this.constructor.reopenDelay);
|
185
|
+
}
|
186
|
+
} else {
|
187
|
+
return this.open();
|
188
|
+
}
|
189
|
+
}
|
190
|
+
getProtocol() {
|
191
|
+
if (this.webSocket) {
|
192
|
+
return this.webSocket.protocol;
|
193
|
+
}
|
194
|
+
}
|
195
|
+
isOpen() {
|
196
|
+
return this.isState("open");
|
197
|
+
}
|
198
|
+
isActive() {
|
199
|
+
return this.isState("open", "connecting");
|
200
|
+
}
|
201
|
+
triedToReconnect() {
|
202
|
+
return this.monitor.reconnectAttempts > 0;
|
203
|
+
}
|
204
|
+
isProtocolSupported() {
|
205
|
+
return indexOf.call(supportedProtocols, this.getProtocol()) >= 0;
|
206
|
+
}
|
207
|
+
isState(...states) {
|
208
|
+
return indexOf.call(states, this.getState()) >= 0;
|
209
|
+
}
|
210
|
+
getState() {
|
211
|
+
if (this.webSocket) {
|
212
|
+
for (let state in adapters.WebSocket) {
|
213
|
+
if (adapters.WebSocket[state] === this.webSocket.readyState) {
|
214
|
+
return state.toLowerCase();
|
215
|
+
}
|
216
|
+
}
|
217
|
+
}
|
218
|
+
return null;
|
219
|
+
}
|
220
|
+
installEventHandlers() {
|
221
|
+
for (let eventName in this.events) {
|
222
|
+
const handler = this.events[eventName].bind(this);
|
223
|
+
this.webSocket[`on${eventName}`] = handler;
|
224
|
+
}
|
225
|
+
}
|
226
|
+
uninstallEventHandlers() {
|
227
|
+
for (let eventName in this.events) {
|
228
|
+
this.webSocket[`on${eventName}`] = function() {};
|
229
|
+
}
|
230
|
+
}
|
231
|
+
}
|
232
|
+
Connection.reopenDelay = 500;
|
233
|
+
Connection.prototype.events = {
|
234
|
+
message(event) {
|
235
|
+
if (!this.isProtocolSupported()) {
|
236
|
+
return;
|
237
|
+
}
|
238
|
+
const {identifier: identifier, message: message, reason: reason, reconnect: reconnect, type: type} = JSON.parse(event.data);
|
239
|
+
switch (type) {
|
240
|
+
case message_types.welcome:
|
241
|
+
if (this.triedToReconnect()) {
|
242
|
+
this.reconnectAttempted = true;
|
243
|
+
}
|
244
|
+
this.monitor.recordConnect();
|
245
|
+
return this.subscriptions.reload();
|
246
|
+
|
247
|
+
case message_types.disconnect:
|
248
|
+
logger.log(`Disconnecting. Reason: ${reason}`);
|
249
|
+
return this.close({
|
250
|
+
allowReconnect: reconnect
|
251
|
+
});
|
252
|
+
|
253
|
+
case message_types.ping:
|
254
|
+
return this.monitor.recordPing();
|
255
|
+
|
256
|
+
case message_types.confirmation:
|
257
|
+
this.subscriptions.confirmSubscription(identifier);
|
258
|
+
if (this.reconnectAttempted) {
|
259
|
+
this.reconnectAttempted = false;
|
260
|
+
return this.subscriptions.notify(identifier, "connected", {
|
261
|
+
reconnected: true
|
262
|
+
});
|
263
|
+
} else {
|
264
|
+
return this.subscriptions.notify(identifier, "connected", {
|
265
|
+
reconnected: false
|
266
|
+
});
|
267
|
+
}
|
268
|
+
|
269
|
+
case message_types.rejection:
|
270
|
+
return this.subscriptions.reject(identifier);
|
271
|
+
|
272
|
+
default:
|
273
|
+
return this.subscriptions.notify(identifier, "received", message);
|
274
|
+
}
|
275
|
+
},
|
276
|
+
open() {
|
277
|
+
logger.log(`WebSocket onopen event, using '${this.getProtocol()}' subprotocol`);
|
278
|
+
this.disconnected = false;
|
279
|
+
if (!this.isProtocolSupported()) {
|
280
|
+
logger.log("Protocol is unsupported. Stopping monitor and disconnecting.");
|
281
|
+
return this.close({
|
282
|
+
allowReconnect: false
|
283
|
+
});
|
284
|
+
}
|
285
|
+
},
|
286
|
+
close(event) {
|
287
|
+
logger.log("WebSocket onclose event");
|
288
|
+
if (this.disconnected) {
|
289
|
+
return;
|
290
|
+
}
|
291
|
+
this.disconnected = true;
|
292
|
+
this.monitor.recordDisconnect();
|
293
|
+
return this.subscriptions.notifyAll("disconnected", {
|
294
|
+
willAttemptReconnect: this.monitor.isRunning()
|
295
|
+
});
|
296
|
+
},
|
297
|
+
error() {
|
298
|
+
logger.log("WebSocket onerror event");
|
299
|
+
}
|
300
|
+
};
|
301
|
+
const extend = function(object, properties) {
|
302
|
+
if (properties != null) {
|
303
|
+
for (let key in properties) {
|
304
|
+
const value = properties[key];
|
305
|
+
object[key] = value;
|
306
|
+
}
|
307
|
+
}
|
308
|
+
return object;
|
309
|
+
};
|
310
|
+
class Subscription {
|
311
|
+
constructor(consumer, params = {}, mixin) {
|
312
|
+
this.consumer = consumer;
|
313
|
+
this.identifier = JSON.stringify(params);
|
314
|
+
extend(this, mixin);
|
315
|
+
}
|
316
|
+
perform(action, data = {}) {
|
317
|
+
data.action = action;
|
318
|
+
return this.send(data);
|
319
|
+
}
|
320
|
+
send(data) {
|
321
|
+
return this.consumer.send({
|
322
|
+
command: "message",
|
323
|
+
identifier: this.identifier,
|
324
|
+
data: JSON.stringify(data)
|
325
|
+
});
|
326
|
+
}
|
327
|
+
unsubscribe() {
|
328
|
+
return this.consumer.subscriptions.remove(this);
|
329
|
+
}
|
330
|
+
}
|
331
|
+
class SubscriptionGuarantor {
|
332
|
+
constructor(subscriptions) {
|
333
|
+
this.subscriptions = subscriptions;
|
334
|
+
this.pendingSubscriptions = [];
|
335
|
+
}
|
336
|
+
guarantee(subscription) {
|
337
|
+
if (this.pendingSubscriptions.indexOf(subscription) == -1) {
|
338
|
+
logger.log(`SubscriptionGuarantor guaranteeing ${subscription.identifier}`);
|
339
|
+
this.pendingSubscriptions.push(subscription);
|
340
|
+
} else {
|
341
|
+
logger.log(`SubscriptionGuarantor already guaranteeing ${subscription.identifier}`);
|
342
|
+
}
|
343
|
+
this.startGuaranteeing();
|
344
|
+
}
|
345
|
+
forget(subscription) {
|
346
|
+
logger.log(`SubscriptionGuarantor forgetting ${subscription.identifier}`);
|
347
|
+
this.pendingSubscriptions = this.pendingSubscriptions.filter((s => s !== subscription));
|
348
|
+
}
|
349
|
+
startGuaranteeing() {
|
350
|
+
this.stopGuaranteeing();
|
351
|
+
this.retrySubscribing();
|
352
|
+
}
|
353
|
+
stopGuaranteeing() {
|
354
|
+
clearTimeout(this.retryTimeout);
|
355
|
+
}
|
356
|
+
retrySubscribing() {
|
357
|
+
this.retryTimeout = setTimeout((() => {
|
358
|
+
if (this.subscriptions && typeof this.subscriptions.subscribe === "function") {
|
359
|
+
this.pendingSubscriptions.map((subscription => {
|
360
|
+
logger.log(`SubscriptionGuarantor resubscribing ${subscription.identifier}`);
|
361
|
+
this.subscriptions.subscribe(subscription);
|
362
|
+
}));
|
363
|
+
}
|
364
|
+
}), 500);
|
365
|
+
}
|
366
|
+
}
|
367
|
+
class Subscriptions {
|
368
|
+
constructor(consumer) {
|
369
|
+
this.consumer = consumer;
|
370
|
+
this.guarantor = new SubscriptionGuarantor(this);
|
371
|
+
this.subscriptions = [];
|
372
|
+
}
|
373
|
+
create(channelName, mixin) {
|
374
|
+
const channel = channelName;
|
375
|
+
const params = typeof channel === "object" ? channel : {
|
376
|
+
channel: channel
|
377
|
+
};
|
378
|
+
const subscription = new Subscription(this.consumer, params, mixin);
|
379
|
+
return this.add(subscription);
|
380
|
+
}
|
381
|
+
add(subscription) {
|
382
|
+
this.subscriptions.push(subscription);
|
383
|
+
this.consumer.ensureActiveConnection();
|
384
|
+
this.notify(subscription, "initialized");
|
385
|
+
this.subscribe(subscription);
|
386
|
+
return subscription;
|
387
|
+
}
|
388
|
+
remove(subscription) {
|
389
|
+
this.forget(subscription);
|
390
|
+
if (!this.findAll(subscription.identifier).length) {
|
391
|
+
this.sendCommand(subscription, "unsubscribe");
|
392
|
+
}
|
393
|
+
return subscription;
|
394
|
+
}
|
395
|
+
reject(identifier) {
|
396
|
+
return this.findAll(identifier).map((subscription => {
|
397
|
+
this.forget(subscription);
|
398
|
+
this.notify(subscription, "rejected");
|
399
|
+
return subscription;
|
400
|
+
}));
|
401
|
+
}
|
402
|
+
forget(subscription) {
|
403
|
+
this.guarantor.forget(subscription);
|
404
|
+
this.subscriptions = this.subscriptions.filter((s => s !== subscription));
|
405
|
+
return subscription;
|
406
|
+
}
|
407
|
+
findAll(identifier) {
|
408
|
+
return this.subscriptions.filter((s => s.identifier === identifier));
|
409
|
+
}
|
410
|
+
reload() {
|
411
|
+
return this.subscriptions.map((subscription => this.subscribe(subscription)));
|
412
|
+
}
|
413
|
+
notifyAll(callbackName, ...args) {
|
414
|
+
return this.subscriptions.map((subscription => this.notify(subscription, callbackName, ...args)));
|
415
|
+
}
|
416
|
+
notify(subscription, callbackName, ...args) {
|
417
|
+
let subscriptions;
|
418
|
+
if (typeof subscription === "string") {
|
419
|
+
subscriptions = this.findAll(subscription);
|
420
|
+
} else {
|
421
|
+
subscriptions = [ subscription ];
|
422
|
+
}
|
423
|
+
return subscriptions.map((subscription => typeof subscription[callbackName] === "function" ? subscription[callbackName](...args) : undefined));
|
424
|
+
}
|
425
|
+
subscribe(subscription) {
|
426
|
+
if (this.sendCommand(subscription, "subscribe")) {
|
427
|
+
this.guarantor.guarantee(subscription);
|
428
|
+
}
|
429
|
+
}
|
430
|
+
confirmSubscription(identifier) {
|
431
|
+
logger.log(`Subscription confirmed ${identifier}`);
|
432
|
+
this.findAll(identifier).map((subscription => this.guarantor.forget(subscription)));
|
433
|
+
}
|
434
|
+
sendCommand(subscription, command) {
|
435
|
+
const {identifier: identifier} = subscription;
|
436
|
+
return this.consumer.send({
|
437
|
+
command: command,
|
438
|
+
identifier: identifier
|
439
|
+
});
|
440
|
+
}
|
441
|
+
}
|
442
|
+
class Consumer {
|
443
|
+
constructor(url) {
|
444
|
+
this._url = url;
|
445
|
+
this.subscriptions = new Subscriptions(this);
|
446
|
+
this.connection = new Connection(this);
|
447
|
+
this.subprotocols = [];
|
448
|
+
}
|
449
|
+
get url() {
|
450
|
+
return createWebSocketURL(this._url);
|
451
|
+
}
|
452
|
+
send(data) {
|
453
|
+
return this.connection.send(data);
|
454
|
+
}
|
455
|
+
connect() {
|
456
|
+
return this.connection.open();
|
457
|
+
}
|
458
|
+
disconnect() {
|
459
|
+
return this.connection.close({
|
460
|
+
allowReconnect: false
|
461
|
+
});
|
462
|
+
}
|
463
|
+
ensureActiveConnection() {
|
464
|
+
if (!this.connection.isActive()) {
|
465
|
+
return this.connection.open();
|
466
|
+
}
|
467
|
+
}
|
468
|
+
addSubProtocol(subprotocol) {
|
469
|
+
this.subprotocols = [ ...this.subprotocols, subprotocol ];
|
470
|
+
}
|
471
|
+
}
|
472
|
+
function createWebSocketURL(url) {
|
473
|
+
if (typeof url === "function") {
|
474
|
+
url = url();
|
475
|
+
}
|
476
|
+
if (url && !/^wss?:/i.test(url)) {
|
477
|
+
const a = document.createElement("a");
|
478
|
+
a.href = url;
|
479
|
+
a.href = a.href;
|
480
|
+
a.protocol = a.protocol.replace("http", "ws");
|
481
|
+
return a.href;
|
482
|
+
} else {
|
483
|
+
return url;
|
484
|
+
}
|
485
|
+
}
|
486
|
+
function createConsumer(url = getConfig("url") || INTERNAL.default_mount_path) {
|
487
|
+
return new Consumer(url);
|
488
|
+
}
|
489
|
+
function getConfig(name) {
|
490
|
+
const element = document.head.querySelector(`meta[name='action-cable-${name}']`);
|
491
|
+
if (element) {
|
492
|
+
return element.getAttribute("content");
|
493
|
+
}
|
494
|
+
}
|
495
|
+
exports.Connection = Connection;
|
496
|
+
exports.ConnectionMonitor = ConnectionMonitor;
|
497
|
+
exports.Consumer = Consumer;
|
498
|
+
exports.INTERNAL = INTERNAL;
|
499
|
+
exports.Subscription = Subscription;
|
500
|
+
exports.SubscriptionGuarantor = SubscriptionGuarantor;
|
501
|
+
exports.Subscriptions = Subscriptions;
|
502
|
+
exports.adapters = adapters;
|
503
|
+
exports.createConsumer = createConsumer;
|
504
|
+
exports.createWebSocketURL = createWebSocketURL;
|
505
|
+
exports.getConfig = getConfig;
|
506
|
+
exports.logger = logger;
|
507
|
+
Object.defineProperty(exports, "__esModule", {
|
508
|
+
value: true
|
509
|
+
});
|
510
|
+
}));
|
package/package.json
CHANGED
@@ -1,8 +1,9 @@
|
|
1
1
|
{
|
2
2
|
"name": "@rails/actioncable",
|
3
|
-
"version": "
|
3
|
+
"version": "7.1.2",
|
4
4
|
"description": "WebSocket framework for Ruby on Rails.",
|
5
|
-
"
|
5
|
+
"module": "app/assets/javascripts/actioncable.esm.js",
|
6
|
+
"main": "app/assets/javascripts/actioncable.js",
|
6
7
|
"files": [
|
7
8
|
"app/assets/javascripts/*.js",
|
8
9
|
"src/*.js"
|
@@ -21,11 +22,10 @@
|
|
21
22
|
"bugs": {
|
22
23
|
"url": "https://github.com/rails/rails/issues"
|
23
24
|
},
|
24
|
-
"homepage": "
|
25
|
+
"homepage": "https://rubyonrails.org/",
|
25
26
|
"devDependencies": {
|
26
|
-
"
|
27
|
-
"
|
28
|
-
"babel-preset-env": "^1.6.0",
|
27
|
+
"@rollup/plugin-node-resolve": "^11.0.1",
|
28
|
+
"@rollup/plugin-commonjs": "^19.0.1",
|
29
29
|
"eslint": "^4.3.0",
|
30
30
|
"eslint-plugin-import": "^2.7.0",
|
31
31
|
"karma": "^3.1.1",
|
@@ -34,11 +34,8 @@
|
|
34
34
|
"karma-sauce-launcher": "^1.2.0",
|
35
35
|
"mock-socket": "^2.0.0",
|
36
36
|
"qunit": "^2.8.0",
|
37
|
-
"rollup": "^
|
38
|
-
"rollup-plugin-
|
39
|
-
"rollup-plugin-commonjs": "^9.1.0",
|
40
|
-
"rollup-plugin-node-resolve": "^3.3.0",
|
41
|
-
"rollup-plugin-uglify": "^3.0.0"
|
37
|
+
"rollup": "^2.35.1",
|
38
|
+
"rollup-plugin-terser": "^7.0.2"
|
42
39
|
},
|
43
40
|
"scripts": {
|
44
41
|
"prebuild": "yarn lint && bundle exec rake assets:codegen",
|
package/src/connection.js
CHANGED
@@ -33,9 +33,10 @@ class Connection {
|
|
33
33
|
logger.log(`Attempted to open WebSocket, but existing socket is ${this.getState()}`)
|
34
34
|
return false
|
35
35
|
} else {
|
36
|
-
|
36
|
+
const socketProtocols = [...protocols, ...this.consumer.subprotocols || []]
|
37
|
+
logger.log(`Opening WebSocket, current state is ${this.getState()}, subprotocols: ${socketProtocols}`)
|
37
38
|
if (this.webSocket) { this.uninstallEventHandlers() }
|
38
|
-
this.webSocket = new adapters.WebSocket(this.consumer.url,
|
39
|
+
this.webSocket = new adapters.WebSocket(this.consumer.url, socketProtocols)
|
39
40
|
this.installEventHandlers()
|
40
41
|
this.monitor.start()
|
41
42
|
return true
|
@@ -44,7 +45,8 @@ class Connection {
|
|
44
45
|
|
45
46
|
close({allowReconnect} = {allowReconnect: true}) {
|
46
47
|
if (!allowReconnect) { this.monitor.stop() }
|
47
|
-
|
48
|
+
// Avoid closing websockets in a "connecting" state due to Safari 15.1+ bug. See: https://github.com/rails/rails/issues/43835#issuecomment-1002288478
|
49
|
+
if (this.isOpen()) {
|
48
50
|
return this.webSocket.close()
|
49
51
|
}
|
50
52
|
}
|
@@ -80,6 +82,10 @@ class Connection {
|
|
80
82
|
return this.isState("open", "connecting")
|
81
83
|
}
|
82
84
|
|
85
|
+
triedToReconnect() {
|
86
|
+
return this.monitor.reconnectAttempts > 0
|
87
|
+
}
|
88
|
+
|
83
89
|
// Private
|
84
90
|
|
85
91
|
isProtocolSupported() {
|
@@ -124,6 +130,9 @@ Connection.prototype.events = {
|
|
124
130
|
const {identifier, message, reason, reconnect, type} = JSON.parse(event.data)
|
125
131
|
switch (type) {
|
126
132
|
case message_types.welcome:
|
133
|
+
if (this.triedToReconnect()) {
|
134
|
+
this.reconnectAttempted = true
|
135
|
+
}
|
127
136
|
this.monitor.recordConnect()
|
128
137
|
return this.subscriptions.reload()
|
129
138
|
case message_types.disconnect:
|
@@ -132,7 +141,13 @@ Connection.prototype.events = {
|
|
132
141
|
case message_types.ping:
|
133
142
|
return this.monitor.recordPing()
|
134
143
|
case message_types.confirmation:
|
135
|
-
|
144
|
+
this.subscriptions.confirmSubscription(identifier)
|
145
|
+
if (this.reconnectAttempted) {
|
146
|
+
this.reconnectAttempted = false
|
147
|
+
return this.subscriptions.notify(identifier, "connected", {reconnected: true})
|
148
|
+
} else {
|
149
|
+
return this.subscriptions.notify(identifier, "connected", {reconnected: false})
|
150
|
+
}
|
136
151
|
case message_types.rejection:
|
137
152
|
return this.subscriptions.reject(identifier)
|
138
153
|
default:
|
@@ -7,8 +7,6 @@ const now = () => new Date().getTime()
|
|
7
7
|
|
8
8
|
const secondsSince = time => (now() - time) / 1000
|
9
9
|
|
10
|
-
const clamp = (number, min, max) => Math.max(min, Math.min(max, number))
|
11
|
-
|
12
10
|
class ConnectionMonitor {
|
13
11
|
constructor(connection) {
|
14
12
|
this.visibilityDidChange = this.visibilityDidChange.bind(this)
|
@@ -22,7 +20,7 @@ class ConnectionMonitor {
|
|
22
20
|
delete this.stoppedAt
|
23
21
|
this.startPolling()
|
24
22
|
addEventListener("visibilitychange", this.visibilityDidChange)
|
25
|
-
logger.log(`ConnectionMonitor started.
|
23
|
+
logger.log(`ConnectionMonitor started. stale threshold = ${this.constructor.staleThreshold} s`)
|
26
24
|
}
|
27
25
|
}
|
28
26
|
|
@@ -75,17 +73,19 @@ class ConnectionMonitor {
|
|
75
73
|
}
|
76
74
|
|
77
75
|
getPollInterval() {
|
78
|
-
const {
|
79
|
-
const
|
80
|
-
|
76
|
+
const { staleThreshold, reconnectionBackoffRate } = this.constructor
|
77
|
+
const backoff = Math.pow(1 + reconnectionBackoffRate, Math.min(this.reconnectAttempts, 10))
|
78
|
+
const jitterMax = this.reconnectAttempts === 0 ? 1.0 : reconnectionBackoffRate
|
79
|
+
const jitter = jitterMax * Math.random()
|
80
|
+
return staleThreshold * 1000 * backoff * (1 + jitter)
|
81
81
|
}
|
82
82
|
|
83
83
|
reconnectIfStale() {
|
84
84
|
if (this.connectionIsStale()) {
|
85
|
-
logger.log(`ConnectionMonitor detected stale connection. reconnectAttempts = ${this.reconnectAttempts},
|
85
|
+
logger.log(`ConnectionMonitor detected stale connection. reconnectAttempts = ${this.reconnectAttempts}, time stale = ${secondsSince(this.refreshedAt)} s, stale threshold = ${this.constructor.staleThreshold} s`)
|
86
86
|
this.reconnectAttempts++
|
87
87
|
if (this.disconnectedRecently()) {
|
88
|
-
logger.log(
|
88
|
+
logger.log(`ConnectionMonitor skipping reopening recent disconnect. time disconnected = ${secondsSince(this.disconnectedAt)} s`)
|
89
89
|
} else {
|
90
90
|
logger.log("ConnectionMonitor reopening")
|
91
91
|
this.connection.reopen()
|
@@ -93,8 +93,12 @@ class ConnectionMonitor {
|
|
93
93
|
}
|
94
94
|
}
|
95
95
|
|
96
|
+
get refreshedAt() {
|
97
|
+
return this.pingedAt ? this.pingedAt : this.startedAt
|
98
|
+
}
|
99
|
+
|
96
100
|
connectionIsStale() {
|
97
|
-
return secondsSince(this.
|
101
|
+
return secondsSince(this.refreshedAt) > this.constructor.staleThreshold
|
98
102
|
}
|
99
103
|
|
100
104
|
disconnectedRecently() {
|
@@ -105,7 +109,7 @@ class ConnectionMonitor {
|
|
105
109
|
if (document.visibilityState === "visible") {
|
106
110
|
setTimeout(() => {
|
107
111
|
if (this.connectionIsStale() || !this.connection.isOpen()) {
|
108
|
-
logger.log(`ConnectionMonitor reopening stale connection on visibilitychange.
|
112
|
+
logger.log(`ConnectionMonitor reopening stale connection on visibilitychange. visibilityState = ${document.visibilityState}`)
|
109
113
|
this.connection.reopen()
|
110
114
|
}
|
111
115
|
}
|
@@ -115,12 +119,7 @@ class ConnectionMonitor {
|
|
115
119
|
|
116
120
|
}
|
117
121
|
|
118
|
-
ConnectionMonitor.pollInterval = {
|
119
|
-
min: 3,
|
120
|
-
max: 30,
|
121
|
-
multiplier: 5
|
122
|
-
}
|
123
|
-
|
124
122
|
ConnectionMonitor.staleThreshold = 6 // Server::Connections::BEAT_INTERVAL * 2 (missed two pings)
|
123
|
+
ConnectionMonitor.reconnectionBackoffRate = 0.15
|
125
124
|
|
126
125
|
export default ConnectionMonitor
|