ぼざクリ タグ広場 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.
 
 
 
 
 
 

511 lines
16 KiB

  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. case message_types.disconnect:
  247. logger.log(`Disconnecting. Reason: ${reason}`);
  248. return this.close({
  249. allowReconnect: reconnect
  250. });
  251. case message_types.ping:
  252. return null;
  253. case message_types.confirmation:
  254. this.subscriptions.confirmSubscription(identifier);
  255. if (this.reconnectAttempted) {
  256. this.reconnectAttempted = false;
  257. return this.subscriptions.notify(identifier, "connected", {
  258. reconnected: true
  259. });
  260. } else {
  261. return this.subscriptions.notify(identifier, "connected", {
  262. reconnected: false
  263. });
  264. }
  265. case message_types.rejection:
  266. return this.subscriptions.reject(identifier);
  267. default:
  268. return this.subscriptions.notify(identifier, "received", message);
  269. }
  270. },
  271. open() {
  272. logger.log(`WebSocket onopen event, using '${this.getProtocol()}' subprotocol`);
  273. this.disconnected = false;
  274. if (!this.isProtocolSupported()) {
  275. logger.log("Protocol is unsupported. Stopping monitor and disconnecting.");
  276. return this.close({
  277. allowReconnect: false
  278. });
  279. }
  280. },
  281. close(event) {
  282. logger.log("WebSocket onclose event");
  283. if (this.disconnected) {
  284. return;
  285. }
  286. this.disconnected = true;
  287. this.monitor.recordDisconnect();
  288. return this.subscriptions.notifyAll("disconnected", {
  289. willAttemptReconnect: this.monitor.isRunning()
  290. });
  291. },
  292. error() {
  293. logger.log("WebSocket onerror event");
  294. }
  295. };
  296. const extend = function(object, properties) {
  297. if (properties != null) {
  298. for (let key in properties) {
  299. const value = properties[key];
  300. object[key] = value;
  301. }
  302. }
  303. return object;
  304. };
  305. class Subscription {
  306. constructor(consumer, params = {}, mixin) {
  307. this.consumer = consumer;
  308. this.identifier = JSON.stringify(params);
  309. extend(this, mixin);
  310. }
  311. perform(action, data = {}) {
  312. data.action = action;
  313. return this.send(data);
  314. }
  315. send(data) {
  316. return this.consumer.send({
  317. command: "message",
  318. identifier: this.identifier,
  319. data: JSON.stringify(data)
  320. });
  321. }
  322. unsubscribe() {
  323. return this.consumer.subscriptions.remove(this);
  324. }
  325. }
  326. class SubscriptionGuarantor {
  327. constructor(subscriptions) {
  328. this.subscriptions = subscriptions;
  329. this.pendingSubscriptions = [];
  330. }
  331. guarantee(subscription) {
  332. if (this.pendingSubscriptions.indexOf(subscription) == -1) {
  333. logger.log(`SubscriptionGuarantor guaranteeing ${subscription.identifier}`);
  334. this.pendingSubscriptions.push(subscription);
  335. } else {
  336. logger.log(`SubscriptionGuarantor already guaranteeing ${subscription.identifier}`);
  337. }
  338. this.startGuaranteeing();
  339. }
  340. forget(subscription) {
  341. logger.log(`SubscriptionGuarantor forgetting ${subscription.identifier}`);
  342. this.pendingSubscriptions = this.pendingSubscriptions.filter((s => s !== subscription));
  343. }
  344. startGuaranteeing() {
  345. this.stopGuaranteeing();
  346. this.retrySubscribing();
  347. }
  348. stopGuaranteeing() {
  349. clearTimeout(this.retryTimeout);
  350. }
  351. retrySubscribing() {
  352. this.retryTimeout = setTimeout((() => {
  353. if (this.subscriptions && typeof this.subscriptions.subscribe === "function") {
  354. this.pendingSubscriptions.map((subscription => {
  355. logger.log(`SubscriptionGuarantor resubscribing ${subscription.identifier}`);
  356. this.subscriptions.subscribe(subscription);
  357. }));
  358. }
  359. }), 500);
  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. class Consumer {
  438. constructor(url) {
  439. this._url = url;
  440. this.subscriptions = new Subscriptions(this);
  441. this.connection = new Connection(this);
  442. this.subprotocols = [];
  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. addSubProtocol(subprotocol) {
  464. this.subprotocols = [ ...this.subprotocols, subprotocol ];
  465. }
  466. }
  467. function createWebSocketURL(url) {
  468. if (typeof url === "function") {
  469. url = url();
  470. }
  471. if (url && !/^wss?:/i.test(url)) {
  472. const a = document.createElement("a");
  473. a.href = url;
  474. a.href = a.href;
  475. a.protocol = a.protocol.replace("http", "ws");
  476. return a.href;
  477. } else {
  478. return url;
  479. }
  480. }
  481. function createConsumer(url = getConfig("url") || INTERNAL.default_mount_path) {
  482. return new Consumer(url);
  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. exports.Connection = Connection;
  491. exports.ConnectionMonitor = ConnectionMonitor;
  492. exports.Consumer = Consumer;
  493. exports.INTERNAL = INTERNAL;
  494. exports.Subscription = Subscription;
  495. exports.SubscriptionGuarantor = SubscriptionGuarantor;
  496. exports.Subscriptions = Subscriptions;
  497. exports.adapters = adapters;
  498. exports.createConsumer = createConsumer;
  499. exports.createWebSocketURL = createWebSocketURL;
  500. exports.getConfig = getConfig;
  501. exports.logger = logger;
  502. Object.defineProperty(exports, "__esModule", {
  503. value: true
  504. });
  505. }));