How to run server-sent events in svelte component in sapper

Tags: , , , ,



I have a svelte component named [symbol].svelte in which I want to initiate a connection to a streaming service to receive server-sent events. I’ve not found a way to do this successfully.

Since EventSource only runs in the browser, I initialized it in the onMount function like so:

<script>
    export let quote;

    let sse = {};

    onMount(async () => {
        sse = new EventSource(`https://myurl.com?symbol=${quote.symbol}`);
        sse.onmessage = (event) => {
            let response = JSON.parse(event.data);
            if(!response.length) return;
            quote = response[0];
        }
    });

    onDestroy(() => {
        if(sse.readyState && sse.readyState === 1) {
            sse.close();
        }
    })  
</script>

<div>{quote.symbol}</div>

This works fine, except when I navigate to another route that uses the same component- since the component doesn’t unmount and remount, onMount() doesn’t fire and thus doesn’t instantiate a new SSE request. I don’t know of any way to easily force the component to remount, which would be simplest (relevant github issue here)

Another try was using a reactive statement like so:

<script>
    export let quote;

    let sse = {};

    $: {
        if(process.browser === true) { //again, this stuff won't run on the server
            if(sse.readyState && sse.readyState === 1) {
                sse.close();
            }
            sse = new EventSource(`https://myurl.com?symbol=${quote.symbol}`);
        }
    }

    sse.onmessage = (event) => {
        let response = JSON.parse(event.data);
        quote = response[0];
        console.log(quote);
    }
</script>

<div>{quote.symbol}</div>

When changing routes, the quote variable changed, thus triggering the reactive statement to kill the existing SSE and instantiate a new one. Exceptthe onmessage handler wouldn’t fire, probably because the onmessage handler gets attached before the eventsource object is created.

Last take was to try with the onmessage handler in the reactive statement like so:

<script>
    export let quote;

    let sse = {};

    $: {
        if(process.browser === true) { //again, this stuff won't run on the server
            if(sse.readyState && sse.readyState === 1) {
                sse.close();
            }
            sse = new EventSource(`https://myurl.com?symbol=${quote.symbol}`);
            sse.onmessage = (event) => {
                let response = JSON.parse(event.data);
                quote = response[0];
                console.log(quote);
            }           
        }
    }
</script>

<div>{quote.symbol}</div>

The problem here is that since quote gets reassigned as a product of the onmessage handler, the reactive statement keeps firing circularly.

At this point I’m at a loss, any input would be appreciated!

Answer

It sounds like you want to use {#key ...}, which causes its contents to be torn down and recreated when the value changes, including components:

{#key quote}
  <!-- destroyed and recreated whenever `quote` changes -->
  <Quote {quote}/>
{/key}

Docs here: https://svelte.dev/docs#key

Incidentally, using onDestroy is unnecessary if it’s only used to clean up work that happens in onMount:

onMount(() => {
  const sse = new EventSource(`https://myurl.com?symbol=${quote.symbol}`);
  sse.onmessage = (event) => {
    let response = JSON.parse(event.data);
    if(!response.length) return;
      quote = response[0];
    }
  };

  return () => {
    if(sse.readyState === 1) {
      sse.close();
    }
  });
});  

This is better because you don’t have the top-level sse variable, and because the returned cleanup function only needs in the browser, you don’t need to have the placeholder ssr = {} assignment or check for sse.readyState.



Source: stackoverflow