1var adapters = { 2 logger: typeof console !== "undefined" ? console : undefined, 3 WebSocket: typeof WebSocket !== "undefined" ? WebSocket : undefined 4}; 5 6var logger = { 7 log(...messages) { 8 if (this.enabled) { 9 messages.push(Date.now()); 10 adapters.logger.log("[ActionCable]", ...messages); 11 } 12 } 13}; 14 15const now = () => (new Date).getTime(); 16 17const secondsSince = time => (now() - time) / 1e3; 18 19class 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 110ConnectionMonitor.staleThreshold = 6; 111 112ConnectionMonitor.reconnectionBackoffRate = .15; 113 114var 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 132const {message_types: message_types, protocols: protocols} = INTERNAL; 133 134const supportedProtocols = protocols.slice(0, protocols.length - 1); 135 136const indexOf = [].indexOf; 137 138class 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 238Connection.reopenDelay = 500; 239 240Connection.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 310const 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 320class 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 342class 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 379class 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 455class 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 486function 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 501function createConsumer(url = getConfig("url") || INTERNAL.default_mount_path) { 502 return new Consumer(url); 503} 504 505function getConfig(name) { 506 const element = document.head.querySelector(`meta[name='action-cable-${name}']`); 507 if (element) { 508 return element.getAttribute("content"); 509 } 510} 511
512export { Connection, ConnectionMonitor, Consumer, INTERNAL, Subscription, SubscriptionGuarantor, Subscriptions, adapters, createConsumer, createWebSocketURL, getConfig, logger };
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.