fix: rewrite 4 stale docs — messaging, primitives, index, getting-started, testing
All PERFORM/RegisterTask/type Task any references replaced with named Action patterns. Every code example now uses the v0.8.0 API. - docs/messaging.md: full rewrite — ACTION/QUERY + named Actions + Task - docs/primitives.md: full rewrite — added Action, Task, Registry, Entitlement - docs/index.md: full rewrite — updated surface table, quick example, doc links - docs/getting-started.md: 2 RegisterTask+PERFORM blocks → Action pattern - docs/testing.md: 1 RegisterTask+PERFORM block → Action pattern An agent reading any doc file now gets compilable code. Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
parent
ba77e029c8
commit
1f0c618b7a
5 changed files with 230 additions and 331 deletions
|
|
@ -69,20 +69,15 @@ c.RegisterQuery(func(_ *core.Core, q core.Query) core.Result {
|
|||
return core.Result{}
|
||||
})
|
||||
|
||||
c.RegisterTask(func(_ *core.Core, t core.Task) core.Result {
|
||||
switch task := t.(type) {
|
||||
case createWorkspaceTask:
|
||||
path := "/tmp/agent-workbench/" + task.Name
|
||||
return core.Result{Value: path, OK: true}
|
||||
}
|
||||
return core.Result{}
|
||||
c.Action("workspace.create", func(_ context.Context, opts core.Options) core.Result {
|
||||
name := opts.String("name")
|
||||
path := "/tmp/agent-workbench/" + name
|
||||
return core.Result{Value: path, OK: true}
|
||||
})
|
||||
|
||||
c.Command("workspace/create", core.Command{
|
||||
Action: func(opts core.Options) core.Result {
|
||||
return c.PERFORM(createWorkspaceTask{
|
||||
Name: opts.String("name"),
|
||||
})
|
||||
return c.Action("workspace.create").Run(context.Background(), opts)
|
||||
},
|
||||
})
|
||||
```
|
||||
|
|
@ -170,20 +165,15 @@ func main() {
|
|||
return core.Result{}
|
||||
})
|
||||
|
||||
c.RegisterTask(func(_ *core.Core, t core.Task) core.Result {
|
||||
switch task := t.(type) {
|
||||
case createWorkspaceTask:
|
||||
path := c.Config().String("workspace.root") + "/" + task.Name
|
||||
return core.Result{Value: path, OK: true}
|
||||
}
|
||||
return core.Result{}
|
||||
c.Action("workspace.create", func(_ context.Context, opts core.Options) core.Result {
|
||||
name := opts.String("name")
|
||||
path := c.Config().String("workspace.root") + "/" + name
|
||||
return core.Result{Value: path, OK: true}
|
||||
})
|
||||
|
||||
c.Command("workspace/create", core.Command{
|
||||
Action: func(opts core.Options) core.Result {
|
||||
return c.PERFORM(createWorkspaceTask{
|
||||
Name: opts.String("name"),
|
||||
})
|
||||
return c.Action("workspace.create").Run(context.Background(), opts)
|
||||
},
|
||||
})
|
||||
|
||||
|
|
|
|||
126
docs/index.md
126
docs/index.md
|
|
@ -5,108 +5,56 @@ description: AX-first documentation for the CoreGO framework.
|
|||
|
||||
# CoreGO
|
||||
|
||||
CoreGO is the foundation layer for the Core ecosystem. It gives you one container, one command tree, one message bus, and a small set of shared primitives that repeat across the whole framework.
|
||||
CoreGO is the foundation layer for the Core ecosystem. Module: `dappco.re/go/core`.
|
||||
|
||||
The current module path is `dappco.re/go/core`.
|
||||
## What CoreGO Provides
|
||||
|
||||
## AX View
|
||||
|
||||
CoreGO already follows the main AX ideas from RFC-025:
|
||||
|
||||
- predictable names such as `Core`, `Service`, `Command`, `Options`, `Result`, `Message`
|
||||
- path-shaped command registration such as `deploy/to/homelab`
|
||||
- one repeated input shape (`Options`) and one repeated return shape (`Result`)
|
||||
- comments and examples that show real usage instead of restating the type signature
|
||||
|
||||
## What CoreGO Owns
|
||||
|
||||
| Surface | Purpose |
|
||||
|---------|---------|
|
||||
| `Core` | Central container and access point |
|
||||
| `Service` | Managed lifecycle component |
|
||||
| `Command` | Path-based command tree node |
|
||||
| `ACTION`, `QUERY`, `PERFORM` | Decoupled communication between components |
|
||||
| `Data`, `Drive`, `Fs`, `Config`, `I18n`, `Cli` | Built-in subsystems for common runtime work |
|
||||
| `E`, `Wrap`, `ErrorLog`, `ErrorPanic` | Structured failures and panic recovery |
|
||||
| Primitive | Purpose |
|
||||
|-----------|---------|
|
||||
| `Core` | Central container — everything registers here |
|
||||
| `Service` | Lifecycle-managed component (Startable/Stoppable return Result) |
|
||||
| `Action` | Named callable with panic recovery + entitlement |
|
||||
| `Task` | Composed sequence of Actions |
|
||||
| `Registry[T]` | Thread-safe named collection (universal brick) |
|
||||
| `Command` | Path-based CLI command tree |
|
||||
| `Process` | Managed execution (Action sugar over go-process) |
|
||||
| `API` | Remote streams (protocol handlers + Drive) |
|
||||
| `Entitlement` | Permission gate (default permissive, consumer replaces) |
|
||||
| `ACTION`, `QUERY` | Anonymous broadcast + request/response |
|
||||
| `Data`, `Drive`, `Fs`, `Config`, `I18n` | Built-in subsystems |
|
||||
|
||||
## Quick Example
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"dappco.re/go/core"
|
||||
)
|
||||
|
||||
type flushCacheTask struct {
|
||||
Name string
|
||||
}
|
||||
import "dappco.re/go/core"
|
||||
|
||||
func main() {
|
||||
c := core.New(core.Options{
|
||||
{Key: "name", Value: "agent-workbench"},
|
||||
})
|
||||
|
||||
c.Service("cache", core.Service{
|
||||
OnStart: func() core.Result {
|
||||
core.Info("cache ready", "app", c.App().Name)
|
||||
return core.Result{OK: true}
|
||||
},
|
||||
OnStop: func() core.Result {
|
||||
core.Info("cache stopped", "app", c.App().Name)
|
||||
return core.Result{OK: true}
|
||||
},
|
||||
})
|
||||
|
||||
c.RegisterTask(func(_ *core.Core, task core.Task) core.Result {
|
||||
switch task.(type) {
|
||||
case flushCacheTask:
|
||||
return core.Result{Value: "cache flushed", OK: true}
|
||||
}
|
||||
return core.Result{}
|
||||
})
|
||||
|
||||
c.Command("cache/flush", core.Command{
|
||||
Action: func(opts core.Options) core.Result {
|
||||
return c.PERFORM(flushCacheTask{Name: opts.String("name")})
|
||||
},
|
||||
})
|
||||
|
||||
if !c.ServiceStartup(context.Background(), nil).OK {
|
||||
panic("startup failed")
|
||||
}
|
||||
|
||||
r := c.Cli().Run("cache", "flush", "--name=session-store")
|
||||
fmt.Println(r.Value)
|
||||
|
||||
_ = c.ServiceShutdown(context.Background())
|
||||
c := core.New(
|
||||
core.WithOption("name", "agent-workbench"),
|
||||
core.WithService(cache.Register),
|
||||
core.WithServiceLock(),
|
||||
)
|
||||
c.Run()
|
||||
}
|
||||
```
|
||||
|
||||
## Documentation Paths
|
||||
## API Specification
|
||||
|
||||
The full contract is `docs/RFC.md` (21 sections, 1476 lines). An agent should be able to write a service from RFC.md alone.
|
||||
|
||||
## Documentation
|
||||
|
||||
| Path | Covers |
|
||||
|------|--------|
|
||||
| [getting-started.md](getting-started.md) | First runnable CoreGO app |
|
||||
| [primitives.md](primitives.md) | `Options`, `Result`, `Service`, `Message`, `Query`, `Task` |
|
||||
| [services.md](services.md) | Service registry, service locks, runtime helpers |
|
||||
| [commands.md](commands.md) | Path-based commands and CLI execution |
|
||||
| [messaging.md](messaging.md) | `ACTION`, `QUERY`, `QUERYALL`, `PERFORM`, `PerformAsync` |
|
||||
| [lifecycle.md](lifecycle.md) | Startup, shutdown, context, background task draining |
|
||||
| [configuration.md](configuration.md) | Constructor options, config state, feature flags |
|
||||
| [subsystems.md](subsystems.md) | `App`, `Data`, `Drive`, `Fs`, `I18n`, `Cli` |
|
||||
| [errors.md](errors.md) | Structured errors, logging helpers, panic recovery |
|
||||
| [testing.md](testing.md) | Test naming and framework-level testing patterns |
|
||||
| [pkg/core.md](pkg/core.md) | Package-level reference summary |
|
||||
| [pkg/log.md](pkg/log.md) | Logging reference for the root package |
|
||||
| [pkg/PACKAGE_STANDARDS.md](pkg/PACKAGE_STANDARDS.md) | AX package-authoring guidance |
|
||||
|
||||
## Good Reading Order
|
||||
|
||||
1. Start with [getting-started.md](getting-started.md).
|
||||
2. Learn the repeated shapes in [primitives.md](primitives.md).
|
||||
3. Pick the integration path you need next: [services.md](services.md), [commands.md](commands.md), or [messaging.md](messaging.md).
|
||||
4. Use [subsystems.md](subsystems.md), [errors.md](errors.md), and [testing.md](testing.md) as reference pages while building.
|
||||
| [RFC.md](RFC.md) | Authoritative API contract (21 sections) |
|
||||
| [primitives.md](primitives.md) | Option, Result, Action, Task, Registry, Entitlement |
|
||||
| [services.md](services.md) | Service registry, ServiceRuntime, service locks |
|
||||
| [commands.md](commands.md) | Path-based commands, Managed field |
|
||||
| [messaging.md](messaging.md) | ACTION, QUERY, named Actions, PerformAsync |
|
||||
| [lifecycle.md](lifecycle.md) | RunE, ServiceStartup, ServiceShutdown |
|
||||
| [subsystems.md](subsystems.md) | App, Data, Drive, Fs, Config, I18n |
|
||||
| [errors.md](errors.md) | core.E(), structured errors, panic recovery |
|
||||
| [testing.md](testing.md) | AX-7 TestFile_Function_{Good,Bad,Ugly} |
|
||||
| [configuration.md](configuration.md) | WithOption, WithService, WithServiceLock |
|
||||
|
|
|
|||
|
|
@ -1,171 +1,127 @@
|
|||
---
|
||||
title: Messaging
|
||||
description: ACTION, QUERY, QUERYALL, PERFORM, and async task flow.
|
||||
description: ACTION, QUERY, QUERYALL, named Actions, and async dispatch.
|
||||
---
|
||||
|
||||
# Messaging
|
||||
|
||||
CoreGO uses one message bus for broadcasts, lookups, and work dispatch.
|
||||
CoreGO has two messaging layers: anonymous broadcast (ACTION/QUERY) and named Actions.
|
||||
|
||||
## Message Types
|
||||
## Anonymous Broadcast
|
||||
|
||||
```go
|
||||
type Message any
|
||||
type Query any
|
||||
type Task any
|
||||
```
|
||||
### `ACTION`
|
||||
|
||||
Your own structs define the protocol.
|
||||
|
||||
```go
|
||||
type repositoryIndexed struct {
|
||||
Name string
|
||||
}
|
||||
|
||||
type repositoryCountQuery struct{}
|
||||
|
||||
type syncRepositoryTask struct {
|
||||
Name string
|
||||
}
|
||||
```
|
||||
|
||||
## `ACTION`
|
||||
|
||||
`ACTION` is a broadcast.
|
||||
Fire-and-forget broadcast to all registered handlers. Each handler is wrapped in panic recovery. Handler return values are ignored — all handlers fire regardless.
|
||||
|
||||
```go
|
||||
c.RegisterAction(func(_ *core.Core, msg core.Message) core.Result {
|
||||
switch m := msg.(type) {
|
||||
case repositoryIndexed:
|
||||
core.Info("repository indexed", "name", m.Name)
|
||||
return core.Result{OK: true}
|
||||
}
|
||||
return core.Result{OK: true}
|
||||
if ev, ok := msg.(repositoryIndexed); ok {
|
||||
core.Info("indexed", "name", ev.Name)
|
||||
}
|
||||
return core.Result{OK: true}
|
||||
})
|
||||
|
||||
r := c.ACTION(repositoryIndexed{Name: "core-go"})
|
||||
c.ACTION(repositoryIndexed{Name: "core-go"})
|
||||
```
|
||||
|
||||
### Behavior
|
||||
### `QUERY`
|
||||
|
||||
- all registered action handlers are called in their current registration order
|
||||
- if a handler returns `OK:false`, dispatch stops and that `Result` is returned
|
||||
- if no handler fails, `ACTION` returns `Result{OK:true}`
|
||||
|
||||
## `QUERY`
|
||||
|
||||
`QUERY` is first-match request-response.
|
||||
First handler to return `OK:true` wins.
|
||||
|
||||
```go
|
||||
c.RegisterQuery(func(_ *core.Core, q core.Query) core.Result {
|
||||
switch q.(type) {
|
||||
case repositoryCountQuery:
|
||||
return core.Result{Value: 42, OK: true}
|
||||
}
|
||||
return core.Result{}
|
||||
if _, ok := q.(repositoryCountQuery); ok {
|
||||
return core.Result{Value: 42, OK: true}
|
||||
}
|
||||
return core.Result{}
|
||||
})
|
||||
|
||||
r := c.QUERY(repositoryCountQuery{})
|
||||
```
|
||||
|
||||
### Behavior
|
||||
### `QUERYALL`
|
||||
|
||||
- handlers run until one returns `OK:true`
|
||||
- the first successful result wins
|
||||
- if nothing handles the query, CoreGO returns an empty `Result`
|
||||
|
||||
## `QUERYALL`
|
||||
|
||||
`QUERYALL` collects every successful non-nil response.
|
||||
Collects every successful non-nil response.
|
||||
|
||||
```go
|
||||
r := c.QUERYALL(repositoryCountQuery{})
|
||||
results := r.Value.([]any)
|
||||
```
|
||||
|
||||
### Behavior
|
||||
## Named Actions
|
||||
|
||||
- every query handler is called
|
||||
- only `OK:true` results with non-nil `Value` are collected
|
||||
- the call itself returns `OK:true` even when the result list is empty
|
||||
Named Actions are the typed, inspectable replacement for anonymous dispatch. See Section 18 of `RFC.md`.
|
||||
|
||||
## `PERFORM`
|
||||
|
||||
`PERFORM` dispatches a task to the first handler that accepts it.
|
||||
### Register and Invoke
|
||||
|
||||
```go
|
||||
c.RegisterTask(func(_ *core.Core, t core.Task) core.Result {
|
||||
switch task := t.(type) {
|
||||
case syncRepositoryTask:
|
||||
return core.Result{Value: "synced " + task.Name, OK: true}
|
||||
}
|
||||
return core.Result{}
|
||||
// Register during OnStartup
|
||||
c.Action("repo.sync", func(ctx context.Context, opts core.Options) core.Result {
|
||||
name := opts.String("name")
|
||||
return core.Result{Value: "synced " + name, OK: true}
|
||||
})
|
||||
|
||||
r := c.PERFORM(syncRepositoryTask{Name: "core-go"})
|
||||
// Invoke by name
|
||||
r := c.Action("repo.sync").Run(ctx, core.NewOptions(
|
||||
core.Option{Key: "name", Value: "core-go"},
|
||||
))
|
||||
```
|
||||
|
||||
### Behavior
|
||||
|
||||
- handlers run until one returns `OK:true`
|
||||
- the first successful result wins
|
||||
- if nothing handles the task, CoreGO returns an empty `Result`
|
||||
|
||||
## `PerformAsync`
|
||||
|
||||
`PerformAsync` runs a task in a background goroutine and returns a generated task identifier.
|
||||
### Capability Check
|
||||
|
||||
```go
|
||||
r := c.PerformAsync(syncRepositoryTask{Name: "core-go"})
|
||||
taskID := r.Value.(string)
|
||||
if c.Action("process.run").Exists() {
|
||||
// go-process is registered
|
||||
}
|
||||
|
||||
c.Actions() // []string of all registered action names
|
||||
```
|
||||
|
||||
### Generated Events
|
||||
### Permission Gate
|
||||
|
||||
Async execution emits three action messages:
|
||||
Every `Action.Run()` checks `c.Entitled(action.Name)` before executing. See Section 21 of `RFC.md`.
|
||||
|
||||
| Message | When |
|
||||
|---------|------|
|
||||
| `ActionTaskStarted` | just before background execution begins |
|
||||
| `ActionTaskProgress` | whenever `Progress` is called |
|
||||
| `ActionTaskCompleted` | after the task finishes or panics |
|
||||
## Task Composition
|
||||
|
||||
Example listener:
|
||||
A Task is a named sequence of Actions:
|
||||
|
||||
```go
|
||||
c.Task("deploy", core.Task{
|
||||
Steps: []core.Step{
|
||||
{Action: "go.build"},
|
||||
{Action: "go.test"},
|
||||
{Action: "docker.push"},
|
||||
{Action: "notify.slack", Async: true},
|
||||
},
|
||||
})
|
||||
|
||||
r := c.Task("deploy").Run(ctx, c, opts)
|
||||
```
|
||||
|
||||
Sequential steps stop on first failure. `Async: true` steps fire without blocking. `Input: "previous"` pipes output.
|
||||
|
||||
## Background Execution
|
||||
|
||||
```go
|
||||
r := c.PerformAsync("repo.sync", opts)
|
||||
taskID := r.Value.(string)
|
||||
|
||||
c.Progress(taskID, 0.5, "indexing commits", "repo.sync")
|
||||
```
|
||||
|
||||
Broadcasts `ActionTaskStarted`, `ActionTaskProgress`, `ActionTaskCompleted` as ACTION messages.
|
||||
|
||||
### Completion Listener
|
||||
|
||||
```go
|
||||
c.RegisterAction(func(_ *core.Core, msg core.Message) core.Result {
|
||||
switch m := msg.(type) {
|
||||
case core.ActionTaskCompleted:
|
||||
core.Info("task completed", "task", m.TaskIdentifier, "err", m.Error)
|
||||
}
|
||||
return core.Result{OK: true}
|
||||
if ev, ok := msg.(core.ActionTaskCompleted); ok {
|
||||
core.Info("done", "task", ev.TaskIdentifier, "ok", ev.Result.OK)
|
||||
}
|
||||
return core.Result{OK: true}
|
||||
})
|
||||
```
|
||||
|
||||
## Progress Updates
|
||||
## Shutdown
|
||||
|
||||
```go
|
||||
c.Progress(taskID, 0.5, "indexing commits", syncRepositoryTask{Name: "core-go"})
|
||||
```
|
||||
|
||||
That broadcasts `ActionTaskProgress`.
|
||||
|
||||
## `TaskWithIdentifier`
|
||||
|
||||
Tasks that implement `TaskWithIdentifier` receive the generated ID before dispatch.
|
||||
|
||||
```go
|
||||
type trackedTask struct {
|
||||
ID string
|
||||
Name string
|
||||
}
|
||||
|
||||
func (t *trackedTask) SetTaskIdentifier(id string) { t.ID = id }
|
||||
func (t *trackedTask) GetTaskIdentifier() string { return t.ID }
|
||||
```
|
||||
|
||||
## Shutdown Interaction
|
||||
|
||||
When shutdown has started, `PerformAsync` returns an empty `Result` instead of scheduling more work.
|
||||
|
||||
This is why `ServiceShutdown` can safely drain the outstanding background tasks before stopping services.
|
||||
When shutdown has started, `PerformAsync` returns an empty `Result`. `ServiceShutdown` drains outstanding background work before stopping services.
|
||||
|
|
|
|||
|
|
@ -5,165 +5,172 @@ description: The repeated shapes that make CoreGO easy to navigate.
|
|||
|
||||
# Core Primitives
|
||||
|
||||
CoreGO is easiest to use when you read it as a small vocabulary repeated everywhere. Most of the framework is built from the same handful of types.
|
||||
CoreGO is built from a small vocabulary repeated everywhere.
|
||||
|
||||
## Primitive Map
|
||||
|
||||
| Type | Used For |
|
||||
|------|----------|
|
||||
| `Options` | Input values and lightweight metadata |
|
||||
| `Option` / `Options` | Input values and metadata |
|
||||
| `Result` | Output values and success state |
|
||||
| `Service` | Lifecycle-managed components |
|
||||
| `Message` | Broadcast events |
|
||||
| `Query` | Request-response lookups |
|
||||
| `Task` | Side-effecting work items |
|
||||
| `Action` | Named callable with panic recovery + entitlement |
|
||||
| `Task` | Composed sequence of Actions |
|
||||
| `Registry[T]` | Thread-safe named collection |
|
||||
| `Entitlement` | Permission check result |
|
||||
| `Message` | Broadcast events (ACTION) |
|
||||
| `Query` | Request-response lookups (QUERY) |
|
||||
|
||||
## `Option` and `Options`
|
||||
|
||||
`Option` is one key-value pair. `Options` is an ordered slice of them.
|
||||
|
||||
```go
|
||||
opts := core.Options{
|
||||
{Key: "name", Value: "brain"},
|
||||
{Key: "path", Value: "prompts"},
|
||||
{Key: "debug", Value: true},
|
||||
}
|
||||
```
|
||||
opts := core.NewOptions(
|
||||
core.Option{Key: "name", Value: "brain"},
|
||||
core.Option{Key: "path", Value: "prompts"},
|
||||
core.Option{Key: "debug", Value: true},
|
||||
)
|
||||
|
||||
Use the helpers to read values:
|
||||
|
||||
```go
|
||||
name := opts.String("name")
|
||||
path := opts.String("path")
|
||||
debug := opts.Bool("debug")
|
||||
hasPath := opts.Has("path")
|
||||
raw := opts.Get("name")
|
||||
raw := opts.Get("name") // Result{Value, OK}
|
||||
opts.Has("path") // true
|
||||
opts.Len() // 3
|
||||
```
|
||||
|
||||
### Important Details
|
||||
|
||||
- `Get` returns the first matching key.
|
||||
- `String`, `Int`, and `Bool` do not convert between types.
|
||||
- Missing keys return zero values.
|
||||
- CLI flags with values are stored as strings, so `--port=8080` should be read with `opts.String("port")`, not `opts.Int("port")`.
|
||||
|
||||
## `Result`
|
||||
|
||||
`Result` is the universal return shape.
|
||||
Universal return shape. Every Core operation returns Result.
|
||||
|
||||
```go
|
||||
r := core.Result{Value: "ready", OK: true}
|
||||
type Result struct {
|
||||
Value any
|
||||
OK bool
|
||||
}
|
||||
|
||||
r := c.Config().Get("host")
|
||||
if r.OK {
|
||||
fmt.Println(r.Value)
|
||||
host := r.Value.(string)
|
||||
}
|
||||
```
|
||||
|
||||
It has two jobs:
|
||||
|
||||
- carry a value when work succeeds
|
||||
- carry either an error or an empty state when work does not succeed
|
||||
|
||||
### `Result.Result(...)`
|
||||
|
||||
The `Result()` method adapts plain Go values and `(value, error)` pairs into a `core.Result`.
|
||||
The `Result()` method adapts Go `(value, error)` pairs:
|
||||
|
||||
```go
|
||||
r1 := core.Result{}.Result("hello")
|
||||
r2 := core.Result{}.Result(file, err)
|
||||
r := core.Result{}.Result(file, err)
|
||||
```
|
||||
|
||||
This is how several built-in helpers bridge standard-library calls.
|
||||
|
||||
## `Service`
|
||||
|
||||
`Service` is the managed lifecycle DTO stored in the registry.
|
||||
Managed lifecycle component stored in the `ServiceRegistry`.
|
||||
|
||||
```go
|
||||
svc := core.Service{
|
||||
Name: "cache",
|
||||
Options: core.Options{
|
||||
{Key: "backend", Value: "memory"},
|
||||
},
|
||||
OnStart: func() core.Result {
|
||||
return core.Result{OK: true}
|
||||
},
|
||||
OnStop: func() core.Result {
|
||||
return core.Result{OK: true}
|
||||
},
|
||||
OnReload: func() core.Result {
|
||||
return core.Result{OK: true}
|
||||
},
|
||||
core.Service{
|
||||
OnStart: func() core.Result { return core.Result{OK: true} },
|
||||
OnStop: func() core.Result { return core.Result{OK: true} },
|
||||
}
|
||||
```
|
||||
|
||||
### Important Details
|
||||
|
||||
- `OnStart` and `OnStop` are used by the framework lifecycle.
|
||||
- `OnReload` is stored on the service DTO, but CoreGO does not currently call it automatically.
|
||||
- The registry stores `*core.Service`, not arbitrary typed service instances.
|
||||
|
||||
## `Message`, `Query`, and `Task`
|
||||
|
||||
These are simple aliases to `any`.
|
||||
Or via `Startable`/`Stoppable` interfaces (preferred for named services):
|
||||
|
||||
```go
|
||||
type Message any
|
||||
type Query any
|
||||
type Task any
|
||||
type Startable interface { OnStartup(ctx context.Context) Result }
|
||||
type Stoppable interface { OnShutdown(ctx context.Context) Result }
|
||||
```
|
||||
|
||||
That means your own structs become the protocol:
|
||||
## `Action`
|
||||
|
||||
Named callable — the atomic unit of work. Registered by name, invoked by name.
|
||||
|
||||
```go
|
||||
type deployStarted struct {
|
||||
Environment string
|
||||
}
|
||||
type ActionHandler func(context.Context, Options) Result
|
||||
|
||||
type workspaceCountQuery struct{}
|
||||
|
||||
type syncRepositoryTask struct {
|
||||
Name string
|
||||
type Action struct {
|
||||
Name string
|
||||
Handler ActionHandler
|
||||
Description string
|
||||
Schema Options
|
||||
}
|
||||
```
|
||||
|
||||
## `TaskWithIdentifier`
|
||||
`Action.Run()` includes panic recovery and entitlement checking.
|
||||
|
||||
Long-running tasks can opt into task identifiers.
|
||||
## `Task`
|
||||
|
||||
Composed sequence of Actions:
|
||||
|
||||
```go
|
||||
type indexedTask struct {
|
||||
ID string
|
||||
type Task struct {
|
||||
Name string
|
||||
Description string
|
||||
Steps []Step
|
||||
}
|
||||
|
||||
func (t *indexedTask) SetTaskIdentifier(id string) { t.ID = id }
|
||||
func (t *indexedTask) GetTaskIdentifier() string { return t.ID }
|
||||
type Step struct {
|
||||
Action string
|
||||
With Options
|
||||
Async bool
|
||||
Input string // "previous" = output of last step
|
||||
}
|
||||
```
|
||||
|
||||
If a task implements `TaskWithIdentifier`, `PerformAsync` injects the generated `task-N` identifier before dispatch.
|
||||
## `Registry[T]`
|
||||
|
||||
Thread-safe named collection with insertion order and 3 lock modes:
|
||||
|
||||
```go
|
||||
r := core.NewRegistry[*MyService]()
|
||||
r.Set("brain", svc)
|
||||
r.Get("brain") // Result
|
||||
r.Has("brain") // bool
|
||||
r.Names() // []string (insertion order)
|
||||
r.Each(func(name string, svc *MyService) { ... })
|
||||
r.Lock() // fully frozen
|
||||
r.Seal() // no new keys, updates OK
|
||||
```
|
||||
|
||||
## `Entitlement`
|
||||
|
||||
Permission check result:
|
||||
|
||||
```go
|
||||
type Entitlement struct {
|
||||
Allowed bool
|
||||
Unlimited bool
|
||||
Limit int
|
||||
Used int
|
||||
Remaining int
|
||||
Reason string
|
||||
}
|
||||
|
||||
e := c.Entitled("social.accounts", 3)
|
||||
e.NearLimit(0.8) // true if > 80% used
|
||||
e.UsagePercent() // 75.0
|
||||
```
|
||||
|
||||
## `Message` and `Query`
|
||||
|
||||
IPC type aliases for the anonymous broadcast system:
|
||||
|
||||
```go
|
||||
type Message any // broadcast via ACTION
|
||||
type Query any // request/response via QUERY
|
||||
```
|
||||
|
||||
For typed, named dispatch use `c.Action("name").Run(ctx, opts)`.
|
||||
|
||||
## `ServiceRuntime[T]`
|
||||
|
||||
`ServiceRuntime[T]` is the small helper for packages that want to keep a Core reference and a typed options struct together.
|
||||
Composition helper for services that need Core access and typed options:
|
||||
|
||||
```go
|
||||
type agentServiceOptions struct {
|
||||
WorkspacePath string
|
||||
type MyService struct {
|
||||
*core.ServiceRuntime[MyOptions]
|
||||
}
|
||||
|
||||
type agentService struct {
|
||||
*core.ServiceRuntime[agentServiceOptions]
|
||||
}
|
||||
|
||||
runtime := core.NewServiceRuntime(c, agentServiceOptions{
|
||||
WorkspacePath: "/srv/agent-workspaces",
|
||||
})
|
||||
runtime := core.NewServiceRuntime(c, MyOptions{BufferSize: 1024})
|
||||
runtime.Core() // *Core
|
||||
runtime.Options() // MyOptions
|
||||
runtime.Config() // shortcut to Core().Config()
|
||||
```
|
||||
|
||||
It exposes:
|
||||
|
||||
- `Core()`
|
||||
- `Options()`
|
||||
- `Config()`
|
||||
|
||||
This helper does not register anything by itself. It is a composition aid for package authors.
|
||||
|
|
|
|||
|
|
@ -78,14 +78,12 @@ assert.Equal(t, "pong", c.QUERY("ping").Value)
|
|||
```
|
||||
|
||||
```go
|
||||
c.RegisterTask(func(_ *core.Core, t core.Task) core.Result {
|
||||
if t == "compute" {
|
||||
return core.Result{Value: 42, OK: true}
|
||||
}
|
||||
return core.Result{}
|
||||
c.Action("compute", func(_ context.Context, _ core.Options) core.Result {
|
||||
return core.Result{Value: 42, OK: true}
|
||||
})
|
||||
|
||||
assert.Equal(t, 42, c.PERFORM("compute").Value)
|
||||
r := c.Action("compute").Run(context.Background(), core.NewOptions())
|
||||
assert.Equal(t, 42, r.Value)
|
||||
```
|
||||
|
||||
## Test Async Work
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue