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
pub opaque type Event

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_checked saw CR / LF / NUL bytes in the event name. Carries the original (un-sanitized) value.

  • IdContainsControlBytes(value: String)

    id_checked saw CR / LF / NUL bytes in the event id. Carries the original (un-sanitized) value.

  • CommentContainsControlBytes(value: String)

    comment_checked saw CR / LF / NUL bytes in the comment text. Carries the original (un-sanitized) value.

pub type Item {
  EventItem(Event)
  CommentItem(Comment)
}

Constructors

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 data(event: Event, data: String) -> Event
pub fn data_of(event: Event) -> String
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_item(event: Event) -> Item
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 message(data: String) -> Event
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 new(data: String) -> Event
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)
Search Document