tygor

package
v0.8.6 Latest Latest
Warning

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

Go to latest
Published: Dec 12, 2025 License: MIT Imports: 17 Imported by: 0

Documentation

Index

Constants

This section is empty.

Variables

View Source
var ErrStreamClosed = errors.New("stream closed")

ErrStreamClosed is returned by StreamWriter.Send when the client has disconnected or the stream has been closed. Handlers should return when they receive this error.

View Source
var ErrWriteTimeout = errors.New("write timeout")

ErrWriteTimeout is returned by StreamWriter.Send when a write to the client timed out. This typically indicates a slow or unresponsive client.

Functions

This section is empty.

Types

type App

type App struct {
	// contains filtered or unexported fields
}

App is the central router for API handlers. It manages route registration, middleware, interceptors, and error handling. Use Handler() to get an http.Handler for use with http.ListenAndServe.

func NewApp

func NewApp() *App

func (*App) Handler

func (a *App) Handler() http.Handler

Handler returns an http.Handler for use with http.ListenAndServe or other HTTP servers. The returned handler includes all configured middleware.

Example:

app := tygor.NewApp().WithMiddleware(cors)
http.ListenAndServe(":8080", app.Handler())

func (*App) Routes

func (a *App) Routes() internal.RouteMap

Routes returns route metadata for code generation. The return type is internal; this method is for use by tygorgen only.

func (*App) Service

func (a *App) Service(name string) *Service

Service returns a Service namespace.

func (*App) WithErrorTransformer

func (a *App) WithErrorTransformer(fn ErrorTransformer) *App

WithErrorTransformer adds a custom error transformer. It returns the app for chaining.

func (*App) WithLogger

func (a *App) WithLogger(logger *slog.Logger) *App

WithLogger sets a custom logger for the app. If not set, slog.Default() will be used.

func (*App) WithMaskInternalErrors

func (a *App) WithMaskInternalErrors() *App

WithMaskInternalErrors enables masking of internal error messages. This is useful in production to avoid leaking sensitive information. The original error is still available to interceptors and logging.

func (*App) WithMaxRequestBodySize

func (a *App) WithMaxRequestBodySize(size uint64) *App

WithMaxRequestBodySize sets the default maximum request body size for all handlers. Individual handlers can override this with Handler.WithMaxRequestBodySize. A value of 0 means no limit. Default is 1MB (1 << 20).

func (*App) WithMiddleware

func (a *App) WithMiddleware(mw func(http.Handler) http.Handler) *App

WithMiddleware adds an HTTP middleware to wrap the app. Middleware is applied in the order added (first added is outermost).

func (*App) WithStreamHeartbeat

func (a *App) WithStreamHeartbeat(d time.Duration) *App

WithStreamHeartbeat sets the default interval for sending SSE heartbeat comments. Heartbeats keep connections alive through proxies with idle timeouts. Individual handlers can override this with StreamHandler.WithHeartbeat.

Default is 30 seconds. Use 0 to disable heartbeats.

func (*App) WithStreamWriteTimeout

func (a *App) WithStreamWriteTimeout(d time.Duration) *App

WithStreamWriteTimeout sets the default timeout for writing SSE events. If a single event write takes longer than this, the stream is closed. Individual handlers can override this with StreamHandler.WithWriteTimeout.

Default is 30 seconds. Use 0 to disable (not recommended - risks goroutine leaks).

func (*App) WithUnaryInterceptor

func (a *App) WithUnaryInterceptor(i UnaryInterceptor) *App

WithUnaryInterceptor adds a global interceptor. Global interceptors are executed before service-level and handler-level interceptors.

Interceptor execution order:

  1. Global interceptors (added via App.WithUnaryInterceptor)
  2. Service interceptors (added via Service.WithUnaryInterceptor)
  3. Handler interceptors (added via Handler.WithUnaryInterceptor)
  4. Handler function

Within each level, interceptors execute in the order they were added.

type CacheConfig

type CacheConfig struct {
	// MaxAge specifies the maximum time a resource is considered fresh (RFC 9111 Section 5.2.2.1).
	// After this time, caches must revalidate before serving the cached response.
	MaxAge time.Duration

	// SMaxAge is like MaxAge but only applies to shared caches like CDNs (RFC 9111 Section 5.2.2.10).
	// Overrides MaxAge for shared caches. Private caches ignore this directive.
	SMaxAge time.Duration

	// StaleWhileRevalidate allows serving stale content while revalidating in the background (RFC 5861).
	// Example: MaxAge=60s, StaleWhileRevalidate=300s means serve from cache for 60s,
	// then serve stale content for up to 300s more while fetching fresh data in background.
	StaleWhileRevalidate time.Duration

	// StaleIfError allows serving stale content if the origin server is unavailable (RFC 5861).
	// Example: StaleIfError=86400 allows serving day-old stale content if origin returns 5xx errors.
	StaleIfError time.Duration

	// Public indicates the response may be cached by any cache, including CDNs (RFC 9111 Section 5.2.2.9).
	// Default is false (private), meaning only the user's browser cache may store it.
	// Set to true for responses that are safe to cache publicly.
	Public bool

	// MustRevalidate requires caches to revalidate stale responses with the origin before serving (RFC 9111 Section 5.2.2.2).
	// Prevents serving stale content. Useful when stale data could cause problems.
	MustRevalidate bool

	// Immutable indicates the response will never change during its freshness lifetime (RFC 8246).
	// Browsers won't send conditional requests for immutable resources within MaxAge period.
	// Useful for content-addressed assets like "bundle.abc123.js".
	Immutable bool
}

CacheConfig defines HTTP cache directives for GET requests. See RFC 9111 (HTTP Caching) for detailed semantics.

Common patterns:

  • Simple caching: CacheConfig{MaxAge: 5*time.Minute}
  • Public CDN caching: CacheConfig{MaxAge: 5*time.Minute, Public: true}
  • Stale-while-revalidate: CacheConfig{MaxAge: 1*time.Minute, StaleWhileRevalidate: 5*time.Minute}
  • Immutable assets: CacheConfig{MaxAge: 365*24*time.Hour, Immutable: true}

type Context

type Context interface {
	context.Context

	// Service returns the name of the service being called.
	Service() string

	// EndpointID returns the full identifier for the endpoint being called (e.g., "Users.Create").
	EndpointID() string

	// HTTPRequest returns the underlying HTTP request.
	HTTPRequest() *http.Request

	// HTTPWriter returns the underlying HTTP response writer.
	// Use with caution in handlers - prefer returning errors to writing directly.
	// This is useful for setting response headers.
	HTTPWriter() http.ResponseWriter
}

Context provides type-safe access to request metadata and HTTP primitives. It embeds context.Context, so it can be used anywhere a context.Context is expected.

Interceptors receive Context directly for convenient access to request metadata. Handlers receive context.Context but can use FromContext to get the Context if needed.

For testing interceptors, implement this interface with your own type:

type testContext struct {
    context.Context
    service, method string
}
func (c *testContext) Service() string                 { return c.service }
func (c *testContext) EndpointID() string              { return c.service + "." + c.method }
func (c *testContext) HTTPRequest() *http.Request      { return nil }
func (c *testContext) HTTPWriter() http.ResponseWriter { return nil }

func FromContext

func FromContext(ctx context.Context) (Context, bool)

FromContext extracts the Context from a context.Context. Returns the Context and true if found, or nil and false if not in a tygor handler context.

This is useful in handlers that receive context.Context but need access to request metadata:

func (s *MyService) GetThing(ctx context.Context, req *GetThingRequest) (*GetThingResponse, error) {
    tc, ok := tygor.FromContext(ctx)
    if ok {
        log.Printf("handling %s", tc.EndpointID())
    }
    // ...
}

type Empty

type Empty = *struct{}

Empty represents a void request or response. Use this for operations that don't return meaningful data. The zero value is nil, which serializes to JSON null.

Example:

func DeleteUser(ctx context.Context, req *DeleteUserRequest) (tygor.Empty, error) {
    // ... delete user
    return nil, nil
}

Wire format: {"result": null}

type Endpoint

type Endpoint interface {
	// Metadata returns route metadata for code generation.
	// The return type is internal; this method is for use by tygorgen only.
	Metadata() *internal.MethodMetadata
}

Endpoint is the interface for handlers that can be registered with Service.Register.

Implementations:

type Error

type Error struct {
	Code    ErrorCode      `json:"code"`
	Message string         `json:"message"`
	Details map[string]any `json:"details,omitempty"`
}

Error is the standard JSON error envelope.

func DefaultErrorTransformer

func DefaultErrorTransformer(err error) *Error

DefaultErrorTransformer maps standard Go errors to service errors.

func Errorf

func Errorf(code ErrorCode, format string, args ...any) *Error

Errorf creates a new service error with a formatted message.

func NewError

func NewError(code ErrorCode, message string) *Error

NewError creates a new service error.

func (*Error) Error

func (e *Error) Error() string

func (*Error) WithDetail

func (e *Error) WithDetail(key string, value any) *Error

WithDetail returns a new Error with the key-value pair added to details.

func (*Error) WithDetails

func (e *Error) WithDetails(details map[string]any) *Error

WithDetails returns a new Error with the provided map merged into details. For multiple details, this is more efficient than chaining WithDetail calls.

type ErrorCode

type ErrorCode string

ErrorCode represents a machine-readable error code.

const (
	CodeInvalidArgument   ErrorCode = "invalid_argument"
	CodeUnauthenticated   ErrorCode = "unauthenticated"
	CodePermissionDenied  ErrorCode = "permission_denied"
	CodeNotFound          ErrorCode = "not_found"
	CodeMethodNotAllowed  ErrorCode = "method_not_allowed"
	CodeConflict          ErrorCode = "conflict"
	CodeAlreadyExists     ErrorCode = "already_exists" // Alias for conflict, used when resource already exists
	CodeGone              ErrorCode = "gone"
	CodeResourceExhausted ErrorCode = "resource_exhausted"
	CodeCanceled          ErrorCode = "canceled"
	CodeInternal          ErrorCode = "internal"
	CodeNotImplemented    ErrorCode = "not_implemented"
	CodeUnavailable       ErrorCode = "unavailable"
	CodeDeadlineExceeded  ErrorCode = "deadline_exceeded"
)

func (ErrorCode) HTTPStatus

func (c ErrorCode) HTTPStatus() int

HTTPStatus maps an ErrorCode to an HTTP status code.

type ErrorTransformer

type ErrorTransformer func(error) *Error

ErrorTransformer is a function that maps an application error to a service error. If it returns nil, the default transformer logic should be applied.

type ExecHandler

type ExecHandler[Req any, Res any] struct {
	// contains filtered or unexported fields
}

ExecHandler implements Endpoint for POST requests (state-changing operations).

Request Type Guidelines:

  • Use struct or pointer types
  • Request is decoded from JSON body

Example:

func CreateUser(ctx context.Context, req *CreateUserRequest) (*User, error) { ... }
Exec(CreateUser)

func UpdatePost(ctx context.Context, req *UpdatePostRequest) (*Post, error) { ... }
Exec(UpdatePost).WithUnaryInterceptor(requireAuth)

func Exec

func Exec[Req any, Res any](fn func(context.Context, Req) (Res, error)) *ExecHandler[Req, Res]

Exec creates a new POST handler from a generic function for non-streaming API calls.

The handler function signature is func(context.Context, Req) (Res, error). Requests are decoded from JSON body.

For GET requests (cacheable reads), use Query instead.

func (*ExecHandler[Req, Res]) Metadata

func (h *ExecHandler[Req, Res]) Metadata() *internal.MethodMetadata

Metadata implements Endpoint.

func (*ExecHandler[Req, Res]) WithMaxRequestBodySize

func (h *ExecHandler[Req, Res]) WithMaxRequestBodySize(size uint64) *ExecHandler[Req, Res]

WithMaxRequestBodySize sets the maximum request body size for this handler. This overrides the registry-level default. A value of 0 means no limit.

func (*ExecHandler[Req, Res]) WithSkipValidation

func (h *ExecHandler[Req, Res]) WithSkipValidation() *ExecHandler[Req, Res]

WithSkipValidation disables validation for this handler. By default, all handlers validate requests using the validator package. Use this when you need to handle validation manually or when the request type has no validation tags.

func (*ExecHandler[Req, Res]) WithUnaryInterceptor

func (h *ExecHandler[Req, Res]) WithUnaryInterceptor(i UnaryInterceptor) *ExecHandler[Req, Res]

WithUnaryInterceptor adds an interceptor to this handler. Handler interceptors execute after global and service interceptors. See App.WithUnaryInterceptor for the complete execution order.

type HandlerFunc

type HandlerFunc func(ctx context.Context, req any) (res any, err error)

HandlerFunc represents the next handler in an interceptor chain. It is passed to UnaryInterceptor functions to invoke the next interceptor or the final handler.

type LiveValue added in v0.8.5

type LiveValue[T any] struct {
	// contains filtered or unexported fields
}

LiveValue holds a single value that can be read, written, and subscribed to. Updates are broadcast to all subscribers via SSE streaming. Thread-safe for concurrent Get/Set operations.

Unlike event streams, LiveValue represents current state - subscribers always receive the latest value, and intermediate updates may be skipped if a subscriber is slow.

Example:

status := tygor.NewLiveValue(&Status{State: "idle"})

// Read current value
current := status.Get()

// Update and broadcast to all subscribers
status.Set(&Status{State: "running"})

// Register SSE endpoint with proper "livevalue" primitive
svc.Register("Status", status.Handler())

func NewLiveValue added in v0.8.5

func NewLiveValue[T any](initial T) *LiveValue[T]

NewLiveValue creates a new LiveValue with the given initial value.

func (*LiveValue[T]) Close added in v0.8.5

func (a *LiveValue[T]) Close()

Close signals all subscribers to disconnect and prevents new subscriptions. Safe to call multiple times. After Close, Set is a no-op.

func (*LiveValue[T]) Get added in v0.8.5

func (a *LiveValue[T]) Get() T

Get returns the current value.

func (*LiveValue[T]) Handler added in v0.8.5

func (a *LiveValue[T]) Handler() *LiveValueHandler[T]

Handler returns a LiveValueHandler for registering with a Service. The handler uses the "livevalue" primitive for proper TypeScript codegen.

Example:

svc.Register("Status", statusLiveValue.Handler())

func (*LiveValue[T]) Set added in v0.8.5

func (a *LiveValue[T]) Set(value T)

Set updates the value and broadcasts to all subscribers. The value is serialized once and the same bytes are sent to all subscribers. No-op if the LiveValue has been closed.

func (*LiveValue[T]) Subscribe added in v0.8.5

func (a *LiveValue[T]) Subscribe(ctx context.Context) iter.Seq[T]

Subscribe returns an iterator that yields the current value and all future updates until ctx is canceled or the LiveValue is closed. For use in Go code, not HTTP handlers.

func (*LiveValue[T]) Update added in v0.8.5

func (a *LiveValue[T]) Update(fn func(T) T)

Update atomically applies fn to the current value. Useful for read-modify-write operations.

type LiveValueHandler added in v0.8.5

type LiveValueHandler[T any] struct {
	// contains filtered or unexported fields
}

LiveValueHandler implements Endpoint for LiveValue subscriptions. It streams the current value immediately, then pushes updates via SSE.

func (*LiveValueHandler[T]) Metadata added in v0.8.5

func (h *LiveValueHandler[T]) Metadata() *internal.MethodMetadata

Metadata implements Endpoint.

func (*LiveValueHandler[T]) WithHeartbeat added in v0.8.5

func (h *LiveValueHandler[T]) WithHeartbeat(d time.Duration) *LiveValueHandler[T]

WithHeartbeat sets the interval for sending SSE heartbeat comments.

func (*LiveValueHandler[T]) WithUnaryInterceptor added in v0.8.5

func (h *LiveValueHandler[T]) WithUnaryInterceptor(i UnaryInterceptor) *LiveValueHandler[T]

WithUnaryInterceptor adds an interceptor that runs during stream setup.

func (*LiveValueHandler[T]) WithWriteTimeout added in v0.8.5

func (h *LiveValueHandler[T]) WithWriteTimeout(d time.Duration) *LiveValueHandler[T]

WithWriteTimeout sets the timeout for writing each event to the client.

type QueryHandler

type QueryHandler[Req any, Res any] struct {
	// contains filtered or unexported fields
}

QueryHandler implements Endpoint for GET requests (cacheable read operations).

Request Type Guidelines:

  • Use struct types for simple cases, pointer types when you need optional fields
  • Request parameters are decoded from URL query string

Struct vs Pointer Types:

  • Struct types (e.g., ListParams): Query parameters are decoded directly into the struct
  • Pointer types (e.g., *ListParams): A new instance is created and query parameters are decoded into it

Example:

func ListPosts(ctx context.Context, req ListPostsParams) ([]*Post, error) { ... }
Query(ListPosts).CacheControl(tygor.CacheConfig{
    MaxAge: 5 * time.Minute,
    Public: true,
})

func Query

func Query[Req any, Res any](fn func(context.Context, Req) (Res, error)) *QueryHandler[Req, Res]

Query creates a new GET handler from a generic function for cacheable read operations.

The handler function signature is func(context.Context, Req) (Res, error). Requests are decoded from URL query parameters.

Use CacheControl() to configure HTTP caching behavior.

func (*QueryHandler[Req, Res]) CacheControl

func (h *QueryHandler[Req, Res]) CacheControl(cfg CacheConfig) *QueryHandler[Req, Res]

CacheControl sets detailed HTTP cache directives for the handler. See CacheConfig documentation and RFC 9111 for directive semantics.

Example:

Query(ListPosts).CacheControl(tygor.CacheConfig{
    MaxAge:               5 * time.Minute,
    StaleWhileRevalidate: 1 * time.Minute,
    Public:               true,
})
// Sets: Cache-Control: public, max-age=300, stale-while-revalidate=60

func (*QueryHandler[Req, Res]) Metadata

func (h *QueryHandler[Req, Res]) Metadata() *internal.MethodMetadata

Metadata implements Endpoint.

func (*QueryHandler[Req, Res]) WithSkipValidation

func (h *QueryHandler[Req, Res]) WithSkipValidation() *QueryHandler[Req, Res]

WithSkipValidation disables validation for this handler. By default, all handlers validate requests using the validator package. Use this when you need to handle validation manually or when the request type has no validation tags.

func (*QueryHandler[Req, Res]) WithStrictQueryParams

func (h *QueryHandler[Req, Res]) WithStrictQueryParams() *QueryHandler[Req, Res]

WithStrictQueryParams enables strict query parameter validation for GET requests. By default, unknown query parameters are ignored (lenient mode). When enabled, requests with unknown query parameters will return an error. This helps catch typos and enforces exact parameter expectations.

func (*QueryHandler[Req, Res]) WithUnaryInterceptor

func (h *QueryHandler[Req, Res]) WithUnaryInterceptor(i UnaryInterceptor) *QueryHandler[Req, Res]

WithUnaryInterceptor adds an interceptor to this handler. Handler interceptors execute after global and service interceptors. See App.WithUnaryInterceptor for the complete execution order.

type Service

type Service struct {
	// contains filtered or unexported fields
}

func (*Service) Register

func (s *Service) Register(name string, handler Endpoint)

Register registers a handler for the given operation name. If a handler is already registered for this service and method, it will be replaced and a warning will be logged.

func (*Service) WithUnaryInterceptor

func (s *Service) WithUnaryInterceptor(i UnaryInterceptor) *Service

WithUnaryInterceptor adds an interceptor to this service. Service interceptors execute after global interceptors but before handler interceptors. See App.WithUnaryInterceptor for the complete execution order.

type StreamHandler

type StreamHandler[Req any, Res any] struct {
	// contains filtered or unexported fields
}

StreamHandler implements Endpoint for SSE streaming responses.

Stream handlers return an iterator that yields events to the client. The connection stays open until the iterator is exhausted, an error occurs, or the client disconnects.

Example:

func SubscribeToFeed(ctx context.Context, req *SubscribeRequest) iter.Seq2[*FeedEvent, error] {
    return func(yield func(*FeedEvent, error) bool) {
        ticker := time.NewTicker(time.Second)
        defer ticker.Stop()
        for {
            select {
            case <-ctx.Done():
                return
            case <-ticker.C:
                if !yield(&FeedEvent{Time: time.Now()}, nil) {
                    return
                }
            }
        }
    }
}

feed.Register("Subscribe", tygor.Stream(SubscribeToFeed))

func Stream

func Stream[Req any, Res any](fn func(context.Context, Req, StreamWriter[Res]) error) *StreamHandler[Req, Res]

Stream creates a new SSE streaming handler from a callback function.

The handler receives a StreamWriter to send events to the client. StreamWriter.Send returns an error when the stream should stop:

  • Client disconnects
  • Context is canceled or times out
  • Write fails

All disconnect-related errors satisfy errors.Is(err, ErrStreamClosed). For finer distinction, you can also check errors.Is(err, context.Canceled) or errors.Is(err, context.DeadlineExceeded).

Handlers should return when Send returns an error. Any error returned by the handler (except ErrStreamClosed) is sent to the client as a final error event.

Example:

func Subscribe(ctx context.Context, req *SubscribeRequest, stream tygor.StreamWriter[*FeedEvent]) error {
    // Check for reconnection
    if lastID := stream.LastEventID(); lastID != "" {
        // Resume from lastID
    }

    sub := broker.Subscribe(req.Topic)
    defer sub.Close()

    for {
        select {
        case <-ctx.Done():
            return nil
        case event := <-sub.Events():
            if err := stream.Send(event); err != nil {
                return err
            }
            // Or with event ID for reconnection support:
            // if err := stream.SendWithID(event.ID, event); err != nil { ... }
        }
    }
}

feed.Register("Subscribe", tygor.Stream(Subscribe))

func (*StreamHandler[Req, Res]) Metadata

func (h *StreamHandler[Req, Res]) Metadata() *internal.MethodMetadata

Metadata implements Endpoint.

func (*StreamHandler[Req, Res]) WithHeartbeat

func (h *StreamHandler[Req, Res]) WithHeartbeat(d time.Duration) *StreamHandler[Req, Res]

WithHeartbeat sets the interval for sending SSE heartbeat comments. This keeps connections alive through proxies with idle timeouts.

Heartbeats are sent as SSE comments (": heartbeat\n\n") which are ignored by the EventSource API but reset idle timers on proxies.

Default is 30 seconds. Use 0 to disable heartbeats.

func (*StreamHandler[Req, Res]) WithMaxRequestBodySize

func (h *StreamHandler[Req, Res]) WithMaxRequestBodySize(size uint64) *StreamHandler[Req, Res]

WithMaxRequestBodySize sets the maximum request body size for this handler.

func (*StreamHandler[Req, Res]) WithSkipValidation

func (h *StreamHandler[Req, Res]) WithSkipValidation() *StreamHandler[Req, Res]

WithSkipValidation disables request validation for this handler.

func (*StreamHandler[Req, Res]) WithStreamInterceptor

func (h *StreamHandler[Req, Res]) WithStreamInterceptor(i StreamInterceptor) *StreamHandler[Req, Res]

WithStreamInterceptor adds an interceptor that wraps the event stream. Stream interceptors can transform, filter, or observe events.

func (*StreamHandler[Req, Res]) WithUnaryInterceptor

func (h *StreamHandler[Req, Res]) WithUnaryInterceptor(i UnaryInterceptor) *StreamHandler[Req, Res]

WithUnaryInterceptor adds an interceptor that runs during stream setup. Unary interceptors execute before the stream starts, useful for auth checks. They do not see the stream response (it doesn't exist yet).

func (*StreamHandler[Req, Res]) WithWriteTimeout

func (h *StreamHandler[Req, Res]) WithWriteTimeout(d time.Duration) *StreamHandler[Req, Res]

WithWriteTimeout sets the timeout for writing each event to the client. If a write takes longer than this duration, the stream is closed and emit returns ErrWriteTimeout.

A zero duration means no timeout (the default).

type StreamHandlerFunc

type StreamHandlerFunc func(ctx context.Context, req any) iter.Seq2[any, error]

StreamHandlerFunc represents the next handler in a stream interceptor chain.

type StreamInterceptor

type StreamInterceptor func(ctx Context, req any, handler StreamHandlerFunc) iter.Seq2[any, error]

StreamInterceptor wraps stream execution.

Unlike UnaryInterceptor which wraps a single request/response, StreamInterceptor wraps the entire event stream. It can:

  • Transform or filter events
  • Add logging for stream lifecycle
  • Implement backpressure or rate limiting

Example:

func loggingStreamInterceptor(ctx tygor.Context, req any, handler tygor.StreamHandlerFunc) iter.Seq2[any, error] {
    start := time.Now()
    events := handler(ctx, req)
    return func(yield func(any, error) bool) {
        count := 0
        for event, err := range events {
            count++
            if !yield(event, err) {
                break
            }
        }
        log.Printf("%s streamed %d events in %v", ctx.EndpointID(), count, time.Since(start))
    }
}

type StreamWriter

type StreamWriter[T any] interface {
	// Send sends an event to the client.
	// Returns an error if the client has disconnected or the context is canceled.
	// All disconnect-related errors satisfy errors.Is(err, [ErrStreamClosed]).
	Send(event T) error

	// SendWithID sends an event with an SSE event ID.
	// The ID is sent as the "id:" field in the SSE stream, allowing clients
	// to resume from this point on reconnection via the Last-Event-ID header.
	SendWithID(id string, event T) error

	// LastEventID returns the client's Last-Event-ID header value.
	// This is set when the client reconnects after a disconnection.
	// Returns empty string on first connection or if the client didn't send the header.
	LastEventID() string
}

StreamWriter sends events to a streaming client. It provides methods for sending events with optional SSE event IDs and for checking the client's last received event ID on reconnection.

This interface enables testing stream handlers without a real HTTP connection:

type mockStreamWriter[T any] struct {
    events []T
}
func (m *mockStreamWriter[T]) Send(event T) error { m.events = append(m.events, event); return nil }
func (m *mockStreamWriter[T]) SendWithID(id string, event T) error { return m.Send(event) }
func (m *mockStreamWriter[T]) LastEventID() string { return "" }

type UnaryInterceptor

type UnaryInterceptor func(ctx Context, req any, handler HandlerFunc) (res any, err error)

UnaryInterceptor is a hook that wraps handler execution for unary (non-streaming) calls.

Interceptors receive Context for type-safe access to request metadata:

func loggingInterceptor(ctx tygor.Context, req any, handler tygor.HandlerFunc) (any, error) {
    start := time.Now()
    res, err := handler(ctx, req)
    log.Printf("%s took %v", ctx.EndpointID(), time.Since(start))
    return res, err
}

The handler parameter is the next handler in the chain. Interceptors can:

  • Inspect/modify the request before calling handler
  • Inspect/modify the response after calling handler
  • Short-circuit by returning an error without calling handler
  • Add values to context using context.WithValue

req/res are pointers to the request/response structs.

Jump to

Keyboard shortcuts

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