omg-actioncable 8.0.0.alpha2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (61) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +5 -0
  3. data/MIT-LICENSE +20 -0
  4. data/README.md +24 -0
  5. data/app/assets/javascripts/action_cable.js +511 -0
  6. data/app/assets/javascripts/actioncable.esm.js +512 -0
  7. data/app/assets/javascripts/actioncable.js +510 -0
  8. data/lib/action_cable/channel/base.rb +335 -0
  9. data/lib/action_cable/channel/broadcasting.rb +50 -0
  10. data/lib/action_cable/channel/callbacks.rb +76 -0
  11. data/lib/action_cable/channel/naming.rb +28 -0
  12. data/lib/action_cable/channel/periodic_timers.rb +78 -0
  13. data/lib/action_cable/channel/streams.rb +215 -0
  14. data/lib/action_cable/channel/test_case.rb +356 -0
  15. data/lib/action_cable/connection/authorization.rb +18 -0
  16. data/lib/action_cable/connection/base.rb +294 -0
  17. data/lib/action_cable/connection/callbacks.rb +57 -0
  18. data/lib/action_cable/connection/client_socket.rb +159 -0
  19. data/lib/action_cable/connection/identification.rb +51 -0
  20. data/lib/action_cable/connection/internal_channel.rb +50 -0
  21. data/lib/action_cable/connection/message_buffer.rb +57 -0
  22. data/lib/action_cable/connection/stream.rb +117 -0
  23. data/lib/action_cable/connection/stream_event_loop.rb +136 -0
  24. data/lib/action_cable/connection/subscriptions.rb +85 -0
  25. data/lib/action_cable/connection/tagged_logger_proxy.rb +47 -0
  26. data/lib/action_cable/connection/test_case.rb +246 -0
  27. data/lib/action_cable/connection/web_socket.rb +45 -0
  28. data/lib/action_cable/deprecator.rb +9 -0
  29. data/lib/action_cable/engine.rb +98 -0
  30. data/lib/action_cable/gem_version.rb +19 -0
  31. data/lib/action_cable/helpers/action_cable_helper.rb +45 -0
  32. data/lib/action_cable/remote_connections.rb +82 -0
  33. data/lib/action_cable/server/base.rb +109 -0
  34. data/lib/action_cable/server/broadcasting.rb +62 -0
  35. data/lib/action_cable/server/configuration.rb +70 -0
  36. data/lib/action_cable/server/connections.rb +44 -0
  37. data/lib/action_cable/server/worker/active_record_connection_management.rb +23 -0
  38. data/lib/action_cable/server/worker.rb +75 -0
  39. data/lib/action_cable/subscription_adapter/async.rb +29 -0
  40. data/lib/action_cable/subscription_adapter/base.rb +36 -0
  41. data/lib/action_cable/subscription_adapter/channel_prefix.rb +30 -0
  42. data/lib/action_cable/subscription_adapter/inline.rb +39 -0
  43. data/lib/action_cable/subscription_adapter/postgresql.rb +134 -0
  44. data/lib/action_cable/subscription_adapter/redis.rb +256 -0
  45. data/lib/action_cable/subscription_adapter/subscriber_map.rb +61 -0
  46. data/lib/action_cable/subscription_adapter/test.rb +41 -0
  47. data/lib/action_cable/test_case.rb +13 -0
  48. data/lib/action_cable/test_helper.rb +163 -0
  49. data/lib/action_cable/version.rb +12 -0
  50. data/lib/action_cable.rb +80 -0
  51. data/lib/rails/generators/channel/USAGE +19 -0
  52. data/lib/rails/generators/channel/channel_generator.rb +127 -0
  53. data/lib/rails/generators/channel/templates/application_cable/channel.rb.tt +4 -0
  54. data/lib/rails/generators/channel/templates/application_cable/connection.rb.tt +4 -0
  55. data/lib/rails/generators/channel/templates/channel.rb.tt +16 -0
  56. data/lib/rails/generators/channel/templates/javascript/channel.js.tt +20 -0
  57. data/lib/rails/generators/channel/templates/javascript/consumer.js.tt +6 -0
  58. data/lib/rails/generators/channel/templates/javascript/index.js.tt +1 -0
  59. data/lib/rails/generators/test_unit/channel_generator.rb +22 -0
  60. data/lib/rails/generators/test_unit/templates/channel_test.rb.tt +8 -0
  61. metadata +181 -0
@@ -0,0 +1,510 @@
1
+ (function(global, factory) {
2
+ typeof exports === "object" && typeof module !== "undefined" ? factory(exports) : typeof define === "function" && define.amd ? define([ "exports" ], factory) : (global = typeof globalThis !== "undefined" ? globalThis : global || self,
3
+ factory(global.ActionCable = {}));
4
+ })(this, (function(exports) {
5
+ "use strict";
6
+ var adapters = {
7
+ logger: typeof console !== "undefined" ? console : undefined,
8
+ WebSocket: typeof WebSocket !== "undefined" ? WebSocket : undefined
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
+ recordMessage() {
47
+ this.pingedAt = now();
48
+ }
49
+ recordConnect() {
50
+ this.reconnectAttempts = 0;
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
+ ConnectionMonitor.staleThreshold = 6;
111
+ ConnectionMonitor.reconnectionBackoffRate = .15;
112
+ var INTERNAL = {
113
+ message_types: {
114
+ welcome: "welcome",
115
+ disconnect: "disconnect",
116
+ ping: "ping",
117
+ confirmation: "confirm_subscription",
118
+ rejection: "reject_subscription"
119
+ },
120
+ disconnect_reasons: {
121
+ unauthorized: "unauthorized",
122
+ invalid_request: "invalid_request",
123
+ server_restart: "server_restart",
124
+ remote: "remote"
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
+ const socketProtocols = [ ...protocols, ...this.consumer.subprotocols || [] ];
154
+ logger.log(`Opening WebSocket, current state is ${this.getState()}, subprotocols: ${socketProtocols}`);
155
+ if (this.webSocket) {
156
+ this.uninstallEventHandlers();
157
+ }
158
+ this.webSocket = new adapters.WebSocket(this.consumer.url, socketProtocols);
159
+ this.installEventHandlers();
160
+ this.monitor.start();
161
+ return true;
162
+ }
163
+ }
164
+ close({allowReconnect: allowReconnect} = {
165
+ allowReconnect: true
166
+ }) {
167
+ if (!allowReconnect) {
168
+ this.monitor.stop();
169
+ }
170
+ if (this.isOpen()) {
171
+ return this.webSocket.close();
172
+ }
173
+ }
174
+ reopen() {
175
+ logger.log(`Reopening WebSocket, current state is ${this.getState()}`);
176
+ if (this.isActive()) {
177
+ try {
178
+ return this.close();
179
+ } catch (error) {
180
+ logger.log("Failed to reopen WebSocket", error);
181
+ } finally {
182
+ logger.log(`Reopening WebSocket in ${this.constructor.reopenDelay}ms`);
183
+ setTimeout(this.open, this.constructor.reopenDelay);
184
+ }
185
+ } else {
186
+ return this.open();
187
+ }
188
+ }
189
+ getProtocol() {
190
+ if (this.webSocket) {
191
+ return this.webSocket.protocol;
192
+ }
193
+ }
194
+ isOpen() {
195
+ return this.isState("open");
196
+ }
197
+ isActive() {
198
+ return this.isState("open", "connecting");
199
+ }
200
+ triedToReconnect() {
201
+ return this.monitor.reconnectAttempts > 0;
202
+ }
203
+ isProtocolSupported() {
204
+ return indexOf.call(supportedProtocols, this.getProtocol()) >= 0;
205
+ }
206
+ isState(...states) {
207
+ return indexOf.call(states, this.getState()) >= 0;
208
+ }
209
+ getState() {
210
+ if (this.webSocket) {
211
+ for (let state in adapters.WebSocket) {
212
+ if (adapters.WebSocket[state] === this.webSocket.readyState) {
213
+ return state.toLowerCase();
214
+ }
215
+ }
216
+ }
217
+ return null;
218
+ }
219
+ installEventHandlers() {
220
+ for (let eventName in this.events) {
221
+ const handler = this.events[eventName].bind(this);
222
+ this.webSocket[`on${eventName}`] = handler;
223
+ }
224
+ }
225
+ uninstallEventHandlers() {
226
+ for (let eventName in this.events) {
227
+ this.webSocket[`on${eventName}`] = function() {};
228
+ }
229
+ }
230
+ }
231
+ Connection.reopenDelay = 500;
232
+ Connection.prototype.events = {
233
+ message(event) {
234
+ if (!this.isProtocolSupported()) {
235
+ return;
236
+ }
237
+ const {identifier: identifier, message: message, reason: reason, reconnect: reconnect, type: type} = JSON.parse(event.data);
238
+ this.monitor.recordMessage();
239
+ switch (type) {
240
+ case message_types.welcome:
241
+ if (this.triedToReconnect()) {
242
+ this.reconnectAttempted = true;
243
+ }
244
+ this.monitor.recordConnect();
245
+ return this.subscriptions.reload();
246
+
247
+ case message_types.disconnect:
248
+ logger.log(`Disconnecting. Reason: ${reason}`);
249
+ return this.close({
250
+ allowReconnect: reconnect
251
+ });
252
+
253
+ case message_types.ping:
254
+ return null;
255
+
256
+ case message_types.confirmation:
257
+ this.subscriptions.confirmSubscription(identifier);
258
+ if (this.reconnectAttempted) {
259
+ this.reconnectAttempted = false;
260
+ return this.subscriptions.notify(identifier, "connected", {
261
+ reconnected: true
262
+ });
263
+ } else {
264
+ return this.subscriptions.notify(identifier, "connected", {
265
+ reconnected: false
266
+ });
267
+ }
268
+
269
+ case message_types.rejection:
270
+ return this.subscriptions.reject(identifier);
271
+
272
+ default:
273
+ return this.subscriptions.notify(identifier, "received", message);
274
+ }
275
+ },
276
+ open() {
277
+ logger.log(`WebSocket onopen event, using '${this.getProtocol()}' subprotocol`);
278
+ this.disconnected = false;
279
+ if (!this.isProtocolSupported()) {
280
+ logger.log("Protocol is unsupported. Stopping monitor and disconnecting.");
281
+ return this.close({
282
+ allowReconnect: false
283
+ });
284
+ }
285
+ },
286
+ close(event) {
287
+ logger.log("WebSocket onclose event");
288
+ if (this.disconnected) {
289
+ return;
290
+ }
291
+ this.disconnected = true;
292
+ this.monitor.recordDisconnect();
293
+ return this.subscriptions.notifyAll("disconnected", {
294
+ willAttemptReconnect: this.monitor.isRunning()
295
+ });
296
+ },
297
+ error() {
298
+ logger.log("WebSocket onerror event");
299
+ }
300
+ };
301
+ const extend = function(object, properties) {
302
+ if (properties != null) {
303
+ for (let key in properties) {
304
+ const value = properties[key];
305
+ object[key] = value;
306
+ }
307
+ }
308
+ return object;
309
+ };
310
+ class Subscription {
311
+ constructor(consumer, params = {}, mixin) {
312
+ this.consumer = consumer;
313
+ this.identifier = JSON.stringify(params);
314
+ extend(this, mixin);
315
+ }
316
+ perform(action, data = {}) {
317
+ data.action = action;
318
+ return this.send(data);
319
+ }
320
+ send(data) {
321
+ return this.consumer.send({
322
+ command: "message",
323
+ identifier: this.identifier,
324
+ data: JSON.stringify(data)
325
+ });
326
+ }
327
+ unsubscribe() {
328
+ return this.consumer.subscriptions.remove(this);
329
+ }
330
+ }
331
+ class SubscriptionGuarantor {
332
+ constructor(subscriptions) {
333
+ this.subscriptions = subscriptions;
334
+ this.pendingSubscriptions = [];
335
+ }
336
+ guarantee(subscription) {
337
+ if (this.pendingSubscriptions.indexOf(subscription) == -1) {
338
+ logger.log(`SubscriptionGuarantor guaranteeing ${subscription.identifier}`);
339
+ this.pendingSubscriptions.push(subscription);
340
+ } else {
341
+ logger.log(`SubscriptionGuarantor already guaranteeing ${subscription.identifier}`);
342
+ }
343
+ this.startGuaranteeing();
344
+ }
345
+ forget(subscription) {
346
+ logger.log(`SubscriptionGuarantor forgetting ${subscription.identifier}`);
347
+ this.pendingSubscriptions = this.pendingSubscriptions.filter((s => s !== subscription));
348
+ }
349
+ startGuaranteeing() {
350
+ this.stopGuaranteeing();
351
+ this.retrySubscribing();
352
+ }
353
+ stopGuaranteeing() {
354
+ clearTimeout(this.retryTimeout);
355
+ }
356
+ retrySubscribing() {
357
+ this.retryTimeout = setTimeout((() => {
358
+ if (this.subscriptions && typeof this.subscriptions.subscribe === "function") {
359
+ this.pendingSubscriptions.map((subscription => {
360
+ logger.log(`SubscriptionGuarantor resubscribing ${subscription.identifier}`);
361
+ this.subscriptions.subscribe(subscription);
362
+ }));
363
+ }
364
+ }), 500);
365
+ }
366
+ }
367
+ class Subscriptions {
368
+ constructor(consumer) {
369
+ this.consumer = consumer;
370
+ this.guarantor = new SubscriptionGuarantor(this);
371
+ this.subscriptions = [];
372
+ }
373
+ create(channelName, mixin) {
374
+ const channel = channelName;
375
+ const params = typeof channel === "object" ? channel : {
376
+ channel: channel
377
+ };
378
+ const subscription = new Subscription(this.consumer, params, mixin);
379
+ return this.add(subscription);
380
+ }
381
+ add(subscription) {
382
+ this.subscriptions.push(subscription);
383
+ this.consumer.ensureActiveConnection();
384
+ this.notify(subscription, "initialized");
385
+ this.subscribe(subscription);
386
+ return subscription;
387
+ }
388
+ remove(subscription) {
389
+ this.forget(subscription);
390
+ if (!this.findAll(subscription.identifier).length) {
391
+ this.sendCommand(subscription, "unsubscribe");
392
+ }
393
+ return subscription;
394
+ }
395
+ reject(identifier) {
396
+ return this.findAll(identifier).map((subscription => {
397
+ this.forget(subscription);
398
+ this.notify(subscription, "rejected");
399
+ return subscription;
400
+ }));
401
+ }
402
+ forget(subscription) {
403
+ this.guarantor.forget(subscription);
404
+ this.subscriptions = this.subscriptions.filter((s => s !== subscription));
405
+ return subscription;
406
+ }
407
+ findAll(identifier) {
408
+ return this.subscriptions.filter((s => s.identifier === identifier));
409
+ }
410
+ reload() {
411
+ return this.subscriptions.map((subscription => this.subscribe(subscription)));
412
+ }
413
+ notifyAll(callbackName, ...args) {
414
+ return this.subscriptions.map((subscription => this.notify(subscription, callbackName, ...args)));
415
+ }
416
+ notify(subscription, callbackName, ...args) {
417
+ let subscriptions;
418
+ if (typeof subscription === "string") {
419
+ subscriptions = this.findAll(subscription);
420
+ } else {
421
+ subscriptions = [ subscription ];
422
+ }
423
+ return subscriptions.map((subscription => typeof subscription[callbackName] === "function" ? subscription[callbackName](...args) : undefined));
424
+ }
425
+ subscribe(subscription) {
426
+ if (this.sendCommand(subscription, "subscribe")) {
427
+ this.guarantor.guarantee(subscription);
428
+ }
429
+ }
430
+ confirmSubscription(identifier) {
431
+ logger.log(`Subscription confirmed ${identifier}`);
432
+ this.findAll(identifier).map((subscription => this.guarantor.forget(subscription)));
433
+ }
434
+ sendCommand(subscription, command) {
435
+ const {identifier: identifier} = subscription;
436
+ return this.consumer.send({
437
+ command: command,
438
+ identifier: identifier
439
+ });
440
+ }
441
+ }
442
+ class Consumer {
443
+ constructor(url) {
444
+ this._url = url;
445
+ this.subscriptions = new Subscriptions(this);
446
+ this.connection = new Connection(this);
447
+ this.subprotocols = [];
448
+ }
449
+ get url() {
450
+ return createWebSocketURL(this._url);
451
+ }
452
+ send(data) {
453
+ return this.connection.send(data);
454
+ }
455
+ connect() {
456
+ return this.connection.open();
457
+ }
458
+ disconnect() {
459
+ return this.connection.close({
460
+ allowReconnect: false
461
+ });
462
+ }
463
+ ensureActiveConnection() {
464
+ if (!this.connection.isActive()) {
465
+ return this.connection.open();
466
+ }
467
+ }
468
+ addSubProtocol(subprotocol) {
469
+ this.subprotocols = [ ...this.subprotocols, subprotocol ];
470
+ }
471
+ }
472
+ function createWebSocketURL(url) {
473
+ if (typeof url === "function") {
474
+ url = url();
475
+ }
476
+ if (url && !/^wss?:/i.test(url)) {
477
+ const a = document.createElement("a");
478
+ a.href = url;
479
+ a.href = a.href;
480
+ a.protocol = a.protocol.replace("http", "ws");
481
+ return a.href;
482
+ } else {
483
+ return url;
484
+ }
485
+ }
486
+ function createConsumer(url = getConfig("url") || INTERNAL.default_mount_path) {
487
+ return new Consumer(url);
488
+ }
489
+ function getConfig(name) {
490
+ const element = document.head.querySelector(`meta[name='action-cable-${name}']`);
491
+ if (element) {
492
+ return element.getAttribute("content");
493
+ }
494
+ }
495
+ exports.Connection = Connection;
496
+ exports.ConnectionMonitor = ConnectionMonitor;
497
+ exports.Consumer = Consumer;
498
+ exports.INTERNAL = INTERNAL;
499
+ exports.Subscription = Subscription;
500
+ exports.SubscriptionGuarantor = SubscriptionGuarantor;
501
+ exports.Subscriptions = Subscriptions;
502
+ exports.adapters = adapters;
503
+ exports.createConsumer = createConsumer;
504
+ exports.createWebSocketURL = createWebSocketURL;
505
+ exports.getConfig = getConfig;
506
+ exports.logger = logger;
507
+ Object.defineProperty(exports, "__esModule", {
508
+ value: true
509
+ });
510
+ }));