alpine-ajax icon indicating copy to clipboard operation
alpine-ajax copied to clipboard

SSE and websockets support

Open devrav opened this issue 1 year ago • 7 comments

Hey, first of all I want to say that this is a great project. HTMX is great but this feels so easier to use especially if one is already using alpinejs. I was wondering if there is a recommended way to handle sse and websockets just like there are extensions in HTMX ? Another nice thing would be something like hx-boost so that no extra library is needed to create an spa like feel when navigating to different urls.

for reference:

  • https://htmx.org/extensions/server-sent-events/
  • https://htmx.org/extensions/web-sockets/
  • https://htmx.org/attributes/hx-boost/

devrav avatar Mar 26 '24 10:03 devrav

Hey, I am currently working on hx-boost style navigation, you can follow along in #65

I don’t have any immediate plans for SSE or websockets, but the refactoring I’m doing in the PR above should help generalize some of the internals of Alpine AJAX. I think that could open up the possibility of building your own Alpine plugins that can use Alpine AJAX features.

imacrayon avatar Mar 26 '24 13:03 imacrayon

Thanks for the quick response. Routing feature does add more value than the sse/websockets support at this time, glad to know that you are already working on it. Thanks for putting in the effort, looking forward to seeing how the project progresses :)

devrav avatar Mar 26 '24 13:03 devrav

@imacrayon @devrav I pulled the cache branch and added this snippet as a quick hack to get working SSE support in alpine-ajax, to see how well it would work:

Alpine.directive('sse', (el, { expression }, { cleanup }) => {
    const es = new EventSource(expression, { withCredentials: true });
    const handler = (event) => {
        const response = {
            ok: true,
            url: event.target.url,
            html: event.data,
        };

        let targets = findTargets(AjaxAttributes.get(el, 'targets', []))
        if (targets.length) {
            targets = addSyncTargets(targets)
        }
        render(response, el, targets, false, false)
    };
    es.addEventListener("html-fragment", handler);

    cleanup(() => {
        es.removeEventListener('html-fragment', handler);
        es.close();
    });
});

It works like a charm and I used it to enhance a few of my pages for a demo. It works great for my usecase for now, it increases the power-to-weight ratio of the library quite a bit since the server events pattern gets live events from a message bus, and it makes it possible write MPAs that progressively enhance to super-interactive collaborative applications where your changes show up immediately on other clients. I'm kind of tempted to implement some multiplayer games with it as well.

Feel free to use it for whatever usecase you have under the license of the original project or to make it more robust. It does suffer from the many downsides of the browser standard EventSource implementation, including not being able to pass headers and having a wonky reconnect logic, but for progressive enhancement usecases it's not a huge issue.

Alpine-Ajax is an amazing library with a great well-thought out API and great docs and I don't mind waiting for a first party implementation. The way targets are handled makes it incredibly easy to refactor an application between different ways to fetch fragments, and a non-SSE application I had basically only needed a one or two extra props to work and update on other peoples changes once the backend SSE endpoint was there, since all the ids and x-merge logic was already set up.

saolof avatar Apr 25 '24 15:04 saolof

Updated version of the above snippet for the 0.7.1 release

Alpine.directive('sse', (el, { expression }, { cleanup }) => {
    const es = new EventSource(expression, { withCredentials: true });
    const handler = (event) => {
        const response = {
            ok: true,
            url: event.target.url,
            html: event.data,
        };
        let attributes = AjaxAttributes.get(el)
        let config = attributes.xxx || {}
        let targets = addSyncTargets(findTargets(config.ids))
        render(response, el, targets, false, false)
    };
    es.addEventListener("html-fragment", handler);

    cleanup(() => {
        es.removeEventListener('html-fragment', handler);
        es.close();
    });
});

(Disclaimer: This is only the result of tinkering with it until it worked rather than something carefully thought out)

saolof avatar Jul 24 '24 14:07 saolof

Hi @saolof @imacrayon .

I love this libtary.

I a bit confused of this Issue. Finally is possible to connect websockets? I see SSE example in the documntation but I am not sure if it covert websockets . Can you add a example?

devMls avatar Dec 14 '24 10:12 devMls

@devMls there isn't official support for SSE or websockets, but I'd be open to adding support if there is enough interest. I think I would want it to work similar to how HTMX supports them through a plugin. The Alpine AJAX internal API is settled enough that we might be able to explore plugin support now.

imacrayon avatar Dec 17 '24 23:12 imacrayon

I recon if we had plugin infrastructure, there would be many plugins ported over from other similar AJAX libraries so should we also have an official centralised imacrayon/alpine-ajax-plugins repo where plugins could be submitted?

nkev avatar Jun 17 '25 06:06 nkev