razshare / sveltekit-sse

Server Sent Events with SvelteKit
https://www.npmjs.com/package/sveltekit-sse
MIT License
274 stars 9 forks source link

How can I use this with Svelte 5 runes? #54

Closed kristianmandrup closed 1 month ago

kristianmandrup commented 1 month ago

I would like aggregate the SSE events in a messages state/store. Would I run the source in an onMount or $effect? It is unclear how to use it with stores

    const connection = source('/events/app');
    const channel = connection.select('project');
<script>
    let messages = $state<string[]>([]);

    messageStore.addMessage($json);

    messageStore.subscribe((value) => {
        messages = value;
     });
</script>

<div>
    <h1>Projects</h1>
    <h2>Messages</h2>
    {#if messages.length === 0}
        <p>No messages yet.</p>
    {:else}
        <ul>
            {#each messages as message}
                <li>{message}</li>
            {/each}
        </ul>
    {/if}
</div>
razshare commented 1 month ago

Hello @kristianmandrup , I'm not very familiar with Svelte 5 and I plan to wait until it's actually released to do any work specific to it.

That being said, in Svelte 4 you would just have to do this

<script>
  import { source } from 'sveltekit-sse'
  import { messageStore } from 'message-store'
  const connection = source('/events/app')
  const message = connection.select('message')
  $: messageStore.addMessage($message)
</script>

// Your markup goes here.

Because .select() gives you a store itself, hence the $message in the code above.

I'm assuming the equivalent in Svelte 5 is something like this

<script>
  import { source } from 'sveltekit-sse'
  import { messageStore } from 'message-store'
  const connection = source('/events/app')
  const message = connection.select('message')
  $effect(function run() {
    messageStore.addMessage($message)
  })
</script>

// Your markup goes here.

I haven't tested it though. I do remember the Svelte team saying at some point that stores will interact with runes as they normally interact with the current reactive system in Svelte 4, so it would be a breaking change on their part if this doesn't work.

Edit: from a quick search I found this https://svelte-5-preview.vercel.app/docs/faq#breaking-changes-and-migration image


Let me know if this answers your question.

kristianmandrup commented 1 month ago

Thanks a lot for your answer. For now I seem to have it working using the following pattern:

    const transformed = channel.transform(function run(data) {
        if (data === '') return;
        // TODO: parse json
        return `${data}`;
    });

    let projects = $state<ProjectPayload[]>([]);
    let lastMessage = $state<string>('');

    transformed.subscribe((value: string) => {
        if (!value) return;
        lastMessage = value;
                // TODO: move to transform function
        const json = JSON.parse(value);
        const { source, model, action, data } = json;
        if (model !== 'project') {
            console.log('not a project event');
            return;
        }
        const project = data;
        // TODO: depending on the event, add, remove or update the project
        projects = [...projects, project];
    });

    import { getToastState } from '$lib/toast-state.svelte';

    const toastState = getToastState();

    const toastMap = new Map<string, unknown>();

    $effect(() => {
        const message = lastMessage;
        if (!message) return;

        const json = JSON.parse(message);
        const { data } = json;
        if (!data) {
            console.error('missing data', json);
        }
        const { name, description } = data;
        if (!name) {
            console.log('missing name', data);
            return;
        }
        // already processed
        if (toastMap.get(name)) {
            console.log('already made toast for', name);
            return;
        }
        toastMap.set(name, data);
        toastState.add(name, description);
    });

Currently overly verbose and complicated, but it gets the job done. The key was to simply subscribe to the readvalue store returned by transform

transformed.subscribe((value: string) => { ... })

Then from there I could set a $state with the value and work from there. I'm sure there is a much simpler way. Btw, I had an issue with the first message received from SSE always being an empty string for some reason, but it might well be an issue with my internal logic, such as undefined being sent as empty string?

razshare commented 1 month ago

Btw, I had an issue with the first message received from SSE always being an empty string for some reason, but it might well be an issue with my internal logic, such as undefined being sent as empty string?

That is expected behavior, the first value is always empty because it takes time to actually open the connection to the server.

It's either that or force userland to deal with a Promise<Readable<string>> and {#await} in the markup.

A third solution would be initializing the value in SSR but that would mean your server would have to open a connection to itself to read the value, which is a waste of resources and will probably slow down the SSR itself by a lot, especially in a cloud environment where you're not 100% the node doing the SSR will actually connect to itself. It could connect to a different twin node and slow down things even more.