ぼざクリ タグ広場 https://hub.nizika.monster
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

513 lines
14 KiB

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