actioncable 7.0.0.alpha2 → 7.0.0.rc1
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 +12 -0
- data/app/assets/javascripts/action_cable.js +51 -2
- data/app/assets/javascripts/actioncable.esm.js +491 -0
- data/app/assets/javascripts/actioncable.js +489 -0
- data/lib/action_cable/channel/test_case.rb +15 -0
- data/lib/action_cable/gem_version.rb +1 -1
- metadata +13 -10
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 77fed9177b7396d25b06497f0415166d69ec9548766d1f4d626f8969f8479182
|
4
|
+
data.tar.gz: 9dab0cc363d5b4a4f9f6c351ca07640ecf51a19e309fe407b6ffc447eaa7537b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 776737a1c60a7f7c32eded16455f2394c9554833746f587578d2ff38f8af074029cd511a933726ad21d7bae0d8f5a369468521531d95473f409ffe545c7b882c
|
7
|
+
data.tar.gz: 276df3bead6e608db6ab342d5de7f460598cb5a23cb1a0623135090e8fefffd2a5ae18e88ec7ec87fd66a8231d1fa5a4f1c0a9b2d66d743c1aee7df291242f19
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,15 @@
|
|
1
|
+
* The Action Cable client now ensures successful channel subscriptions:
|
2
|
+
|
3
|
+
* The client maintains a set of pending subscriptions until either
|
4
|
+
the server confirms the subscription or the channel is torn down.
|
5
|
+
* Rectifies the race condition where an unsubscribe is rapidly followed
|
6
|
+
by a subscribe (on the same channel identifier) and the requests are
|
7
|
+
handled out of order by the ActionCable server, thereby ignoring the
|
8
|
+
subscribe command.
|
9
|
+
|
10
|
+
*Daniel Spinosa*
|
11
|
+
|
12
|
+
|
1
13
|
## Rails 7.0.0.alpha2 (September 15, 2021) ##
|
2
14
|
|
3
15
|
* No changes.
|
@@ -246,6 +246,7 @@
|
|
246
246
|
return this.monitor.recordPing();
|
247
247
|
|
248
248
|
case message_types.confirmation:
|
249
|
+
this.subscriptions.confirmSubscription(identifier);
|
249
250
|
return this.subscriptions.notify(identifier, "connected");
|
250
251
|
|
251
252
|
case message_types.rejection:
|
@@ -310,9 +311,46 @@
|
|
310
311
|
return this.consumer.subscriptions.remove(this);
|
311
312
|
}
|
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
|
+
}
|
313
350
|
class Subscriptions {
|
314
351
|
constructor(consumer) {
|
315
352
|
this.consumer = consumer;
|
353
|
+
this.guarantor = new SubscriptionGuarantor(this);
|
316
354
|
this.subscriptions = [];
|
317
355
|
}
|
318
356
|
create(channelName, mixin) {
|
@@ -327,7 +365,7 @@
|
|
327
365
|
this.subscriptions.push(subscription);
|
328
366
|
this.consumer.ensureActiveConnection();
|
329
367
|
this.notify(subscription, "initialized");
|
330
|
-
this.
|
368
|
+
this.subscribe(subscription);
|
331
369
|
return subscription;
|
332
370
|
}
|
333
371
|
remove(subscription) {
|
@@ -345,6 +383,7 @@
|
|
345
383
|
}));
|
346
384
|
}
|
347
385
|
forget(subscription) {
|
386
|
+
this.guarantor.forget(subscription);
|
348
387
|
this.subscriptions = this.subscriptions.filter((s => s !== subscription));
|
349
388
|
return subscription;
|
350
389
|
}
|
@@ -352,7 +391,7 @@
|
|
352
391
|
return this.subscriptions.filter((s => s.identifier === identifier));
|
353
392
|
}
|
354
393
|
reload() {
|
355
|
-
return this.subscriptions.map((subscription => this.
|
394
|
+
return this.subscriptions.map((subscription => this.subscribe(subscription)));
|
356
395
|
}
|
357
396
|
notifyAll(callbackName, ...args) {
|
358
397
|
return this.subscriptions.map((subscription => this.notify(subscription, callbackName, ...args)));
|
@@ -366,6 +405,15 @@
|
|
366
405
|
}
|
367
406
|
return subscriptions.map((subscription => typeof subscription[callbackName] === "function" ? subscription[callbackName](...args) : undefined));
|
368
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
|
+
}
|
369
417
|
sendCommand(subscription, command) {
|
370
418
|
const {identifier: identifier} = subscription;
|
371
419
|
return this.consumer.send({
|
@@ -429,6 +477,7 @@
|
|
429
477
|
exports.Consumer = Consumer;
|
430
478
|
exports.INTERNAL = INTERNAL;
|
431
479
|
exports.Subscription = Subscription;
|
480
|
+
exports.SubscriptionGuarantor = SubscriptionGuarantor;
|
432
481
|
exports.Subscriptions = Subscriptions;
|
433
482
|
exports.adapters = adapters;
|
434
483
|
exports.createConsumer = createConsumer;
|
@@ -0,0 +1,491 @@
|
|
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
|
+
},
|
128
|
+
default_mount_path: "/cable",
|
129
|
+
protocols: [ "actioncable-v1-json", "actioncable-unsupported" ]
|
130
|
+
};
|
131
|
+
|
132
|
+
const {message_types: message_types, protocols: protocols} = INTERNAL;
|
133
|
+
|
134
|
+
const supportedProtocols = protocols.slice(0, protocols.length - 1);
|
135
|
+
|
136
|
+
const indexOf = [].indexOf;
|
137
|
+
|
138
|
+
class Connection {
|
139
|
+
constructor(consumer) {
|
140
|
+
this.open = this.open.bind(this);
|
141
|
+
this.consumer = consumer;
|
142
|
+
this.subscriptions = this.consumer.subscriptions;
|
143
|
+
this.monitor = new ConnectionMonitor(this);
|
144
|
+
this.disconnected = true;
|
145
|
+
}
|
146
|
+
send(data) {
|
147
|
+
if (this.isOpen()) {
|
148
|
+
this.webSocket.send(JSON.stringify(data));
|
149
|
+
return true;
|
150
|
+
} else {
|
151
|
+
return false;
|
152
|
+
}
|
153
|
+
}
|
154
|
+
open() {
|
155
|
+
if (this.isActive()) {
|
156
|
+
logger.log(`Attempted to open WebSocket, but existing socket is ${this.getState()}`);
|
157
|
+
return false;
|
158
|
+
} else {
|
159
|
+
logger.log(`Opening WebSocket, current state is ${this.getState()}, subprotocols: ${protocols}`);
|
160
|
+
if (this.webSocket) {
|
161
|
+
this.uninstallEventHandlers();
|
162
|
+
}
|
163
|
+
this.webSocket = new adapters.WebSocket(this.consumer.url, protocols);
|
164
|
+
this.installEventHandlers();
|
165
|
+
this.monitor.start();
|
166
|
+
return true;
|
167
|
+
}
|
168
|
+
}
|
169
|
+
close({allowReconnect: allowReconnect} = {
|
170
|
+
allowReconnect: true
|
171
|
+
}) {
|
172
|
+
if (!allowReconnect) {
|
173
|
+
this.monitor.stop();
|
174
|
+
}
|
175
|
+
if (this.isActive()) {
|
176
|
+
return this.webSocket.close();
|
177
|
+
}
|
178
|
+
}
|
179
|
+
reopen() {
|
180
|
+
logger.log(`Reopening WebSocket, current state is ${this.getState()}`);
|
181
|
+
if (this.isActive()) {
|
182
|
+
try {
|
183
|
+
return this.close();
|
184
|
+
} catch (error) {
|
185
|
+
logger.log("Failed to reopen WebSocket", error);
|
186
|
+
} finally {
|
187
|
+
logger.log(`Reopening WebSocket in ${this.constructor.reopenDelay}ms`);
|
188
|
+
setTimeout(this.open, this.constructor.reopenDelay);
|
189
|
+
}
|
190
|
+
} else {
|
191
|
+
return this.open();
|
192
|
+
}
|
193
|
+
}
|
194
|
+
getProtocol() {
|
195
|
+
if (this.webSocket) {
|
196
|
+
return this.webSocket.protocol;
|
197
|
+
}
|
198
|
+
}
|
199
|
+
isOpen() {
|
200
|
+
return this.isState("open");
|
201
|
+
}
|
202
|
+
isActive() {
|
203
|
+
return this.isState("open", "connecting");
|
204
|
+
}
|
205
|
+
isProtocolSupported() {
|
206
|
+
return indexOf.call(supportedProtocols, this.getProtocol()) >= 0;
|
207
|
+
}
|
208
|
+
isState(...states) {
|
209
|
+
return indexOf.call(states, this.getState()) >= 0;
|
210
|
+
}
|
211
|
+
getState() {
|
212
|
+
if (this.webSocket) {
|
213
|
+
for (let state in adapters.WebSocket) {
|
214
|
+
if (adapters.WebSocket[state] === this.webSocket.readyState) {
|
215
|
+
return state.toLowerCase();
|
216
|
+
}
|
217
|
+
}
|
218
|
+
}
|
219
|
+
return null;
|
220
|
+
}
|
221
|
+
installEventHandlers() {
|
222
|
+
for (let eventName in this.events) {
|
223
|
+
const handler = this.events[eventName].bind(this);
|
224
|
+
this.webSocket[`on${eventName}`] = handler;
|
225
|
+
}
|
226
|
+
}
|
227
|
+
uninstallEventHandlers() {
|
228
|
+
for (let eventName in this.events) {
|
229
|
+
this.webSocket[`on${eventName}`] = function() {};
|
230
|
+
}
|
231
|
+
}
|
232
|
+
}
|
233
|
+
|
234
|
+
Connection.reopenDelay = 500;
|
235
|
+
|
236
|
+
Connection.prototype.events = {
|
237
|
+
message(event) {
|
238
|
+
if (!this.isProtocolSupported()) {
|
239
|
+
return;
|
240
|
+
}
|
241
|
+
const {identifier: identifier, message: message, reason: reason, reconnect: reconnect, type: type} = JSON.parse(event.data);
|
242
|
+
switch (type) {
|
243
|
+
case message_types.welcome:
|
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
|
+
return this.subscriptions.notify(identifier, "connected");
|
259
|
+
|
260
|
+
case message_types.rejection:
|
261
|
+
return this.subscriptions.reject(identifier);
|
262
|
+
|
263
|
+
default:
|
264
|
+
return this.subscriptions.notify(identifier, "received", message);
|
265
|
+
}
|
266
|
+
},
|
267
|
+
open() {
|
268
|
+
logger.log(`WebSocket onopen event, using '${this.getProtocol()}' subprotocol`);
|
269
|
+
this.disconnected = false;
|
270
|
+
if (!this.isProtocolSupported()) {
|
271
|
+
logger.log("Protocol is unsupported. Stopping monitor and disconnecting.");
|
272
|
+
return this.close({
|
273
|
+
allowReconnect: false
|
274
|
+
});
|
275
|
+
}
|
276
|
+
},
|
277
|
+
close(event) {
|
278
|
+
logger.log("WebSocket onclose event");
|
279
|
+
if (this.disconnected) {
|
280
|
+
return;
|
281
|
+
}
|
282
|
+
this.disconnected = true;
|
283
|
+
this.monitor.recordDisconnect();
|
284
|
+
return this.subscriptions.notifyAll("disconnected", {
|
285
|
+
willAttemptReconnect: this.monitor.isRunning()
|
286
|
+
});
|
287
|
+
},
|
288
|
+
error() {
|
289
|
+
logger.log("WebSocket onerror event");
|
290
|
+
}
|
291
|
+
};
|
292
|
+
|
293
|
+
const extend = function(object, properties) {
|
294
|
+
if (properties != null) {
|
295
|
+
for (let key in properties) {
|
296
|
+
const value = properties[key];
|
297
|
+
object[key] = value;
|
298
|
+
}
|
299
|
+
}
|
300
|
+
return object;
|
301
|
+
};
|
302
|
+
|
303
|
+
class Subscription {
|
304
|
+
constructor(consumer, params = {}, mixin) {
|
305
|
+
this.consumer = consumer;
|
306
|
+
this.identifier = JSON.stringify(params);
|
307
|
+
extend(this, mixin);
|
308
|
+
}
|
309
|
+
perform(action, data = {}) {
|
310
|
+
data.action = action;
|
311
|
+
return this.send(data);
|
312
|
+
}
|
313
|
+
send(data) {
|
314
|
+
return this.consumer.send({
|
315
|
+
command: "message",
|
316
|
+
identifier: this.identifier,
|
317
|
+
data: JSON.stringify(data)
|
318
|
+
});
|
319
|
+
}
|
320
|
+
unsubscribe() {
|
321
|
+
return this.consumer.subscriptions.remove(this);
|
322
|
+
}
|
323
|
+
}
|
324
|
+
|
325
|
+
class SubscriptionGuarantor {
|
326
|
+
constructor(subscriptions) {
|
327
|
+
this.subscriptions = subscriptions;
|
328
|
+
this.pendingSubscriptions = [];
|
329
|
+
}
|
330
|
+
guarantee(subscription) {
|
331
|
+
if (this.pendingSubscriptions.indexOf(subscription) == -1) {
|
332
|
+
logger.log(`SubscriptionGuarantor guaranteeing ${subscription.identifier}`);
|
333
|
+
this.pendingSubscriptions.push(subscription);
|
334
|
+
} else {
|
335
|
+
logger.log(`SubscriptionGuarantor already guaranteeing ${subscription.identifier}`);
|
336
|
+
}
|
337
|
+
this.startGuaranteeing();
|
338
|
+
}
|
339
|
+
forget(subscription) {
|
340
|
+
logger.log(`SubscriptionGuarantor forgetting ${subscription.identifier}`);
|
341
|
+
this.pendingSubscriptions = this.pendingSubscriptions.filter((s => s !== subscription));
|
342
|
+
}
|
343
|
+
startGuaranteeing() {
|
344
|
+
this.stopGuaranteeing();
|
345
|
+
this.retrySubscribing();
|
346
|
+
}
|
347
|
+
stopGuaranteeing() {
|
348
|
+
clearTimeout(this.retryTimeout);
|
349
|
+
}
|
350
|
+
retrySubscribing() {
|
351
|
+
this.retryTimeout = setTimeout((() => {
|
352
|
+
if (this.subscriptions && typeof this.subscriptions.subscribe === "function") {
|
353
|
+
this.pendingSubscriptions.map((subscription => {
|
354
|
+
logger.log(`SubscriptionGuarantor resubscribing ${subscription.identifier}`);
|
355
|
+
this.subscriptions.subscribe(subscription);
|
356
|
+
}));
|
357
|
+
}
|
358
|
+
}), 500);
|
359
|
+
}
|
360
|
+
}
|
361
|
+
|
362
|
+
class Subscriptions {
|
363
|
+
constructor(consumer) {
|
364
|
+
this.consumer = consumer;
|
365
|
+
this.guarantor = new SubscriptionGuarantor(this);
|
366
|
+
this.subscriptions = [];
|
367
|
+
}
|
368
|
+
create(channelName, mixin) {
|
369
|
+
const channel = channelName;
|
370
|
+
const params = typeof channel === "object" ? channel : {
|
371
|
+
channel: channel
|
372
|
+
};
|
373
|
+
const subscription = new Subscription(this.consumer, params, mixin);
|
374
|
+
return this.add(subscription);
|
375
|
+
}
|
376
|
+
add(subscription) {
|
377
|
+
this.subscriptions.push(subscription);
|
378
|
+
this.consumer.ensureActiveConnection();
|
379
|
+
this.notify(subscription, "initialized");
|
380
|
+
this.subscribe(subscription);
|
381
|
+
return subscription;
|
382
|
+
}
|
383
|
+
remove(subscription) {
|
384
|
+
this.forget(subscription);
|
385
|
+
if (!this.findAll(subscription.identifier).length) {
|
386
|
+
this.sendCommand(subscription, "unsubscribe");
|
387
|
+
}
|
388
|
+
return subscription;
|
389
|
+
}
|
390
|
+
reject(identifier) {
|
391
|
+
return this.findAll(identifier).map((subscription => {
|
392
|
+
this.forget(subscription);
|
393
|
+
this.notify(subscription, "rejected");
|
394
|
+
return subscription;
|
395
|
+
}));
|
396
|
+
}
|
397
|
+
forget(subscription) {
|
398
|
+
this.guarantor.forget(subscription);
|
399
|
+
this.subscriptions = this.subscriptions.filter((s => s !== subscription));
|
400
|
+
return subscription;
|
401
|
+
}
|
402
|
+
findAll(identifier) {
|
403
|
+
return this.subscriptions.filter((s => s.identifier === identifier));
|
404
|
+
}
|
405
|
+
reload() {
|
406
|
+
return this.subscriptions.map((subscription => this.subscribe(subscription)));
|
407
|
+
}
|
408
|
+
notifyAll(callbackName, ...args) {
|
409
|
+
return this.subscriptions.map((subscription => this.notify(subscription, callbackName, ...args)));
|
410
|
+
}
|
411
|
+
notify(subscription, callbackName, ...args) {
|
412
|
+
let subscriptions;
|
413
|
+
if (typeof subscription === "string") {
|
414
|
+
subscriptions = this.findAll(subscription);
|
415
|
+
} else {
|
416
|
+
subscriptions = [ subscription ];
|
417
|
+
}
|
418
|
+
return subscriptions.map((subscription => typeof subscription[callbackName] === "function" ? subscription[callbackName](...args) : undefined));
|
419
|
+
}
|
420
|
+
subscribe(subscription) {
|
421
|
+
if (this.sendCommand(subscription, "subscribe")) {
|
422
|
+
this.guarantor.guarantee(subscription);
|
423
|
+
}
|
424
|
+
}
|
425
|
+
confirmSubscription(identifier) {
|
426
|
+
logger.log(`Subscription confirmed ${identifier}`);
|
427
|
+
this.findAll(identifier).map((subscription => this.guarantor.forget(subscription)));
|
428
|
+
}
|
429
|
+
sendCommand(subscription, command) {
|
430
|
+
const {identifier: identifier} = subscription;
|
431
|
+
return this.consumer.send({
|
432
|
+
command: command,
|
433
|
+
identifier: identifier
|
434
|
+
});
|
435
|
+
}
|
436
|
+
}
|
437
|
+
|
438
|
+
class Consumer {
|
439
|
+
constructor(url) {
|
440
|
+
this._url = url;
|
441
|
+
this.subscriptions = new Subscriptions(this);
|
442
|
+
this.connection = new Connection(this);
|
443
|
+
}
|
444
|
+
get url() {
|
445
|
+
return createWebSocketURL(this._url);
|
446
|
+
}
|
447
|
+
send(data) {
|
448
|
+
return this.connection.send(data);
|
449
|
+
}
|
450
|
+
connect() {
|
451
|
+
return this.connection.open();
|
452
|
+
}
|
453
|
+
disconnect() {
|
454
|
+
return this.connection.close({
|
455
|
+
allowReconnect: false
|
456
|
+
});
|
457
|
+
}
|
458
|
+
ensureActiveConnection() {
|
459
|
+
if (!this.connection.isActive()) {
|
460
|
+
return this.connection.open();
|
461
|
+
}
|
462
|
+
}
|
463
|
+
}
|
464
|
+
|
465
|
+
function createWebSocketURL(url) {
|
466
|
+
if (typeof url === "function") {
|
467
|
+
url = url();
|
468
|
+
}
|
469
|
+
if (url && !/^wss?:/i.test(url)) {
|
470
|
+
const a = document.createElement("a");
|
471
|
+
a.href = url;
|
472
|
+
a.href = a.href;
|
473
|
+
a.protocol = a.protocol.replace("http", "ws");
|
474
|
+
return a.href;
|
475
|
+
} else {
|
476
|
+
return url;
|
477
|
+
}
|
478
|
+
}
|
479
|
+
|
480
|
+
function createConsumer(url = getConfig("url") || INTERNAL.default_mount_path) {
|
481
|
+
return new Consumer(url);
|
482
|
+
}
|
483
|
+
|
484
|
+
function getConfig(name) {
|
485
|
+
const element = document.head.querySelector(`meta[name='action-cable-${name}']`);
|
486
|
+
if (element) {
|
487
|
+
return element.getAttribute("content");
|
488
|
+
}
|
489
|
+
}
|
490
|
+
|
491
|
+
export { Connection, ConnectionMonitor, Consumer, INTERNAL, Subscription, SubscriptionGuarantor, Subscriptions, adapters, createConsumer, createWebSocketURL, getConfig, logger };
|
@@ -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.isActive()) {
|
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
|
+
}));
|
@@ -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.
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: actioncable
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 7.0.0.
|
4
|
+
version: 7.0.0.rc1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Pratik Naik
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2021-
|
12
|
+
date: 2021-12-06 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: activesupport
|
@@ -17,28 +17,28 @@ dependencies:
|
|
17
17
|
requirements:
|
18
18
|
- - '='
|
19
19
|
- !ruby/object:Gem::Version
|
20
|
-
version: 7.0.0.
|
20
|
+
version: 7.0.0.rc1
|
21
21
|
type: :runtime
|
22
22
|
prerelease: false
|
23
23
|
version_requirements: !ruby/object:Gem::Requirement
|
24
24
|
requirements:
|
25
25
|
- - '='
|
26
26
|
- !ruby/object:Gem::Version
|
27
|
-
version: 7.0.0.
|
27
|
+
version: 7.0.0.rc1
|
28
28
|
- !ruby/object:Gem::Dependency
|
29
29
|
name: actionpack
|
30
30
|
requirement: !ruby/object:Gem::Requirement
|
31
31
|
requirements:
|
32
32
|
- - '='
|
33
33
|
- !ruby/object:Gem::Version
|
34
|
-
version: 7.0.0.
|
34
|
+
version: 7.0.0.rc1
|
35
35
|
type: :runtime
|
36
36
|
prerelease: false
|
37
37
|
version_requirements: !ruby/object:Gem::Requirement
|
38
38
|
requirements:
|
39
39
|
- - '='
|
40
40
|
- !ruby/object:Gem::Version
|
41
|
-
version: 7.0.0.
|
41
|
+
version: 7.0.0.rc1
|
42
42
|
- !ruby/object:Gem::Dependency
|
43
43
|
name: nio4r
|
44
44
|
requirement: !ruby/object:Gem::Requirement
|
@@ -80,6 +80,8 @@ files:
|
|
80
80
|
- MIT-LICENSE
|
81
81
|
- README.md
|
82
82
|
- app/assets/javascripts/action_cable.js
|
83
|
+
- app/assets/javascripts/actioncable.esm.js
|
84
|
+
- app/assets/javascripts/actioncable.js
|
83
85
|
- lib/action_cable.rb
|
84
86
|
- lib/action_cable/channel.rb
|
85
87
|
- lib/action_cable/channel/base.rb
|
@@ -140,10 +142,11 @@ licenses:
|
|
140
142
|
- MIT
|
141
143
|
metadata:
|
142
144
|
bug_tracker_uri: https://github.com/rails/rails/issues
|
143
|
-
changelog_uri: https://github.com/rails/rails/blob/v7.0.0.
|
144
|
-
documentation_uri: https://api.rubyonrails.org/v7.0.0.
|
145
|
+
changelog_uri: https://github.com/rails/rails/blob/v7.0.0.rc1/actioncable/CHANGELOG.md
|
146
|
+
documentation_uri: https://api.rubyonrails.org/v7.0.0.rc1/
|
145
147
|
mailing_list_uri: https://discuss.rubyonrails.org/c/rubyonrails-talk
|
146
|
-
source_code_uri: https://github.com/rails/rails/tree/v7.0.0.
|
148
|
+
source_code_uri: https://github.com/rails/rails/tree/v7.0.0.rc1/actioncable
|
149
|
+
rubygems_mfa_required: 'true'
|
147
150
|
post_install_message:
|
148
151
|
rdoc_options: []
|
149
152
|
require_paths:
|
@@ -159,7 +162,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
159
162
|
- !ruby/object:Gem::Version
|
160
163
|
version: 1.3.1
|
161
164
|
requirements: []
|
162
|
-
rubygems_version: 3.
|
165
|
+
rubygems_version: 3.2.22
|
163
166
|
signing_key:
|
164
167
|
specification_version: 4
|
165
168
|
summary: WebSocket framework for Rails.
|