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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +140 -31
- data/MIT-LICENSE +1 -1
- data/README.md +1 -1
- data/app/assets/javascripts/action_cable.js +230 -257
- data/app/assets/javascripts/actioncable.esm.js +491 -0
- data/app/assets/javascripts/actioncable.js +489 -0
- data/lib/action_cable/channel/base.rb +1 -1
- data/lib/action_cable/channel/broadcasting.rb +1 -1
- data/lib/action_cable/channel/naming.rb +1 -1
- data/lib/action_cable/channel/streams.rb +5 -7
- data/lib/action_cable/channel/test_case.rb +16 -1
- data/lib/action_cable/connection/base.rb +5 -5
- data/lib/action_cable/connection/identification.rb +1 -1
- data/lib/action_cable/connection/subscriptions.rb +1 -1
- data/lib/action_cable/connection/tagged_logger_proxy.rb +3 -3
- data/lib/action_cable/connection/test_case.rb +1 -1
- data/lib/action_cable/engine.rb +10 -1
- data/lib/action_cable/gem_version.rb +5 -5
- data/lib/action_cable/helpers/action_cable_helper.rb +3 -2
- data/lib/action_cable/remote_connections.rb +1 -1
- data/lib/action_cable/server/broadcasting.rb +1 -1
- data/lib/action_cable/server/configuration.rb +1 -0
- data/lib/action_cable/server/worker/active_record_connection_management.rb +2 -2
- data/lib/action_cable/server/worker.rb +3 -4
- data/lib/action_cable/subscription_adapter/postgresql.rb +2 -2
- data/lib/action_cable/subscription_adapter/redis.rb +98 -22
- data/lib/action_cable/subscription_adapter/test.rb +1 -1
- data/lib/action_cable/test_helper.rb +2 -2
- data/lib/action_cable/version.rb +1 -1
- data/lib/action_cable.rb +1 -1
- data/lib/rails/generators/channel/USAGE +1 -1
- data/lib/rails/generators/channel/channel_generator.rb +79 -20
- data/lib/rails/generators/channel/templates/javascript/index.js.tt +1 -5
- metadata +16 -13
- /data/lib/rails/generators/channel/templates/application_cable/{channel.rb.tt → channel.rb} +0 -0
- /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,
|
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)
|
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(":")
|
@@ -124,13 +124,11 @@ module ActionCable
|
|
124
124
|
end.clear
|
125
125
|
end
|
126
126
|
|
127
|
-
# Calls stream_for if
|
128
|
-
#
|
129
|
-
|
130
|
-
|
131
|
-
|
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.
|
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
|
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)
|
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)
|
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.
|
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.
|
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.
|
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
|
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
|
-
#
|
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)
|
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
|
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" }
|
data/lib/action_cable/engine.rb
CHANGED
@@ -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
|