chrono_cable 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +69 -0
  4. data/Rakefile +6 -0
  5. data/app/assets/javascripts/chronocable.js +607 -0
  6. data/app/assets/javascripts/chronocable_turbo.js +5970 -0
  7. data/app/assets/javascripts/chronocable_turbo.min.js +29 -0
  8. data/app/javascript/action_cable/adapters.js +4 -0
  9. data/app/javascript/action_cable/connection.js +183 -0
  10. data/app/javascript/action_cable/connection_monitor.js +124 -0
  11. data/app/javascript/action_cable/consumer.js +80 -0
  12. data/app/javascript/action_cable/index.js +33 -0
  13. data/app/javascript/action_cable/internal.js +21 -0
  14. data/app/javascript/action_cable/logger.js +22 -0
  15. data/app/javascript/action_cable/stream.js +73 -0
  16. data/app/javascript/action_cable/subscription.js +103 -0
  17. data/app/javascript/action_cable/subscription_guarantor.js +50 -0
  18. data/app/javascript/action_cable/subscriptions.js +141 -0
  19. data/app/javascript/turbo/cable.js +19 -0
  20. data/app/javascript/turbo/cable_stream_source_element.js +53 -0
  21. data/app/javascript/turbo/fetch_requests.js +59 -0
  22. data/app/javascript/turbo/index.js +13 -0
  23. data/app/javascript/turbo/snakeize.js +31 -0
  24. data/app/models/chrono_cable/application_record.rb +5 -0
  25. data/app/models/solid_cable/channel.rb +12 -0
  26. data/db/migrate/20260628205427_create_solid_cable_channels.rb +12 -0
  27. data/db/migrate/20260629194211_add_channel_id_to_solid_cable_messages.rb +7 -0
  28. data/lib/action_cable/subscription_adapter/message.rb +12 -0
  29. data/lib/chrono_cable/channel_base.rb +30 -0
  30. data/lib/chrono_cable/channel_streams.rb +121 -0
  31. data/lib/chrono_cable/connection_subscriptions.rb +16 -0
  32. data/lib/chrono_cable/engine.rb +11 -0
  33. data/lib/chrono_cable/solid_cable_adapter.rb +26 -0
  34. data/lib/chrono_cable/solid_cable_adapter_listener.rb +28 -0
  35. data/lib/chrono_cable/solid_cable_broadcasting.rb +19 -0
  36. data/lib/chrono_cable/solid_cable_message_class_methods.rb +9 -0
  37. data/lib/chrono_cable/subscription_adapter_base_extensions.rb +5 -0
  38. data/lib/chrono_cable/turbo_streams_channel.rb +5 -0
  39. data/lib/chrono_cable/version.rb +3 -0
  40. data/lib/chrono_cable.rb +38 -0
  41. data/lib/generators/chrono_cable/install/USAGE +9 -0
  42. data/lib/generators/chrono_cable/install/install_generator.rb +32 -0
  43. data/lib/generators/chrono_cable/install/templates/add_channel_id_to_solid_cable_messages.rb +7 -0
  44. data/lib/generators/chrono_cable/install/templates/create_solid_cable_channels.rb +12 -0
  45. data/lib/tasks/chrono_cable_tasks.rake +6 -0
  46. metadata +116 -0
@@ -0,0 +1,80 @@
1
+ import Connection from "./connection"
2
+ import Subscriptions from "./subscriptions"
3
+
4
+ // The ActionCable.Consumer establishes the connection to a server-side Ruby Connection object. Once established,
5
+ // the ActionCable.ConnectionMonitor will ensure that its properly maintained through heartbeats and checking for stale updates.
6
+ // The Consumer instance is also the gateway to establishing subscriptions to desired channels through the #createSubscription
7
+ // method.
8
+ //
9
+ // The following example shows how this can be set up:
10
+ //
11
+ // App = {}
12
+ // App.cable = ActionCable.createConsumer("ws://example.com/accounts/1")
13
+ // App.appearance = App.cable.subscriptions.create("AppearanceChannel")
14
+ //
15
+ // For more details on how you'd configure an actual channel subscription, see ActionCable.Subscription.
16
+ //
17
+ // When a consumer is created, it automatically connects with the server.
18
+ //
19
+ // To disconnect from the server, call
20
+ //
21
+ // App.cable.disconnect()
22
+ //
23
+ // and to restart the connection:
24
+ //
25
+ // App.cable.connect()
26
+ //
27
+ // Any channel subscriptions which existed prior to disconnecting will
28
+ // automatically resubscribe.
29
+
30
+ export default class Consumer {
31
+ constructor(url) {
32
+ this._url = url
33
+ this.subscriptions = new Subscriptions(this)
34
+ this.connection = new Connection(this)
35
+ this.subprotocols = []
36
+ }
37
+
38
+ get url() {
39
+ return createWebSocketURL(this._url)
40
+ }
41
+
42
+ send(data) {
43
+ return this.connection.send(data)
44
+ }
45
+
46
+ connect() {
47
+ return this.connection.open()
48
+ }
49
+
50
+ disconnect() {
51
+ return this.connection.close({allowReconnect: false})
52
+ }
53
+
54
+ ensureActiveConnection() {
55
+ if (!this.connection.isActive()) {
56
+ return this.connection.open()
57
+ }
58
+ }
59
+
60
+ addSubProtocol(subprotocol) {
61
+ this.subprotocols = [...this.subprotocols, subprotocol]
62
+ }
63
+ }
64
+
65
+ export function createWebSocketURL(url) {
66
+ if (typeof url === "function") {
67
+ url = url()
68
+ }
69
+
70
+ if (url && !/^wss?:/i.test(url)) {
71
+ const a = document.createElement("a")
72
+ a.href = url
73
+ // Fix populating Location properties in IE. Otherwise, protocol will be blank.
74
+ a.href = a.href // eslint-disable-line
75
+ a.protocol = a.protocol.replace("http", "ws")
76
+ return a.href
77
+ } else {
78
+ return url
79
+ }
80
+ }
@@ -0,0 +1,33 @@
1
+ import adapters from "./adapters"
2
+ import Connection from "./connection"
3
+ import ConnectionMonitor from "./connection_monitor"
4
+ import Consumer, { createWebSocketURL } from "./consumer"
5
+ import INTERNAL from "./internal"
6
+ import logger from "./logger"
7
+ import Subscription from "./subscription"
8
+ import SubscriptionGuarantor from "./subscription_guarantor"
9
+ import Subscriptions from "./subscriptions"
10
+
11
+ export {
12
+ Connection,
13
+ ConnectionMonitor,
14
+ Consumer,
15
+ INTERNAL,
16
+ Subscription,
17
+ Subscriptions,
18
+ SubscriptionGuarantor,
19
+ adapters,
20
+ createWebSocketURL,
21
+ logger,
22
+ }
23
+
24
+ export function createConsumer(url = getConfig("url") || INTERNAL.default_mount_path) {
25
+ return new Consumer(url)
26
+ }
27
+
28
+ export function getConfig(name) {
29
+ const element = document.head.querySelector(`meta[name='action-cable-${name}']`)
30
+ if (element) {
31
+ return element.getAttribute("content")
32
+ }
33
+ }
@@ -0,0 +1,21 @@
1
+ export default {
2
+ "message_types": {
3
+ "welcome": "welcome",
4
+ "disconnect": "disconnect",
5
+ "ping": "ping",
6
+ "history": "history",
7
+ "confirmation": "confirm_subscription",
8
+ "rejection": "reject_subscription"
9
+ },
10
+ "disconnect_reasons": {
11
+ "unauthorized": "unauthorized",
12
+ "invalid_request": "invalid_request",
13
+ "server_restart": "server_restart",
14
+ "remote": "remote"
15
+ },
16
+ "default_mount_path": "/cable",
17
+ "protocols": [
18
+ "actioncable-v1-json",
19
+ "actioncable-unsupported"
20
+ ]
21
+ }
@@ -0,0 +1,22 @@
1
+ import adapters from "./adapters"
2
+
3
+ // The logger is disabled by default. You can enable it with:
4
+ //
5
+ // ActionCable.logger.enabled = true
6
+ //
7
+ // Example:
8
+ //
9
+ // import * as ActionCable from '@rails/actioncable'
10
+ //
11
+ // ActionCable.logger.enabled = true
12
+ // ActionCable.logger.log('Connection Established.')
13
+ //
14
+
15
+ export default {
16
+ log(...messages) {
17
+ if (this.enabled) {
18
+ messages.push(Date.now())
19
+ adapters.logger.log("[ActionCable]", ...messages)
20
+ }
21
+ },
22
+ }
@@ -0,0 +1,73 @@
1
+ export default class Stream {
2
+ constructor(subscription, broadcasting, id) {
3
+ this.subscription = subscription
4
+ this.broadcasting = broadcasting
5
+ this.id = id
6
+ this.recovering = false
7
+ this.queue = []
8
+ }
9
+
10
+ isBehind(id) {
11
+ return this.id != null && this.id < id
12
+ }
13
+
14
+ recover(subscriptions, id, message) {
15
+ this.queue.push({id, message})
16
+ this.requestHistory(subscriptions)
17
+ }
18
+
19
+ requestHistory(subscriptions) {
20
+ if (!this.recovering) {
21
+ this.recovering = true
22
+ subscriptions.sendCommand(this.subscription, "history",
23
+ {broadcasting: this.broadcasting, id: this.id})
24
+ }
25
+ }
26
+
27
+ restartRecovery(subscriptions) {
28
+ this.recovering = false
29
+ this.requestHistory(subscriptions)
30
+ }
31
+
32
+ processMessage(id, payload, callback) {
33
+ if (this.id != null && id <= this.id) return true
34
+
35
+ if (this.id == null || id === this.id + 1) {
36
+ this.id = id
37
+ callback(payload)
38
+ return true
39
+ }
40
+
41
+ return false
42
+ }
43
+
44
+ processMessages(messages, callback) {
45
+ const history = messages.map(({ id, payload }) => ({ id, message: JSON.parse(payload) }))
46
+ this.queue = history.concat(this.queue)
47
+
48
+ this.queue.sort((a, b) => a.id - b.id).forEach(({ id, message }) => {
49
+ if (this.id == null || id > this.id) {
50
+ this.id = id
51
+ callback(message)
52
+ }
53
+ })
54
+
55
+ this.queue = []
56
+ this.recovering = false
57
+
58
+ return this.subscription
59
+ }
60
+
61
+ processQueue(callback) {
62
+ const remaining = []
63
+
64
+ this.queue
65
+ .sort((a, b) => a.id - b.id)
66
+ .filter(({id}) => id > this.id)
67
+ .forEach(({ id, message }) => {
68
+ if (!this.processMessage(id, message, callback)) remaining.push({id, message})
69
+ })
70
+
71
+ this.queue = remaining
72
+ }
73
+ }
@@ -0,0 +1,103 @@
1
+ import Stream from "./stream"
2
+
3
+ // A new subscription is created through the ActionCable.Subscriptions instance available on the consumer.
4
+ // It provides a number of callbacks and a method for calling remote procedure calls on the corresponding
5
+ // Channel instance on the server side.
6
+ //
7
+ // An example demonstrates the basic functionality:
8
+ //
9
+ // App.appearance = App.cable.subscriptions.create("AppearanceChannel", {
10
+ // connected() {
11
+ // // Called once the subscription has been successfully completed
12
+ // },
13
+ //
14
+ // disconnected({ willAttemptReconnect: boolean }) {
15
+ // // Called when the client has disconnected with the server.
16
+ // // The object will have an `willAttemptReconnect` property which
17
+ // // says whether the client has the intention of attempting
18
+ // // to reconnect.
19
+ // },
20
+ //
21
+ // appear() {
22
+ // this.perform('appear', {appearing_on: this.appearingOn()})
23
+ // },
24
+ //
25
+ // away() {
26
+ // this.perform('away')
27
+ // },
28
+ //
29
+ // appearingOn() {
30
+ // $('main').data('appearing-on')
31
+ // }
32
+ // })
33
+ //
34
+ // The methods #appear and #away forward their intent to the remote AppearanceChannel instance on the server
35
+ // by calling the `perform` method with the first parameter being the action (which maps to AppearanceChannel#appear/away).
36
+ // The second parameter is a hash that'll get JSON encoded and made available on the server in the data parameter.
37
+ //
38
+ // This is how the server component would look:
39
+ //
40
+ // class AppearanceChannel < ApplicationActionCable::Channel
41
+ // def subscribed
42
+ // current_user.appear
43
+ // end
44
+ //
45
+ // def unsubscribed
46
+ // current_user.disappear
47
+ // end
48
+ //
49
+ // def appear(data)
50
+ // current_user.appear on: data['appearing_on']
51
+ // end
52
+ //
53
+ // def away
54
+ // current_user.away
55
+ // end
56
+ // end
57
+ //
58
+ // The "AppearanceChannel" name is automatically mapped between the client-side subscription creation and the server-side Ruby class name.
59
+ // The AppearanceChannel#appear/away public methods are exposed automatically to client-side invocation through the perform method.
60
+
61
+ const extend = function(object, properties) {
62
+ if (properties != null) {
63
+ for (let key in properties) {
64
+ const value = properties[key]
65
+ object[key] = value
66
+ }
67
+ }
68
+ return object
69
+ }
70
+
71
+ export default class Subscription {
72
+ constructor(consumer, params = {}, mixin) {
73
+ this.consumer = consumer
74
+ this.identifier = JSON.stringify(params)
75
+ this.streams = {}
76
+ extend(this, mixin)
77
+ }
78
+
79
+ // Perform a channel action with the optional data passed as an attribute
80
+ perform(action, data = {}) {
81
+ data.action = action
82
+ return this.send(data)
83
+ }
84
+
85
+ send(data) {
86
+ return this.consumer.send({command: "message", identifier: this.identifier, data: JSON.stringify(data)})
87
+ }
88
+
89
+ unsubscribe() {
90
+ return this.consumer.subscriptions.remove(this)
91
+ }
92
+
93
+ reset() {
94
+ this.streams = {}
95
+ }
96
+
97
+ findOrCreateStream(broadcasting, id = null) {
98
+ if (this.streams[broadcasting] == null) {
99
+ this.streams[broadcasting] = new Stream(this, broadcasting, id)
100
+ }
101
+ return this.streams[broadcasting]
102
+ }
103
+ }
@@ -0,0 +1,50 @@
1
+ import logger from "./logger"
2
+
3
+ // Responsible for ensuring channel subscribe command is confirmed, retrying until confirmation is received.
4
+ // Internal class, not intended for direct user manipulation.
5
+
6
+ class SubscriptionGuarantor {
7
+ constructor(subscriptions) {
8
+ this.subscriptions = subscriptions
9
+ this.pendingSubscriptions = []
10
+ }
11
+
12
+ guarantee(subscription) {
13
+ if(this.pendingSubscriptions.indexOf(subscription) == -1){
14
+ logger.log(`SubscriptionGuarantor guaranteeing ${subscription.identifier}`)
15
+ this.pendingSubscriptions.push(subscription)
16
+ }
17
+ else {
18
+ logger.log(`SubscriptionGuarantor already guaranteeing ${subscription.identifier}`)
19
+ }
20
+ this.startGuaranteeing()
21
+ }
22
+
23
+ forget(subscription) {
24
+ logger.log(`SubscriptionGuarantor forgetting ${subscription.identifier}`)
25
+ this.pendingSubscriptions = (this.pendingSubscriptions.filter((s) => s !== subscription))
26
+ }
27
+
28
+ startGuaranteeing() {
29
+ this.stopGuaranteeing()
30
+ this.retrySubscribing()
31
+ }
32
+
33
+ stopGuaranteeing() {
34
+ clearTimeout(this.retryTimeout)
35
+ }
36
+
37
+ retrySubscribing() {
38
+ this.retryTimeout = setTimeout(() => {
39
+ if (this.subscriptions && typeof(this.subscriptions.subscribe) === "function") {
40
+ this.pendingSubscriptions.map((subscription) => {
41
+ logger.log(`SubscriptionGuarantor resubscribing ${subscription.identifier}`)
42
+ this.subscriptions.subscribe(subscription)
43
+ })
44
+ }
45
+ }
46
+ , 500)
47
+ }
48
+ }
49
+
50
+ export default SubscriptionGuarantor
@@ -0,0 +1,141 @@
1
+ import logger from "./logger"
2
+ import Subscription from "./subscription"
3
+ import SubscriptionGuarantor from "./subscription_guarantor"
4
+
5
+ // Collection class for creating (and internally managing) channel subscriptions.
6
+ // The only method intended to be triggered by the user is ActionCable.Subscriptions#create,
7
+ // and it should be called through the consumer like so:
8
+ //
9
+ // App = {}
10
+ // App.cable = ActionCable.createConsumer("ws://example.com/accounts/1")
11
+ // App.appearance = App.cable.subscriptions.create("AppearanceChannel")
12
+ //
13
+ // For more details on how you'd configure an actual channel subscription, see ActionCable.Subscription.
14
+
15
+ export default class Subscriptions {
16
+ constructor(consumer) {
17
+ this.consumer = consumer
18
+ this.guarantor = new SubscriptionGuarantor(this)
19
+ this.subscriptions = []
20
+ }
21
+
22
+ create(channelName, mixin) {
23
+ const channel = channelName
24
+ const params = typeof channel === "object" ? channel : {channel}
25
+ const subscription = new Subscription(this.consumer, params, mixin)
26
+ return this.add(subscription)
27
+ }
28
+
29
+ // Private
30
+
31
+ add(subscription) {
32
+ this.subscriptions.push(subscription)
33
+ this.consumer.ensureActiveConnection()
34
+ this.notify(subscription, "initialized")
35
+ this.subscribe(subscription)
36
+ return subscription
37
+ }
38
+
39
+ remove(subscription) {
40
+ this.forget(subscription)
41
+ if (!this.findAll(subscription.identifier).length) {
42
+ this.sendCommand(subscription, "unsubscribe")
43
+ }
44
+ return subscription
45
+ }
46
+
47
+ reject(identifier) {
48
+ return this.findAll(identifier).map((subscription) => {
49
+ this.forget(subscription)
50
+ this.notify(subscription, "rejected")
51
+ return subscription
52
+ })
53
+ }
54
+
55
+ forget(subscription) {
56
+ this.guarantor.forget(subscription)
57
+ subscription.reset()
58
+ this.subscriptions = (this.subscriptions.filter((s) => s !== subscription))
59
+ return subscription
60
+ }
61
+
62
+ findAll(identifier) {
63
+ return this.subscriptions.filter((s) => s.identifier === identifier)
64
+ }
65
+
66
+ reload() {
67
+ return this.subscriptions.map((subscription) =>
68
+ this.subscribe(subscription))
69
+ }
70
+
71
+ notifyAll(callbackName, ...args) {
72
+ return this.subscriptions.map((subscription) =>
73
+ this.notify(subscription, callbackName, ...args))
74
+ }
75
+
76
+ notify(subscription, callbackName, ...args) {
77
+ let subscriptions
78
+ if (typeof subscription === "string") {
79
+ subscriptions = this.findAll(subscription)
80
+ } else {
81
+ subscriptions = [subscription]
82
+ }
83
+
84
+ return subscriptions.map((subscription) =>
85
+ (typeof subscription[callbackName] === "function" ? subscription[callbackName](...args) : undefined))
86
+ }
87
+
88
+ subscribe(subscription) {
89
+ if (this.sendCommand(subscription, "subscribe")) {
90
+ this.guarantor.guarantee(subscription)
91
+ }
92
+ }
93
+
94
+ confirmSubscription(identifier, ids) {
95
+ logger.log(`Subscription confirmed ${identifier}`)
96
+ this.findAll(identifier).forEach((subscription) => {
97
+ if (ids != null) {
98
+ Object.entries(ids).forEach(([broadcasting, id]) => {
99
+ const stream = subscription.findOrCreateStream(broadcasting, id)
100
+ if (stream.isBehind(id)) stream.restartRecovery(this)
101
+ })
102
+ }
103
+ this.guarantor.forget(subscription)
104
+ })
105
+ }
106
+
107
+ sendCommand(subscription, command, data = {}) {
108
+ const {identifier} = subscription
109
+ return this.consumer.send({command, identifier, ...data})
110
+ }
111
+
112
+ receive(identifier, message, id, broadcasting) {
113
+ if (id == null || broadcasting == null) {
114
+ return this.notify(identifier, "received", message)
115
+ }
116
+
117
+ return this.findAll(identifier).map((subscription) => {
118
+ const stream = subscription.findOrCreateStream(broadcasting)
119
+ const receive = (message) => this.notify(subscription, "received", message)
120
+
121
+ if (stream.processMessage(id, message, receive)) {
122
+ stream.processQueue(receive)
123
+ } else {
124
+ stream.recover(this, id, message)
125
+ }
126
+
127
+ return subscription
128
+ })
129
+ }
130
+
131
+ ingestHistory(identifier, broadcasting, {messages = [], id}) {
132
+ return this.findAll(identifier).map((subscription) => {
133
+ const stream = subscription.findOrCreateStream(broadcasting, id)
134
+
135
+ stream.processMessages(messages,
136
+ (message) => this.notify(subscription, "received", message))
137
+
138
+ return subscription
139
+ })
140
+ }
141
+ }
@@ -0,0 +1,19 @@
1
+ import { createConsumer as createActionCableConsumer } from "../action_cable"
2
+ let consumer
3
+
4
+ export async function getConsumer() {
5
+ return consumer || setConsumer(createConsumer().then(setConsumer))
6
+ }
7
+
8
+ export function setConsumer(newConsumer) {
9
+ return consumer = newConsumer
10
+ }
11
+
12
+ export async function createConsumer(...args) {
13
+ return createActionCableConsumer(...args)
14
+ }
15
+
16
+ export async function subscribeTo(channel, mixin) {
17
+ const { subscriptions } = await getConsumer()
18
+ return subscriptions.create(channel, mixin)
19
+ }
@@ -0,0 +1,53 @@
1
+ import { connectStreamSource, disconnectStreamSource } from "@hotwired/turbo"
2
+ import { subscribeTo } from "./cable"
3
+ import snakeize from "./snakeize"
4
+
5
+ class TurboCableStreamSourceElement extends HTMLElement {
6
+ static observedAttributes = ["channel", "signed-stream-name"]
7
+
8
+ async connectedCallback() {
9
+ connectStreamSource(this)
10
+ this.subscription = await subscribeTo(this.channel, {
11
+ received: this.dispatchMessageEvent.bind(this),
12
+ connected: this.subscriptionConnected.bind(this),
13
+ disconnected: this.subscriptionDisconnected.bind(this)
14
+ })
15
+ }
16
+
17
+ disconnectedCallback() {
18
+ disconnectStreamSource(this)
19
+ if (this.subscription) this.subscription.unsubscribe()
20
+ this.subscriptionDisconnected()
21
+ }
22
+
23
+ attributeChangedCallback() {
24
+ if (this.subscription) {
25
+ this.disconnectedCallback()
26
+ this.connectedCallback()
27
+ }
28
+ }
29
+
30
+ dispatchMessageEvent(data) {
31
+ const event = new MessageEvent("message", { data })
32
+ return this.dispatchEvent(event)
33
+ }
34
+
35
+ subscriptionConnected() {
36
+ this.setAttribute("connected", "")
37
+ }
38
+
39
+ subscriptionDisconnected() {
40
+ this.removeAttribute("connected")
41
+ }
42
+
43
+ get channel() {
44
+ const channel = this.getAttribute("channel")
45
+ const signed_stream_name = this.getAttribute("signed-stream-name")
46
+ return { channel, signed_stream_name, ...snakeize({ ...this.dataset }) }
47
+ }
48
+ }
49
+
50
+
51
+ if (customElements.get("turbo-cable-stream-source") === undefined) {
52
+ customElements.define("turbo-cable-stream-source", TurboCableStreamSourceElement)
53
+ }
@@ -0,0 +1,59 @@
1
+ export function encodeMethodIntoRequestBody(event) {
2
+ if (event.target instanceof HTMLFormElement) {
3
+ const { target: form, detail: { fetchOptions } } = event
4
+
5
+ form.addEventListener("turbo:submit-start", ({ detail: { formSubmission: { submitter } } }) => {
6
+ const body = isBodyInit(fetchOptions.body) ? fetchOptions.body : new URLSearchParams()
7
+ const method = determineFetchMethod(submitter, body, form)
8
+
9
+ if (!/get/i.test(method)) {
10
+ if (/post/i.test(method)) {
11
+ body.delete("_method")
12
+ } else {
13
+ body.set("_method", method)
14
+ }
15
+
16
+ fetchOptions.method = "post"
17
+ }
18
+ }, { once: true })
19
+ }
20
+ }
21
+
22
+ function determineFetchMethod(submitter, body, form) {
23
+ const formMethod = determineFormMethod(submitter)
24
+ const overrideMethod = body.get("_method")
25
+ const method = form.getAttribute("method") || "get"
26
+
27
+ if (typeof formMethod == "string") {
28
+ return formMethod
29
+ } else if (typeof overrideMethod == "string") {
30
+ return overrideMethod
31
+ } else {
32
+ return method
33
+ }
34
+ }
35
+
36
+ function determineFormMethod(submitter) {
37
+ if (submitter instanceof HTMLButtonElement || submitter instanceof HTMLInputElement) {
38
+ // Rails 7 ActionView::Helpers::FormBuilder#button method has an override
39
+ // for formmethod if the button does not have name or value attributes
40
+ // set, which is the default. This means that if you use <%= f.button
41
+ // formmethod: :delete %>, it will generate a <button name="_method"
42
+ // value="delete" formmethod="post">. Therefore, if the submitter's name
43
+ // is already _method, it's value attribute already contains the desired
44
+ // method.
45
+ if (submitter.name === '_method') {
46
+ return submitter.value
47
+ } else if (submitter.hasAttribute("formmethod")) {
48
+ return submitter.formMethod
49
+ } else {
50
+ return null
51
+ }
52
+ } else {
53
+ return null
54
+ }
55
+ }
56
+
57
+ function isBodyInit(body) {
58
+ return body instanceof FormData || body instanceof URLSearchParams
59
+ }
@@ -0,0 +1,13 @@
1
+ import "./cable_stream_source_element"
2
+
3
+ import * as Turbo from "@hotwired/turbo"
4
+ export { Turbo }
5
+
6
+ import * as cable from "./cable"
7
+ export { cable }
8
+
9
+ import { encodeMethodIntoRequestBody } from "./fetch_requests"
10
+
11
+ window.Turbo = Turbo
12
+
13
+ addEventListener("turbo:before-fetch-request", encodeMethodIntoRequestBody)