@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.
@@ -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": "6.0.2",
3
+ "version": "7.1.2",
4
4
  "description": "WebSocket framework for Ruby on Rails.",
5
- "main": "app/assets/javascripts/action_cable.js",
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": "http://rubyonrails.org/",
25
+ "homepage": "https://rubyonrails.org/",
25
26
  "devDependencies": {
26
- "babel-core": "^6.25.0",
27
- "babel-plugin-external-helpers": "^6.22.0",
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": "^0.58.2",
38
- "rollup-plugin-babel": "^3.0.4",
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
- logger.log(`Opening WebSocket, current state is ${this.getState()}, subprotocols: ${protocols}`)
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, protocols)
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
- if (this.isActive()) {
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
- return this.subscriptions.notify(identifier, "connected")
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. pollInterval = ${this.getPollInterval()} ms`)
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 {min, max, multiplier} = this.constructor.pollInterval
79
- const interval = multiplier * Math.log(this.reconnectAttempts + 1)
80
- return Math.round(clamp(interval, min, max) * 1000)
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}, pollInterval = ${this.getPollInterval()} ms, time disconnected = ${secondsSince(this.disconnectedAt)} s, stale threshold = ${this.constructor.staleThreshold} s`)
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("ConnectionMonitor skipping reopening recent disconnect")
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.pingedAt ? this.pingedAt : this.startedAt) > this.constructor.staleThreshold
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. visbilityState = ${document.visibilityState}`)
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