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

Re-connect Stream Source when attribute change #1287

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from

Conversation

seanpdoyle
Copy link
Contributor

Related to hotwired/turbo-rails#638

Recent changes to integrate with morphing have altered the mental model for some Turbo custom elements, including the
<turbo-stream-source> element.

Custom Elements' connectedCallback() and disconnectedCallback() (along with Stimulus' connect() and disconnect()) improved upon invoking code immediately, or listening for DOMContentLoaded events.

There are similar improvements to be made to integrate with morphing. First, observe attribute changes by declaring their own static observedAttributes properties along with
attributeChangedCallback(name, oldValue, newValue) callbacks. Those callbacks execute the same initialization code as their current connectedCallback() and disconnectedCallback() methods.

That'll help resolve this issue. In addition to those changes, it's important to emphasize this pattern for consumer applications moving forward. JavaScript code (whether Stimulus controller or otherwise) should be implemented in a way that' resilient to both asynchronous connection and disconnection as well as asynchronous modification of attributes.

Related to [hotwired/turbo-rails#638][]

Recent changes to integrate with morphing have altered the mental model
for some Turbo custom elements, including the
`<turbo-stream-source>` element.

Custom Elements' `connectedCallback()` and `disconnectedCallback()`
(along with Stimulus' `connect()` and `disconnect()`) improved upon
invoking code immediately, or listening for `DOMContentLoaded` events.

There are similar improvements to be made to integrate with morphing.
First, [observe attribute changes][] by declaring their own `static
observedAttributes` properties along with
`attributeChangedCallback(name, oldValue, newValue)` callbacks. Those
callbacks execute the same initialization code as their current
`connectedCallback()` and `disconnectedCallback()` methods.

That'll help resolve this issue. In addition to those changes, it's
important to emphasize this pattern for consumer applications moving
forward. JavaScript code (whether Stimulus controller or otherwise)
should be implemented in a way that' resilient to both asynchronous
connection and disconnection *as well as* asynchronous modification of
attributes.

[hotwired/turbo-rails#638]: hotwired/turbo-rails#638
[observe attribute changes]: https://developer.mozilla.org/en-US/docs/Web/API/Web_Components/Using_custom_elements#responding_to_attribute_changes
@seanpdoyle
Copy link
Contributor Author

@jorgemanrubia I've opened this in response to hotwired/turbo-rails#638. In addition to re-connected when the [src] changes, this commit also communicates connection state for the <turbo-stream-source> element through a [connected] attribute in the same style as <turbo-cable-stream-source> element.

@freund17
Copy link

freund17 commented Jul 25, 2024

Hi, I was about to create an issue about this, but it seems you beat me to it. 😉

Keep in mind, that both, connectedCallback and attributeChangedCallback, will be triggered on first render - so your implementation would connect, disconnect and connect again in that case. This may break use-cases (such as mine) where the SSE URL is single-use.

This is my solution: (feel free to use, MIT licence, yada yada)

class StreamSourceElement extends HTMLElement {
  static get observedAttributes() {
    return ["src"];
  }

  #streamSource: EventSource | WebSocket | undefined;

  get src() {
    const src = this.getAttribute("src");

    return src === null ? null : new URL(src, this.baseURI).href;
  }

  connectedCallback() {
    const src = this.src;

    if (src !== null) {
      this.#connectStreamSource(src);
    }
  }

  attributeChangedCallback() {
    if (this.isConnected) {
      const src = this.src;

      if (src === null) {
        this.#disconnectStreamSource();
      } else {
        this.#connectStreamSource(src);
      }
    }
  }

  disconnectedCallback() {
    this.#disconnectStreamSource();
  }

  #connectStreamSource(src: string) {
    if (this.#streamSource?.url !== src) {
      this.#disconnectStreamSource();

      this.#streamSource = src.match(/^wss?:/)
        ? new WebSocket(src)
        : new EventSource(src);

      connectStreamSource(this.#streamSource);
    }
  }

  #disconnectStreamSource() {
    if (this.#streamSource) {
      this.#streamSource.close();

      disconnectStreamSource(this.#streamSource);

      this.#streamSource = undefined;
    }
  }
}

EDIT: this.#streamSource.url is always absolute, so src needs to be resolved before comparing; and attributeChangedCallback must only alter the stream, when the component is connected to the DOM.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

Successfully merging this pull request may close these issues.

2 participants