@rails/actioncable 6.0.2 → 7.1.2

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