refactor
Build and Publish / BuildAndDeployArm64 (push) Failing after 6s
Build and Publish / BuildAndDeployAmd64 (push) Failing after 4s

This commit is contained in:
2026-06-29 12:34:58 +02:00
parent d711348863
commit 46be260fcc
35 changed files with 1765 additions and 198 deletions
+63 -13
View File
@@ -54,42 +54,78 @@ type HookInfo struct {
// — so observability/notification hooks can't break the business transaction.
type Hook func(ctx context.Context, st *State, info HookInfo, params json.RawMessage) error
// CapabilityMeta is the editor-facing documentation attached at registration
// time. The server generates `/sagas/capabilities` from these registrations, so
// UIs should consume this payload instead of hardcoding per-tool help.
//
// Register every action/predicate/hook with the matching *WithMeta helper when
// it should be discoverable in editors. ExampleParams is arbitrary JSON shown as
// a starter payload for params-capable tools.
type CapabilityMeta struct {
Description string `json:"description,omitempty"`
ExampleParams json.RawMessage `json:"exampleParams,omitempty"`
}
// Registry holds the named actions, predicates and hooks a flow can reference.
type Registry struct {
mu sync.RWMutex
actions map[string]Action
predicates map[string]Predicate
hooks map[string]Hook
mu sync.RWMutex
actions map[string]Action
actionMeta map[string]CapabilityMeta
predicates map[string]Predicate
predicateMeta map[string]CapabilityMeta
hooks map[string]Hook
hookMeta map[string]CapabilityMeta
}
// NewRegistry returns an empty registry.
func NewRegistry() *Registry {
return &Registry{
actions: map[string]Action{},
predicates: map[string]Predicate{},
hooks: map[string]Hook{},
actions: map[string]Action{},
actionMeta: map[string]CapabilityMeta{},
predicates: map[string]Predicate{},
predicateMeta: map[string]CapabilityMeta{},
hooks: map[string]Hook{},
hookMeta: map[string]CapabilityMeta{},
}
}
// Action registers an action under name (overwrites an existing one).
func (r *Registry) Action(name string, fn Action) {
r.ActionWithMeta(name, fn, CapabilityMeta{})
}
// ActionWithMeta registers an action plus the metadata editors should display.
func (r *Registry) ActionWithMeta(name string, fn Action, meta CapabilityMeta) {
r.mu.Lock()
defer r.mu.Unlock()
r.actions[name] = fn
r.actionMeta[name] = meta
}
// Predicate registers a predicate under name.
func (r *Registry) Predicate(name string, fn Predicate) {
r.PredicateWithMeta(name, fn, CapabilityMeta{})
}
// PredicateWithMeta registers a predicate plus the metadata editors should display.
func (r *Registry) PredicateWithMeta(name string, fn Predicate, meta CapabilityMeta) {
r.mu.Lock()
defer r.mu.Unlock()
r.predicates[name] = fn
r.predicateMeta[name] = meta
}
// Hook registers a hook under name.
func (r *Registry) Hook(name string, fn Hook) {
r.HookWithMeta(name, fn, CapabilityMeta{})
}
// HookWithMeta registers a hook plus the metadata editors should display.
func (r *Registry) HookWithMeta(name string, fn Hook, meta CapabilityMeta) {
r.mu.Lock()
defer r.mu.Unlock()
r.hooks[name] = fn
r.hookMeta[name] = meta
}
func (r *Registry) action(name string) (Action, bool) {
@@ -116,9 +152,12 @@ func (r *Registry) hook(name string) (Hook, bool) {
// Capabilities is the set of registered names, surfaced so an editor UI can
// offer exactly the actions/predicates/hooks a flow may reference.
type Capabilities struct {
Actions []string `json:"actions"`
Predicates []string `json:"predicates"`
Hooks []string `json:"hooks"`
Actions []string `json:"actions"`
ActionMeta map[string]CapabilityMeta `json:"actionMeta,omitempty"`
Predicates []string `json:"predicates"`
PredicateMeta map[string]CapabilityMeta `json:"predicateMeta,omitempty"`
Hooks []string `json:"hooks"`
HookMeta map[string]CapabilityMeta `json:"hookMeta,omitempty"`
}
// Capabilities returns the sorted registered names.
@@ -126,12 +165,23 @@ func (r *Registry) Capabilities() Capabilities {
r.mu.RLock()
defer r.mu.RUnlock()
return Capabilities{
Actions: sortedKeys(r.actions),
Predicates: sortedKeys(r.predicates),
Hooks: sortedKeys(r.hooks),
Actions: sortedKeys(r.actions),
ActionMeta: cloneMeta(r.actionMeta),
Predicates: sortedKeys(r.predicates),
PredicateMeta: cloneMeta(r.predicateMeta),
Hooks: sortedKeys(r.hooks),
HookMeta: cloneMeta(r.hookMeta),
}
}
func cloneMeta(in map[string]CapabilityMeta) map[string]CapabilityMeta {
out := make(map[string]CapabilityMeta, len(in))
for k, v := range in {
out[k] = v
}
return out
}
func sortedKeys[V any](m map[string]V) []string {
out := make([]string, 0, len(m))
for k := range m {
+11 -3
View File
@@ -13,9 +13,17 @@ import (
// (structured log line) and "webhook" (HTTP POST of the event). Domain packages
// register their own hooks (e.g. "amqp_emit") on the same registry.
func RegisterBuiltinHooks(reg *Registry) {
reg.Hook("log", logHook)
reg.Hook("noop", func(context.Context, *State, HookInfo, json.RawMessage) error { return nil })
reg.Hook("webhook", webhookHook(&http.Client{Timeout: 10 * time.Second}))
reg.HookWithMeta("log", logHook, CapabilityMeta{
Description: "Write a structured log line for the current step and phase.",
ExampleParams: json.RawMessage(`{"message":"payment captured"}`),
})
reg.HookWithMeta("noop", func(context.Context, *State, HookInfo, json.RawMessage) error { return nil }, CapabilityMeta{
Description: "Do nothing. Useful as a placeholder while shaping a flow.",
})
reg.HookWithMeta("webhook", webhookHook(&http.Client{Timeout: 10 * time.Second}), CapabilityMeta{
Description: "POST the flow event payload to an external HTTP endpoint.",
ExampleParams: json.RawMessage(`{"url":"https://example.test/order-events","headers":{"X-Flow":"place-and-pay"}}`),
})
}
// logHook emits a structured log line for the step/phase. Params (optional):