react

package module
v0.2.0 Latest Latest
Warning

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

Go to latest
Published: Aug 23, 2026 License: MIT Imports: 8 Imported by: 0

README

 ███████████   ██████████   █████████     █████████  ███████████
░░███░░░░░███ ░░███░░░░░█  ███░░░░░███   ███░░░░░███░█░░░███░░░█
 ░███    ░███  ░███  █ ░  ░███    ░███  ███     ░░░ ░   ░███  ░ 
 ░██████████   ░██████    ░███████████ ░███             ░███    
 ░███░░░░░███  ░███░░█    ░███░░░░░███ ░███             ░███    
 ░███    ░███  ░███ ░   █ ░███    ░███ ░░███     ███    ░███    
 █████   █████ ██████████ █████   █████ ░░█████████     █████   
░░░░░   ░░░░░ ░░░░░░░░░░ ░░░░░   ░░░░░   ░░░░░░░░░     ░░░░░    
    

React

Composable Go application modules built on github.com/0x626f/gioc

Overview

react provides small application building blocks for Go services that use gioc dependency injection. It includes:

  • application lifecycle and cancellation context management
  • logger provider configuration using github.com/0x626f/author
  • environment-backed config registration and derived config providers
  • reusable base service structs for context, logger, and config injection
  • optional Redis, PostgreSQL, and RabbitMQ modules
  • a generic transactional outbox with PostgreSQL and Redis storage

Installation

go get github.com/0x626f/react

Optional subpackages are imported only when needed:

import (
    "github.com/0x626f/react"
    reactredis "github.com/0x626f/react/redis"
    reactrmq "github.com/0x626f/react/rmq"
    reactpostgres "github.com/0x626f/react/postgres"
)

Quick Example

package main

import (
    "context"
    "os"
    "syscall"

    "github.com/0x626f/author"
    "github.com/0x626f/gioc"
    "github.com/0x626f/react"
)

func main() {
    container := gioc.NewContainer()

    configModule := gioc.NewModule("Config").Global().Provide(
        gioc.ValueProvider(
            react.LoggerConfigToken,
            &react.LoggerModuleConfig{Level: author.INFO},
            true,
        ),
    )

    appModule := react.ApplicationModuleFor(react.ApplicationConfig{
        Parent:              context.Background(),
        Interruptions:       []os.Signal{os.Interrupt, syscall.SIGTERM},
        EnableShutDownHooks: true,
    })

    if err := container.AddModules(configModule, appModule, react.LoggerModule()); err != nil {
        panic(err)
    }
    if err := container.Run(); err != nil {
        panic(err)
    }

    app, err := gioc.Get[*react.ApplicationService](container, react.ApplicationContextServiceToken)
    if err != nil {
        panic(err)
    }
    app.Await()
}

Config Module

Use RegisterConfigModule to load .env files and expose a typed app config. Providers can derive module-specific configs from the root config.

package main

import (
    "github.com/0x626f/gioc"
    "github.com/0x626f/react"
    reactredis "github.com/0x626f/react/redis"
)

const AppConfigToken = gioc.Token("AppConfig")

type AppConfig struct {
    RedisURL string `env:"REDIS_URL"`
}

var ConfigModule = react.RegisterConfigModule[AppConfig](react.ConfigModuleParams[AppConfig]{
    AppConfigToken: AppConfigToken,
    LoadEnvs: []react.EnvEntry{
        {Path: ".env", Relative: true},
    },
    Providers: []react.ConfigProviders[AppConfig]{
        {
            Token: reactredis.ConfigToken,
            Provider: func(config *AppConfig) (any, error) {
                return &reactredis.Config{URL: config.RedisURL}, nil
            },
        },
    },
})

Redis Example

package main

import (
    "time"

    "github.com/0x626f/gioc"
    "github.com/0x626f/react"
    reactredis "github.com/0x626f/react/redis"
)

func resolveRedis(container *gioc.Container) (*reactredis.Service, error) {
    return gioc.Get[*reactredis.Service](container, reactredis.ServiceToken)
}

func cacheValue(service *reactredis.Service) error {
    if err := service.Set("example:key", "value", time.Minute); err != nil {
        return err
    }

    value, ok, err := service.Get("example:key")
    if err != nil || !ok {
        return err
    }

    _ = value
    return nil
}

func modules() []*gioc.Module {
    return []*gioc.Module{
        react.ApplicationModuleFor(react.ApplicationConfig{EnableShutDownHooks: true}),
        react.LoggerModule(),
        reactredis.Module,
    }
}

reactredis.ConfigToken must be provided with a *redis.Config containing URL, usually through RegisterConfigModule.

Redis Streams Feature

Redis Streams is opt-in. Provide both configurations and select the feature; StreamsServiceToken is not registered by the base redis.Module.

streamsConfig := reactredis.DefaultStreamsConfig()
streamsConfig.WorkerCount = 8
streamsConfig.ChannelSize = 128

configModule := gioc.NewModule("RedisConfig").Global().Provide(
    reactredis.ProvideConfig(&reactredis.Config{URL: "redis://localhost:6379/0"}),
    reactredis.ProvideStreamsConfig(&streamsConfig),
)

redisModule := reactredis.ForFeature(reactredis.Streams)

Publish JSON values and consume them through a bounded manual-acknowledgement channel:

type OrderCreated struct {
    OrderID string `json:"order_id"`
}

func runOrderConsumer(ctx context.Context, container *gioc.Container) error {
    streams, err := gioc.Get[*reactredis.StreamsService](
        container,
        reactredis.StreamsServiceToken,
    )
    if err != nil {
        return err
    }

    messages, err := streams.Consume(
        ctx,
        "billing",
        "orders",
        reactredis.StreamsConsumerConfig{ConsumerCount: 2, BatchSize: 32},
    )
    if err != nil {
        return err
    }

    for message := range messages {
        var event OrderCreated
        if err = message.Decode(&event); err != nil {
            continue // no Ack: the pending message is retried
        }
        if err = chargeOrder(ctx, event); err != nil {
            continue // no Ack: the pending message is retried
        }
        if err = streams.Ack(ctx, message); err != nil {
            return err
        }
    }
    return ctx.Err()
}

func publishOrder(ctx context.Context, streams *reactredis.StreamsService) error {
    return streams.Publish(ctx, "orders", "order-42", OrderCreated{OrderID: "order-42"})
}

The service starts exactly WorkerCount pool workers. A Consume subscription reserves ConsumerCount of them until its context is cancelled; requesting more than the remaining capacity returns ErrStreamsWorkerCapacity. Every returned channel has ChannelSize capacity. Unacknowledged entries are reclaimed after ReclaimAfter; after MaximumDeliveries, they are atomically acknowledged and copied to <stream><DeadLetterSuffix> (default :dead). Publish confirms XADD; persistence and replication are Redis server policy.

RabbitMQ Example

package main

import (
    "time"

    "github.com/0x626f/gioc"
    "github.com/0x626f/react/rmq"
    "github.com/rabbitmq/amqp091-go"
)

var rabbitMQConfig = &rmq.ModuleConfig{
    Host:        "localhost",
    Port:        5672,
    User:        "guest",
    Password:    "guest",
    VirtualHost: "operator",
}

func publish(container *gioc.Container) error {
    producer, err := gioc.Get[*rmq.ProducerService](container, rmq.ProducerServiceToken)
    if err != nil {
        return err
    }

    exchange := rmq.NewExchange("events").SetKind("direct").SetDurable(true)
    queue := rmq.NewQueue("events.created").SetDurable(true).SetBindings(
        rmq.Bind(exchange, "created")...,
    )

    service, err := gioc.Get[*rmq.Service](container, rmq.ServiceToken)
    if err != nil {
        return err
    }
    if _, err = service.CreateQueues(queue); err != nil {
        return err
    }

    producer.Bind(exchange)
    producer.WithTimeout(5 * time.Second)

    return producer.Produce(&rmq.Publication{
        Destination: "created",
        Message: amqp091.Publishing{
            ContentType: "application/json",
            Body:        []byte(`{"id":"example"}`),
        },
    })
}

Provide rmq.ModuleConfigToken with ProvideModuleConfig or a config derivation before adding rmq.Module. ModuleConfig.VirtualHost is loaded from RMQ_VIRTUAL_HOST; leave it empty to use RabbitMQ's default / virtual host. The example above connects to the operator virtual host.

PostgreSQL Example

package main

import (
    "github.com/0x626f/gioc"
    "github.com/0x626f/pgxext"
    reactpostgres "github.com/0x626f/react/postgres"
)

const UsersRepositoryToken = gioc.Token("UsersRepository")

type UsersRepository struct {
    ds *pgxext.DataSource
}

func NewUsersRepository(ds *pgxext.DataSource) *UsersRepository {
    return &UsersRepository{ds: ds}
}

var PostgresModule = reactpostgres.ForRepositories([]reactpostgres.RepositoryConstructor{
    reactpostgres.Repository(UsersRepositoryToken, NewUsersRepository),
})

Provide reactpostgres.ConfigToken with a *postgres.Config containing URL before adding the PostgreSQL module.

Testing

go test ./...
just test-integration
REACT_RACE=1 just test-integration

just test-integration follows the pgxext integration flow: it creates isolated PostgreSQL, Redis, and RabbitMQ containers, requires every package's integration environment, writes coverage profiles under .coverage, and always removes the containers. Its 12-row Cartesian matrix exercises PostgreSQL 16/17/18, Redis 7.2/8.10, and RabbitMQ 4.2.9/4.3.5.

Transactional Outbox

The storage-independent outbox, adapters, worker pool, operational APIs, and production guidance are documented in outbox/README.md.

Direct integration-test runs skip when their service URL is absent. For example:

RMQ_TEST_URL=amqp://guest:guest@localhost:5672 go test ./rmq -run Integration
REDIS_TEST_URL=redis://localhost:6379/0 go test ./redis -run Integration
POSTGRES_TEST_URL='postgres://postgres:postgres@localhost/react?sslmode=disable' go test ./postgres -run Integration

License

MIT. See LICENSE.

Documentation

Index

Constants

View Source
const (
	ApplicationContextServiceToken = "ApplicationService"
	ApplicationContextToken        = "ApplicationContext"
)
View Source
const LoggerConfigToken gioc.Token = "LoggerConfig"
View Source
const LoggerToken = "Logger"

Variables

View Source
var BaseServiceInjections = []gioc.Token{
	ApplicationContextServiceToken,
	LoggerToken,
}

Functions

func ApplicationContext

func ApplicationContext(ctx context.Context) gioc.IProvider

func ApplicationModuleFor

func ApplicationModuleFor(config ApplicationConfig) *gioc.Module

func DeriveConfig

func DeriveConfig[T any, R any](originToken, targetToken gioc.Token, provider func(T) (R, error)) gioc.IProvider

func InjectFromBase

func InjectFromBase(tokens ...gioc.Token) []gioc.Token

func Logger

func Logger(config author.Config) gioc.IProvider

func LoggerModule

func LoggerModule() *gioc.Module

func RegisterConfigModule

func RegisterConfigModule[T any](params ConfigModuleParams[T]) *gioc.Module

Types

type ApplicationConfig

type ApplicationConfig struct {
	Parent              context.Context
	Interruptions       []os.Signal
	EnableShutDownHooks bool
}

type ApplicationService

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

func (*ApplicationService) AddHook

func (service *ApplicationService) AddHook(hook ShutDownHook)

func (*ApplicationService) AddPreShutdownHook added in v0.2.0

func (service *ApplicationService) AddPreShutdownHook(hook ShutDownHook)

AddPreShutdownHook registers work that must finish before ordinary resource close hooks. Hooks within each tier retain registration order.

func (*ApplicationService) Await

func (service *ApplicationService) Await()

func (*ApplicationService) DeriveContext

func (service *ApplicationService) DeriveContext() (context.Context, context.CancelFunc)

func (*ApplicationService) Shutdown

func (service *ApplicationService) Shutdown()

type BaseConfigurableService

type BaseConfigurableService[T any] struct {
	BaseService
	Config T
}

func (*BaseConfigurableService[T]) Bootstrap

func (service *BaseConfigurableService[T]) Bootstrap(impl gioc.Token, config gioc.Token, injections gioc.Injections)

type BaseService

type BaseService struct {
	Ctx  context.Context
	Stop context.CancelFunc

	ApplicationService *ApplicationService
	Logger             ILogger
}

func (*BaseService) Bootstrap

func (service *BaseService) Bootstrap(_ gioc.Token, injections gioc.Injections)

type ConfigModuleParams

type ConfigModuleParams[T any] struct {
	AppConfigToken gioc.Token
	LoadEnvs       []EnvEntry
	Providers      []ConfigProviders[T]
}

type ConfigProviders

type ConfigProviders[T any] struct {
	Token    gioc.Token
	Provider func(*T) (any, error)
}

type EnvEntry

type EnvEntry struct {
	Path     string
	Relative bool
}

type ILogger added in v0.2.0

type ILogger = author.ILogger

ILogger is React's alias of the logging contract provided by author.

type LoggerModuleConfig

type LoggerModuleConfig struct {
	Level author.LogLevel `env:"LOG_LEVEL"`
}

type ShutDownHook

type ShutDownHook func()

Directories

Path Synopsis
PostgresStore implements durable outbox storage using pgx.
PostgresStore implements durable outbox storage using pgx.

Jump to

Keyboard shortcuts

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