turbo-rails 1.4.0 → 1.5.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 40ccdc123db34929af83c559e6bf6c65ca3027b5fd765546143f6093d34662a7
4
- data.tar.gz: 7236691e0dbf00c61fb51039212c9316b7d0fc5d03370e939c5c318b56ab70c7
3
+ metadata.gz: 216ef0fb1d4b07f9231e52702506e6888495d7c15f07a0778eee156692c84d74
4
+ data.tar.gz: 964ea9a3f3111be08955e9c707a591941dfe7b5f943566716bcd26fbd3d9b19b
5
5
  SHA512:
6
- metadata.gz: e7b6bdb711d3e712ac96bd52fc6f909d2ea5beaa2ba664d10e4678792afc58bb61ecce45ccb9b745e8f3706e5221917c754c2a461caff4be864e294050aba10d
7
- data.tar.gz: a6d86dc5249f31161e4f7c3e31cfcda725b1173f0c63348c81f0cd62135688408b4b963540f80564d5eec1fb86f7601090e24c00ed5a55fa5e9c5277a18d75a4
6
+ metadata.gz: 2f42f0e27dcb80e83b3bae71416b1c9c0cdd82ef462dfa96c57bc3fa2d2c88b548785d9e7b10945575ab53538cfc0cf99c49c3c7f5d6f4fdd0608f251ae38bad
7
+ data.tar.gz: 21bd467eed15705e4c46f3da62ae84d8466adc85dcd770abc837c1fbcb517ccca2d7a004f9c856bd2af1f6aed9f83df51825929de463428699a35a58c4b89846
data/README.md CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  On top of accelerating web applications, Turbo was built from the ground-up to form the foundation of hybrid native applications. Write the navigational shell of your [Android](https://github.com/hotwired/turbo-android) or [iOS](https://github.com/hotwired/turbo-ios) app using the standard platform tooling, then seamlessly fill in features from the web, following native navigation patterns. Not every mobile screen needs to be written in Swift or Kotlin to feel native. With Turbo, you spend less time wrangling JSON, waiting on app stores to approve updates, or reimplementing features you've already created in HTML.
6
6
 
7
- Turbo is a language-agnostic framework written in TypeScript, but this gem builds on top of those basics to make the integration with Rails as smooth as possible. You can deliver turbo updates via model callbacks over Action Cable, respond to controller actions with native navigation or standard redirects, and render turbo frames with helpers and layout-free responses.
7
+ Turbo is a language-agnostic framework written in JavaScript, but this gem builds on top of those basics to make the integration with Rails as smooth as possible. You can deliver turbo updates via model callbacks over Action Cable, respond to controller actions with native navigation or standard redirects, and render turbo frames with helpers and layout-free responses.
8
8
 
9
9
 
10
10
  ## Navigate with Turbo Drive
@@ -18,7 +18,7 @@ Whereas Turbolinks previously just dealt with links, Turbo can now also process
18
18
  Turbo Drive can be disabled on a per-element basis by annotating the element or any of its ancestors with `data-turbo="false"`. If you want Turbo Drive to be disabled by default, then you can adjust your import like this:
19
19
 
20
20
  ```js
21
- import { Turbo } from "@hotwired/turbo-rails"
21
+ import "@hotwired/turbo-rails"
22
22
  Turbo.session.drive = false
23
23
  ```
24
24
 
@@ -55,6 +55,37 @@ When the user will click on the `Edit this todo` link, as direct response to thi
55
55
 
56
56
  [See documentation](https://turbo.hotwired.dev/handbook/frames).
57
57
 
58
+ ### A note on custom layouts
59
+
60
+ In order to render turbo frame requests without the application layout, Turbo registers a custom [layout method](https://api.rubyonrails.org/classes/ActionView/Layouts/ClassMethods.html#method-i-layout).
61
+ If your application uses custom layout resolution, you have to make sure to return `"turbo_rails/frame"` (or `false` for TurboRails < 1.4.0) for turbo frame requests:
62
+
63
+ ```ruby
64
+ layout :custom_layout
65
+
66
+ def custom_layout
67
+ return "turbo_rails/frame" if turbo_frame_request?
68
+
69
+ # ... your custom layout logic
70
+ ```
71
+
72
+ If you are using a custom, but "static" layout,
73
+
74
+ ```ruby
75
+ layout "some_static_layout"
76
+ ```
77
+
78
+ you **have** to change it to a layout method in order to conditionally return `false` for turbo frame requests:
79
+
80
+ ```ruby
81
+ layout :custom_layout
82
+
83
+ def custom_layout
84
+ return "turbo_rails/frame" if turbo_frame_request?
85
+
86
+ "some_static_layout"
87
+ ```
88
+
58
89
  ## Come Alive with Turbo Streams
59
90
 
60
91
  Partial page updates that are **delivered asynchronously over a web socket connection** is the hallmark of modern, reactive web applications. With Turbo Streams, you can get all of that modern goodness using the existing server-side HTML you're already rendering to deliver the first page load. With a set of simple CRUD container tags, you can send HTML fragments over the web socket (or in response to direct interactions), and see the page change in response to new data. Again, **no need to construct an entirely separate API**, **no need to wrangle JSON**, **no need to reimplement the HTML construction in JavaScript**. Take the HTML you're already making, wrap it in an update tag, and, voila, your page comes alive.
@@ -96,11 +127,25 @@ import "@hotwired/turbo-rails"
96
127
 
97
128
  You can watch [the video introduction to Hotwire](https://hotwired.dev/#screencast), which focuses extensively on demonstrating Turbo in a Rails demo. Then you should familiarize yourself with [Turbo handbook](https://turbo.hotwired.dev/handbook/introduction) to understand Drive, Frames, and Streams in-depth. Finally, dive into the code documentation by starting with [`Turbo::FramesHelper`](https://github.com/hotwired/turbo-rails/blob/main/app/helpers/turbo/frames_helper.rb), [`Turbo::StreamsHelper`](https://github.com/hotwired/turbo-rails/blob/main/app/helpers/turbo/streams_helper.rb), [`Turbo::Streams::TagBuilder`](https://github.com/hotwired/turbo-rails/blob/main/app/models/turbo/streams/tag_builder.rb), and [`Turbo::Broadcastable`](https://github.com/hotwired/turbo-rails/blob/main/app/models/concerns/turbo/broadcastable.rb).
98
129
 
130
+ ### RubyDoc Documentation
131
+
132
+ For the API documentation covering this gem's classes and packages, [visit the
133
+ RubyDoc page][].
134
+
135
+ [visit the RubyDoc page](https://rubydoc.info/github/hotwired/turbo-rails/main)
99
136
 
100
137
  ## Compatibility with Rails UJS
101
138
 
102
139
  Turbo can coexist with Rails UJS, but you need to take a series of upgrade steps to make it happen. See [the upgrading guide](https://github.com/hotwired/turbo-rails/blob/main/UPGRADING.md).
103
140
 
141
+ ## Testing
142
+
143
+
144
+ The [`Turbo::TestAssertions`](./lib/turbo/test_assertions.rb) concern provides Turbo Stream test helpers that assert the presence or absence of `<turbo-stream>` elements in a rendered fragment of HTML. `Turbo::TestAssertions` are automatically included in [`ActiveSupport::TestCase`](https://edgeapi.rubyonrails.org/classes/ActiveSupport/TestCase.html) and depend on the presence of [`rails-dom-testing`](https://github.com/rails/rails-dom-testing/) assertions.
145
+
146
+ The [`Turbo::TestAssertions::IntegrationTestAssertions`](./lib/turbo/test_assertions/integration_test_assertions.rb) are built on top of `Turbo::TestAssertions`, and add support for passing a `status:` keyword. They are automatically included in [`ActionDispatch::IntegrationTest`](https://edgeguides.rubyonrails.org/testing.html#integration-testing).
147
+
148
+ The [`Turbo::Broadcastable::TestHelper`](./lib/turbo/broadcastable/test_helper.rb) concern provides Action Cable-aware test helpers that assert that `<turbo-stream>` elements were or were not broadcast over Action Cable. They are not automatically included. To use them in your tests, make sure to `include Turbo::Broadcastable::TestHelper`.
104
149
 
105
150
  ## Development
106
151
 
@@ -4177,7 +4177,9 @@ function determineFetchMethod(submitter, body, form) {
4177
4177
 
4178
4178
  function determineFormMethod(submitter) {
4179
4179
  if (submitter instanceof HTMLButtonElement || submitter instanceof HTMLInputElement) {
4180
- if (submitter.hasAttribute("formmethod")) {
4180
+ if (submitter.name === "_method") {
4181
+ return submitter.value;
4182
+ } else if (submitter.hasAttribute("formmethod")) {
4181
4183
  return submitter.formMethod;
4182
4184
  } else {
4183
4185
  return null;
@@ -21,5 +21,5 @@
21
21
 
22
22
  ——
23
23
  Suppress this warning by adding a "data-turbo-suppress-warning" attribute to: %s
24
- `,e.outerHTML);e=e.parentElement}})(),window.Turbo=Oe,Ce();var xe=Object.freeze({__proto__:null,FrameElement:c,get FrameLoadingStyle(){return s},FrameRenderer:J,PageRenderer:ge,PageSnapshot:Z,StreamActions:Ee,StreamElement:We,StreamSourceElement:Ve,cache:Le,clearCache:Me,connectStreamSource:Pe,disconnectStreamSource:ke,navigator:Re,registerAdapter:Ae,renderStreamMessage:Fe,session:ye,setConfirmMethod:He,setFormMode:qe,setProgressBarDelay:Ie,start:Ce,visit:Te});let _e;async function Ue(){return _e||$e(je().then($e))}function $e(e){return _e=e}async function je(){const{createConsumer:e}=await Promise.resolve().then((function(){return mt}));return e()}async function ze(e,t){const{subscriptions:s}=await Ue();return s.create(e,t)}var Ge=Object.freeze({__proto__:null,getConsumer:Ue,setConsumer:$e,createConsumer:je,subscribeTo:ze});function Je(e){return e&&"object"==typeof e?e instanceof Date||e instanceof RegExp?e:Array.isArray(e)?e.map(Je):Object.keys(e).reduce((function(t,s){return t[s[0].toLowerCase()+s.slice(1).replace(/([A-Z]+)/g,(function(e,t){return"_"+t.toLowerCase()}))]=Je(e[s]),t}),{}):e}class Ke extends HTMLElement{async connectedCallback(){Pe(this),this.subscription=await ze(this.channel,{received:this.dispatchMessageEvent.bind(this),connected:this.subscriptionConnected.bind(this),disconnected:this.subscriptionDisconnected.bind(this)})}disconnectedCallback(){ke(this),this.subscription&&this.subscription.unsubscribe()}dispatchMessageEvent(e){const t=new MessageEvent("message",{data:e});return this.dispatchEvent(t)}subscriptionConnected(){this.setAttribute("connected","")}subscriptionDisconnected(){this.removeAttribute("connected")}get channel(){return{channel:this.getAttribute("channel"),signed_stream_name:this.getAttribute("signed-stream-name"),...Je({...this.dataset})}}}void 0===customElements.get("turbo-cable-stream-source")&&customElements.define("turbo-cable-stream-source",Ke),addEventListener("turbo:before-fetch-request",(function(e){if(e.target instanceof HTMLFormElement){const{target:t,detail:{fetchOptions:s}}=e;t.addEventListener("turbo:submit-start",(({detail:{formSubmission:{submitter:e}}})=>{const i=function(e){return e instanceof FormData||e instanceof URLSearchParams}(s.body)?s.body:new URLSearchParams,r=function(e,t,s){const i=function(e){return(e instanceof HTMLButtonElement||e instanceof HTMLInputElement)&&e.hasAttribute("formmethod")?e.formMethod:null}(e),r=t.get("_method"),n=s.getAttribute("method")||"get";return"string"==typeof i?i:"string"==typeof r?r:n}(e,i,t);/get/i.test(r)||(/post/i.test(r)?i.delete("_method"):i.set("_method",r),s.method="post")}),{once:!0})}}));var Qe={logger:self.console,WebSocket:self.WebSocket},Xe={log(...e){this.enabled&&(e.push(Date.now()),Qe.logger.log("[ActionCable]",...e))}};const Ye=()=>(new Date).getTime(),Ze=e=>(Ye()-e)/1e3;class et{constructor(e){this.visibilityDidChange=this.visibilityDidChange.bind(this),this.connection=e,this.reconnectAttempts=0}start(){this.isRunning()||(this.startedAt=Ye(),delete this.stoppedAt,this.startPolling(),addEventListener("visibilitychange",this.visibilityDidChange),Xe.log(`ConnectionMonitor started. stale threshold = ${this.constructor.staleThreshold} s`))}stop(){this.isRunning()&&(this.stoppedAt=Ye(),this.stopPolling(),removeEventListener("visibilitychange",this.visibilityDidChange),Xe.log("ConnectionMonitor stopped"))}isRunning(){return this.startedAt&&!this.stoppedAt}recordPing(){this.pingedAt=Ye()}recordConnect(){this.reconnectAttempts=0,this.recordPing(),delete this.disconnectedAt,Xe.log("ConnectionMonitor recorded connect")}recordDisconnect(){this.disconnectedAt=Ye(),Xe.log("ConnectionMonitor recorded disconnect")}startPolling(){this.stopPolling(),this.poll()}stopPolling(){clearTimeout(this.pollTimeout)}poll(){this.pollTimeout=setTimeout((()=>{this.reconnectIfStale(),this.poll()}),this.getPollInterval())}getPollInterval(){const{staleThreshold:e,reconnectionBackoffRate:t}=this.constructor;return 1e3*e*Math.pow(1+t,Math.min(this.reconnectAttempts,10))*(1+(0===this.reconnectAttempts?1:t)*Math.random())}reconnectIfStale(){this.connectionIsStale()&&(Xe.log(`ConnectionMonitor detected stale connection. reconnectAttempts = ${this.reconnectAttempts}, time stale = ${Ze(this.refreshedAt)} s, stale threshold = ${this.constructor.staleThreshold} s`),this.reconnectAttempts++,this.disconnectedRecently()?Xe.log(`ConnectionMonitor skipping reopening recent disconnect. time disconnected = ${Ze(this.disconnectedAt)} s`):(Xe.log("ConnectionMonitor reopening"),this.connection.reopen()))}get refreshedAt(){return this.pingedAt?this.pingedAt:this.startedAt}connectionIsStale(){return Ze(this.refreshedAt)>this.constructor.staleThreshold}disconnectedRecently(){return this.disconnectedAt&&Ze(this.disconnectedAt)<this.constructor.staleThreshold}visibilityDidChange(){"visible"===document.visibilityState&&setTimeout((()=>{!this.connectionIsStale()&&this.connection.isOpen()||(Xe.log(`ConnectionMonitor reopening stale connection on visibilitychange. visibilityState = ${document.visibilityState}`),this.connection.reopen())}),200)}}et.staleThreshold=6,et.reconnectionBackoffRate=.15;var tt={message_types:{welcome:"welcome",disconnect:"disconnect",ping:"ping",confirmation:"confirm_subscription",rejection:"reject_subscription"},disconnect_reasons:{unauthorized:"unauthorized",invalid_request:"invalid_request",server_restart:"server_restart"},default_mount_path:"/cable",protocols:["actioncable-v1-json","actioncable-unsupported"]};const{message_types:st,protocols:it}=tt,rt=it.slice(0,it.length-1),nt=[].indexOf;class ot{constructor(e){this.open=this.open.bind(this),this.consumer=e,this.subscriptions=this.consumer.subscriptions,this.monitor=new et(this),this.disconnected=!0}send(e){return!!this.isOpen()&&(this.webSocket.send(JSON.stringify(e)),!0)}open(){return this.isActive()?(Xe.log(`Attempted to open WebSocket, but existing socket is ${this.getState()}`),!1):(Xe.log(`Opening WebSocket, current state is ${this.getState()}, subprotocols: ${it}`),this.webSocket&&this.uninstallEventHandlers(),this.webSocket=new Qe.WebSocket(this.consumer.url,it),this.installEventHandlers(),this.monitor.start(),!0)}close({allowReconnect:e}={allowReconnect:!0}){if(e||this.monitor.stop(),this.isActive())return this.webSocket.close()}reopen(){if(Xe.log(`Reopening WebSocket, current state is ${this.getState()}`),!this.isActive())return this.open();try{return this.close()}catch(e){Xe.log("Failed to reopen WebSocket",e)}finally{Xe.log(`Reopening WebSocket in ${this.constructor.reopenDelay}ms`),setTimeout(this.open,this.constructor.reopenDelay)}}getProtocol(){if(this.webSocket)return this.webSocket.protocol}isOpen(){return this.isState("open")}isActive(){return this.isState("open","connecting")}isProtocolSupported(){return nt.call(rt,this.getProtocol())>=0}isState(...e){return nt.call(e,this.getState())>=0}getState(){if(this.webSocket)for(let e in Qe.WebSocket)if(Qe.WebSocket[e]===this.webSocket.readyState)return e.toLowerCase();return null}installEventHandlers(){for(let e in this.events){const t=this.events[e].bind(this);this.webSocket[`on${e}`]=t}}uninstallEventHandlers(){for(let e in this.events)this.webSocket[`on${e}`]=function(){}}}ot.reopenDelay=500,ot.prototype.events={message(e){if(!this.isProtocolSupported())return;const{identifier:t,message:s,reason:i,reconnect:r,type:n}=JSON.parse(e.data);switch(n){case st.welcome:return this.monitor.recordConnect(),this.subscriptions.reload();case st.disconnect:return Xe.log(`Disconnecting. Reason: ${i}`),this.close({allowReconnect:r});case st.ping:return this.monitor.recordPing();case st.confirmation:return this.subscriptions.confirmSubscription(t),this.subscriptions.notify(t,"connected");case st.rejection:return this.subscriptions.reject(t);default:return this.subscriptions.notify(t,"received",s)}},open(){if(Xe.log(`WebSocket onopen event, using '${this.getProtocol()}' subprotocol`),this.disconnected=!1,!this.isProtocolSupported())return Xe.log("Protocol is unsupported. Stopping monitor and disconnecting."),this.close({allowReconnect:!1})},close(e){if(Xe.log("WebSocket onclose event"),!this.disconnected)return this.disconnected=!0,this.monitor.recordDisconnect(),this.subscriptions.notifyAll("disconnected",{willAttemptReconnect:this.monitor.isRunning()})},error(){Xe.log("WebSocket onerror event")}};class at{constructor(e,t={},s){this.consumer=e,this.identifier=JSON.stringify(t),function(e,t){if(null!=t)for(let s in t){const i=t[s];e[s]=i}}(this,s)}perform(e,t={}){return t.action=e,this.send(t)}send(e){return this.consumer.send({command:"message",identifier:this.identifier,data:JSON.stringify(e)})}unsubscribe(){return this.consumer.subscriptions.remove(this)}}class ct{constructor(e){this.subscriptions=e,this.pendingSubscriptions=[]}guarantee(e){-1==this.pendingSubscriptions.indexOf(e)?(Xe.log(`SubscriptionGuarantor guaranteeing ${e.identifier}`),this.pendingSubscriptions.push(e)):Xe.log(`SubscriptionGuarantor already guaranteeing ${e.identifier}`),this.startGuaranteeing()}forget(e){Xe.log(`SubscriptionGuarantor forgetting ${e.identifier}`),this.pendingSubscriptions=this.pendingSubscriptions.filter((t=>t!==e))}startGuaranteeing(){this.stopGuaranteeing(),this.retrySubscribing()}stopGuaranteeing(){clearTimeout(this.retryTimeout)}retrySubscribing(){this.retryTimeout=setTimeout((()=>{this.subscriptions&&"function"==typeof this.subscriptions.subscribe&&this.pendingSubscriptions.map((e=>{Xe.log(`SubscriptionGuarantor resubscribing ${e.identifier}`),this.subscriptions.subscribe(e)}))}),500)}}class lt{constructor(e){this.consumer=e,this.guarantor=new ct(this),this.subscriptions=[]}create(e,t){const s="object"==typeof e?e:{channel:e},i=new at(this.consumer,s,t);return this.add(i)}add(e){return this.subscriptions.push(e),this.consumer.ensureActiveConnection(),this.notify(e,"initialized"),this.subscribe(e),e}remove(e){return this.forget(e),this.findAll(e.identifier).length||this.sendCommand(e,"unsubscribe"),e}reject(e){return this.findAll(e).map((e=>(this.forget(e),this.notify(e,"rejected"),e)))}forget(e){return this.guarantor.forget(e),this.subscriptions=this.subscriptions.filter((t=>t!==e)),e}findAll(e){return this.subscriptions.filter((t=>t.identifier===e))}reload(){return this.subscriptions.map((e=>this.subscribe(e)))}notifyAll(e,...t){return this.subscriptions.map((s=>this.notify(s,e,...t)))}notify(e,t,...s){let i;return i="string"==typeof e?this.findAll(e):[e],i.map((e=>"function"==typeof e[t]?e[t](...s):void 0))}subscribe(e){this.sendCommand(e,"subscribe")&&this.guarantor.guarantee(e)}confirmSubscription(e){Xe.log(`Subscription confirmed ${e}`),this.findAll(e).map((e=>this.guarantor.forget(e)))}sendCommand(e,t){const{identifier:s}=e;return this.consumer.send({command:t,identifier:s})}}class ht{constructor(e){this._url=e,this.subscriptions=new lt(this),this.connection=new ot(this)}get url(){return dt(this._url)}send(e){return this.connection.send(e)}connect(){return this.connection.open()}disconnect(){return this.connection.close({allowReconnect:!1})}ensureActiveConnection(){if(!this.connection.isActive())return this.connection.open()}}function dt(e){if("function"==typeof e&&(e=e()),e&&!/^wss?:/i.test(e)){const t=document.createElement("a");return t.href=e,t.href=t.href,t.protocol=t.protocol.replace("http","ws"),t.href}return e}function ut(e){const t=document.head.querySelector(`meta[name='action-cable-${e}']`);if(t)return t.getAttribute("content")}var mt=Object.freeze({__proto__:null,Connection:ot,ConnectionMonitor:et,Consumer:ht,INTERNAL:tt,Subscription:at,Subscriptions:lt,SubscriptionGuarantor:ct,adapters:Qe,createWebSocketURL:dt,logger:Xe,createConsumer:function(e=ut("url")||tt.default_mount_path){return new ht(e)},getConfig:ut});export{xe as Turbo,Ge as cable};
24
+ `,e.outerHTML);e=e.parentElement}})(),window.Turbo=Oe,Ce();var xe=Object.freeze({__proto__:null,FrameElement:c,get FrameLoadingStyle(){return s},FrameRenderer:J,PageRenderer:ge,PageSnapshot:Z,StreamActions:Ee,StreamElement:We,StreamSourceElement:Ve,cache:Le,clearCache:Me,connectStreamSource:Pe,disconnectStreamSource:ke,navigator:Re,registerAdapter:Ae,renderStreamMessage:Fe,session:ye,setConfirmMethod:He,setFormMode:qe,setProgressBarDelay:Ie,start:Ce,visit:Te});let _e;async function Ue(){return _e||$e(je().then($e))}function $e(e){return _e=e}async function je(){const{createConsumer:e}=await Promise.resolve().then((function(){return mt}));return e()}async function ze(e,t){const{subscriptions:s}=await Ue();return s.create(e,t)}var Ge=Object.freeze({__proto__:null,getConsumer:Ue,setConsumer:$e,createConsumer:je,subscribeTo:ze});function Je(e){return e&&"object"==typeof e?e instanceof Date||e instanceof RegExp?e:Array.isArray(e)?e.map(Je):Object.keys(e).reduce((function(t,s){return t[s[0].toLowerCase()+s.slice(1).replace(/([A-Z]+)/g,(function(e,t){return"_"+t.toLowerCase()}))]=Je(e[s]),t}),{}):e}class Ke extends HTMLElement{async connectedCallback(){Pe(this),this.subscription=await ze(this.channel,{received:this.dispatchMessageEvent.bind(this),connected:this.subscriptionConnected.bind(this),disconnected:this.subscriptionDisconnected.bind(this)})}disconnectedCallback(){ke(this),this.subscription&&this.subscription.unsubscribe()}dispatchMessageEvent(e){const t=new MessageEvent("message",{data:e});return this.dispatchEvent(t)}subscriptionConnected(){this.setAttribute("connected","")}subscriptionDisconnected(){this.removeAttribute("connected")}get channel(){return{channel:this.getAttribute("channel"),signed_stream_name:this.getAttribute("signed-stream-name"),...Je({...this.dataset})}}}void 0===customElements.get("turbo-cable-stream-source")&&customElements.define("turbo-cable-stream-source",Ke),addEventListener("turbo:before-fetch-request",(function(e){if(e.target instanceof HTMLFormElement){const{target:t,detail:{fetchOptions:s}}=e;t.addEventListener("turbo:submit-start",(({detail:{formSubmission:{submitter:e}}})=>{const i=function(e){return e instanceof FormData||e instanceof URLSearchParams}(s.body)?s.body:new URLSearchParams,r=function(e,t,s){const i=function(e){return e instanceof HTMLButtonElement||e instanceof HTMLInputElement?"_method"===e.name?e.value:e.hasAttribute("formmethod")?e.formMethod:null:null}(e),r=t.get("_method"),n=s.getAttribute("method")||"get";return"string"==typeof i?i:"string"==typeof r?r:n}(e,i,t);/get/i.test(r)||(/post/i.test(r)?i.delete("_method"):i.set("_method",r),s.method="post")}),{once:!0})}}));var Qe={logger:self.console,WebSocket:self.WebSocket},Xe={log(...e){this.enabled&&(e.push(Date.now()),Qe.logger.log("[ActionCable]",...e))}};const Ye=()=>(new Date).getTime(),Ze=e=>(Ye()-e)/1e3;class et{constructor(e){this.visibilityDidChange=this.visibilityDidChange.bind(this),this.connection=e,this.reconnectAttempts=0}start(){this.isRunning()||(this.startedAt=Ye(),delete this.stoppedAt,this.startPolling(),addEventListener("visibilitychange",this.visibilityDidChange),Xe.log(`ConnectionMonitor started. stale threshold = ${this.constructor.staleThreshold} s`))}stop(){this.isRunning()&&(this.stoppedAt=Ye(),this.stopPolling(),removeEventListener("visibilitychange",this.visibilityDidChange),Xe.log("ConnectionMonitor stopped"))}isRunning(){return this.startedAt&&!this.stoppedAt}recordPing(){this.pingedAt=Ye()}recordConnect(){this.reconnectAttempts=0,this.recordPing(),delete this.disconnectedAt,Xe.log("ConnectionMonitor recorded connect")}recordDisconnect(){this.disconnectedAt=Ye(),Xe.log("ConnectionMonitor recorded disconnect")}startPolling(){this.stopPolling(),this.poll()}stopPolling(){clearTimeout(this.pollTimeout)}poll(){this.pollTimeout=setTimeout((()=>{this.reconnectIfStale(),this.poll()}),this.getPollInterval())}getPollInterval(){const{staleThreshold:e,reconnectionBackoffRate:t}=this.constructor;return 1e3*e*Math.pow(1+t,Math.min(this.reconnectAttempts,10))*(1+(0===this.reconnectAttempts?1:t)*Math.random())}reconnectIfStale(){this.connectionIsStale()&&(Xe.log(`ConnectionMonitor detected stale connection. reconnectAttempts = ${this.reconnectAttempts}, time stale = ${Ze(this.refreshedAt)} s, stale threshold = ${this.constructor.staleThreshold} s`),this.reconnectAttempts++,this.disconnectedRecently()?Xe.log(`ConnectionMonitor skipping reopening recent disconnect. time disconnected = ${Ze(this.disconnectedAt)} s`):(Xe.log("ConnectionMonitor reopening"),this.connection.reopen()))}get refreshedAt(){return this.pingedAt?this.pingedAt:this.startedAt}connectionIsStale(){return Ze(this.refreshedAt)>this.constructor.staleThreshold}disconnectedRecently(){return this.disconnectedAt&&Ze(this.disconnectedAt)<this.constructor.staleThreshold}visibilityDidChange(){"visible"===document.visibilityState&&setTimeout((()=>{!this.connectionIsStale()&&this.connection.isOpen()||(Xe.log(`ConnectionMonitor reopening stale connection on visibilitychange. visibilityState = ${document.visibilityState}`),this.connection.reopen())}),200)}}et.staleThreshold=6,et.reconnectionBackoffRate=.15;var tt={message_types:{welcome:"welcome",disconnect:"disconnect",ping:"ping",confirmation:"confirm_subscription",rejection:"reject_subscription"},disconnect_reasons:{unauthorized:"unauthorized",invalid_request:"invalid_request",server_restart:"server_restart"},default_mount_path:"/cable",protocols:["actioncable-v1-json","actioncable-unsupported"]};const{message_types:st,protocols:it}=tt,rt=it.slice(0,it.length-1),nt=[].indexOf;class ot{constructor(e){this.open=this.open.bind(this),this.consumer=e,this.subscriptions=this.consumer.subscriptions,this.monitor=new et(this),this.disconnected=!0}send(e){return!!this.isOpen()&&(this.webSocket.send(JSON.stringify(e)),!0)}open(){return this.isActive()?(Xe.log(`Attempted to open WebSocket, but existing socket is ${this.getState()}`),!1):(Xe.log(`Opening WebSocket, current state is ${this.getState()}, subprotocols: ${it}`),this.webSocket&&this.uninstallEventHandlers(),this.webSocket=new Qe.WebSocket(this.consumer.url,it),this.installEventHandlers(),this.monitor.start(),!0)}close({allowReconnect:e}={allowReconnect:!0}){if(e||this.monitor.stop(),this.isActive())return this.webSocket.close()}reopen(){if(Xe.log(`Reopening WebSocket, current state is ${this.getState()}`),!this.isActive())return this.open();try{return this.close()}catch(e){Xe.log("Failed to reopen WebSocket",e)}finally{Xe.log(`Reopening WebSocket in ${this.constructor.reopenDelay}ms`),setTimeout(this.open,this.constructor.reopenDelay)}}getProtocol(){if(this.webSocket)return this.webSocket.protocol}isOpen(){return this.isState("open")}isActive(){return this.isState("open","connecting")}isProtocolSupported(){return nt.call(rt,this.getProtocol())>=0}isState(...e){return nt.call(e,this.getState())>=0}getState(){if(this.webSocket)for(let e in Qe.WebSocket)if(Qe.WebSocket[e]===this.webSocket.readyState)return e.toLowerCase();return null}installEventHandlers(){for(let e in this.events){const t=this.events[e].bind(this);this.webSocket[`on${e}`]=t}}uninstallEventHandlers(){for(let e in this.events)this.webSocket[`on${e}`]=function(){}}}ot.reopenDelay=500,ot.prototype.events={message(e){if(!this.isProtocolSupported())return;const{identifier:t,message:s,reason:i,reconnect:r,type:n}=JSON.parse(e.data);switch(n){case st.welcome:return this.monitor.recordConnect(),this.subscriptions.reload();case st.disconnect:return Xe.log(`Disconnecting. Reason: ${i}`),this.close({allowReconnect:r});case st.ping:return this.monitor.recordPing();case st.confirmation:return this.subscriptions.confirmSubscription(t),this.subscriptions.notify(t,"connected");case st.rejection:return this.subscriptions.reject(t);default:return this.subscriptions.notify(t,"received",s)}},open(){if(Xe.log(`WebSocket onopen event, using '${this.getProtocol()}' subprotocol`),this.disconnected=!1,!this.isProtocolSupported())return Xe.log("Protocol is unsupported. Stopping monitor and disconnecting."),this.close({allowReconnect:!1})},close(e){if(Xe.log("WebSocket onclose event"),!this.disconnected)return this.disconnected=!0,this.monitor.recordDisconnect(),this.subscriptions.notifyAll("disconnected",{willAttemptReconnect:this.monitor.isRunning()})},error(){Xe.log("WebSocket onerror event")}};class at{constructor(e,t={},s){this.consumer=e,this.identifier=JSON.stringify(t),function(e,t){if(null!=t)for(let s in t){const i=t[s];e[s]=i}}(this,s)}perform(e,t={}){return t.action=e,this.send(t)}send(e){return this.consumer.send({command:"message",identifier:this.identifier,data:JSON.stringify(e)})}unsubscribe(){return this.consumer.subscriptions.remove(this)}}class ct{constructor(e){this.subscriptions=e,this.pendingSubscriptions=[]}guarantee(e){-1==this.pendingSubscriptions.indexOf(e)?(Xe.log(`SubscriptionGuarantor guaranteeing ${e.identifier}`),this.pendingSubscriptions.push(e)):Xe.log(`SubscriptionGuarantor already guaranteeing ${e.identifier}`),this.startGuaranteeing()}forget(e){Xe.log(`SubscriptionGuarantor forgetting ${e.identifier}`),this.pendingSubscriptions=this.pendingSubscriptions.filter((t=>t!==e))}startGuaranteeing(){this.stopGuaranteeing(),this.retrySubscribing()}stopGuaranteeing(){clearTimeout(this.retryTimeout)}retrySubscribing(){this.retryTimeout=setTimeout((()=>{this.subscriptions&&"function"==typeof this.subscriptions.subscribe&&this.pendingSubscriptions.map((e=>{Xe.log(`SubscriptionGuarantor resubscribing ${e.identifier}`),this.subscriptions.subscribe(e)}))}),500)}}class lt{constructor(e){this.consumer=e,this.guarantor=new ct(this),this.subscriptions=[]}create(e,t){const s="object"==typeof e?e:{channel:e},i=new at(this.consumer,s,t);return this.add(i)}add(e){return this.subscriptions.push(e),this.consumer.ensureActiveConnection(),this.notify(e,"initialized"),this.subscribe(e),e}remove(e){return this.forget(e),this.findAll(e.identifier).length||this.sendCommand(e,"unsubscribe"),e}reject(e){return this.findAll(e).map((e=>(this.forget(e),this.notify(e,"rejected"),e)))}forget(e){return this.guarantor.forget(e),this.subscriptions=this.subscriptions.filter((t=>t!==e)),e}findAll(e){return this.subscriptions.filter((t=>t.identifier===e))}reload(){return this.subscriptions.map((e=>this.subscribe(e)))}notifyAll(e,...t){return this.subscriptions.map((s=>this.notify(s,e,...t)))}notify(e,t,...s){let i;return i="string"==typeof e?this.findAll(e):[e],i.map((e=>"function"==typeof e[t]?e[t](...s):void 0))}subscribe(e){this.sendCommand(e,"subscribe")&&this.guarantor.guarantee(e)}confirmSubscription(e){Xe.log(`Subscription confirmed ${e}`),this.findAll(e).map((e=>this.guarantor.forget(e)))}sendCommand(e,t){const{identifier:s}=e;return this.consumer.send({command:t,identifier:s})}}class ht{constructor(e){this._url=e,this.subscriptions=new lt(this),this.connection=new ot(this)}get url(){return dt(this._url)}send(e){return this.connection.send(e)}connect(){return this.connection.open()}disconnect(){return this.connection.close({allowReconnect:!1})}ensureActiveConnection(){if(!this.connection.isActive())return this.connection.open()}}function dt(e){if("function"==typeof e&&(e=e()),e&&!/^wss?:/i.test(e)){const t=document.createElement("a");return t.href=e,t.href=t.href,t.protocol=t.protocol.replace("http","ws"),t.href}return e}function ut(e){const t=document.head.querySelector(`meta[name='action-cable-${e}']`);if(t)return t.getAttribute("content")}var mt=Object.freeze({__proto__:null,Connection:ot,ConnectionMonitor:et,Consumer:ht,INTERNAL:tt,Subscription:at,Subscriptions:lt,SubscriptionGuarantor:ct,adapters:Qe,createWebSocketURL:dt,logger:Xe,createConsumer:function(e=ut("url")||tt.default_mount_path){return new ht(e)},getConfig:ut});export{xe as Turbo,Ge as cable};
25
25
  //# sourceMappingURL=turbo.min.js.map