#Payload with server sent event

1 messages · Page 1 of 1 (latest)

bitter fox
#

Hi all,

I'm hoping to use @clear bane 's awesome remix utils Server Sent Events flow for this.

How would this flow be implemented at a high level?

  1. User submits form
  2. Form object payload is sent in some API request that starts a stream
  3. UI using server sent events to stream the results from the API

Question
The key difference here vs. the examples I've seen on the docs is that I'm not sure **how we send the UI form payload to the server sent event to initiate it. ** Each eventStream's setup function can send an event with a string event key, but what if you want to pass a payload to the loader with the eventStream?

export async function loader({ request }: LoaderFunctionArgs) {
  const formPayloadFromUI = ...// where would we get this from?
  return eventStream(request.signal, function setup(send) {
    async function run() {
      someApi(formPayloadFromUI, (results) => {
        results.on('data', (data) => {
           send({event: 'someStream' /* if we have multiple streams, how would we keep track of them on the UI if the event key needs to match? */, data: JSON.stringify(data) })
        })
      })
    }

    run();
    return () => {};
  });
}

Once the server sent event begins the stream, I understand that you can just stringify the object response to return to the client and parse it there. Hoping I won't run into any stringify over the wire limits with that.

agile stone
#

Let me explain how I would do this. Hopefully it helps to answer you question.

When the user visits the form page, the browser connects to SSE endpoint and starts receiving events. That SSE connection is open as long as the user is on that page.

The SSE itself is very simple - it subscribes to a pubsub, filters the published data for each connected user, and if it should be sent to the connected user, it sends the published payload to the browser.

The action which is triggered by the form triggers publish events which are consumed by the SSE pubsub. You might also need to publish events from some other place if there are events which happen outside of the form action which affect the UI.

bitter fox
#

That SSE connection is open as long as the user is on that page.
👍🏻

form triggers publish events which are consumed by the SSE pubsub
This is the part that I don't understand. How to connect the action (that contains a payload) to the SSE pubsub.

clear bane
#

you need some pubsub system that let's you publish events from the action and subscribe to them on the event stream

#

the simplest way is to use EventEmitter

#

you create an EventEmitter instance in some file, then import it in both routes

#

in the action you do emitter.emit("event-name", data)

#

and in the event stream you do

emitter.on("event-name", data => {
  // here you run your logic and send the message to the browser
})
#

and also use emitter.off to unsubscribe when the connection is closed, otherwise you will keep trying to send events

#

on a prod env you may need something like Redis

#

as you need to deal with multiple instances of your process

bitter fox
#

Thank you!

Just curious, does this node message have anything to do with SSEs?

agile stone
#

Not SSEs. It has to do with the AbortSignal that you're using.

bitter fox
#

Got it, thanks. Maybe something to do with Vite HMR?

#

Now, to make this SSE question more complicated.

What if I want to have N streams happening at once? Would I have to instantiate N event emitters?

agile stone
#

Nope. Each stream would just publish to the same event emitter.

bitter fox
#

Interesting.

How about concurrency?

If you have 5 users accessing the Remix app, and the route's action calls an emitter with the same event key (but different data payloads for each users), will the emitter handle 5 separate .emit() invocations, keeping them separate?

clear bane
#

the action will emit the event once, and every subscriber will receive the event

bitter fox
#

So, I'm seeing, with two browser tabs open, if I emit the event, it appears in both browser tabs, which I don't want. I want it to be separated per request.

#

So I had the idea to put the event emitter into asyncLocalStorage but haven't quite figured it out. The emitter is in the storage, but emitting the event does nothing.

#

Or maybe the behavior I'm seeing is because of vite HMR?

clear bane
#

Events are global

bitter fox
#

Where would you filter it? In the SSE resource route or the UI component?

clear bane
#

In the server

#

Where you subscribe to the events

#

You should never filter them on the component since that’s client side

bitter fox
#

Hmm. I’m still not clear how filtering in the subscriber would help me separate calls to the event emitter.

Basically, using event emitters and the remix utils SSE, I’m trying to implement something that allows users to submit payloads that trigger some operation that calls emitter.emit().

That part works, though it seems like the global nature of the emitter causes it to be shared across multiple instances of the app.

bitter fox
#

Oh, maybe I’m starting to understand.

In useEventSource event key, you could include a dynamic identifier that you also use in the SSE send function.

You would send the identifier also in the fetcher.submit so the action would pick it up when it calls the event emitter.

Is that what you mean by filtering?