typemux

package module
v0.0.0-...-eb396eb Latest Latest
Warning

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

Go to latest
Published: Feb 2, 2026 License: MIT Imports: 7 Imported by: 0

README

Typemux

Go Reference Go Report Card Coverage

Overview

Typemux is a Go library that provides a fast and efficient way to route values to their appropriate handlers based on type. It's designed with performance in mind, offering both thread-safe and immutable variants with different performance characteristics.

Features

  • Type Safety: Compile-time type safety with generic handlers
  • Zero Allocations: Dispatch operations produce zero allocations in steady state
  • High Performance: Optimized for concurrent access patterns
  • Middleware Support: Both typed and generic middleware for cross-cutting concerns
  • Factory System: Create typed values from raw data (JSON, etc.) using registered factories
  • Multiple Registry Types:
    • Registry: Thread-safe composite registry (dispatch + factory)
    • SealedRegistry: Immutable composite with zero mutex overhead
    • DispatchRegistry / FactoryRegistry: Specialized single-purpose registries

Installation

go get github.com/struct0x/typemux

Usage

See the example/ directory for a complete HTTP server demonstrating the factory + dispatch pattern.

Basic Example
package main

import (
	"context"
	"fmt"
	"log"

	"github.com/struct0x/typemux"
)

type UserCreated struct {
	ID   int
	Name string
}

type OrderPlaced struct {
	OrderID string
	Amount  float64
}

func main() {
	// Create a new registry
	reg := typemux.NewRegistry()

	// RegisterDispatch handlers for different types
	typemux.RegisterDispatch[UserCreated](reg, func(ctx context.Context, event UserCreated) error {
		fmt.Printf("User created: %s (ID: %d)\n", event.Name, event.ID)
		return nil
	})

	typemux.RegisterDispatch[OrderPlaced](reg, func(ctx context.Context, event OrderPlaced) error {
		fmt.Printf("Order placed: %s for $%.2f\n", event.OrderID, event.Amount)
		return nil
	})

	// Dispatch events
	ctx := context.Background()

	if err := typemux.Dispatch(reg, ctx, UserCreated{ID: 1, Name: "Alice"}); err != nil {
		log.Fatal(err)
	}

	if err := typemux.Dispatch(reg, ctx, OrderPlaced{OrderID: "ORD-001", Amount: 99.99}); err != nil {
		log.Fatal(err)
	}
}
Typed Middleware (Applied at Registration)
package main

import (
	"context"
	"fmt"

	"github.com/struct0x/typemux"
)

func main() {
	reg := typemux.NewRegistry()

	// Define typed middleware - has access to the concrete event type
	loggingMiddleware := func(next typemux.HandlerFunc[UserCreated]) typemux.HandlerFunc[UserCreated] {
		return func(ctx context.Context, event UserCreated) error {
			fmt.Printf("Processing user: %s\n", event.Name)
			err := next(ctx, event)
			fmt.Printf("Finished processing user: %s\n", event.Name)
			return err
		}
	}

	// Register handler with typed middleware
	typemux.RegisterDispatch(reg, handler, loggingMiddleware)
}
Generic Middleware (Applied at Dispatch)
package main

import (
	"context"
	"fmt"
	"time"

	"github.com/struct0x/typemux"
)

func main() {
	reg := typemux.NewRegistry()
	// ... register handlers ...

	// Define generic middleware - works across all event types
	timingMiddleware := func(ctx context.Context, event any, next func(context.Context) error) error {
		start := time.Now()
		err := next(ctx)
		fmt.Printf("Dispatch took %v\n", time.Since(start))
		return err
	}

	loggingMiddleware := func(ctx context.Context, event any, next func(context.Context) error) error {
		fmt.Printf("Dispatching event: %T\n", event)
		return next(ctx)
	}

	// Apply generic middleware at dispatch time
	typemux.Dispatch(reg, ctx, event, loggingMiddleware, timingMiddleware)
}
Sealed Registry for Maximum Performance

Concurrent access to map is fine as long as it's read-only. That way we can avoid mutex overhead when the registry is constructed at runtime.

Use Seal() method to get immutable registry.

package main

import (
	"log"

	"github.com/struct0x/typemux"
)

func main() {
	// ...
	// After registering all handlers, seal the registry for better performance
	sealedReg := reg.Seal()

	// SealedRegistry has zero mutex overhead
	if err := typemux.Dispatch(sealedReg, ctx, UserCreated{ID: 2, Name: "Bob"}); err != nil {
		log.Fatal(err)
	}
}
Factory System

Create typed values from raw data using registered factories. Useful for deserializing messages from wire formats and then dispatching them.

package main

import (
	"context"
	"fmt"
	"log"

	"github.com/struct0x/typemux"
)

type UserCreated struct {
	ID   int    `json:"id"`
	Name string `json:"name"`
}

func main() {
	reg := typemux.NewRegistry()

	// Register a handler for UserCreated
	typemux.RegisterDispatch(reg, func(ctx context.Context, event UserCreated) error {
		fmt.Printf("User created: %s\n", event.Name)
		return nil
	})

	// Register a JSON factory for creating UserCreated from bytes
	typemux.RegisterFactory(reg, "user_created", typemux.JSONFactory[UserCreated]())

	// Later: create typed value from JSON and dispatch
	jsonData := []byte(`{"id": 1, "name": "Alice"}`)
	value, err := typemux.CreateType(reg, "user_created", jsonData)
	if err != nil {
		log.Fatal(err)
	}

	if err := typemux.Dispatch(reg, context.Background(), value); err != nil {
		log.Fatal(err)
	}
}
Custom Factories

// Register a custom factory with any data type
typemux.RegisterFactory(reg, "user_from_map", func(data map[string]any) (UserCreated, error) {
	return UserCreated{
		ID:   data["id"].(int),
		Name: data["name"].(string),
	}, nil
})

// Keys can be any comparable type
type EventType int
const UserCreatedEvent EventType = 1

typemux.RegisterFactory(reg, UserCreatedEvent, typemux.JSONFactory[UserCreated]())

Performance

Typemux is optimized for high-performance scenarios:

  • Zero Allocations: Dispatch operations after initialization produce zero allocations
  • Concurrent Safe: Registry can be used safely across goroutines
  • Sealed Optimization: SealedRegistry eliminates all mutex overhead
  • Benchmark Results (Apple M2 Max):
    • Registry:
      • 1 CPU: 21.46 ns/op
      • 4 CPU: 64.80 ns/op
      • 8 CPU: 122.0 ns/op
    • SealedRegistry:
      • 1 CPU: 14.59 ns/op
      • 4 CPU: 28.95 ns/op
      • 8 CPU: 45.53 ns/op

Note: Performance may vary based on your system and workload. Run benchmarks on your target system for accurate measurements.

Run benchmarks with:

go test -bench=. -benchmem

How It Works

  1. Registration: Handlers and factories are registered using Go generics for type safety
  2. Type Mapping: Types are mapped to handlers using reflect.Type as keys
  3. Dispatch: Values are routed to appropriate handlers based on their runtime type
  4. Factory Creation: Raw data (JSON, etc.) is converted to typed values using registered factories
  5. Middleware Chains:
    • Typed middleware is applied at registration.
    • Generic middleware is applied at dispatch time.
  6. Sealing: Registries can be sealed for immutable, zero-mutex runtime use

API Reference

Registry Types
Type Description
Registry Thread-safe composite registry (dispatch + factory)
SealedRegistry Immutable composite registry with zero mutex overhead
DispatchRegistry Thread-safe registry for dispatch handlers only
SealedDispatchRegistry Immutable dispatch-only registry
FactoryRegistry Thread-safe registry for factories only
SealedFactoryRegistry Immutable factory-only registry
Handler & Middleware Types
Type Signature
HandlerFunc[T] func(ctx context.Context, val T) error
Middleware[T] func(next HandlerFunc[T]) HandlerFunc[T]
DispatchMiddleware func(ctx context.Context, event any, next func(context.Context) error) error
Functions

Registry Creation:

  • NewRegistry() - Creates a composite registry (dispatch + factory)
  • NewDispatchRegistry() - Creates a dispatch-only registry
  • NewFactoryRegistry() - Creates a factory-only registry

Dispatch:

  • RegisterDispatch[T](reg, handler, middleware...) - Registers a handler for type T
    • ⚠️ Later registrations for the same type overwrite earlier ones
  • Dispatch(reg, ctx, value, middleware...) - Dispatches a value to its handler
  • MiddlewareFunc[T](f) - Creates middleware from a simple validation function

Factory:

  • RegisterFactory[KEY, DATA, T](reg, key, factory) - Registers a factory for a key
  • CreateType[KEY, DATA](reg, key, data) - Creates a typed value using a registered factory
  • JSONFactory[T]() - Returns a factory that unmarshals JSON into type T

Sealing:

  • registry.Seal() - Returns an immutable sealed copy of the registry
Error Types
Error Description
ErrHandlerNotFound No handler registered for the dispatched value's type
ErrFactoryNotFound No factory registered for the given key
ErrDataTypeNotSupported Data type doesn't match the factory's expected input type
Pointer/Value Dispatch

When dispatching a pointer, if no handler is registered for the pointer type, typemux automatically falls back to the element type's handler:

typemux.RegisterDispatch(reg, func(ctx context.Context, e UserCreated) error {
	return nil
})

// Both work:
typemux.Dispatch(reg, ctx, UserCreated{})   // Direct match
typemux.Dispatch(reg, ctx, &UserCreated{})  // Falls back to value handler

Use Cases

  • Event-driven architectures
  • Message routing systems
  • Command handlers in CQRS
  • Plugin systems
  • Any scenario requiring type-based dispatch

License

This project is licensed under the MIT License - see the LICENSE file for details.

Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

Documentation

Overview

Package typemux is a type-safe multiplexer for Go. It enables routing values to handlers by type and creating typed values from envelopes.

Index

Examples

Constants

This section is empty.

Variables

View Source
var ErrDataTypeNotSupported = errors.New("data type not supported")

ErrDataTypeNotSupported is returned when CreateType is called with not supported DATA type.

View Source
var ErrFactoryNotFound = errors.New("factory not found")

ErrFactoryNotFound is returned when no factory is found for the given key.

View Source
var ErrHandlerNotFound = errors.New("handler not found")

ErrHandlerNotFound is returned when no handler is found for the given value's type.

Functions

func CreateType

func CreateType[KEY comparable, DATA any](reg factoryResolver, key KEY, data DATA) (any, error)

CreateType looks up a factory by key and uses it to create a value from the provided data. It returns ErrFactoryNotFound if no factory is registered for the given key.

Example
package main

import (
	"context"
	"fmt"

	"github.com/struct0x/typemux"
)

func main() {
	// Event types
	type UserCreated struct {
		ID   string `json:"id"`
		Name string `json:"name"`
	}

	type OrderPlaced struct {
		OrderID string `json:"order_id"`
		Amount  int    `json:"amount"`
	}

	// Create registry and register factories + handlers
	reg := typemux.NewRegistry()

	// Register factories using JSONFactory helper
	typemux.RegisterFactory(reg, "user_created", typemux.JSONFactory[UserCreated]())
	typemux.RegisterFactory(reg, "order_placed", typemux.JSONFactory[OrderPlaced]())

	// Register handlers
	typemux.RegisterDispatch(reg, func(ctx context.Context, e UserCreated) error {
		fmt.Printf("User created: %s (ID: %s)\n", e.Name, e.ID)
		return nil
	})

	typemux.RegisterDispatch(reg, func(ctx context.Context, e OrderPlaced) error {
		fmt.Printf("Order placed: %s for $%d\n", e.OrderID, e.Amount)
		return nil
	})

	// Seal for production use
	sealed := reg.Seal()
	ctx := context.Background()

	// Simulate receiving envelopes (e.g., from a message queue)
	envelopes := []struct {
		Type string
		Data []byte
	}{
		{"user_created", []byte(`{"id": "u1", "name": "Alice"}`)},
		{"order_placed", []byte(`{"order_id": "ORD-001", "amount": 150}`)},
		{"user_created", []byte(`{"id": "u2", "name": "Bob"}`)},
	}

	// Process each envelope: create typed value, then dispatch
	for _, env := range envelopes {
		value, err := typemux.CreateType(sealed, env.Type, env.Data)
		if err != nil {
			fmt.Printf("Failed to create type: %v\n", err)
			continue
		}

		if err := typemux.Dispatch(sealed, ctx, value); err != nil {
			fmt.Printf("Failed to dispatch: %v\n", err)
		}
	}

}
Output:

User created: Alice (ID: u1)
Order placed: ORD-001 for $150
User created: Bob (ID: u2)

func Dispatch

func Dispatch(disp dispatcher, ctx context.Context, v any, middleware ...DispatchMiddleware) error

Dispatch dispatches the given value to a registered handler based on its concrete type. Optional generic middleware is applied outermost-first, wrapping the typed middleware chain. It returns ErrHandlerNotFound if no handler is registered for the value's type.

Example
package main

import (
	"context"
	"fmt"

	"github.com/struct0x/typemux"
)

func loggingMiddleware[T any]() typemux.Middleware[T] {
	return func(next typemux.HandlerFunc[T]) typemux.HandlerFunc[T] {
		return func(ctx context.Context, event T) error {
			fmt.Printf("Processing event: %T\n", event)
			err := next(ctx, event)
			if err != nil {
				fmt.Printf("Error processing event: %v\n", err)
				return err
			}
			fmt.Printf("Successfully processed event: %T\n", event)
			return nil
		}
	}
}

func main() {
	reg := typemux.NewRegistry()

	// Define event types
	type UserCreated struct {
		ID   int
		Name string
	}

	type OrderPlaced struct {
		OrderID string
		Amount  float64
	}

	type Unknown struct {
		Foo string
	}

	// Create middleware using the helper function
	validationMiddleware := typemux.MiddlewareFunc(func(ctx context.Context, event UserCreated) (bool, error) {
		if event.ID <= 0 {
			return false, fmt.Errorf("invalid user ID: %d", event.ID)
		}
		if event.Name == "" {
			return false, fmt.Errorf("user name cannot be empty")
		}
		return true, nil // Continue processing
	})

	// RegisterDispatch handlers for different types
	typemux.RegisterDispatch(
		reg,
		func(ctx context.Context, event UserCreated) error {
			fmt.Printf("User created: %s (ID: %d)\n", event.Name, event.ID)
			return nil
		},
		validationMiddleware,
		loggingMiddleware[UserCreated](),
	)

	typemux.RegisterDispatch(
		reg,
		func(ctx context.Context, event OrderPlaced) error {
			fmt.Printf("Order placed: %s for $%.2f\n", event.OrderID, event.Amount)
			return nil
		},
		loggingMiddleware[OrderPlaced](),
	)

	// Dispatch events
	ctx := context.Background()

	_ = typemux.Dispatch(reg, ctx, UserCreated{ID: 1, Name: "Alice"})
	_ = typemux.Dispatch(reg, ctx, OrderPlaced{OrderID: "ORD-001", Amount: 99.99})

	sealedReg := reg.Seal()
	_ = typemux.Dispatch(sealedReg, ctx, UserCreated{ID: 2, Name: "Alice"})
	_ = typemux.Dispatch(sealedReg, ctx, OrderPlaced{OrderID: "ORD-002", Amount: 99.99})

	if err := typemux.Dispatch(reg, ctx, Unknown{Foo: "bar"}); err != nil {
		fmt.Printf("Dispatch err: %v", err)
	}

}
Output:

Processing event: typemux_test.UserCreated
User created: Alice (ID: 1)
Successfully processed event: typemux_test.UserCreated
Processing event: typemux_test.OrderPlaced
Order placed: ORD-001 for $99.99
Successfully processed event: typemux_test.OrderPlaced
Processing event: typemux_test.UserCreated
User created: Alice (ID: 2)
Successfully processed event: typemux_test.UserCreated
Processing event: typemux_test.OrderPlaced
Order placed: ORD-002 for $99.99
Successfully processed event: typemux_test.OrderPlaced
Dispatch err: typemux: handler not found for type typemux_test.Unknown

func JSONFactory

func JSONFactory[T any]() func([]byte) (T, error)

JSONFactory returns a factory function that unmarshals JSON data into type T. Use with RegisterFactory for convenient JSON-based type creation.

Example:

RegisterFactory(reg, "user_created", JSONFactory[UserCreated]())

func RegisterDispatch

func RegisterDispatch[T any](reg dispatchRegistry, handler HandlerFunc[T], middleware ...Middleware[T])

RegisterDispatch adds a handler for values of type T, with optional middleware.

If a handler for the same type T has already been registered, it will be replaced by the new handler and middleware chain.

Middleware is applied outermost first (i.e., the last middleware wraps the others).

func RegisterFactory

func RegisterFactory[KEY comparable, DATA any, T any](reg factoryRegistry, key KEY, factory func(DATA) (T, error))

RegisterFactory registers a factory function that creates values of type T from data of type DATA, associated with the given key.

The key can be any comparable type (string, int, custom enum, etc.). If a factory for the same key has already been registered, it will be replaced.

Types

type DispatchMiddleware

type DispatchMiddleware func(ctx context.Context, event any, next func(context.Context) error) error

DispatchMiddleware wraps a dispatch call with access to the event as any. Use for cross-cutting concerns like logging, timing, and tracing that don't need type-specific access to the event.

type DispatchRegistry

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

DispatchRegistry holds registered type-safe handlers. Use NewDispatchRegistry() to create one, then RegisterDispatch() handlers.

func NewDispatchRegistry

func NewDispatchRegistry() *DispatchRegistry

NewDispatchRegistry creates a new empty DispatchRegistry.

DispatchRegistry holds registered type-safe handlers.

func (*DispatchRegistry) Seal

Seal finalizes the DispatchRegistry and returns a SealedDispatchRegistry.

type FactoryRegistry

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

FactoryRegistry holds registered type factories. Use NewFactoryRegistry() to create one, then RegisterFactory().

func NewFactoryRegistry

func NewFactoryRegistry() *FactoryRegistry

NewFactoryRegistry creates a new empty FactoryRegistry.

func (*FactoryRegistry) Seal

Seal finalizes the FactoryRegistry and returns a SealedFactoryRegistry.

type HandlerFunc

type HandlerFunc[T any] func(ctx context.Context, val T) error

HandlerFunc is a type-safe handler for values of type T. It receives a context and a value, and may return an error.

type Middleware

type Middleware[T any] func(next HandlerFunc[T]) HandlerFunc[T]

Middleware is a type-safe wrapper around a HandlerFunc. It allows injecting logic before/after the handler.

func MiddlewareFunc

func MiddlewareFunc[T any](f func(context.Context, T) (cont bool, err error)) Middleware[T]

MiddlewareFunc is a simple convenience function to create middleware from a function that optionally short-circuits the call chain.

type Registry

type Registry struct {
	*DispatchRegistry
	*FactoryRegistry
}

Registry is a composite registry that supports both handlers and factories. Use NewRegistry() to create one.

func NewRegistry

func NewRegistry() *Registry

NewRegistry creates a new composite Registry with both handler and factory support.

func (*Registry) Seal

func (r *Registry) Seal() *SealedRegistry

Seal finalizes the Registry and returns a SealedRegistry.

The resulting SealedRegistry is immutable and safe for concurrent use with no mutex overhead.

type SealedDispatchRegistry

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

SealedDispatchRegistry is an immutable, thread-safe dispatcher.

type SealedFactoryRegistry

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

SealedFactoryRegistry is an immutable factory resolver.

type SealedRegistry

type SealedRegistry struct {
	*SealedDispatchRegistry
	*SealedFactoryRegistry
}

SealedRegistry is an immutable composite registry for runtime use.

Directories

Path Synopsis

Jump to

Keyboard shortcuts

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