feat: complete DTO pattern — struct literals, no constructors
- All New* constructors removed (NewApp, NewIO, NewCoreCli, NewBus, NewService, NewCoreI18n, NewConfig)
- New() uses pure struct literals: &App{}, &Fs{}, &Config{ConfigOpts:}, &Cli{opts:}, &Service{}, &Ipc{}, &I18n{}
- Ipc methods moved to func (c *Core) — Ipc is now a DTO
- LockApply only called from WithServiceLock, not on every New()
- Service map lazy-inits on first write
- CliOpts DTO with Version/Name/Description
Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
parent
173067719e
commit
c2227fbb33
10 changed files with 122 additions and 205 deletions
|
|
@ -6,7 +6,6 @@
|
|||
package core
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
)
|
||||
|
|
@ -33,25 +32,6 @@ type App struct {
|
|||
Runtime any
|
||||
}
|
||||
|
||||
// NewApp creates a App with the given identity.
|
||||
// Filename and Path are auto-detected from the running binary.
|
||||
func NewApp(name, description, version string) *App {
|
||||
app := &App{
|
||||
Name: name,
|
||||
Version: version,
|
||||
Description: description,
|
||||
}
|
||||
|
||||
// Auto-detect executable identity
|
||||
if exe, err := os.Executable(); err == nil {
|
||||
if abs, err := filepath.Abs(exe); err == nil {
|
||||
app.Path = abs
|
||||
app.Filename = filepath.Base(abs)
|
||||
}
|
||||
}
|
||||
|
||||
return app
|
||||
}
|
||||
|
||||
// Find locates a program on PATH and returns a App for it.
|
||||
// Returns nil if not found.
|
||||
|
|
|
|||
|
|
@ -13,9 +13,16 @@ import (
|
|||
// CliAction represents a function called when a command is invoked.
|
||||
type CliAction func() error
|
||||
|
||||
// CliOpts configures a Cli.
|
||||
type CliOpts struct {
|
||||
Version string
|
||||
Name string
|
||||
Description string
|
||||
}
|
||||
|
||||
// Cli is the CLI command framework.
|
||||
type Cli struct {
|
||||
app *App
|
||||
opts *CliOpts
|
||||
rootCommand *Command
|
||||
defaultCommand *Command
|
||||
preRunCommand func(*Cli) error
|
||||
|
|
@ -27,14 +34,14 @@ type Cli struct {
|
|||
// defaultBannerFunction prints a banner for the application.
|
||||
func defaultBannerFunction(c *Cli) string {
|
||||
version := ""
|
||||
if c.app != nil && c.app.Version != "" {
|
||||
version = " " + c.app.Version
|
||||
if c.opts != nil && c.opts.Version != "" {
|
||||
version = " " + c.opts.Version
|
||||
}
|
||||
name := ""
|
||||
description := ""
|
||||
if c.app != nil {
|
||||
name = c.app.Name
|
||||
description = c.app.Description
|
||||
if c.opts != nil {
|
||||
name = c.opts.Name
|
||||
description = c.opts.Description
|
||||
}
|
||||
if description != "" {
|
||||
return fmt.Sprintf("%s%s - %s", name, version, description)
|
||||
|
|
@ -42,24 +49,6 @@ func defaultBannerFunction(c *Cli) string {
|
|||
return fmt.Sprintf("%s%s", name, version)
|
||||
}
|
||||
|
||||
// NewCoreCli creates a new CLI bound to the given App identity.
|
||||
func NewCoreCli(app *App) *Cli {
|
||||
name := ""
|
||||
description := ""
|
||||
if app != nil {
|
||||
name = app.Name
|
||||
description = app.Description
|
||||
}
|
||||
|
||||
result := &Cli{
|
||||
app: app,
|
||||
bannerFunction: defaultBannerFunction,
|
||||
}
|
||||
result.rootCommand = NewCommand(name, description)
|
||||
result.rootCommand.setApp(result)
|
||||
result.rootCommand.setParentCommandPath("")
|
||||
return result
|
||||
}
|
||||
|
||||
// Command returns the root command.
|
||||
func (c *Cli) Command() *Command {
|
||||
|
|
@ -68,24 +57,24 @@ func (c *Cli) Command() *Command {
|
|||
|
||||
// Version returns the application version string.
|
||||
func (c *Cli) Version() string {
|
||||
if c.app != nil {
|
||||
return c.app.Version
|
||||
if c.opts != nil {
|
||||
return c.opts.Version
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// Name returns the application name.
|
||||
func (c *Cli) Name() string {
|
||||
if c.app != nil {
|
||||
return c.app.Name
|
||||
if c.opts != nil {
|
||||
return c.opts.Name
|
||||
}
|
||||
return c.rootCommand.name
|
||||
}
|
||||
|
||||
// ShortDescription returns the application short description.
|
||||
func (c *Cli) ShortDescription() string {
|
||||
if c.app != nil {
|
||||
return c.app.Description
|
||||
if c.opts != nil {
|
||||
return c.opts.Description
|
||||
}
|
||||
return c.rootCommand.shortdescription
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
// SPDX-License-Identifier: EUPL-1.2
|
||||
|
||||
// Settings, feature flags, and typed configuration for the Core framework.
|
||||
// Named after /etc — the configuration directory.
|
||||
|
||||
package core
|
||||
|
||||
|
|
@ -9,62 +8,62 @@ import (
|
|||
"sync"
|
||||
)
|
||||
|
||||
// Var is a variable that can be set, unset, and queried for its state.
|
||||
// Zero value is unset.
|
||||
// ConfigVar is a variable that can be set, unset, and queried for its state.
|
||||
type ConfigVar[T any] struct {
|
||||
val T
|
||||
set bool
|
||||
}
|
||||
|
||||
// Get returns the value, or the zero value if unset.
|
||||
func (v *ConfigVar[T]) Get() T { return v.val }
|
||||
|
||||
// Set sets the value and marks it as set.
|
||||
func (v *ConfigVar[T]) Set(val T) { v.val = val; v.set = true }
|
||||
|
||||
// IsSet returns true when a value has been set.
|
||||
func (v *ConfigVar[T]) Get() T { return v.val }
|
||||
func (v *ConfigVar[T]) Set(val T) { v.val = val; v.set = true }
|
||||
func (v *ConfigVar[T]) IsSet() bool { return v.set }
|
||||
|
||||
// Unset resets to zero value and marks as unset.
|
||||
func (v *ConfigVar[T]) Unset() {
|
||||
v.set = false
|
||||
var zero T
|
||||
v.val = zero
|
||||
}
|
||||
|
||||
// NewVar creates a Var with the given value (marked as set).
|
||||
func NewConfigVar[T any](val T) ConfigVar[T] {
|
||||
return ConfigVar[T]{val: val, set: true}
|
||||
}
|
||||
|
||||
// Config holds configuration settings and feature flags.
|
||||
type Config struct {
|
||||
mu sync.RWMutex
|
||||
settings map[string]any
|
||||
features map[string]bool
|
||||
// ConfigOpts holds configuration data.
|
||||
type ConfigOpts struct {
|
||||
Settings map[string]any
|
||||
Features map[string]bool
|
||||
}
|
||||
|
||||
// NewConfig creates a new configuration store.
|
||||
func NewConfig() *Config {
|
||||
return &Config{
|
||||
settings: make(map[string]any),
|
||||
features: make(map[string]bool),
|
||||
func (o *ConfigOpts) init() {
|
||||
if o.Settings == nil {
|
||||
o.Settings = make(map[string]any)
|
||||
}
|
||||
if o.Features == nil {
|
||||
o.Features = make(map[string]bool)
|
||||
}
|
||||
}
|
||||
|
||||
// Config holds configuration settings and feature flags.
|
||||
type Config struct {
|
||||
*ConfigOpts
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
// Set stores a configuration value by key.
|
||||
func (e *Config) Set(key string, val any) {
|
||||
e.mu.Lock()
|
||||
e.settings[key] = val
|
||||
e.ConfigOpts.init()
|
||||
e.Settings[key] = val
|
||||
e.mu.Unlock()
|
||||
}
|
||||
|
||||
// Get retrieves a configuration value by key.
|
||||
// Returns (value, true) if found, (zero, false) if not.
|
||||
func (e *Config) Get(key string) (any, bool) {
|
||||
e.mu.RLock()
|
||||
val, ok := e.settings[key]
|
||||
e.mu.RUnlock()
|
||||
defer e.mu.RUnlock()
|
||||
if e.ConfigOpts == nil || e.Settings == nil {
|
||||
return nil, false
|
||||
}
|
||||
val, ok := e.Settings[key]
|
||||
return val, ok
|
||||
}
|
||||
|
||||
|
|
@ -73,7 +72,6 @@ func (e *Config) Int(key string) int { return ConfigGet[int](e, key) }
|
|||
func (e *Config) Bool(key string) bool { return ConfigGet[bool](e, key) }
|
||||
|
||||
// ConfigGet retrieves a typed configuration value.
|
||||
// Returns zero value if key is missing or type doesn't match.
|
||||
func ConfigGet[T any](e *Config, key string) T {
|
||||
val, ok := e.Get(key)
|
||||
if !ok {
|
||||
|
|
@ -86,34 +84,32 @@ func ConfigGet[T any](e *Config, key string) T {
|
|||
|
||||
// --- Feature Flags ---
|
||||
|
||||
// Enable enables a feature flag.
|
||||
func (e *Config) Enable(feature string) {
|
||||
e.mu.Lock()
|
||||
e.features[feature] = true
|
||||
e.ConfigOpts.init()
|
||||
e.Features[feature] = true
|
||||
e.mu.Unlock()
|
||||
}
|
||||
|
||||
// Disable disables a feature flag.
|
||||
func (e *Config) Disable(feature string) {
|
||||
e.mu.Lock()
|
||||
e.features[feature] = false
|
||||
e.ConfigOpts.init()
|
||||
e.Features[feature] = false
|
||||
e.mu.Unlock()
|
||||
}
|
||||
|
||||
// Enabled returns true if the feature is enabled.
|
||||
func (e *Config) Enabled(feature string) bool {
|
||||
e.mu.RLock()
|
||||
v := e.features[feature]
|
||||
v := e.Features[feature]
|
||||
e.mu.RUnlock()
|
||||
return v
|
||||
}
|
||||
|
||||
// Features returns all enabled feature names.
|
||||
func (e *Config) EnabledFeatures() []string {
|
||||
e.mu.RLock()
|
||||
defer e.mu.RUnlock()
|
||||
var result []string
|
||||
for k, v := range e.features {
|
||||
for k, v := range e.Features {
|
||||
if v {
|
||||
result = append(result, k)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import (
|
|||
"embed"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"strings"
|
||||
)
|
||||
|
|
@ -88,20 +89,18 @@ type ActionTaskCompleted struct {
|
|||
|
||||
// New creates a Core instance with the provided options.
|
||||
func New(opts ...Option) (*Core, error) {
|
||||
defaultFS, _ := NewIO("/")
|
||||
app := NewApp("", "", "")
|
||||
c := &Core{
|
||||
app: app,
|
||||
fs: defaultFS,
|
||||
cfg: &Config{settings: make(map[string]any), features: make(map[string]bool)},
|
||||
app: &App{},
|
||||
fs: &Fs{root: "/"},
|
||||
cfg: &Config{ConfigOpts: &ConfigOpts{}},
|
||||
err: &ErrPan{},
|
||||
log: &ErrLog{&ErrOpts{Log: defaultLog}},
|
||||
cli: NewCoreCli(app),
|
||||
srv: &Service{Services: make(map[string]any)},
|
||||
cli: &Cli{opts: &CliOpts{}},
|
||||
srv: &Service{},
|
||||
lock: &Lock{},
|
||||
ipc: &Ipc{},
|
||||
i18n: &I18n{},
|
||||
}
|
||||
c.ipc = &Ipc{core: c}
|
||||
|
||||
for _, o := range opts {
|
||||
if err := o(c); err != nil {
|
||||
|
|
@ -109,7 +108,6 @@ func New(opts ...Option) (*Core, error) {
|
|||
}
|
||||
}
|
||||
|
||||
c.LockApply()
|
||||
return c, nil
|
||||
}
|
||||
|
||||
|
|
@ -193,11 +191,14 @@ func WithAssets(efs embed.FS) Option {
|
|||
// WithIO sandboxes filesystem I/O to a root path.
|
||||
func WithIO(root string) Option {
|
||||
return func(c *Core) error {
|
||||
io, err := NewIO(root)
|
||||
abs, err := filepath.Abs(root)
|
||||
if err != nil {
|
||||
return E("core.WithIO", "failed to create IO at "+root, err)
|
||||
}
|
||||
c.fs = io
|
||||
if resolved, err := filepath.EvalSymlinks(abs); err == nil {
|
||||
abs = resolved
|
||||
}
|
||||
c.fs = &Fs{root: abs}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
|
@ -218,6 +219,7 @@ func WithMount(fsys fs.FS, basedir string) Option {
|
|||
func WithServiceLock() Option {
|
||||
return func(c *Core) error {
|
||||
c.LockEnable()
|
||||
c.LockApply()
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,9 +15,9 @@ import (
|
|||
// Core is the central application object that manages services, assets, and communication.
|
||||
type Core struct {
|
||||
app *App // c.App() — Application identity + optional GUI runtime
|
||||
emb *Embed // c.Embed() — Mounted embedded assets (read-only)
|
||||
emb *Embed // c.Embed() — Mounted embedded assets (read-only)
|
||||
fs *Fs // c.Fs() — Local filesystem I/O (sandboxable)
|
||||
cfg *Config // c.Config() — Configuration, settings, feature flags
|
||||
cfg *Config // c.Config() — Configuration, settings, feature flags
|
||||
err *ErrPan // c.Error() — Panic recovery and crash reporting
|
||||
log *ErrLog // c.Log() — Structured logging + error wrapping
|
||||
cli *Cli // c.Cli() — CLI command framework
|
||||
|
|
@ -34,9 +34,9 @@ type Core struct {
|
|||
// --- Accessors ---
|
||||
|
||||
func (c *Core) App() *App { return c.app }
|
||||
func (c *Core) Embed() *Embed { return c.emb }
|
||||
func (c *Core) Embed() *Embed { return c.emb }
|
||||
func (c *Core) Fs() *Fs { return c.fs }
|
||||
func (c *Core) Config() *Config { return c.cfg }
|
||||
func (c *Core) Config() *Config { return c.cfg }
|
||||
func (c *Core) Error() *ErrPan { return c.err }
|
||||
func (c *Core) Log() *ErrLog { return c.log }
|
||||
func (c *Core) Cli() *Cli { return c.cli }
|
||||
|
|
@ -44,16 +44,14 @@ func (c *Core) IPC() *Ipc { return c.ipc }
|
|||
func (c *Core) I18n() *I18n { return c.i18n }
|
||||
func (c *Core) Core() *Core { return c }
|
||||
|
||||
// --- IPC ---
|
||||
// --- IPC (uppercase aliases) ---
|
||||
|
||||
func (c *Core) ACTION(msg Message) error { return c.ipc.Action(msg) }
|
||||
func (c *Core) RegisterAction(handler func(*Core, Message) error) { c.ipc.RegisterAction(handler) }
|
||||
func (c *Core) RegisterActions(handlers ...func(*Core, Message) error) { c.ipc.RegisterActions(handlers...) }
|
||||
func (c *Core) QUERY(q Query) (any, bool, error) { return c.ipc.Query(q) }
|
||||
func (c *Core) QUERYALL(q Query) ([]any, error) { return c.ipc.QueryAll(q) }
|
||||
func (c *Core) PERFORM(t Task) (any, bool, error) { return c.ipc.Perform(t) }
|
||||
func (c *Core) RegisterQuery(handler QueryHandler) { c.ipc.RegisterQuery(handler) }
|
||||
func (c *Core) RegisterTask(handler TaskHandler) { c.ipc.RegisterTask(handler) }
|
||||
func (c *Core) ACTION(msg Message) error { return c.Action(msg) }
|
||||
func (c *Core) RegisterAction(handler func(*Core, Message) error) { c.RegisterAction(handler) }
|
||||
func (c *Core) RegisterActions(handlers ...func(*Core, Message) error) { c.RegisterActions(handlers...) }
|
||||
func (c *Core) QUERY(q Query) (any, bool, error) { return c.Query(q) }
|
||||
func (c *Core) QUERYALL(q Query) ([]any, error) { return c.QueryAll(q) }
|
||||
func (c *Core) PERFORM(t Task) (any, bool, error) { return c.Perform(t) }
|
||||
|
||||
// --- Error+Log ---
|
||||
|
||||
|
|
@ -73,4 +71,3 @@ func (c *Core) Must(err error, op, msg string) {
|
|||
}
|
||||
|
||||
// --- Global Instance ---
|
||||
|
||||
|
|
|
|||
|
|
@ -17,22 +17,6 @@ type Fs struct {
|
|||
root string
|
||||
}
|
||||
|
||||
// NewIO creates a Fs rooted at the given directory.
|
||||
// Pass "/" for full filesystem access, or a specific path to sandbox.
|
||||
func NewIO(root string) (*Fs, error) {
|
||||
abs, err := filepath.Abs(root)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Resolve symlinks so sandbox checks compare like-for-like.
|
||||
// On macOS, /var is a symlink to /private/var — without this,
|
||||
// EvalSymlinks on child paths resolves to /private/var/... while
|
||||
// root stays /var/..., causing false sandbox escape detections.
|
||||
if resolved, err := filepath.EvalSymlinks(abs); err == nil {
|
||||
abs = resolved
|
||||
}
|
||||
return &Fs{root: abs}, nil
|
||||
}
|
||||
|
||||
// path sanitises and returns the full path.
|
||||
// Absolute paths are sandboxed under root (unless root is "/").
|
||||
|
|
|
|||
|
|
@ -47,10 +47,6 @@ type I18n struct {
|
|||
translator Translator // registered implementation (nil until set)
|
||||
}
|
||||
|
||||
// NewCoreI18n creates a new i18n manager.
|
||||
func NewCoreI18n() *I18n {
|
||||
return &I18n{}
|
||||
}
|
||||
|
||||
// AddLocales adds locale mounts (called during service registration).
|
||||
func (i *I18n) AddLocales(mounts ...*Embed) {
|
||||
|
|
|
|||
|
|
@ -12,10 +12,8 @@ import (
|
|||
"sync"
|
||||
)
|
||||
|
||||
// Ipc owns action, query, and task dispatch between services.
|
||||
// Ipc holds IPC dispatch data.
|
||||
type Ipc struct {
|
||||
core *Core
|
||||
|
||||
ipcMu sync.RWMutex
|
||||
ipcHandlers []func(*Core, Message) error
|
||||
|
||||
|
|
@ -26,48 +24,27 @@ type Ipc struct {
|
|||
taskHandlers []TaskHandler
|
||||
}
|
||||
|
||||
// NewBus creates an empty message bus bound to the given Core.
|
||||
func NewBus(c *Core) *Ipc {
|
||||
return &Ipc{core: c}
|
||||
}
|
||||
|
||||
// Action dispatches a message to all registered IPC handlers.
|
||||
func (b *Ipc) Action(msg Message) error {
|
||||
b.ipcMu.RLock()
|
||||
handlers := slices.Clone(b.ipcHandlers)
|
||||
b.ipcMu.RUnlock()
|
||||
func (c *Core) Action(msg Message) error {
|
||||
c.ipc.ipcMu.RLock()
|
||||
handlers := slices.Clone(c.ipc.ipcHandlers)
|
||||
c.ipc.ipcMu.RUnlock()
|
||||
|
||||
var agg error
|
||||
for _, h := range handlers {
|
||||
if err := h(b.core, msg); err != nil {
|
||||
if err := h(c, msg); err != nil {
|
||||
agg = errors.Join(agg, err)
|
||||
}
|
||||
}
|
||||
return agg
|
||||
}
|
||||
|
||||
// RegisterAction adds a single IPC handler.
|
||||
func (b *Ipc) RegisterAction(handler func(*Core, Message) error) {
|
||||
b.ipcMu.Lock()
|
||||
b.ipcHandlers = append(b.ipcHandlers, handler)
|
||||
b.ipcMu.Unlock()
|
||||
}
|
||||
|
||||
// RegisterActions adds multiple IPC handlers.
|
||||
func (b *Ipc) RegisterActions(handlers ...func(*Core, Message) error) {
|
||||
b.ipcMu.Lock()
|
||||
b.ipcHandlers = append(b.ipcHandlers, handlers...)
|
||||
b.ipcMu.Unlock()
|
||||
}
|
||||
|
||||
// Query dispatches a query to handlers until one responds.
|
||||
func (b *Ipc) Query(q Query) (any, bool, error) {
|
||||
b.queryMu.RLock()
|
||||
handlers := slices.Clone(b.queryHandlers)
|
||||
b.queryMu.RUnlock()
|
||||
func (c *Core) Query(q Query) (any, bool, error) {
|
||||
c.ipc.queryMu.RLock()
|
||||
handlers := slices.Clone(c.ipc.queryHandlers)
|
||||
c.ipc.queryMu.RUnlock()
|
||||
|
||||
for _, h := range handlers {
|
||||
result, handled, err := h(b.core, q)
|
||||
result, handled, err := h(c, q)
|
||||
if handled {
|
||||
return result, true, err
|
||||
}
|
||||
|
|
@ -75,16 +52,15 @@ func (b *Ipc) Query(q Query) (any, bool, error) {
|
|||
return nil, false, nil
|
||||
}
|
||||
|
||||
// QueryAll dispatches a query to all handlers and collects all responses.
|
||||
func (b *Ipc) QueryAll(q Query) ([]any, error) {
|
||||
b.queryMu.RLock()
|
||||
handlers := slices.Clone(b.queryHandlers)
|
||||
b.queryMu.RUnlock()
|
||||
func (c *Core) QueryAll(q Query) ([]any, error) {
|
||||
c.ipc.queryMu.RLock()
|
||||
handlers := slices.Clone(c.ipc.queryHandlers)
|
||||
c.ipc.queryMu.RUnlock()
|
||||
|
||||
var results []any
|
||||
var agg error
|
||||
for _, h := range handlers {
|
||||
result, handled, err := h(b.core, q)
|
||||
result, handled, err := h(c, q)
|
||||
if err != nil {
|
||||
agg = errors.Join(agg, err)
|
||||
}
|
||||
|
|
@ -95,31 +71,8 @@ func (b *Ipc) QueryAll(q Query) ([]any, error) {
|
|||
return results, agg
|
||||
}
|
||||
|
||||
// RegisterQuery adds a query handler.
|
||||
func (b *Ipc) RegisterQuery(handler QueryHandler) {
|
||||
b.queryMu.Lock()
|
||||
b.queryHandlers = append(b.queryHandlers, handler)
|
||||
b.queryMu.Unlock()
|
||||
}
|
||||
|
||||
// Perform dispatches a task to handlers until one executes it.
|
||||
func (b *Ipc) Perform(t Task) (any, bool, error) {
|
||||
b.taskMu.RLock()
|
||||
handlers := slices.Clone(b.taskHandlers)
|
||||
b.taskMu.RUnlock()
|
||||
|
||||
for _, h := range handlers {
|
||||
result, handled, err := h(b.core, t)
|
||||
if handled {
|
||||
return result, true, err
|
||||
}
|
||||
}
|
||||
return nil, false, nil
|
||||
}
|
||||
|
||||
// RegisterTask adds a task handler.
|
||||
func (b *Ipc) RegisterTask(handler TaskHandler) {
|
||||
b.taskMu.Lock()
|
||||
b.taskHandlers = append(b.taskHandlers, handler)
|
||||
b.taskMu.Unlock()
|
||||
func (c *Core) RegisterQuery(handler QueryHandler) {
|
||||
c.ipc.queryMu.Lock()
|
||||
c.ipc.queryHandlers = append(c.ipc.queryHandlers, handler)
|
||||
c.ipc.queryMu.Unlock()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,12 +17,6 @@ type Service struct {
|
|||
locked bool
|
||||
}
|
||||
|
||||
// NewService creates an empty service registry.
|
||||
func NewService() *Service {
|
||||
return &Service{
|
||||
Services: make(map[string]any),
|
||||
}
|
||||
}
|
||||
|
||||
// --- Core service methods ---
|
||||
|
||||
|
|
@ -58,6 +52,9 @@ func (c *Core) Service(args ...any) any {
|
|||
return E("core.Service", fmt.Sprintf("service %q already registered", name), nil)
|
||||
}
|
||||
svc := args[1]
|
||||
if c.srv.Services == nil {
|
||||
c.srv.Services = make(map[string]any)
|
||||
}
|
||||
c.srv.Services[name] = svc
|
||||
if st, ok := svc.(Startable); ok {
|
||||
c.srv.startables = append(c.srv.startables, st)
|
||||
|
|
|
|||
|
|
@ -4,7 +4,10 @@
|
|||
|
||||
package core
|
||||
|
||||
import "fmt"
|
||||
import (
|
||||
"fmt"
|
||||
"slices"
|
||||
)
|
||||
|
||||
// TaskState holds background task state.
|
||||
type TaskState struct {
|
||||
|
|
@ -38,3 +41,23 @@ func (c *Core) PerformAsync(t Task) string {
|
|||
func (c *Core) Progress(taskID string, progress float64, message string, t Task) {
|
||||
_ = c.ACTION(ActionTaskProgress{TaskID: taskID, Task: t, Progress: progress, Message: message})
|
||||
}
|
||||
|
||||
func (c *Core) Perform(t Task) (any, bool, error) {
|
||||
c.ipc.taskMu.RLock()
|
||||
handlers := slices.Clone(c.ipc.taskHandlers)
|
||||
c.ipc.taskMu.RUnlock()
|
||||
|
||||
for _, h := range handlers {
|
||||
result, handled, err := h(c, t)
|
||||
if handled {
|
||||
return result, true, err
|
||||
}
|
||||
}
|
||||
return nil, false, nil
|
||||
}
|
||||
|
||||
func (c *Core) RegisterTask(handler TaskHandler) {
|
||||
c.ipc.taskMu.Lock()
|
||||
c.ipc.taskHandlers = append(c.ipc.taskHandlers, handler)
|
||||
c.ipc.taskMu.Unlock()
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue