215 lines
6 KiB
Markdown
215 lines
6 KiB
Markdown
---
|
|
title: Services
|
|
description: Service registration, retrieval, ServiceRuntime, and factory patterns.
|
|
---
|
|
|
|
# Services
|
|
|
|
Services are the building blocks of a Core application. They are plain Go structs registered into a named registry and retrieved by name with optional type assertions.
|
|
|
|
## Registration
|
|
|
|
### Factory Functions
|
|
|
|
The primary way to register a service is via a **factory function** -- a function with the signature `func(*Core) (any, error)`. The factory receives the `Core` instance so it can access other services or register message handlers during construction.
|
|
|
|
```go
|
|
func NewMyService(c *core.Core) (any, error) {
|
|
return &MyService{}, nil
|
|
}
|
|
```
|
|
|
|
### WithService (auto-named)
|
|
|
|
`WithService` registers a service and automatically discovers its name from the Go package path. The last segment of the package path becomes the service name, lowercased.
|
|
|
|
```go
|
|
// If MyService lives in package "myapp/services/calculator",
|
|
// it is registered as "calculator".
|
|
c, err := core.New(
|
|
core.WithService(calculator.NewService),
|
|
)
|
|
```
|
|
|
|
`WithService` also performs **IPC handler discovery**: if the returned service has a method named `HandleIPCEvents` with the signature `func(*Core, Message) error`, it is automatically registered as an action handler.
|
|
|
|
```go
|
|
type Service struct{}
|
|
|
|
func (s *Service) HandleIPCEvents(c *core.Core, msg core.Message) error {
|
|
// Handle messages
|
|
return nil
|
|
}
|
|
```
|
|
|
|
### WithName (explicitly named)
|
|
|
|
When you need to control the service name (or the factory is an anonymous function), use `WithName`:
|
|
|
|
```go
|
|
c, err := core.New(
|
|
core.WithName("my-service", func(c *core.Core) (any, error) {
|
|
return &MyService{}, nil
|
|
}),
|
|
)
|
|
```
|
|
|
|
Unlike `WithService`, `WithName` does **not** auto-discover IPC handlers. Register them manually if needed.
|
|
|
|
### Direct Registration
|
|
|
|
You can also register a service directly on an existing `Core` instance:
|
|
|
|
```go
|
|
err := c.RegisterService("my-service", &MyService{})
|
|
```
|
|
|
|
This is useful for tests or when constructing services outside the `New()` options flow.
|
|
|
|
### Registration Rules
|
|
|
|
- Service names **must not be empty**.
|
|
- **Duplicate names** are rejected with an error.
|
|
- If `WithServiceLock()` was passed to `New()`, registration after initialisation is rejected.
|
|
|
|
## Retrieval
|
|
|
|
### By Name (untyped)
|
|
|
|
```go
|
|
svc := c.Service("calculator")
|
|
if svc == nil {
|
|
// not found
|
|
}
|
|
```
|
|
|
|
Returns `nil` if no service is registered under that name.
|
|
|
|
### Type-Safe Retrieval
|
|
|
|
`ServiceFor[T]` retrieves and type-asserts in one step:
|
|
|
|
```go
|
|
calc, err := core.ServiceFor[*calculator.Service](c, "calculator")
|
|
if err != nil {
|
|
// "service 'calculator' not found"
|
|
// or "service 'calculator' is of type *Foo, but expected *calculator.Service"
|
|
}
|
|
```
|
|
|
|
### Panicking Retrieval
|
|
|
|
For init-time wiring where a missing service is a fatal programming error:
|
|
|
|
```go
|
|
calc := core.MustServiceFor[*calculator.Service](c, "calculator")
|
|
// panics if not found or wrong type
|
|
```
|
|
|
|
## ServiceRuntime
|
|
|
|
`ServiceRuntime[T]` is a generic helper you embed in your service struct. It provides typed access to the `Core` instance and your service's options struct.
|
|
|
|
```go
|
|
type Options struct {
|
|
Precision int
|
|
}
|
|
|
|
type Service struct {
|
|
*core.ServiceRuntime[Options]
|
|
}
|
|
|
|
func NewService(opts Options) func(*core.Core) (any, error) {
|
|
return func(c *core.Core) (any, error) {
|
|
return &Service{
|
|
ServiceRuntime: core.NewServiceRuntime(c, opts),
|
|
}, nil
|
|
}
|
|
}
|
|
```
|
|
|
|
`ServiceRuntime` provides these methods:
|
|
|
|
| Method | Returns | Description |
|
|
|--------|---------|-------------|
|
|
| `Core()` | `*Core` | The central Core instance |
|
|
| `Opts()` | `T` | The service's typed options |
|
|
| `Config()` | `Config` | Convenience shortcut for `Core().Config()` |
|
|
|
|
### Real-World Example: The Log Service
|
|
|
|
The `pkg/log` package in this repository is the reference implementation of a Core service:
|
|
|
|
```go
|
|
type Service struct {
|
|
*core.ServiceRuntime[Options]
|
|
*Logger
|
|
}
|
|
|
|
func NewService(opts Options) func(*core.Core) (any, error) {
|
|
return func(c *core.Core) (any, error) {
|
|
logger := New(opts)
|
|
return &Service{
|
|
ServiceRuntime: core.NewServiceRuntime(c, opts),
|
|
Logger: logger,
|
|
}, nil
|
|
}
|
|
}
|
|
|
|
func (s *Service) OnStartup(ctx context.Context) error {
|
|
s.Core().RegisterQuery(s.handleQuery)
|
|
s.Core().RegisterTask(s.handleTask)
|
|
return nil
|
|
}
|
|
```
|
|
|
|
Key patterns to note:
|
|
|
|
1. The factory is a **closure** -- `NewService` takes options and returns a factory function.
|
|
2. `ServiceRuntime` is embedded, giving access to `Core()` and `Opts()`.
|
|
3. The service implements `Startable` to register its query/task handlers at startup.
|
|
|
|
## Runtime and NewWithFactories
|
|
|
|
For applications that wire services from a map of named factories, `NewWithFactories` offers a bulk registration path:
|
|
|
|
```go
|
|
type ServiceFactory func() (any, error)
|
|
|
|
rt, err := core.NewWithFactories(app, map[string]core.ServiceFactory{
|
|
"config": configFactory,
|
|
"database": dbFactory,
|
|
"cache": cacheFactory,
|
|
})
|
|
```
|
|
|
|
Factories are called in sorted key order. The resulting `Runtime` wraps a `Core` and exposes `ServiceStartup`/`ServiceShutdown` for GUI runtime integration.
|
|
|
|
For the simplest case with no custom services:
|
|
|
|
```go
|
|
rt, err := core.NewRuntime(app)
|
|
```
|
|
|
|
## Well-Known Services
|
|
|
|
Core provides convenience methods for commonly needed services. These use `MustServiceFor` internally and will panic if the service is not registered:
|
|
|
|
| Method | Expected Name | Expected Interface |
|
|
|--------|--------------|-------------------|
|
|
| `c.Config()` | `"config"` | `Config` |
|
|
| `c.Display()` | `"display"` | `Display` |
|
|
| `c.Workspace()` | `"workspace"` | `Workspace` |
|
|
| `c.Crypt()` | `"crypt"` | `Crypt` |
|
|
|
|
These are optional -- only call them if you have registered the corresponding service.
|
|
|
|
## Thread Safety
|
|
|
|
The service registry is protected by `sync.RWMutex`. Registration, retrieval, and lifecycle operations are safe to call from multiple goroutines.
|
|
|
|
## Related Pages
|
|
|
|
- [Lifecycle](lifecycle.md) -- `Startable` and `Stoppable` interfaces
|
|
- [Messaging](messaging.md) -- how services communicate
|
|
- [Configuration](configuration.md) -- all `With*` options
|