openapi-ts icon indicating copy to clipboard operation
openapi-ts copied to clipboard

Support server sent events (SSE)

Open himself65 opened this issue 1 year ago • 21 comments

Description

text/event-stream

https://gist.github.com/montasaurus/b517b1306fdd8d41cbf857d6ec5e7c82

himself65 avatar Jul 11 '24 21:07 himself65

Not very descriptive, but I know what you mean

mrlubos avatar Jul 11 '24 22:07 mrlubos

This would be great

ohnoah avatar Oct 17 '24 15:10 ohnoah

@ohnoah can you provide more details about your API/how you're currently calling it?

mrlubos avatar Oct 17 '24 15:10 mrlubos

image This is the API endpoint. The generated fetch client thing just returns the stream type as a response. How I want to use this is as an event stream with .onmessage .close etc etc.. Therefore, it doesn't make sense to generate a fetch method, but instead I only really need the types so I can do that.

I saw the "stream" option in parseAs but didn't see any docs about it and not sure what that does. Is that for SSE like this?

ohnoah avatar Oct 17 '24 16:10 ohnoah

@ohnoah oh no, stream just "streams" the raw response to you. Maybe a different naming would work better. Please let me know if you manage to hack around your issue in the meantime before it's fully supported!

mrlubos avatar Oct 17 '24 16:10 mrlubos

I guess I'm saying it should just not generate an endpoint if the type is event stream, since the semantics of an event stream endpoint is different. It's not correct to say that you can call it with fetch and get a response back with that type.

ohnoah avatar Oct 18 '24 09:10 ohnoah

but yeah, I can just ignore it for now so can definitely work around it

ohnoah avatar Oct 18 '24 09:10 ohnoah

Fern support this now: https://buildwithfern.com/learn/sdks/capabilities/server-sent-events

himself65 avatar Nov 29 '24 22:11 himself65

@himself65 yup, but it's a paid feature, right ($250+ per month)? I'd prioritise it too if you sponsored a similar amount

mrlubos avatar Nov 30 '24 04:11 mrlubos

@ohnoah would it not be good to have autogeneration of types at least, so events coming through can be typed?

robertlagrant avatar Dec 18 '24 13:12 robertlagrant

@robertlagrant sounds like it would be, at least for you! Do you have an example OpenAPI spec with such events?

mrlubos avatar Dec 18 '24 13:12 mrlubos

Hah, fair response. OpenAPI are discussing it now - might be worth a read. https://github.com/OAI/OpenAPI-Specification/discussions/4171#discussioncomment-11354714

robertlagrant avatar Dec 18 '24 14:12 robertlagrant

(FYI it's Server Sent Events.) 🤓

robertlagrant avatar Jan 10 '25 18:01 robertlagrant

Oops, updated the title, thanks!

mrlubos avatar Jan 10 '25 19:01 mrlubos

I don't think SSE will get added to the OAI spec anytime soon. The closest you'll see reading the OAI proposal thread is the proposal from Speakeasy (a commercial SDK generator service), which is what they use for their SDK generation:

https://github.com/OAI/OpenAPI-Specification/discussions/4171#discussioncomment-11341721 https://www.speakeasy.com/docs/customize/runtime/server-sent-events#modeling-sse-in-openapi

From my research so far, because there's no official spec around SSE, implementations on the OAI side are going to be completely custom.

But if I was to implement something, probably the Speakeasy one since it's the most defined proposal ATM and is being used in the wild commercially. Another unfortunate side to this is that the proposal is compatible with OAI 3.1, not 3.0 (and I think hey-api only supports 3.0 at the moment)

theogravity avatar Feb 07 '25 01:02 theogravity

@theogravity we support 3.1 too. Why do you have the impression we don't? Are you missing any feature? Except for SSE 😀

mrlubos avatar Feb 07 '25 01:02 mrlubos

@theogravity we support 3.1 too. Why do you have the impression we don't? Are you missing any feature? Except for SSE 😀

Oh yay! Thought I read somewhere that 3.0 is only supported or there's only partial support for 3.1. Sorry. I'm glad that you do!

The SSE thing is unfortunately a blocker for us ATM; other than that, we love using hey-api in our projects where SSE isn't involved, and it's our go-to for Typescript generation.

theogravity avatar Feb 07 '25 01:02 theogravity

@theogravity that was probably the experimental parser which has now been enabled by default and supports all versions (except OpenAPI 1.2) if you're running the latest. SSE will be prioritised this year for sure!

mrlubos avatar Feb 07 '25 01:02 mrlubos

Thanks, I once also built my own OAI client generator from the ground up years ago and understand how difficult it is to remotely build something fully-featured when it comes to OAI. I just put in a donation to support the overall effort on this project in general; I'm not expecting anything in return (nor am I expecting SSE to be built here).

Thanks so much for the proactive response and just being awesome!

theogravity avatar Feb 07 '25 01:02 theogravity

Thank you so much @theogravity! I was just coming here to thank you when I saw the notification. I'll definitely keep this thread updated when this gets prioritised, but feel free to connect with me on LinkedIn or through email if you've got any more feedback or issue!

mrlubos avatar Feb 07 '25 01:02 mrlubos

Any updates on this? Here is the custom hook I created as a workaround for this:

// Defines the structured return shape of an SSE
type SseEventResponse = {
  data: unknown;
  event: string;
  id?: number;
  retry?: number;
};

// Converts the array of responses into a key-value pair, where the key is the event name and the value is a
// handler function receiving the event data and the sse client as arguments.
type SseHandlers<T extends SseEventResponse[]> = {
  [k in T[number]["event"]]: (
    data: Extract<T[number], { event: k }>["data"],
    client: EventSource,
  ) => void | Promise<void>;
};

// Used just for typesafing in `Object.entries`
type SseHandlerEntry<T extends SseEventResponse[]> = [
  keyof SseHandlers<T>,
  SseHandlers<T>[keyof SseHandlers<T>],
];

// Hook options
type Opts = {
  disabled?: boolean;
};

export const useSSE = <T extends SseEventResponse[]>(
  endpoint: string,
  handlers: SseHandlers<T>,
  opts: Opts = {
    disabled: false,
  },
) => {
  const [error, setError] = useState(false);

  useEffect(() => {
    if (opts.disabled) return undefined;

    const sseClient = new EventSource(
      env.BASE_URL+ endpoint,
    );

    sseClient.onerror = () => setError(true);

    for (const [event, handler] of Object.entries(
      handlers,
    ) as SseHandlerEntry<T>[]) {
      sseClient.addEventListener(event, (ev) => {
        const data = JSON.parse(ev.data as string) as T[number]["data"];

        const execHandler = async () => await handler(data, sseClient);

        void execHandler();
      });
    }

    return () => sseClient.close();
  }, [opts.disabled]);

  return { error };
};

And here is an example usage:

// Generic type here is imported from `types.gen.ts`, be sure to import the correct one.
const { error } = useSSE<GenerateQrCodeResponse>(
  `/session/${id}/qr`,
  {
    qrCodeReceived: (data) => setQrCode(data.base64Code),
    linkedSuccessfully: async (_, client) => {
      client.close();
      await updateSession();
    },
  },
);

kareemmahlees avatar May 08 '25 09:05 kareemmahlees

thanks @kareemmahlees,

I modified a bit your code to do a bit of types gymnastic to determine the types based on the path and only allow path that have text/event-stream

import { paths } from "@/lib/api/api.types"; // import your path from your generated code

import { useState, useEffect } from "react";

// Defines the structured return shape of an SSE
type SseEventResponse = {
  data: unknown;
  event: string;
  id?: number;
  retry?: number;
};

// Converts the array of responses into a key-value pair, where the key is the event name and the value is a
// handler function receiving the event data and the sse client as arguments.
type SseHandlers<T extends SseEventResponse[]> = {
  [k in T[number]["event"]]: (
    data: Extract<T[number], { event: k }>["data"],
    client: EventSource
  ) => void | Promise<void>;
};

// Used just for typesafing in `Object.entries`
type SseHandlerEntry<T extends SseEventResponse[]> = [
  keyof SseHandlers<T>,
  SseHandlers<T>[keyof SseHandlers<T>]
];

// Hook options
type Opts = {
  disabled?: boolean;
};

type HasEventStream<P> = {
  [K in keyof P]: P[K] extends {
    get: {
      responses: {
        200: {
          content: {
            "text/event-stream": any;
          };
        };
      };
    };
  }
    ? K
    : never;
}[keyof P];

type EventStreamOf<Ep extends HasEventStream<paths>> = Ep extends keyof paths
  ? paths[Ep]["get"]["responses"][200]["content"]["text/event-stream"]
  : never;

export const useSSE = <
  Ep extends HasEventStream<paths>,
  T extends SseEventResponse[] = EventStreamOf<Ep>
>(
  endpoint: Ep,
  handlers: SseHandlers<T>,
  opts: Opts = {
    disabled: false,
  }
) => {
// same as before ...
}

froz42 avatar Jun 09 '25 16:06 froz42

yoohoo, this will be available in v0.81.0 🍿 looking forward to your feedback as there'll be many edge cases!

mrlubos avatar Aug 22 '25 21:08 mrlubos

Oooh! I have code that I had to hack around to get SSE to work with Hey API, I'll be able to test this out soon. Thanks! 🙏🏻

jscarle avatar Aug 23 '25 00:08 jscarle

Let me know! Most notably it doesn't handle cases where the same endpoint returns stream OR regular response. If you have a use case for that, we can improve it

mrlubos avatar Aug 23 '25 01:08 mrlubos

@mrlubos I have a use case for that, similar to how the OpenAI (not OpenAPI) Responses API supports passing in stream: true or stream: false to have a single endpoint return either application/json or text/event-stream.

Bdthomson avatar Aug 27 '25 23:08 Bdthomson

@Bdthomson please open a new issue with your spec and let's chat there!

mrlubos avatar Aug 27 '25 23:08 mrlubos