foundation

package
v0.30.0 Latest Latest
Warning

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

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

Documentation

Overview

Package foundation boots the application.

It is the single place where an application is composed. One difference matters: the Application is built ONCE, at process start, not per request, so nothing here may assume request scope.

Where Application came from

Application was kernel.Kernel until this package landed, and github.com/arandu-io/framework/kernel is now the old name pointing here. Nothing in this file has a hesape/foundation counterpart.

What is aliased and what is declared

The composition vocabulary is github.com/arandu-io/hesape/foundation and is aliased, not restated: Bootable, Background, Closable, Diagnostic, Health, Schedulable, Migratable, Scope, Global, PerTenant, Task, Migration and ReloadTagger are the hesape types under the same names. A module written against either name is one type to the compiler.

Three names are declared here, each for a reason recorded on the declaration in module.go:

Module            names a *http.Router, and hesape/foundation.Module names
                  a *routing.Router -- http.Router is the one envelope the
                  request bridge could not make an alias
RendererProvider  hesape/foundation has none, deliberately: it names the
                  boot sequence that looks for it, and that sequence is here
Locker            it is retired, and the events bridge kept it --
                  hesape/cache.Locks cannot be built from what
                  github.com/arandu-io/kv implements

FormatRoutes is neither: it is a call through to hesape/routing.FormatRoutes, which the alias on http.Route makes possible without translating anything.

There is no list of built-in commands

This package answers no Builtins() []console.Command -- the set a project's own binary would dispatch without the aru CLI. Of the eleven commands a project dispatches today, only serve, routes and Version are answerable from an Application at all; the rest need a *data.DB it does not hold, project code it does not know, or a queue store from a module that imports this one. Letting them register themselves would be a container under another name, and there is no container.

What is written instead is the switch in the skeleton's bootstrap/console.go. It is in the project's own repository, the whole set reads at once, and adding one is writing a case.

Index

Constants

View Source
const (
	// Global runs the task once for the whole instance.
	//
	// It gets the zero Grant, because SystemGrant refuses an empty tenant -- so
	// a global task cannot pass any Check and cannot reach a repository. That
	// is a constraint rather than an oversight: global work is cleaning
	// temporary files, warming a cache, checking a certificate. Work that reads
	// a customer's rows is PerTenant, and having to say so is the point.
	Global = hfoundation.Global
	// PerTenant expands the task to every active tenant, each with its own
	// Grant and its own lock.
	PerTenant = hfoundation.PerTenant
)

Variables

This section is empty.

Functions

func FormatRoutes

func FormatRoutes(routes []*fhttp.Route) string

FormatRoutes renders the route table for the terminal, grouped by module and sorted by pattern. It is here, and not in the CLI, so that every project prints the same table.

The table itself is hesape/routing.FormatRoutes, and this is a call through rather than a copy: fhttp.Route is an alias for routing.Route, so the slice needs no translation and there is one implementation of the format. A wrapper and not an alias, because Go has no alias form for a function.

Types

type Application

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

Application holds the composed application: configuration, modules, the global middleware pipeline and the router.

func New

New assembles the Application. It opens no connection and listens on no port -- that is Boot and Run.

func (*Application) Boot

func (a *Application) Boot(ctx context.Context) error

Boot initializes modules and registers routes. It fails fast: if any module fails to boot the process does not come up. There is no silent degraded mode.

func (*Application) Config

func (a *Application) Config() bootstrap.Configuration

Config returns the configuration the Application was built with.

func (*Application) Diagnose

func (a *Application) Diagnose(ctx context.Context) []string

Diagnose asks every module that implements Diagnostic what is wrong right now, and returns what they say.

Pass it to errorpage.Options.Diagnose. It is not wired automatically because the pipeline is assembled in the open, in the application.

func (*Application) Handler

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

Handler returns the composed handler: the router wrapped in the global pipeline, with the application logger installed above everything else. Useful for tests, which drive the whole stack without a socket.

The logger has to be outermost. Without it, every Log(ctx) call in a request would fall back to slog.Default() and ignore the configured handler and level, which in production means request logs in the wrong format.

func (*Application) Logger

func (a *Application) Logger() *slog.Logger

Logger returns the root logger. Request handlers must use observability.Log(ctx) instead, which carries the request id.

func (*Application) Migrations

func (a *Application) Migrations() []Migration

Migrations collects the migrations of every module, in registration order. Hand the result to migrations.Migrator.RunPending.

This is the module half of migration discovery, and migrations.Register is the other half: a module is a value the Application already holds, so it is asked; an application's own migrations live in a package nothing calls, so they announce themselves from init(). What the two must never both hold is one migration -- the registry orders by name and this orders by registration, so a migration in both has two positions and no rule that says which one wins.

func (*Application) Recorder

func (a *Application) Recorder() *observability.Recorder

Recorder returns the buffer behind /_arandu/debug, or nil when nothing is recording.

Pass it to middleware.Observe, and to the background loops that deserve the same page:

app.Use(middleware.Observe(dev, cfg.Observability.TracingSecret, app.Recorder()))
w := jobs.NewWorker(store, jobs.WorkerOptions{Recorder: app.Recorder()})
scheduler.NewModule(app.Tasks(), scheduler.Options{Recorder: app.Recorder()})

The nil is the point. Outside development, without a tracing secret, there is no recorder -- so those loops build no Collector, record nothing, and cost nothing.

It is not wired automatically because the pipeline is assembled in the project, in the open, and a middleware that reached back into the Application for state would be the kind of hidden coupling the explicit wiring exists to avoid.

func (*Application) Register

func (a *Application) Register(mods ...Module) *Application

Register adds modules in the order they will be booted. Order matters: a module may depend on another one already being up.

func (*Application) Routes

func (a *Application) Routes() []*fhttp.Route

Routes returns the registered routes. It is empty before Boot, because a module only registers its routes when it boots.

func (*Application) Run

func (a *Application) Run(ctx context.Context) error

Run starts the server and blocks until SIGINT or SIGTERM, then shuts down gracefully.

func (*Application) Shutdown

func (a *Application) Shutdown() error

Shutdown stops the server and closes the modules in reverse registration order, which is the only order that respects dependencies between them.

func (*Application) Tasks

func (a *Application) Tasks() []Task

Tasks collects the scheduled work from every registered module, in registration order.

Same shape as Migrations(): the module declares, the Application collects, and the scheduler module runs. Pass it to scheduler.NewModule, which is why that one is registered last.

func (*Application) Use

func (a *Application) Use(mw ...fhttp.Middleware) *Application

Use adds global middleware to the pipeline. The pipeline order is the order of execution on the way in, and its reverse on the way out.

type Background

type Background = hfoundation.Background

Background is optional: the module runs a loop of its own -- the scheduler and the outbox relay do.

Start is called by Run, never by Boot, and that distinction is the difference between a process that serves and a process that does something else.

The alias drops one thing this interface used to say: it embedded Module, and hesape/foundation.Background declares Start alone. Nothing changes at a call site -- Register takes a Module, so anything the Application asks for a loop is already one -- and the embed was a second statement of a fact the registration already enforced.

type Bootable

type Bootable = hfoundation.Bootable

Bootable is optional: implement it when the module needs to prepare state at boot -- open a pool, warm a cache, register codecs.

Boot wires; it does not run. A module that needs a loop of its own implements Background instead, and validates in Boot whatever would make that loop fail.

type Closable

type Closable = hfoundation.Closable

Closable is optional: implement it to release resources on shutdown.

type Diagnostic

type Diagnostic = hfoundation.Diagnostic

Diagnostic is optional: the module reports what it knows about the state of the system, in sentences a person can act on.

It feeds the error page. The most useful hint is often about something that happened outside the failing request -- the outbox stuck for four minutes, a job that has not run -- and a page that only looks at the request cannot see any of it.

type Health

type Health = hfoundation.Health

Health is optional and feeds `aru doctor` and the /_arandu/health endpoint.

type Locker

type Locker interface {
	Run(ctx context.Context, name string, ttl time.Duration, fn func(context.Context) error) error
}

Locker is a distributed lock, for work that must happen once across replicas.

It lives here because two things need it -- the outbox relay and the scheduler -- and two identical interfaces in two packages is the duplication that the second one would create. github.com/arandu-io/kv implements it.

Nil is correct for a single replica and wrong for two. What it costs is duplicate work, which every task here has to tolerate anyway.

Why it is still here

It is retired in favour of one kind of lock in the collection, cache.Lock. The events bridge did not delete it, because hesape/cache.Locks is a concrete issuer over a store that acquires and releases by owner, and a Locker -- which only knows how to run a function under a lock it takes and gives back itself -- cannot be turned into one. events.Locker is an alias to this name and kv asserts against it, so deleting the declaration deletes the wiring rather than the duplication.

It moved with the rest of the package instead, and stays one declaration.

type Migratable

type Migratable = hfoundation.Migratable

Migratable is optional: the module declares its migrations, and the Application collects them from every registered module in registration order.

type Migration

type Migration = hfoundation.Migration

Migration is a versioned, immutable-once-published schema change.

It is an alias, not a copy: the migration runner lives in hesape, and a module must be able to hand its migrations straight to it. Both sides resolve to hesape/database/migrations.Migration, which is what data.Migration already aliases.

It is an interface -- embed migrations.BaseMigration and only GetName and Up are left to write.

type Module

type Module interface {
	// Name is the stable identifier of the module: a lowercase slug, no spaces.
	Name() string

	// Routes registers the module's HTTP routes.
	Routes(r *http.Router)
}

Module is the only unit of composition in the framework.

A module is a directory. It registers its own routes, its own migrations and its own dependency graph. There is no injection container and no reflection based resolution: the wiring is explicit, and the CLI generates the file that instantiates everything.

Every third-party module implements this interface and nothing else. It is the public contract of the framework -- change it and the whole ecosystem breaks, so change it with great care.

Why this one is declared and not aliased

hesape/foundation.Module names a *routing.Router, and this one names a *http.Router. Those are two types: http.Router is the one envelope the request bridge could not turn into an alias -- it carries the renderer and the flash, and hesape/routing.Router deliberately carries neither -- so the two interfaces cannot be the same interface while that envelope exists.

Aliasing this name to the hesape one would change the signature every module in every project is written against, in the two repositories this framework ships and in every application built on it. A bridge that changes a signature is not a bridge. It becomes an alias the day http.Router stops being an envelope, and not before.

type ReloadTagger

type ReloadTagger = hfoundation.ReloadTagger

ReloadTagger is what a module implements to supply the development live-reload tag.

Optional, and asked for the same way the renderer is: this package cannot import the view package -- that package imports this one in order to be a module -- so what it needs arrives through an interface declared there and satisfied here. The Application supplies the address of its own endpoint, because the route is its own, and two constants for one address is how a client and a server come to disagree about it.

type RendererProvider

type RendererProvider interface {
	Renderer() http.Renderer
}

RendererProvider is optional: the module supplies the view renderer.

The view package implements it. This package cannot import that package -- it implements Module, so the import would be a cycle -- and an application calling a wiring function by hand is a line somebody forgets. An optional interface solves both: the Application asks every module whether it brings a renderer, before any route is registered.

Two modules providing one is a wiring mistake, and the Application refuses to boot rather than pick one.

Why this one is declared and not aliased

hesape/foundation has no counterpart: its doc says so in as many words, and the reason is that declaring one from up there would be a second definition of the boot sequence that looks for it. The renderer type is not the divergence -- http.Renderer is an alias for hesape/http.Renderer, which is what hesape/view.Module.Renderer already returns, so that module satisfies this interface structurally and without importing this package.

type Schedulable

type Schedulable = hfoundation.Schedulable

Schedulable is optional: the module declares its scheduled work.

type Scope

type Scope = hfoundation.Scope

Scope says whether a task runs once or once per tenant.

type Task

type Task = hfoundation.Task

Task is scheduled work.

The shape mirrors Migrations(): the module declares, the Application collects, and nothing runs until something asks. What a module never does is start its own goroutine.

The alias holds because Task.Action and Task.Run name security.Action and security.Grant, and both of those are already aliases for the hesape/auth types the hesape declaration uses.

Directories

Path Synopsis
Package bootstrap is the boot sequence: what runs once, in order, before the application answers anything.
Package bootstrap is the boot sequence: what runs once, in order, before the application answers anything.

Jump to

Keyboard shortcuts

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