Server-Sent Events: a parser, channels and an encoder (Effect's
encoding/Sse).
decode reads chunks of text and writes Event values; a retry:
directive fails it with Retry (after the events before it), so the
caller can reconnect after Retry.duration. encode writes events as
SSE text and ends with a retry: line when its upstream fails with
Retry. make_parser is the push parser both build on. The schema
variants (decodeSchema, decodeDataSchema, encodeSchema) come
with Schema.
| Attribute | Type | Description |
|---|---|---|
DEFAULT_MAX_EVENT_SIZE | = 10 * 1024 * 1024 | The most characters a pending event may hold by default (10 Mi). |
Classes#
Functions#
make_parser#
make_parser(on_parse, *, max_event_size=DEFAULT_MAX_EVENT_SIZE, dispatch_empty_named_events=False) -> ParserA parser reporting to on_parse. max_event_size bounds the
characters of the pending line and event. With
dispatch_empty_named_events, an event with an event: name but no
data: line is reported with empty data (WHATWG drops it; some
protocols, such as graphql-sse's complete, use it).
| Parameter | Type | Description |
|---|---|---|
on_parse | Callable[[AnyEvent], object] | |
max_event_size | int = DEFAULT_MAX_EVENT_SIZE | |
dispatch_empty_named_events | bool = False |
Returns Parser
def make_parser(
on_parse: Callable[[AnyEvent], object],
*,
max_event_size: int = DEFAULT_MAX_EVENT_SIZE,
dispatch_empty_named_events: bool = False,
) -> Parser:
"""A parser reporting to ``on_parse``. ``max_event_size`` bounds the
characters of the pending line and event. With
``dispatch_empty_named_events``, an event with an ``event:`` name but no
``data:`` line is reported with empty data (WHATWG drops it; some
protocols, such as graphql-sse's ``complete``, use it)."""
return Parser(on_parse, max_event_size, dispatch_empty_named_events)decode#
decode(*, max_event_size=DEFAULT_MAX_EVENT_SIZE, dispatch_empty_named_events=False) -> Channel[tuple[Event, ...], E | Retry | SseError, D, tuple[str, ...], E, D]Parse chunks of SSE text into events (see make_parser). A
retry: directive fails the channel with Retry once the events
before it are written; an oversized event fails it with SseError.
| Parameter | Type | Description |
|---|---|---|
max_event_size | int = DEFAULT_MAX_EVENT_SIZE | |
dispatch_empty_named_events | bool = False |
Returns Channel[tuple[Event, ...], E | Retry | SseError, D, tuple[str, ...], E, D]
def decode[E = Never, D = None](
*,
max_event_size: int = DEFAULT_MAX_EVENT_SIZE,
dispatch_empty_named_events: bool = False,
) -> Channel[tuple[Event, ...], E | Retry | SseError, D, tuple[str, ...], E, D]:
"""Parse chunks of SSE text into events (see ``make_parser``). A
``retry:`` directive fails the channel with ``Retry`` once the events
before it are written; an oversized event fails it with ``SseError``."""
def build(upstream: AnyPull) -> AnyPull:
events: list[Event] = []
retry: Retry | None = None
def parsed(event: AnyEvent) -> None:
nonlocal retry
if isinstance(event, Retry):
retry = event
else:
events.append(event)
parser = make_parser(
parsed,
max_event_size=max_event_size,
dispatch_empty_named_events=dispatch_empty_named_events,
)
def feed(chunk: tuple[str, ...]) -> AnyPull:
for text in chunk:
error = parser.feed(text)
if error is not None:
return _FailCause(fail_cause(error))
return _Succeed(None)
pump = upstream.flat_map(feed)
def loop() -> AnyPull:
if events:
out = tuple(events)
events.clear()
return _Succeed(out)
if retry is not None:
return _FailCause(fail_cause(retry))
return pump.flat_map(lambda _: loop())
return _Suspend(loop)
channel: Channel[tuple[Event, ...], E | Retry | SseError, D, tuple[str, ...], E, D] = (
from_transform(lambda upstream, _scope: _Sync(lambda: build(upstream)))
)
return channelwrite#
write(event) -> strThe SSE text of event (Effect's encoder.write): id:,
event: (unless "message") and one data: line per line of
data, then a blank line; a Retry is a retry: line in
milliseconds.
| Parameter | Type | Description |
|---|---|---|
event | AnyEvent |
Returns str
def write(event: AnyEvent) -> str:
"""The SSE text of ``event`` (Effect's ``encoder.write``): ``id:``,
``event:`` (unless ``"message"``) and one ``data:`` line per line of
data, then a blank line; a ``Retry`` is a ``retry:`` line in
milliseconds."""
if isinstance(event, Retry):
millis = to_millis(event.duration)
return f"retry: {int(millis) if millis.is_integer() else millis}\n\n"
text = "" if event.id is None else f"id: {event.id}\n"
if event.event != "message":
text += f"event: {event.event}\n"
return text + "data: " + _DATA_BREAK.sub("\ndata: ", event.data) + "\n\n"encode#
encode() -> Channel[tuple[str, ...], E, None, tuple[Event, ...], E | Retry, D]Write events as SSE text. When the upstream fails with Retry,
write its retry: line and end. Pipe a stream that cannot fail with
Retry through it with pipe_through_channel_or_fail.
Returns Channel[tuple[str, ...], E, None, tuple[Event, ...], E | Retry, D]
def encode[E = Never, D = None]() -> Channel[
tuple[str, ...], E, None, tuple[Event, ...], E | Retry, D
]:
"""Write events as SSE text. When the upstream fails with ``Retry``,
write its ``retry:`` line and end. Pipe a stream that cannot fail with
``Retry`` through it with ``pipe_through_channel_or_fail``."""
def build(upstream: AnyPull) -> AnyPull:
retried = False
def on_retry(retry: Any) -> AnyPull:
nonlocal retried
retried = True
return _Succeed((write(retry),))
written = upstream.map(lambda chunk: tuple(map(write, chunk)))
pull = catch_done(written.catch_if(_is_retry, on_retry), lambda _: end_with(None))
return _Suspend(lambda: end_with(None) if retried else pull)
channel: Channel[tuple[str, ...], E, None, tuple[Event, ...], E | Retry, D] = from_transform(
lambda upstream, _scope: _Sync(lambda: build(upstream))
)
return channel