sse

package
v0.2.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 5, 2026 License: MIT Imports: 12 Imported by: 0

README

host/sse — Server-Sent Events (EventSource) for gojs

An optional host package that installs a WHATWG-compatible EventSource client into a gojs VM. Scripts get the familiar browser API; the network I/O is handled by Go on a background goroutine and marshaled back onto the single VM goroutine safely.

The core VM has no dependency on this package — it is purely additive.

Usage

import (
	"github.com/iceisfun/gojs"
	"github.com/iceisfun/gojs/host/sse"
)

vm := gojs.New(
	gojs.WithPrintProvider(gojs.NewDefaultPrintProvider()),
	gojs.WithTimerProvider(gojs.NewDefaultTimerProvider()), // needed only if the script uses setTimeout/setInterval
)
defer vm.Close()

if err := sse.Install(vm); err != nil {
	log.Fatal(err)
}

vm.RunString("app.js", `
	const es = new EventSource("http://localhost:8080/events");
	es.onopen    = ()  => console.log("open");
	es.onmessage = (e) => console.log("message:", e.data);
	es.addEventListener("tick", (e) => console.log("tick:", e.data));
	es.onerror   = ()  => console.log("error / reconnecting, readyState =", es.readyState);
	// es.close(); // stop and let RunString return
`)

RunString/RunLoop stay alive while an EventSource is open (the package holds the event loop the way a pending timer would). Calling es.close() — or exhausting the reconnect cap — releases the loop so the call can return.

Install

func Install(vm *gojs.VM, opts ...Option) error

Options:

Option Default Meaning
WithRetry(d time.Duration) 3s (DefaultRetry) Base reconnection delay, until a server retry: field overrides it.
WithMaxReconnects(n int) 0 (unlimited) Cap on consecutive failed connection attempts before giving up. A successful connection resets the streak.
WithHTTPClient(c *http.Client) fresh, timeout-free client Custom transport/TLS/proxy. Must not set a per-request Timeout (it would abort long-lived streams).

JavaScript API

globalThis.EventSource is a class with:

  • Constructor new EventSource(url[, { withCredentials }]). url must be an absolute HTTP(S) URL.
  • Properties: url, withCredentials, readyState.
  • Ready-state constants CONNECTING (0), OPEN (1), CLOSED (2) — both static (EventSource.OPEN) and instance (es.OPEN).
  • Handler properties: onopen, onmessage, onerror.
  • addEventListener(type, fn) / removeEventListener(type, fn) / dispatchEvent(event) so named events (event: foo) reach addEventListener("foo", …).
  • close().

Delivered events are plain objects shaped like a DOM MessageEvent: { type, data, lastEventId, origin, target }.

Stream parsing (per the SSE spec)

  • Request sends Accept: text/event-stream, Cache-Control: no-cache, and Last-Event-ID once an id: has been seen.
  • A 200 response with a text/event-stream content type fires open.
  • Lines split on \n, \r, or \r\n. field: value strips one leading space after the colon. A line starting with : is a comment (ignored). A blank line dispatches the accumulated event.
  • Fields: data (multiple lines join with \n; a trailing \n is stripped before dispatch), event (event type, default message), id (persisted for reconnection; ignored if it contains a NUL), retry (digits only → new reconnection delay in ms). Unknown fields are ignored.
  • An event with no data is not dispatched (an event:/id:/retry:-only block fires nothing, though id/retry still take effect).

Reconnection

On a network error or a stream that simply ends, the client sets readyState to CONNECTING, fires error, waits the reconnection time, and reconnects with Last-Event-ID. close() sets CLOSED and stops permanently. A non-200 status or a wrong content type is a fatal error: it fires a non-reconnecting error (readyStateCLOSED) and does not retry.

WithMaxReconnects bounds consecutive failures, guaranteeing the process can exit even if a server is unreachable and the script never calls close(). With the default (unlimited), a live-but-flapping server reconnects forever, exactly like a browser tab — rely on close() to stop it.

Threading

The blocking HTTP read runs on a dedicated goroutine that never touches any gojs.Value/*gojs.Object. Every open/message/error is delivered to JS through vm.Enqueue, which runs the continuation on the VM goroutine. The package keeps the loop alive for the stream's lifetime with vm.Pin() and releases it on close() / give-up, so the loop blocks idle (no busy-polling) and can exit cleanly. close() cancels the request context, unblocking the reader.

Differences from a browser EventSource

  • URLs must be absolute; there is no document base URL for relative resolution.
  • withCredentials is accepted and exposed but has no cookie/CORS effect (there is no browser security context on the host).
  • No same-origin/CORS enforcement — the host decides what a script may reach via the injected URL and the *http.Client.
  • An exception thrown by an event handler propagates to the embedder (it surfaces from RunString), rather than being swallowed and merely reported to a developer console.

Documentation

Overview

Package sse installs a WHATWG-compatible Server-Sent Events client (the EventSource web API) into a gojs VM.

Overview

SSE is a one-way "server pushes text to client" protocol layered on a long lived HTTP response with content type text/event-stream. This package is an OPTIONAL host add-on: the VM has no dependency on it. Call Install to expose a browser-shaped EventSource constructor to scripts:

vm := gojs.New(gojs.WithPrintProvider(gojs.NewDefaultPrintProvider()))
if err := sse.Install(vm); err != nil { ... }
vm.RunString("app.js", `
    const es = new EventSource("http://localhost:8080/events");
    es.onopen    = () => console.log("open");
    es.onmessage = (e) => console.log("message", e.data);
    es.addEventListener("tick", (e) => console.log("tick", e.data));
    es.onerror   = () => console.log("error/reconnecting");
`)

Architecture

The public EventSource surface is defined as a JavaScript prelude (see prelude.go) that Install loads with vm.RunString. The prelude delegates the actual networking to two hidden natives under globalThis.__gojs_sse:

__gojs_sse.connect(url, opts, onEvent) -> handle
__gojs_sse.close(handle)

connect opens the HTTP stream on its own goroutine and reports every state change back to the prelude by calling onEvent(kind, payload) via vm.Enqueue, where kind is "open", "event", or "error". The prelude turns those into DOM-style events (open/message/named/error) and dispatches them to the matching handler property (onopen/onmessage/onerror) and any listeners registered with addEventListener.

Threading

The VM is single-threaded. The blocking HTTP read runs on a background goroutine that never touches any JavaScript value directly; it only marshals results onto the VM goroutine with vm.Enqueue. While a stream is open the package holds the event loop alive with vm.Pin so RunString/RunLoop do not return prematurely; close() and giving up on reconnection release the hold so the loop can end.

Reconnection

On a network error or a stream that ends, the client waits the reconnection time (default 3s, overridable per-stream by a retry: field), fires an error event, and reconnects — sending Last-Event-ID when an id: has been seen. A non-200 status or a wrong content type is a fatal error: it fires a non-reconnecting error and stops. close() stops permanently. To bound automatic reconnection (and guarantee the process can exit even if a script never calls close), set a cap with WithMaxReconnects.

Index

Constants

View Source
const DefaultRetry = 3 * time.Second

DefaultRetry is the initial reconnection delay used when no WithRetry option is given and the server has not sent a retry: field. It matches the value browsers commonly default to.

Variables

This section is empty.

Functions

func Install

func Install(vm *gojs.VM, opts ...Option) error

Install exposes the EventSource constructor to scripts running in vm. It is opt-in and safe to call once per VM. Options tune the reconnection behavior and HTTP client; a plain Install(vm) uses sensible defaults (see DefaultRetry, unlimited reconnects, a timeout-free http.Client).

Types

type Option

type Option func(*config)

Option configures Install.

func WithHTTPClient

func WithHTTPClient(client *http.Client) Option

WithHTTPClient supplies a custom *http.Client (for a custom transport, TLS config, proxy, etc.). The client must not impose a per-request Timeout, since that would abort long-lived streams; cancellation is handled via request contexts instead. If unset, a fresh client with no timeout is used.

func WithMaxReconnects

func WithMaxReconnects(n int) Option

WithMaxReconnects caps the number of consecutive failed reconnection attempts before the client gives up, fires a final (non-reconnecting) error, and releases the event loop. The counter resets whenever a connection succeeds. n <= 0 means unlimited reconnection (browser behavior); with unlimited reconnection a stream stays alive until the script calls close().

func WithRetry

func WithRetry(d time.Duration) Option

WithRetry sets the base reconnection delay. It is the wait before the first (and each subsequent) reconnect attempt until the server overrides it with a retry: field. Tests use a very small value for determinism. A non-positive value is ignored.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL