Documentation
¶
Index ¶
- Variables
- type App
- func (a *App) Handler() http.Handler
- func (a *App) Routes() internal.RouteMap
- func (a *App) Service(name string) *Service
- func (a *App) WithErrorTransformer(fn ErrorTransformer) *App
- func (a *App) WithLogger(logger *slog.Logger) *App
- func (a *App) WithMaskInternalErrors() *App
- func (a *App) WithMaxRequestBodySize(size uint64) *App
- func (a *App) WithMiddleware(mw func(http.Handler) http.Handler) *App
- func (a *App) WithStreamHeartbeat(d time.Duration) *App
- func (a *App) WithStreamWriteTimeout(d time.Duration) *App
- func (a *App) WithUnaryInterceptor(i UnaryInterceptor) *App
- type CacheConfig
- type Context
- type Empty
- type Endpoint
- type Error
- type ErrorCode
- type ErrorTransformer
- type ExecHandler
- func (h *ExecHandler[Req, Res]) Metadata() *internal.MethodMetadata
- func (h *ExecHandler[Req, Res]) WithMaxRequestBodySize(size uint64) *ExecHandler[Req, Res]
- func (h *ExecHandler[Req, Res]) WithSkipValidation() *ExecHandler[Req, Res]
- func (h *ExecHandler[Req, Res]) WithUnaryInterceptor(i UnaryInterceptor) *ExecHandler[Req, Res]
- type HandlerFunc
- type LiveValue
- type LiveValueHandler
- func (h *LiveValueHandler[T]) Metadata() *internal.MethodMetadata
- func (h *LiveValueHandler[T]) WithHeartbeat(d time.Duration) *LiveValueHandler[T]
- func (h *LiveValueHandler[T]) WithUnaryInterceptor(i UnaryInterceptor) *LiveValueHandler[T]
- func (h *LiveValueHandler[T]) WithWriteTimeout(d time.Duration) *LiveValueHandler[T]
- type QueryHandler
- func (h *QueryHandler[Req, Res]) CacheControl(cfg CacheConfig) *QueryHandler[Req, Res]
- func (h *QueryHandler[Req, Res]) Metadata() *internal.MethodMetadata
- func (h *QueryHandler[Req, Res]) WithSkipValidation() *QueryHandler[Req, Res]
- func (h *QueryHandler[Req, Res]) WithStrictQueryParams() *QueryHandler[Req, Res]
- func (h *QueryHandler[Req, Res]) WithUnaryInterceptor(i UnaryInterceptor) *QueryHandler[Req, Res]
- type Service
- type StreamHandler
- func (h *StreamHandler[Req, Res]) Metadata() *internal.MethodMetadata
- func (h *StreamHandler[Req, Res]) WithHeartbeat(d time.Duration) *StreamHandler[Req, Res]
- func (h *StreamHandler[Req, Res]) WithMaxRequestBodySize(size uint64) *StreamHandler[Req, Res]
- func (h *StreamHandler[Req, Res]) WithSkipValidation() *StreamHandler[Req, Res]
- func (h *StreamHandler[Req, Res]) WithStreamInterceptor(i StreamInterceptor) *StreamHandler[Req, Res]
- func (h *StreamHandler[Req, Res]) WithUnaryInterceptor(i UnaryInterceptor) *StreamHandler[Req, Res]
- func (h *StreamHandler[Req, Res]) WithWriteTimeout(d time.Duration) *StreamHandler[Req, Res]
- type StreamHandlerFunc
- type StreamInterceptor
- type StreamWriter
- type UnaryInterceptor
Constants ¶
This section is empty.
Variables ¶
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.
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 (*App) 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 ¶
Routes returns route metadata for code generation. The return type is internal; this method is for use by tygorgen only.
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 ¶
WithLogger sets a custom logger for the app. If not set, slog.Default() will be used.
func (*App) WithMaskInternalErrors ¶
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 ¶
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 ¶
WithMiddleware adds an HTTP middleware to wrap the app. Middleware is applied in the order added (first added is outermost).
func (*App) WithStreamHeartbeat ¶
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 ¶
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:
- Global interceptors (added via App.WithUnaryInterceptor)
- Service interceptors (added via Service.WithUnaryInterceptor)
- Handler interceptors (added via Handler.WithUnaryInterceptor)
- 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 ¶
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:
- *ExecHandler - for POST requests (created with Exec)
- *QueryHandler - for GET requests (created with Query)
- *StreamHandler - for SSE streaming (created with Stream)
- *LiveValueHandler - for synchronized state (created with LiveValue.Handler)
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 ¶
DefaultErrorTransformer maps standard Go errors to service errors.
func (*Error) WithDetail ¶
WithDetail returns a new Error with the key-value pair added to details.
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" CodeDeadlineExceeded ErrorCode = "deadline_exceeded" )
func (ErrorCode) HTTPStatus ¶
HTTPStatus maps an ErrorCode to an HTTP status code.
type ErrorTransformer ¶
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 ¶
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 ¶
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 ¶
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
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.
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
StreamHandlerFunc represents the next handler in a stream interceptor chain.
type StreamInterceptor ¶
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.