feat: IPC, task, lifecycle all return Result
Action, Query, QueryAll, Perform → Result QueryHandler, TaskHandler → func returning Result RegisterAction/RegisterActions → handler returns Result ServiceStartup, ServiceShutdown → Result LogError, LogWarn → Result ACTION, QUERY, QUERYALL, PERFORM aliases → Result Tests updated to match new signatures. Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
parent
f5611b1002
commit
94f2e54abe
9 changed files with 119 additions and 112 deletions
|
|
@ -24,11 +24,11 @@ type TaskWithID interface {
|
|||
GetTaskID() string
|
||||
}
|
||||
|
||||
// QueryHandler handles Query requests. Returns (result, handled, error).
|
||||
type QueryHandler func(*Core, Query) (any, bool, error)
|
||||
// QueryHandler handles Query requests. Returns Result{Value, OK}.
|
||||
type QueryHandler func(*Core, Query) Result
|
||||
|
||||
// TaskHandler handles Task requests. Returns (result, handled, error).
|
||||
type TaskHandler func(*Core, Task) (any, bool, error)
|
||||
// TaskHandler handles Task requests. Returns Result{Value, OK}.
|
||||
type TaskHandler func(*Core, Task) Result
|
||||
|
||||
// Startable is implemented by services that need startup initialisation.
|
||||
type Startable interface {
|
||||
|
|
|
|||
|
|
@ -52,21 +52,29 @@ func (c *Core) Core() *Core { return c }
|
|||
|
||||
// --- IPC (uppercase aliases) ---
|
||||
|
||||
func (c *Core) ACTION(msg Message) error { return c.Action(msg) }
|
||||
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) }
|
||||
func (c *Core) ACTION(msg Message) Result { return c.Action(msg) }
|
||||
func (c *Core) QUERY(q Query) Result { return c.Query(q) }
|
||||
func (c *Core) QUERYALL(q Query) Result { return c.QueryAll(q) }
|
||||
func (c *Core) PERFORM(t Task) Result { return c.Perform(t) }
|
||||
|
||||
// --- Error+Log ---
|
||||
|
||||
// LogError logs an error and returns a wrapped error.
|
||||
func (c *Core) LogError(err error, op, msg string) error {
|
||||
return c.log.Error(err, op, msg)
|
||||
// LogError logs an error and returns a Result with the wrapped error.
|
||||
func (c *Core) LogError(err error, op, msg string) Result {
|
||||
wrapped := c.log.Error(err, op, msg)
|
||||
if wrapped == nil {
|
||||
return Result{OK: true}
|
||||
}
|
||||
return Result{Value: wrapped}
|
||||
}
|
||||
|
||||
// LogWarn logs a warning and returns a wrapped error.
|
||||
func (c *Core) LogWarn(err error, op, msg string) error {
|
||||
return c.log.Warn(err, op, msg)
|
||||
// LogWarn logs a warning and returns a Result with the wrapped error.
|
||||
func (c *Core) LogWarn(err error, op, msg string) Result {
|
||||
wrapped := c.log.Warn(err, op, msg)
|
||||
if wrapped == nil {
|
||||
return Result{OK: true}
|
||||
}
|
||||
return Result{Value: wrapped}
|
||||
}
|
||||
|
||||
// Must logs and panics if err is not nil.
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@
|
|||
package core
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"slices"
|
||||
"sync"
|
||||
)
|
||||
|
|
@ -15,7 +14,7 @@ import (
|
|||
// Ipc holds IPC dispatch data.
|
||||
type Ipc struct {
|
||||
ipcMu sync.RWMutex
|
||||
ipcHandlers []func(*Core, Message) error
|
||||
ipcHandlers []func(*Core, Message) Result
|
||||
|
||||
queryMu sync.RWMutex
|
||||
queryHandlers []QueryHandler
|
||||
|
|
@ -24,51 +23,46 @@ type Ipc struct {
|
|||
taskHandlers []TaskHandler
|
||||
}
|
||||
|
||||
func (c *Core) Action(msg Message) error {
|
||||
func (c *Core) Action(msg Message) Result {
|
||||
c.ipc.ipcMu.RLock()
|
||||
handlers := slices.Clone(c.ipc.ipcHandlers)
|
||||
c.ipc.ipcMu.RUnlock()
|
||||
|
||||
var agg error
|
||||
for _, h := range handlers {
|
||||
if err := h(c, msg); err != nil {
|
||||
agg = errors.Join(agg, err)
|
||||
if r := h(c, msg); !r.OK {
|
||||
return r
|
||||
}
|
||||
}
|
||||
return agg
|
||||
return Result{OK: true}
|
||||
}
|
||||
|
||||
func (c *Core) Query(q Query) (any, bool, error) {
|
||||
func (c *Core) Query(q Query) Result {
|
||||
c.ipc.queryMu.RLock()
|
||||
handlers := slices.Clone(c.ipc.queryHandlers)
|
||||
c.ipc.queryMu.RUnlock()
|
||||
|
||||
for _, h := range handlers {
|
||||
result, handled, err := h(c, q)
|
||||
if handled {
|
||||
return result, true, err
|
||||
r := h(c, q)
|
||||
if r.OK {
|
||||
return r
|
||||
}
|
||||
}
|
||||
return nil, false, nil
|
||||
return Result{}
|
||||
}
|
||||
|
||||
func (c *Core) QueryAll(q Query) ([]any, error) {
|
||||
func (c *Core) QueryAll(q Query) Result {
|
||||
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(c, q)
|
||||
if err != nil {
|
||||
agg = errors.Join(agg, err)
|
||||
}
|
||||
if handled && result != nil {
|
||||
results = append(results, result)
|
||||
r := h(c, q)
|
||||
if r.OK && r.Value != nil {
|
||||
results = append(results, r.Value)
|
||||
}
|
||||
}
|
||||
return results, agg
|
||||
return Result{Value: results, OK: true}
|
||||
}
|
||||
|
||||
func (c *Core) RegisterQuery(handler QueryHandler) {
|
||||
|
|
|
|||
|
|
@ -32,29 +32,27 @@ func (r *ServiceRuntime[T]) Config() *Config { return r.core.Config() }
|
|||
// --- Lifecycle ---
|
||||
|
||||
// ServiceStartup runs OnStart for all registered services that have one.
|
||||
func (c *Core) ServiceStartup(ctx context.Context, options any) error {
|
||||
func (c *Core) ServiceStartup(ctx context.Context, options any) Result {
|
||||
for _, s := range c.Startables() {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return err
|
||||
return Result{Value: err}
|
||||
}
|
||||
r := s.OnStart()
|
||||
if !r.OK {
|
||||
if err, ok := r.Value.(error); ok {
|
||||
return err
|
||||
}
|
||||
return r
|
||||
}
|
||||
}
|
||||
_ = c.ACTION(ActionServiceStartup{})
|
||||
return nil
|
||||
c.ACTION(ActionServiceStartup{})
|
||||
return Result{OK: true}
|
||||
}
|
||||
|
||||
// ServiceShutdown runs OnStop for all registered services that have one.
|
||||
func (c *Core) ServiceShutdown(ctx context.Context) error {
|
||||
func (c *Core) ServiceShutdown(ctx context.Context) Result {
|
||||
c.shutdown.Store(true)
|
||||
_ = c.ACTION(ActionServiceShutdown{})
|
||||
c.ACTION(ActionServiceShutdown{})
|
||||
for _, s := range c.Stoppables() {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return err
|
||||
return Result{Value: err}
|
||||
}
|
||||
s.OnStop()
|
||||
}
|
||||
|
|
@ -66,9 +64,9 @@ func (c *Core) ServiceShutdown(ctx context.Context) error {
|
|||
select {
|
||||
case <-done:
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
return Result{Value: ctx.Err()}
|
||||
}
|
||||
return nil
|
||||
return Result{OK: true}
|
||||
}
|
||||
|
||||
// --- Runtime DTO (GUI binding) ---
|
||||
|
|
@ -110,12 +108,12 @@ func NewRuntime(app any) Result {
|
|||
}
|
||||
|
||||
func (r *Runtime) ServiceName() string { return "Core" }
|
||||
func (r *Runtime) ServiceStartup(ctx context.Context, options any) error {
|
||||
func (r *Runtime) ServiceStartup(ctx context.Context, options any) Result {
|
||||
return r.Core.ServiceStartup(ctx, options)
|
||||
}
|
||||
func (r *Runtime) ServiceShutdown(ctx context.Context) error {
|
||||
func (r *Runtime) ServiceShutdown(ctx context.Context) Result {
|
||||
if r.Core != nil {
|
||||
return r.Core.ServiceShutdown(ctx)
|
||||
}
|
||||
return nil
|
||||
return Result{OK: true}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,43 +27,48 @@ func (c *Core) PerformAsync(t Task) string {
|
|||
if tid, ok := t.(TaskWithID); ok {
|
||||
tid.SetTaskID(taskID)
|
||||
}
|
||||
_ = c.ACTION(ActionTaskStarted{TaskID: taskID, Task: t})
|
||||
c.ACTION(ActionTaskStarted{TaskID: taskID, Task: t})
|
||||
c.wg.Go(func() {
|
||||
result, handled, err := c.PERFORM(t)
|
||||
if !handled && err == nil {
|
||||
err = E("core.PerformAsync", Join(" ", "no handler found for task type", reflect.TypeOf(t).String()), nil)
|
||||
r := c.PERFORM(t)
|
||||
var err error
|
||||
if !r.OK {
|
||||
if e, ok := r.Value.(error); ok {
|
||||
err = e
|
||||
} else {
|
||||
err = E("core.PerformAsync", Join(" ", "no handler found for task type", reflect.TypeOf(t).String()), nil)
|
||||
}
|
||||
}
|
||||
_ = c.ACTION(ActionTaskCompleted{TaskID: taskID, Task: t, Result: result, Error: err})
|
||||
c.ACTION(ActionTaskCompleted{TaskID: taskID, Task: t, Result: r.Value, Error: err})
|
||||
})
|
||||
return taskID
|
||||
}
|
||||
|
||||
// Progress broadcasts a progress update for a background task.
|
||||
func (c *Core) Progress(taskID string, progress float64, message string, t Task) {
|
||||
_ = c.ACTION(ActionTaskProgress{TaskID: taskID, Task: t, Progress: progress, Message: message})
|
||||
c.ACTION(ActionTaskProgress{TaskID: taskID, Task: t, Progress: progress, Message: message})
|
||||
}
|
||||
|
||||
func (c *Core) Perform(t Task) (any, bool, error) {
|
||||
func (c *Core) Perform(t Task) Result {
|
||||
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
|
||||
r := h(c, t)
|
||||
if r.OK {
|
||||
return r
|
||||
}
|
||||
}
|
||||
return nil, false, nil
|
||||
return Result{}
|
||||
}
|
||||
|
||||
func (c *Core) RegisterAction(handler func(*Core, Message) error) {
|
||||
func (c *Core) RegisterAction(handler func(*Core, Message) Result) {
|
||||
c.ipc.ipcMu.Lock()
|
||||
c.ipc.ipcHandlers = append(c.ipc.ipcHandlers, handler)
|
||||
c.ipc.ipcMu.Unlock()
|
||||
}
|
||||
|
||||
func (c *Core) RegisterActions(handlers ...func(*Core, Message) error) {
|
||||
func (c *Core) RegisterActions(handlers ...func(*Core, Message) Result) {
|
||||
c.ipc.ipcMu.Lock()
|
||||
c.ipc.ipcHandlers = append(c.ipc.ipcHandlers, handlers...)
|
||||
c.ipc.ipcMu.Unlock()
|
||||
|
|
|
|||
|
|
@ -67,15 +67,19 @@ func TestOptions_Accessor_Nil(t *testing.T) {
|
|||
func TestCore_LogError_Good(t *testing.T) {
|
||||
c := New()
|
||||
cause := assert.AnError
|
||||
err := c.LogError(cause, "test.Op", "something broke")
|
||||
assert.Error(t, err)
|
||||
r := c.LogError(cause, "test.Op", "something broke")
|
||||
assert.False(t, r.OK)
|
||||
err, ok := r.Value.(error)
|
||||
assert.True(t, ok)
|
||||
assert.ErrorIs(t, err, cause)
|
||||
}
|
||||
|
||||
func TestCore_LogWarn_Good(t *testing.T) {
|
||||
c := New()
|
||||
err := c.LogWarn(assert.AnError, "test.Op", "heads up")
|
||||
assert.Error(t, err)
|
||||
r := c.LogWarn(assert.AnError, "test.Op", "heads up")
|
||||
assert.False(t, r.OK)
|
||||
_, ok := r.Value.(error)
|
||||
assert.True(t, ok)
|
||||
}
|
||||
|
||||
func TestCore_Must_Ugly(t *testing.T) {
|
||||
|
|
|
|||
|
|
@ -14,67 +14,66 @@ type testMessage struct{ payload string }
|
|||
func TestAction_Good(t *testing.T) {
|
||||
c := New()
|
||||
var received Message
|
||||
c.RegisterAction(func(_ *Core, msg Message) error {
|
||||
c.RegisterAction(func(_ *Core, msg Message) Result {
|
||||
received = msg
|
||||
return nil
|
||||
return Result{OK: true}
|
||||
})
|
||||
err := c.ACTION(testMessage{payload: "hello"})
|
||||
assert.NoError(t, err)
|
||||
r := c.ACTION(testMessage{payload: "hello"})
|
||||
assert.True(t, r.OK)
|
||||
assert.Equal(t, testMessage{payload: "hello"}, received)
|
||||
}
|
||||
|
||||
func TestAction_Multiple_Good(t *testing.T) {
|
||||
c := New()
|
||||
count := 0
|
||||
handler := func(_ *Core, _ Message) error { count++; return nil }
|
||||
handler := func(_ *Core, _ Message) Result { count++; return Result{OK: true} }
|
||||
c.RegisterActions(handler, handler, handler)
|
||||
_ = c.ACTION(nil)
|
||||
c.ACTION(nil)
|
||||
assert.Equal(t, 3, count)
|
||||
}
|
||||
|
||||
func TestAction_None_Good(t *testing.T) {
|
||||
c := New()
|
||||
// No handlers registered — should not error
|
||||
err := c.ACTION(nil)
|
||||
assert.NoError(t, err)
|
||||
// No handlers registered — should succeed
|
||||
r := c.ACTION(nil)
|
||||
assert.True(t, r.OK)
|
||||
}
|
||||
|
||||
// --- IPC: Queries ---
|
||||
|
||||
func TestQuery_Good(t *testing.T) {
|
||||
c := New()
|
||||
c.RegisterQuery(func(_ *Core, q Query) (any, bool, error) {
|
||||
c.RegisterQuery(func(_ *Core, q Query) Result {
|
||||
if q == "ping" {
|
||||
return "pong", true, nil
|
||||
return Result{Value: "pong", OK: true}
|
||||
}
|
||||
return nil, false, nil
|
||||
return Result{}
|
||||
})
|
||||
result, handled, err := c.QUERY("ping")
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, handled)
|
||||
assert.Equal(t, "pong", result)
|
||||
r := c.QUERY("ping")
|
||||
assert.True(t, r.OK)
|
||||
assert.Equal(t, "pong", r.Value)
|
||||
}
|
||||
|
||||
func TestQuery_Unhandled_Good(t *testing.T) {
|
||||
c := New()
|
||||
c.RegisterQuery(func(_ *Core, q Query) (any, bool, error) {
|
||||
return nil, false, nil
|
||||
c.RegisterQuery(func(_ *Core, q Query) Result {
|
||||
return Result{}
|
||||
})
|
||||
_, handled, err := c.QUERY("unknown")
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, handled)
|
||||
r := c.QUERY("unknown")
|
||||
assert.False(t, r.OK)
|
||||
}
|
||||
|
||||
func TestQueryAll_Good(t *testing.T) {
|
||||
c := New()
|
||||
c.RegisterQuery(func(_ *Core, _ Query) (any, bool, error) {
|
||||
return "a", true, nil
|
||||
c.RegisterQuery(func(_ *Core, _ Query) Result {
|
||||
return Result{Value: "a", OK: true}
|
||||
})
|
||||
c.RegisterQuery(func(_ *Core, _ Query) (any, bool, error) {
|
||||
return "b", true, nil
|
||||
c.RegisterQuery(func(_ *Core, _ Query) Result {
|
||||
return Result{Value: "b", OK: true}
|
||||
})
|
||||
results, err := c.QUERYALL("anything")
|
||||
assert.NoError(t, err)
|
||||
r := c.QUERYALL("anything")
|
||||
assert.True(t, r.OK)
|
||||
results := r.Value.([]any)
|
||||
assert.Len(t, results, 2)
|
||||
assert.Contains(t, results, "a")
|
||||
assert.Contains(t, results, "b")
|
||||
|
|
@ -84,14 +83,13 @@ func TestQueryAll_Good(t *testing.T) {
|
|||
|
||||
func TestPerform_Good(t *testing.T) {
|
||||
c := New()
|
||||
c.RegisterTask(func(_ *Core, t Task) (any, bool, error) {
|
||||
c.RegisterTask(func(_ *Core, t Task) Result {
|
||||
if t == "compute" {
|
||||
return 42, true, nil
|
||||
return Result{Value: 42, OK: true}
|
||||
}
|
||||
return nil, false, nil
|
||||
return Result{}
|
||||
})
|
||||
result, handled, err := c.PERFORM("compute")
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, handled)
|
||||
assert.Equal(t, 42, result)
|
||||
r := c.PERFORM("compute")
|
||||
assert.True(t, r.OK)
|
||||
assert.Equal(t, 42, r.Value)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -70,7 +70,7 @@ func TestRuntime_Lifecycle_Good(t *testing.T) {
|
|||
assert.True(t, r.OK)
|
||||
rt := r.Value.(*Runtime)
|
||||
|
||||
err := rt.ServiceStartup(context.Background(), nil)
|
||||
assert.NoError(t, err)
|
||||
result := rt.ServiceStartup(context.Background(), nil)
|
||||
assert.True(t, result.OK)
|
||||
assert.True(t, started)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,11 +16,11 @@ func TestPerformAsync_Good(t *testing.T) {
|
|||
var mu sync.Mutex
|
||||
var result string
|
||||
|
||||
c.RegisterTask(func(_ *Core, task Task) (any, bool, error) {
|
||||
c.RegisterTask(func(_ *Core, task Task) Result {
|
||||
mu.Lock()
|
||||
result = "done"
|
||||
mu.Unlock()
|
||||
return "completed", true, nil
|
||||
return Result{Value: "completed", OK: true}
|
||||
})
|
||||
|
||||
taskID := c.PerformAsync("work")
|
||||
|
|
@ -35,8 +35,8 @@ func TestPerformAsync_Good(t *testing.T) {
|
|||
|
||||
func TestPerformAsync_Progress_Good(t *testing.T) {
|
||||
c := New()
|
||||
c.RegisterTask(func(_ *Core, task Task) (any, bool, error) {
|
||||
return nil, true, nil
|
||||
c.RegisterTask(func(_ *Core, task Task) Result {
|
||||
return Result{OK: true}
|
||||
})
|
||||
|
||||
taskID := c.PerformAsync("work")
|
||||
|
|
@ -48,19 +48,19 @@ func TestPerformAsync_Progress_Good(t *testing.T) {
|
|||
func TestRegisterAction_Good(t *testing.T) {
|
||||
c := New()
|
||||
called := false
|
||||
c.RegisterAction(func(_ *Core, _ Message) error {
|
||||
c.RegisterAction(func(_ *Core, _ Message) Result {
|
||||
called = true
|
||||
return nil
|
||||
return Result{OK: true}
|
||||
})
|
||||
_ = c.Action(nil)
|
||||
c.Action(nil)
|
||||
assert.True(t, called)
|
||||
}
|
||||
|
||||
func TestRegisterActions_Good(t *testing.T) {
|
||||
c := New()
|
||||
count := 0
|
||||
h := func(_ *Core, _ Message) error { count++; return nil }
|
||||
h := func(_ *Core, _ Message) Result { count++; return Result{OK: true} }
|
||||
c.RegisterActions(h, h)
|
||||
_ = c.Action(nil)
|
||||
c.Action(nil)
|
||||
assert.Equal(t, 2, count)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue