ssevents/event
Event and item domain values.
Event and Comment are opaque so the package can evolve their
representation without a breaking change. Construct via new,
from_parts, and the builder helpers (for Event) or comment
(for Comment). Item stays transparent so callers and helper
modules can pattern match on whether a stream element is an event
or a comment.
Types
A :-prefixed comment line in an SSE stream.
Opaque — construct with comment/1 and inspect with
comment_text_of/1. Comment text is sanitised at construction
(CR / LF / NUL stripped) so decode(encode([CommentItem(c)]))
returns the same Comment value: WHATWG SSE §9.2.6 has no
notion of a multi-line comment, so any embedded line break would
fan out to multiple comments on the wire and break the
round-trip law.
pub opaque type Comment
Reasons the strict event-builder variants reject input.
The non-strict event / id / named / comment constructors
silently strip CR / LF / NUL bytes from the values that flow into
SSE field lines (so named("\n", _) produces an event with
name = "", and id(_, "ab\u{0000}cd") produces an event with
id = "abcd" — a different valid id). The silent strip is
data loss the caller cannot observe — a naive equality check on
the recovered name silently matches the wrong subscription
channel. The *_checked variants surface this as a typed error
so callers can render “name foo\\nbar contains forbidden
control bytes” rather than producing the wrong wire silently.
(#81)
pub type EventError {
NameContainsControlBytes(value: String)
IdContainsControlBytes(value: String)
CommentContainsControlBytes(value: String)
}
Constructors
-
NameContainsControlBytes(value: String)event_checkedsaw CR / LF / NUL bytes in the event name. Carries the original (un-sanitized) value. -
IdContainsControlBytes(value: String)id_checkedsaw CR / LF / NUL bytes in the event id. Carries the original (un-sanitized) value. -
CommentContainsControlBytes(value: String)comment_checkedsaw CR / LF / NUL bytes in the comment text. Carries the original (un-sanitized) value.
Values
pub fn comment(text: String) -> Comment
Build a Comment from a text payload. CR (U+000D), LF (U+000A),
and NUL (U+0000) are stripped at construction so the result
round-trips through encode → decode without fanning out into
multiple comments. Matches the sanitize_field_value posture
already used for event_name and id.
The silent strip is data loss the caller cannot observe. Reach
for comment_checked/1 instead when comment text comes from
user-typed or upstream input and a typed error is preferable to
a silently-truncated comment. (#81)
pub fn comment_checked(
text: String,
) -> Result(Comment, EventError)
Strict counterpart of comment/1: rejects comment text
containing CR / LF / NUL bytes with
Error(CommentContainsControlBytes(value:)).
The non-strict comment/1 silently strips these bytes.
WHATWG SSE §9.2.6 has no notion of a multi-line comment, so
embedded line breaks would fan out into multiple comments on
the wire; the strict variant surfaces this as an explicit
error rather than silently splitting the caller’s intent. (#81)
pub fn comment_item(text: String) -> Item
Wrap a Comment as a stream item. Convenience for the common
CommentItem(comment(text)) two-step.
pub fn comment_item_of(c: Comment) -> Item
Wrap an already-validated Comment as a stream item. Companion
to comment_checked/1 so callers can keep the typed-error
pipeline text -> Result(Comment, EventError) -> Result(Item, _)
without reaching into Item’s constructors. (#81)
pub fn comment_text_of(c: Comment) -> String
Extract the sanitised comment text.
pub fn comment_text_of_item(item: Item) -> option.Option(String)
Return the comment text when the item is a comment, None
otherwise. Mirrors event_of_item/1 for the comment side.
pub fn event(event: Event, name: String) -> Event
Set the SSE event: field name on an event. CR / LF / NUL bytes
are silently stripped to keep the wire spec-compliant — so
named("\n", _) produces an event with name = "". The strip
is data loss the caller cannot observe; reach for
event_checked/2 when the name comes from user-typed or
upstream input and a typed error is preferable to silent data
loss. (#81)
pub fn event_checked(
source_event: Event,
name: String,
) -> Result(Event, EventError)
Strict counterpart of event/2: rejects names containing CR /
LF / NUL bytes with Error(NameContainsControlBytes(value:)).
The non-strict event/2 silently strips these bytes (so
named("\n", _) produces a part with name = ""). For callers
passing user-typed or upstream data into the event name and want
to surface bad inputs as a typed error rather than silent data
loss, use this variant. The value payload carries the
caller’s original input so the error renders as
“event name foo\\nbar contains forbidden control bytes”. (#81)
pub fn event_of_item(item: Item) -> option.Option(Event)
Return the event payload when the item is an event, None
otherwise. Use with option.then / case for stream processing
that ignores comments.
pub fn from_parts(
event_name event_name: option.Option(String),
data data: String,
id id: option.Option(String),
retry retry: option.Option(Int),
) -> Event
pub fn id(event: Event, id: String) -> Event
Set the SSE id: Last-Event-ID on an event. CR / LF / NUL bytes
are silently stripped — so id(_, "ab\u{0000}cd") produces an
event with id = "abcd", a different valid id, which can
silently match the wrong subscription channel on reconnect.
Reach for id_checked/2 when the id comes from user-typed or
upstream input and a typed error is preferable to silent
authorization-relevant identifier mutation. (#81)
pub fn id_checked(
event: Event,
id: String,
) -> Result(Event, EventError)
Strict counterpart of id/2: rejects ids containing CR / LF /
NUL bytes with Error(IdContainsControlBytes(value:)).
The non-strict id/2 silently strips these bytes. The strip on
the id is especially dangerous — id(_, "ab\u{0000}cd")
produces an event with id = "abcd", a different valid id,
which can silently match the wrong subscription channel on
reconnect (Last-Event-ID resume). The strict variant catches
this at the builder boundary so the wrong wire never gets
produced. (#81)
pub fn id_of(event: Event) -> option.Option(String)
pub fn is_comment(item: Item) -> Bool
True when the item is a :-prefixed comment line.
pub fn is_event(item: Item) -> Bool
Issue #77: Item-level accessors. The Item variants EventItem /
CommentItem are not visible through the top-level ssevents
module (a Gleam type alias does not re-export its constructors), so
callers that decode an SSE stream and want to pattern-match on the
result would otherwise need to reach into ssevents/event directly.
These helpers let ssevents-only callers walk decoded items without
the second import.
True when the item is an event (carries an SSE Event payload).
pub fn name_of(event: Event) -> option.Option(String)
pub fn named(name: String, data: String) -> Event
Build an event with both name and data. CR / LF / NUL bytes
in name are silently stripped — see the warning on event/2.
Reach for named_checked/2 when the bad-input case must be
surfaced as a typed error. (#81)
pub fn named_checked(
name: String,
data: String,
) -> Result(Event, EventError)
Strict counterpart of named/2: rejects names containing CR /
LF / NUL bytes with Error(NameContainsControlBytes(value:)).
Convenience for the common new |> event_checked pipeline that
also constructs a fresh Event. (#81)
pub fn retry(event: Event, milliseconds: Int) -> Event
Set the SSE retry: reconnection time on an event.
milliseconds must be >= 0. WHATWG SSE §9.2.6 only recognises a
retry value whose textual form “consists of only ASCII digits”, so a
negative value would be either dropped on the wire (the leading -
breaks the digits-only check) or interpreted as 0 and trigger a
tight reconnect loop against the server. Either outcome is a
contract violation, so the builder panics on ms < 0 with
"ssevents.retry: milliseconds must be >= 0 (got <n>); the SSE spec mandates a non-negative reconnection time.". Use retry_clamp/2
instead when the caller wants the lenient (clamp-to-0) behaviour.
Values above limit.default_max_retry_value (24 h in ms) are still
silently dropped to None, matching from_parts/4 and the
decode(encode(_)) round-trip property pinned in #60.
pub fn retry_clamp(event: Event, milliseconds: Int) -> Event
Like retry/2, but clamps milliseconds < 0 to 0 instead of
panicking. Use this when the caller wants the lenient posture
(e.g. when forwarding a value computed from possibly-noisy input).
All other behaviour matches retry/2, including the > max_retry
silent drop to None for round-trip with the default decoder.
pub fn retry_of(event: Event) -> option.Option(Int)