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