actioncable 6.1.3.2 → 7.0.8

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.
Files changed (37) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +140 -31
  3. data/MIT-LICENSE +1 -1
  4. data/README.md +1 -1
  5. data/app/assets/javascripts/action_cable.js +230 -257
  6. data/app/assets/javascripts/actioncable.esm.js +491 -0
  7. data/app/assets/javascripts/actioncable.js +489 -0
  8. data/lib/action_cable/channel/base.rb +1 -1
  9. data/lib/action_cable/channel/broadcasting.rb +1 -1
  10. data/lib/action_cable/channel/naming.rb +1 -1
  11. data/lib/action_cable/channel/streams.rb +5 -7
  12. data/lib/action_cable/channel/test_case.rb +16 -1
  13. data/lib/action_cable/connection/base.rb +5 -5
  14. data/lib/action_cable/connection/identification.rb +1 -1
  15. data/lib/action_cable/connection/subscriptions.rb +1 -1
  16. data/lib/action_cable/connection/tagged_logger_proxy.rb +3 -3
  17. data/lib/action_cable/connection/test_case.rb +1 -1
  18. data/lib/action_cable/engine.rb +10 -1
  19. data/lib/action_cable/gem_version.rb +5 -5
  20. data/lib/action_cable/helpers/action_cable_helper.rb +3 -2
  21. data/lib/action_cable/remote_connections.rb +1 -1
  22. data/lib/action_cable/server/broadcasting.rb +1 -1
  23. data/lib/action_cable/server/configuration.rb +1 -0
  24. data/lib/action_cable/server/worker/active_record_connection_management.rb +2 -2
  25. data/lib/action_cable/server/worker.rb +3 -4
  26. data/lib/action_cable/subscription_adapter/postgresql.rb +2 -2
  27. data/lib/action_cable/subscription_adapter/redis.rb +98 -22
  28. data/lib/action_cable/subscription_adapter/test.rb +1 -1
  29. data/lib/action_cable/test_helper.rb +2 -2
  30. data/lib/action_cable/version.rb +1 -1
  31. data/lib/action_cable.rb +1 -1
  32. data/lib/rails/generators/channel/USAGE +1 -1
  33. data/lib/rails/generators/channel/channel_generator.rb +79 -20
  34. data/lib/rails/generators/channel/templates/javascript/index.js.tt +1 -5
  35. metadata +16 -13
  36. /data/lib/rails/generators/channel/templates/application_cable/{channel.rb.tt → channel.rb} +0 -0
  37. /data/lib/rails/generators/channel/templates/application_cable/{connection.rb.tt → connection.rb} +0 -0
@@ -0,0 +1,489 @@
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
+ },
126
+ default_mount_path: "/cable",
127
+ protocols: [ "actioncable-v1-json", "actioncable-unsupported" ]
128
+ };
129
+ const {message_types: message_types, protocols: protocols} = INTERNAL;
130
+ const supportedProtocols = protocols.slice(0, protocols.length - 1);
131
+ const indexOf = [].indexOf;
132
+ class Connection {
133
+ constructor(consumer) {
134
+ this.open = this.open.bind(this);
135
+ this.consumer = consumer;
136
+ this.subscriptions = this.consumer.subscriptions;
137
+ this.monitor = new ConnectionMonitor(this);
138
+ this.disconnected = true;
139
+ }
140
+ send(data) {
141
+ if (this.isOpen()) {
142
+ this.webSocket.send(JSON.stringify(data));
143
+ return true;
144
+ } else {
145
+ return false;
146
+ }
147
+ }
148
+ open() {
149
+ if (this.isActive()) {
150
+ logger.log(`Attempted to open WebSocket, but existing socket is ${this.getState()}`);
151
+ return false;
152
+ } else {
153
+ logger.log(`Opening WebSocket, current state is ${this.getState()}, subprotocols: ${protocols}`);
154
+ if (this.webSocket) {
155
+ this.uninstallEventHandlers();
156
+ }
157
+ this.webSocket = new adapters.WebSocket(this.consumer.url, protocols);
158
+ this.installEventHandlers();
159
+ this.monitor.start();
160
+ return true;
161
+ }
162
+ }
163
+ close({allowReconnect: allowReconnect} = {
164
+ allowReconnect: true
165
+ }) {
166
+ if (!allowReconnect) {
167
+ this.monitor.stop();
168
+ }
169
+ if (this.isOpen()) {
170
+ return this.webSocket.close();
171
+ }
172
+ }
173
+ reopen() {
174
+ logger.log(`Reopening WebSocket, current state is ${this.getState()}`);
175
+ if (this.isActive()) {
176
+ try {
177
+ return this.close();
178
+ } catch (error) {
179
+ logger.log("Failed to reopen WebSocket", error);
180
+ } finally {
181
+ logger.log(`Reopening WebSocket in ${this.constructor.reopenDelay}ms`);
182
+ setTimeout(this.open, this.constructor.reopenDelay);
183
+ }
184
+ } else {
185
+ return this.open();
186
+ }
187
+ }
188
+ getProtocol() {
189
+ if (this.webSocket) {
190
+ return this.webSocket.protocol;
191
+ }
192
+ }
193
+ isOpen() {
194
+ return this.isState("open");
195
+ }
196
+ isActive() {
197
+ return this.isState("open", "connecting");
198
+ }
199
+ isProtocolSupported() {
200
+ return indexOf.call(supportedProtocols, this.getProtocol()) >= 0;
201
+ }
202
+ isState(...states) {
203
+ return indexOf.call(states, this.getState()) >= 0;
204
+ }
205
+ getState() {
206
+ if (this.webSocket) {
207
+ for (let state in adapters.WebSocket) {
208
+ if (adapters.WebSocket[state] === this.webSocket.readyState) {
209
+ return state.toLowerCase();
210
+ }
211
+ }
212
+ }
213
+ return null;
214
+ }
215
+ installEventHandlers() {
216
+ for (let eventName in this.events) {
217
+ const handler = this.events[eventName].bind(this);
218
+ this.webSocket[`on${eventName}`] = handler;
219
+ }
220
+ }
221
+ uninstallEventHandlers() {
222
+ for (let eventName in this.events) {
223
+ this.webSocket[`on${eventName}`] = function() {};
224
+ }
225
+ }
226
+ }
227
+ Connection.reopenDelay = 500;
228
+ Connection.prototype.events = {
229
+ message(event) {
230
+ if (!this.isProtocolSupported()) {
231
+ return;
232
+ }
233
+ const {identifier: identifier, message: message, reason: reason, reconnect: reconnect, type: type} = JSON.parse(event.data);
234
+ switch (type) {
235
+ case message_types.welcome:
236
+ this.monitor.recordConnect();
237
+ return this.subscriptions.reload();
238
+
239
+ case message_types.disconnect:
240
+ logger.log(`Disconnecting. Reason: ${reason}`);
241
+ return this.close({
242
+ allowReconnect: reconnect
243
+ });
244
+
245
+ case message_types.ping:
246
+ return this.monitor.recordPing();
247
+
248
+ case message_types.confirmation:
249
+ this.subscriptions.confirmSubscription(identifier);
250
+ return this.subscriptions.notify(identifier, "connected");
251
+
252
+ case message_types.rejection:
253
+ return this.subscriptions.reject(identifier);
254
+
255
+ default:
256
+ return this.subscriptions.notify(identifier, "received", message);
257
+ }
258
+ },
259
+ open() {
260
+ logger.log(`WebSocket onopen event, using '${this.getProtocol()}' subprotocol`);
261
+ this.disconnected = false;
262
+ if (!this.isProtocolSupported()) {
263
+ logger.log("Protocol is unsupported. Stopping monitor and disconnecting.");
264
+ return this.close({
265
+ allowReconnect: false
266
+ });
267
+ }
268
+ },
269
+ close(event) {
270
+ logger.log("WebSocket onclose event");
271
+ if (this.disconnected) {
272
+ return;
273
+ }
274
+ this.disconnected = true;
275
+ this.monitor.recordDisconnect();
276
+ return this.subscriptions.notifyAll("disconnected", {
277
+ willAttemptReconnect: this.monitor.isRunning()
278
+ });
279
+ },
280
+ error() {
281
+ logger.log("WebSocket onerror event");
282
+ }
283
+ };
284
+ const extend = function(object, properties) {
285
+ if (properties != null) {
286
+ for (let key in properties) {
287
+ const value = properties[key];
288
+ object[key] = value;
289
+ }
290
+ }
291
+ return object;
292
+ };
293
+ class Subscription {
294
+ constructor(consumer, params = {}, mixin) {
295
+ this.consumer = consumer;
296
+ this.identifier = JSON.stringify(params);
297
+ extend(this, mixin);
298
+ }
299
+ perform(action, data = {}) {
300
+ data.action = action;
301
+ return this.send(data);
302
+ }
303
+ send(data) {
304
+ return this.consumer.send({
305
+ command: "message",
306
+ identifier: this.identifier,
307
+ data: JSON.stringify(data)
308
+ });
309
+ }
310
+ unsubscribe() {
311
+ return this.consumer.subscriptions.remove(this);
312
+ }
313
+ }
314
+ class SubscriptionGuarantor {
315
+ constructor(subscriptions) {
316
+ this.subscriptions = subscriptions;
317
+ this.pendingSubscriptions = [];
318
+ }
319
+ guarantee(subscription) {
320
+ if (this.pendingSubscriptions.indexOf(subscription) == -1) {
321
+ logger.log(`SubscriptionGuarantor guaranteeing ${subscription.identifier}`);
322
+ this.pendingSubscriptions.push(subscription);
323
+ } else {
324
+ logger.log(`SubscriptionGuarantor already guaranteeing ${subscription.identifier}`);
325
+ }
326
+ this.startGuaranteeing();
327
+ }
328
+ forget(subscription) {
329
+ logger.log(`SubscriptionGuarantor forgetting ${subscription.identifier}`);
330
+ this.pendingSubscriptions = this.pendingSubscriptions.filter((s => s !== subscription));
331
+ }
332
+ startGuaranteeing() {
333
+ this.stopGuaranteeing();
334
+ this.retrySubscribing();
335
+ }
336
+ stopGuaranteeing() {
337
+ clearTimeout(this.retryTimeout);
338
+ }
339
+ retrySubscribing() {
340
+ this.retryTimeout = setTimeout((() => {
341
+ if (this.subscriptions && typeof this.subscriptions.subscribe === "function") {
342
+ this.pendingSubscriptions.map((subscription => {
343
+ logger.log(`SubscriptionGuarantor resubscribing ${subscription.identifier}`);
344
+ this.subscriptions.subscribe(subscription);
345
+ }));
346
+ }
347
+ }), 500);
348
+ }
349
+ }
350
+ class Subscriptions {
351
+ constructor(consumer) {
352
+ this.consumer = consumer;
353
+ this.guarantor = new SubscriptionGuarantor(this);
354
+ this.subscriptions = [];
355
+ }
356
+ create(channelName, mixin) {
357
+ const channel = channelName;
358
+ const params = typeof channel === "object" ? channel : {
359
+ channel: channel
360
+ };
361
+ const subscription = new Subscription(this.consumer, params, mixin);
362
+ return this.add(subscription);
363
+ }
364
+ add(subscription) {
365
+ this.subscriptions.push(subscription);
366
+ this.consumer.ensureActiveConnection();
367
+ this.notify(subscription, "initialized");
368
+ this.subscribe(subscription);
369
+ return subscription;
370
+ }
371
+ remove(subscription) {
372
+ this.forget(subscription);
373
+ if (!this.findAll(subscription.identifier).length) {
374
+ this.sendCommand(subscription, "unsubscribe");
375
+ }
376
+ return subscription;
377
+ }
378
+ reject(identifier) {
379
+ return this.findAll(identifier).map((subscription => {
380
+ this.forget(subscription);
381
+ this.notify(subscription, "rejected");
382
+ return subscription;
383
+ }));
384
+ }
385
+ forget(subscription) {
386
+ this.guarantor.forget(subscription);
387
+ this.subscriptions = this.subscriptions.filter((s => s !== subscription));
388
+ return subscription;
389
+ }
390
+ findAll(identifier) {
391
+ return this.subscriptions.filter((s => s.identifier === identifier));
392
+ }
393
+ reload() {
394
+ return this.subscriptions.map((subscription => this.subscribe(subscription)));
395
+ }
396
+ notifyAll(callbackName, ...args) {
397
+ return this.subscriptions.map((subscription => this.notify(subscription, callbackName, ...args)));
398
+ }
399
+ notify(subscription, callbackName, ...args) {
400
+ let subscriptions;
401
+ if (typeof subscription === "string") {
402
+ subscriptions = this.findAll(subscription);
403
+ } else {
404
+ subscriptions = [ subscription ];
405
+ }
406
+ return subscriptions.map((subscription => typeof subscription[callbackName] === "function" ? subscription[callbackName](...args) : undefined));
407
+ }
408
+ subscribe(subscription) {
409
+ if (this.sendCommand(subscription, "subscribe")) {
410
+ this.guarantor.guarantee(subscription);
411
+ }
412
+ }
413
+ confirmSubscription(identifier) {
414
+ logger.log(`Subscription confirmed ${identifier}`);
415
+ this.findAll(identifier).map((subscription => this.guarantor.forget(subscription)));
416
+ }
417
+ sendCommand(subscription, command) {
418
+ const {identifier: identifier} = subscription;
419
+ return this.consumer.send({
420
+ command: command,
421
+ identifier: identifier
422
+ });
423
+ }
424
+ }
425
+ class Consumer {
426
+ constructor(url) {
427
+ this._url = url;
428
+ this.subscriptions = new Subscriptions(this);
429
+ this.connection = new Connection(this);
430
+ }
431
+ get url() {
432
+ return createWebSocketURL(this._url);
433
+ }
434
+ send(data) {
435
+ return this.connection.send(data);
436
+ }
437
+ connect() {
438
+ return this.connection.open();
439
+ }
440
+ disconnect() {
441
+ return this.connection.close({
442
+ allowReconnect: false
443
+ });
444
+ }
445
+ ensureActiveConnection() {
446
+ if (!this.connection.isActive()) {
447
+ return this.connection.open();
448
+ }
449
+ }
450
+ }
451
+ function createWebSocketURL(url) {
452
+ if (typeof url === "function") {
453
+ url = url();
454
+ }
455
+ if (url && !/^wss?:/i.test(url)) {
456
+ const a = document.createElement("a");
457
+ a.href = url;
458
+ a.href = a.href;
459
+ a.protocol = a.protocol.replace("http", "ws");
460
+ return a.href;
461
+ } else {
462
+ return url;
463
+ }
464
+ }
465
+ function createConsumer(url = getConfig("url") || INTERNAL.default_mount_path) {
466
+ return new Consumer(url);
467
+ }
468
+ function getConfig(name) {
469
+ const element = document.head.querySelector(`meta[name='action-cable-${name}']`);
470
+ if (element) {
471
+ return element.getAttribute("content");
472
+ }
473
+ }
474
+ exports.Connection = Connection;
475
+ exports.ConnectionMonitor = ConnectionMonitor;
476
+ exports.Consumer = Consumer;
477
+ exports.INTERNAL = INTERNAL;
478
+ exports.Subscription = Subscription;
479
+ exports.SubscriptionGuarantor = SubscriptionGuarantor;
480
+ exports.Subscriptions = Subscriptions;
481
+ exports.adapters = adapters;
482
+ exports.createConsumer = createConsumer;
483
+ exports.createWebSocketURL = createWebSocketURL;
484
+ exports.getConfig = getConfig;
485
+ exports.logger = logger;
486
+ Object.defineProperty(exports, "__esModule", {
487
+ value: true
488
+ });
489
+ }));
@@ -186,7 +186,7 @@ module ActionCable
186
186
  end
187
187
 
188
188
  # Called by the cable connection when it's cut, so the channel has a chance to cleanup with callbacks.
189
- # This method is not intended to be called directly by the user. Instead, overwrite the #unsubscribed callback.
189
+ # This method is not intended to be called directly by the user. Instead, override the #unsubscribed callback.
190
190
  def unsubscribe_from_channel # :nodoc:
191
191
  run_callbacks :unsubscribe do
192
192
  unsubscribed
@@ -25,7 +25,7 @@ module ActionCable
25
25
  serialize_broadcasting([ channel_name, model ])
26
26
  end
27
27
 
28
- def serialize_broadcasting(object) #:nodoc:
28
+ def serialize_broadcasting(object) # :nodoc:
29
29
  case
30
30
  when object.is_a?(Array)
31
31
  object.map { |m| serialize_broadcasting(m) }.join(":")
@@ -18,7 +18,7 @@ module ActionCable
18
18
  end
19
19
  end
20
20
 
21
- # Delegates to the class' <tt>channel_name</tt>
21
+ # Delegates to the class's ::channel_name.
22
22
  delegate :channel_name, to: :class
23
23
  end
24
24
  end
@@ -124,13 +124,11 @@ module ActionCable
124
124
  end.clear
125
125
  end
126
126
 
127
- # Calls stream_for if record is present, otherwise calls reject.
128
- # This method is intended to be called when you're looking
129
- # for a record based on a parameter, if its found it will start
130
- # streaming. If the record is nil then it will reject the connection.
131
- def stream_or_reject_for(record)
132
- if record
133
- stream_for record
127
+ # Calls stream_for with the given <tt>model</tt> if it's present to start streaming,
128
+ # otherwise rejects the subscription.
129
+ def stream_or_reject_for(model)
130
+ if model
131
+ stream_for model
134
132
  else
135
133
  reject
136
134
  end
@@ -62,6 +62,21 @@ module ActionCable
62
62
  def transmit(cable_message)
63
63
  transmissions << cable_message.with_indifferent_access
64
64
  end
65
+
66
+ def connection_identifier
67
+ @connection_identifier ||= connection_gid(identifiers.filter_map { |id| send(id.to_sym) if id })
68
+ end
69
+
70
+ private
71
+ def connection_gid(ids)
72
+ ids.map do |o|
73
+ if o.respond_to?(:to_gid_param)
74
+ o.to_gid_param
75
+ else
76
+ o.to_s
77
+ end
78
+ end.sort.join(":")
79
+ end
65
80
  end
66
81
 
67
82
  # Superclass for Action Cable channel functional tests.
@@ -246,7 +261,7 @@ module ActionCable
246
261
  # Returns messages transmitted into channel
247
262
  def transmissions
248
263
  # Return only directly sent message (via #transmit)
249
- connection.transmissions.map { |data| data["message"] }.compact
264
+ connection.transmissions.filter_map { |data| data["message"] }
250
265
  end
251
266
 
252
267
  # Enhance TestHelper assertions to handle non-String
@@ -68,7 +68,7 @@ module ActionCable
68
68
 
69
69
  # Called by the server when a new WebSocket connection is established. This configures the callbacks intended for overwriting by the user.
70
70
  # This method should not be called directly -- instead rely upon on the #connect (and #disconnect) callbacks.
71
- def process #:nodoc:
71
+ def process # :nodoc:
72
72
  logger.info started_request_message
73
73
 
74
74
  if websocket.possible? && allow_request_origin?
@@ -80,11 +80,11 @@ module ActionCable
80
80
 
81
81
  # Decodes WebSocket messages and dispatches them to subscribed channels.
82
82
  # WebSocket message transfer encoding is always JSON.
83
- def receive(websocket_message) #:nodoc:
83
+ def receive(websocket_message) # :nodoc:
84
84
  send_async :dispatch_websocket_message, websocket_message
85
85
  end
86
86
 
87
- def dispatch_websocket_message(websocket_message) #:nodoc:
87
+ def dispatch_websocket_message(websocket_message) # :nodoc:
88
88
  if websocket.alive?
89
89
  subscriptions.execute_command decode(websocket_message)
90
90
  else
@@ -237,7 +237,7 @@ module ActionCable
237
237
  request.filtered_path,
238
238
  websocket.possible? ? " [WebSocket]" : "[non-WebSocket]",
239
239
  request.ip,
240
- Time.now.to_s ]
240
+ Time.now.to_default_s ]
241
241
  end
242
242
 
243
243
  def finished_request_message
@@ -245,7 +245,7 @@ module ActionCable
245
245
  request.filtered_path,
246
246
  websocket.possible? ? " [WebSocket]" : "[non-WebSocket]",
247
247
  request.ip,
248
- Time.now.to_s ]
248
+ Time.now.to_default_s ]
249
249
  end
250
250
 
251
251
  def invalid_request_message
@@ -26,7 +26,7 @@ module ActionCable
26
26
  # Return a single connection identifier that combines the value of all the registered identifiers into a single gid.
27
27
  def connection_identifier
28
28
  unless defined? @connection_identifier
29
- @connection_identifier = connection_gid identifiers.map { |id| instance_variable_get("@#{id}") }.compact
29
+ @connection_identifier = connection_gid identifiers.filter_map { |id| instance_variable_get("@#{id}") }
30
30
  end
31
31
 
32
32
  @connection_identifier
@@ -33,7 +33,7 @@ module ActionCable
33
33
 
34
34
  subscription_klass = id_options[:channel].safe_constantize
35
35
 
36
- if subscription_klass && ActionCable::Channel::Base >= subscription_klass
36
+ if subscription_klass && ActionCable::Channel::Base > subscription_klass
37
37
  subscription = subscription_klass.new(connection, id_key, id_options)
38
38
  subscriptions[id_key] = subscription
39
39
  subscription.subscribe_to_channel
@@ -3,7 +3,7 @@
3
3
  module ActionCable
4
4
  module Connection
5
5
  # Allows the use of per-connection tags against the server logger. This wouldn't work using the traditional
6
- # <tt>ActiveSupport::TaggedLogging</tt> enhanced Rails.logger, as that logger will reset the tags between requests.
6
+ # ActiveSupport::TaggedLogging enhanced Rails.logger, as that logger will reset the tags between requests.
7
7
  # The connection is long-lived, so it needs its own set of tags for its independent duration.
8
8
  class TaggedLoggerProxy
9
9
  attr_reader :tags
@@ -18,10 +18,10 @@ module ActionCable
18
18
  @tags = @tags.uniq
19
19
  end
20
20
 
21
- def tag(logger)
21
+ def tag(logger, &block)
22
22
  if logger.respond_to?(:tagged)
23
23
  current_tags = tags - logger.formatter.current_tags
24
- logger.tagged(*current_tags) { yield }
24
+ logger.tagged(*current_tags, &block)
25
25
  else
26
26
  yield
27
27
  end
@@ -86,7 +86,7 @@ module ActionCable
86
86
  # end
87
87
  #
88
88
  # +connect+ accepts additional information about the HTTP request with the
89
- # +params+, +headers+, +session+ and Rack +env+ options.
89
+ # +params+, +headers+, +session+, and Rack +env+ options.
90
90
  #
91
91
  # def test_connect_with_headers_and_query_string
92
92
  # connect params: { user_id: 1 }, headers: { "X-API-TOKEN" => "secret-my" }
@@ -9,6 +9,7 @@ module ActionCable
9
9
  class Engine < Rails::Engine # :nodoc:
10
10
  config.action_cable = ActiveSupport::OrderedOptions.new
11
11
  config.action_cable.mount_path = ActionCable::INTERNAL[:default_mount_path]
12
+ config.action_cable.precompile_assets = true
12
13
 
13
14
  config.eager_load_namespaces << ActionCable
14
15
 
@@ -22,6 +23,14 @@ module ActionCable
22
23
  ActiveSupport.on_load(:action_cable) { self.logger ||= ::Rails.logger }
23
24
  end
24
25
 
26
+ initializer "action_cable.asset" do
27
+ config.after_initialize do |app|
28
+ if app.config.respond_to?(:assets) && app.config.action_cable.precompile_assets
29
+ app.config.assets.precompile += %w( actioncable.js actioncable.esm.js )
30
+ end
31
+ end
32
+ end
33
+
25
34
  initializer "action_cable.set_configs" do |app|
26
35
  options = app.config.action_cable
27
36
  options.allowed_request_origins ||= /https?:\/\/localhost:\d+/ if ::Rails.env.development?
@@ -45,7 +54,7 @@ module ActionCable
45
54
  config = app.config
46
55
  unless config.action_cable.mount_path.nil?
47
56
  app.routes.prepend do
48
- mount ActionCable.server => config.action_cable.mount_path, internal: true
57
+ mount ActionCable.server => config.action_cable.mount_path, internal: true, anchor: true
49
58
  end
50
59
  end
51
60
  end