Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature Request: declarative connection to a Turbo Stream using the turbo-stream custom tag #413

Closed
delitescere opened this issue Oct 2, 2021 · 7 comments · Fixed by #415

Comments

@delitescere
Copy link

Currently, some JavaScript is needed to connect a Turbo Stream to an EventSource (SSE) or WebSocket (by the way, the docs aren't very clear about how to do this).

It would be superb if it could be done without JavaScript, rather using an incantation of the turbo-stream custom element.

Current imperative mechanism:

(window['EventSource'] && window['Turbo']) ?
  Turbo.connectStreamSource(new EventSource('/my-turbo-stream')) :
  console.warn('Turbo Streams over SSE not available');

Suggested declarative mechanism:

<turbo-stream src="/my-turbo-stream">

If the URL scheme was ws: then Turbo would create a new WebSocket, otherwise it would be an EventSource.

The addition of a src attribute to the custom turbo-stream tag need not (but may) be conflated with the use of it in directives to altering the DOM as content arrives on the stream or as the result of a form submission (i.e. the existing capabilities of the custom tag).

@seanpdoyle
Copy link
Contributor

Could you share a use case that you have in mind to help demonstrate this feature's utility?

There seems to be some confusion between concepts.

A <turbo-stream> represents a single DOM operation, and disappears once it's connected to the page.

The @hotwired/turbo-rails package's <turbo-cable-stream-source> is a persistently connected element. Its [channel] and [signed-stream-name] attributes are forwarded to the Rails application's underlying Action Cable connection. Action Cable does not yet support connecting over EventSource instances, or ActionController::Live integration. My hunch is that the channel and stream-name attributes are serving the purpose of a ws:// or wss:// web socket URL at a different layer of abstraction.

Since the <turbo-cable-stream-source> element is declared in @hotwired/turbo-rails, that repository might be a better place to open an Issue. If your use case is outside of Rails, I'm interpreting the scope of the Issue as much larger, including the introduction of a backend agnostic version of the <turbo-cable-stream-source>.

Am I misinterpreting anything here?

@delitescere
Copy link
Author

Yes, this has nothing to do with Rails, the turbo-rails package, nor any server-side code. This is something entirely for @hotwired/turbo.js on the client side, just like a <turbo-frame>, &c.

In the case the <turbo-stream> custom element is present in a response entity of type text/vnd.turbo-stream.html, the treatment of the element would not change.

This proposal is for allowing the use of the same custom element in a HTML page (of type text/html) such that the turbo library would process it in a declarative manner in order to open a Turbo Stream, replacing the need for imperative, error-prone JavaScript.

The likely placement of the <turbo-stream src="..."> element when used this way would be as a child of <head> element, as that is where the imperative JavaScript incantation would currently go too.

Your "seems to be some confusion between concepts" comment indeed mirrors my "need not (but may) be conflated with" thought ;-)

@seanpdoyle
Copy link
Contributor

Thank you for clarifying. Could you share some code or pseudo code to demonstrate the utility?

Presuming the turbo-stream were rendered in the HTML on the server side, what value is there in making an additional HTTP request to the src URL? Would rendering the element directly with contents suffice?

@delitescere
Copy link
Author

delitescere commented Oct 3, 2021

I think you're still confused? This isn't about the content of a turbo-stream, this is about connecting to a turbo-stream.

Without Rails, how do you currently get a browser to connect to an EventStream (or WebSocket) that contains TurboStream content?

You add this to the page (that could be a static HTML file) that's going to receive that stream and have parts of its DOM updated by Turbo:

<head>
    <script>
        (window['EventSource'] && window['Turbo']) ?
          Turbo.connectStreamSource(new EventSource('/my-turbo-stream')) :
          console.warn('Turbo Streams over SSE not available');
    </script>
</head>
<body>
    <div id="some_target_of_my_turbo_stream"><div>
</body>

All this feature request proposes is to remove the need for the <script> tag and its contents, replacing it with something Hotwire-esque and declarative:

<head>
    <turbo-stream src="/my-turbo-stream" />
</head>
<body>
    <div id="some_target_of_my_turbo_stream"><div>
</body>

That <turbo-stream> tag is processed by @hotwire/turbo.js at the same time as <turbo-frame> elements (on DOMContentLoaded) to do the same thing as the JavaScript it would replace, along with checking the URL scheme to decide whether to open a WebSocket or an EventStream.

Does calling it <turbo-stream-connection src="/my-turbo-stream" /> make it more obvious (albeit wholly unnecessary otherwise)?

@seanpdoyle
Copy link
Contributor

seanpdoyle commented Oct 3, 2021

Thank you for sharing that code, I think we're aligned now.

To re-contextualize a part of my original comment:

If your use case is outside of Rails, I'm interpreting the scope of the Issue as much larger, including the introduction of a backend agnostic version of the turbo-cable-stream-source.

I think we're still discussing the details of a backend agnostic version of the turbo-cable-stream-source element. Based on your code sample, I'm interpreting that you're suggesting the current turbo-stream element be extended. Is that correct?

I think overloading the element to behave differently based on the presence or absence of a src attribute could be tricky. Maybe a new turbo-stream-source element (like the Rails version, but without the "cable") could serve that single purpose.

Is there something about extending the semantics and behavior of the existing turbo-stream element that's more compelling than introducing a new element?

seanpdoyle added a commit to seanpdoyle/turbo that referenced this issue Oct 3, 2021
Closes hotwired#413

The `<turbo-stream-source>` element accepts a `[src]` attribute, and
uses that to connect Turbo to poll for streams published on the server
side.

When the element is connected to the document, the stream source is
connected. When the element is disconnected, the stream is disconnected.

When declared with an `ws://` or `wss://` URL, the underlying Stream
Source will be a `WebSocket` instance. Otherwise, the connection is
through an `EventSource`.

Since the document's `<head>` is persistent across navigations, the
`<turbo-stream-source>` is meant to be mounted within the `<body>`
element.

Typical full page navigations driven by Turbo will result in the
`<body>` being discarded and replaced with the resulting document. It's
the server's responsibility to ensure that the element is present on
each page that requires streaming.
@seanpdoyle
Copy link
Contributor

I've opened #415 to implement this. @delitescere Could you weigh in there on whether or not it fits your use case?

seanpdoyle added a commit to seanpdoyle/turbo that referenced this issue Oct 3, 2021
Closes hotwired#413

The `<turbo-stream-source>` element accepts a `[src]` attribute, and
uses that to connect Turbo to poll for streams published on the server
side.

When the element is connected to the document, the stream source is
connected. When the element is disconnected, the stream is disconnected.

When declared with an `ws://` or `wss://` URL, the underlying Stream
Source will be a `WebSocket` instance. Otherwise, the connection is
through an `EventSource`.

Since the document's `<head>` is persistent across navigations, the
`<turbo-stream-source>` is meant to be mounted within the `<body>`
element.

Typical full page navigations driven by Turbo will result in the
`<body>` being discarded and replaced with the resulting document. It's
the server's responsibility to ensure that the element is present on
each page that requires streaming.
@delitescere
Copy link
Author

I appreciate it might be tricky, indeed.

To me, it’s the same duality as “turbo-frame acting as a source” and “turbo-frame acting as a target”.

But if it were named slightly differently, that is less important than having it at all. Of course, as I’m not writing it, I defer to the developers to choose the smoothest implementation.

Much appreciated!

Also see https://link.medium.com/hcGxj0262jb for more (a lot more) context

@dhh dhh closed this as completed in #415 Jun 19, 2022
dhh added a commit that referenced this issue Jun 19, 2022
* Improve test coverage for streams over SSE

Add a `<form>` element to the `src/tests/fixtures/stream.html` file so
that tests can exercise receiving `<turbo-stream>` elements
asynchronously over an `EventSource` instance that polls for Server-sent
Events.

Extend the `src/tests/server.ts` to account for the `Accept:
text/event-stream` requests that are made by browsers via the `new
EventSource(...)` instance.

* Introduce `<turbo-stream-source>`

Closes #413

The `<turbo-stream-source>` element accepts a `[src]` attribute, and
uses that to connect Turbo to poll for streams published on the server
side.

When the element is connected to the document, the stream source is
connected. When the element is disconnected, the stream is disconnected.

When declared with an `ws://` or `wss://` URL, the underlying Stream
Source will be a `WebSocket` instance. Otherwise, the connection is
through an `EventSource`.

Since the document's `<head>` is persistent across navigations, the
`<turbo-stream-source>` is meant to be mounted within the `<body>`
element.

Typical full page navigations driven by Turbo will result in the
`<body>` being discarded and replaced with the resulting document. It's
the server's responsibility to ensure that the element is present on
each page that requires streaming.

* fix lint errors

Co-authored-by: David Heinemeier Hansson <david@hey.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

Successfully merging a pull request may close this issue.

2 participants