Decompose handle-event.go into DDD domain services (v0.36.15)
Some checks failed
Go / build-and-release (push) Has been cancelled
Some checks failed
Go / build-and-release (push) Has been cancelled
Major refactoring of event handling into clean, testable domain services: - Add pkg/event/validation: JSON hex validation, signature verification, timestamp bounds, NIP-70 protected tag validation - Add pkg/event/authorization: Policy and ACL authorization decisions, auth challenge handling, access level determination - Add pkg/event/routing: Event router registry with ephemeral and delete handlers, kind-based dispatch - Add pkg/event/processing: Event persistence, delivery to subscribers, and post-save hooks (ACL reconfig, sync, relay groups) - Reduce handle-event.go from 783 to 296 lines (62% reduction) - Add comprehensive unit tests for all new domain services - Refactor database tests to use shared TestMain setup - Fix blossom URL test expectations (missing "/" separator) - Add go-memory-optimization skill and analysis documentation - Update DDD_ANALYSIS.md to reflect completed decomposition Files modified: - app/handle-event.go: Slim orchestrator using domain services - app/server.go: Service initialization and interface wrappers - app/handle-event-types.go: Shared types (OkHelper, result types) - pkg/event/validation/*: New validation service package - pkg/event/authorization/*: New authorization service package - pkg/event/routing/*: New routing service package - pkg/event/processing/*: New processing service package - pkg/database/*_test.go: Refactored to shared TestMain - pkg/blossom/http_test.go: Fixed URL format expectations 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
50
pkg/event/routing/delete.go
Normal file
50
pkg/event/routing/delete.go
Normal file
@@ -0,0 +1,50 @@
|
||||
package routing
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"git.mleku.dev/mleku/nostr/encoders/event"
|
||||
)
|
||||
|
||||
// DeleteProcessor handles event deletion operations.
|
||||
type DeleteProcessor interface {
|
||||
// SaveDeleteEvent saves the delete event itself.
|
||||
SaveDeleteEvent(ctx context.Context, ev *event.E) error
|
||||
// ProcessDeletion removes the target events.
|
||||
ProcessDeletion(ctx context.Context, ev *event.E) error
|
||||
// DeliverEvent sends the delete event to subscribers.
|
||||
DeliverEvent(ev *event.E)
|
||||
}
|
||||
|
||||
// MakeDeleteHandler creates a handler for delete events (kind 5).
|
||||
// Delete events:
|
||||
// - Save the delete event itself first
|
||||
// - Process target event deletions
|
||||
// - Deliver the delete event to subscribers
|
||||
func MakeDeleteHandler(processor DeleteProcessor) Handler {
|
||||
return func(ev *event.E, authedPubkey []byte) Result {
|
||||
ctx := context.Background()
|
||||
|
||||
// Save delete event first
|
||||
if err := processor.SaveDeleteEvent(ctx, ev); err != nil {
|
||||
return ErrorResult(err)
|
||||
}
|
||||
|
||||
// Process the deletion (remove target events)
|
||||
if err := processor.ProcessDeletion(ctx, ev); err != nil {
|
||||
// Log but don't fail - delete event was saved
|
||||
// Some targets may not exist or may be owned by others
|
||||
}
|
||||
|
||||
// Deliver the delete event to subscribers
|
||||
cloned := ev.Clone()
|
||||
go processor.DeliverEvent(cloned)
|
||||
|
||||
return HandledResult("")
|
||||
}
|
||||
}
|
||||
|
||||
// IsDeleteKind returns true if the kind is a delete event (kind 5).
|
||||
func IsDeleteKind(k uint16) bool {
|
||||
return k == 5
|
||||
}
|
||||
30
pkg/event/routing/ephemeral.go
Normal file
30
pkg/event/routing/ephemeral.go
Normal file
@@ -0,0 +1,30 @@
|
||||
package routing
|
||||
|
||||
import (
|
||||
"git.mleku.dev/mleku/nostr/encoders/event"
|
||||
"git.mleku.dev/mleku/nostr/encoders/kind"
|
||||
)
|
||||
|
||||
// Publisher abstracts event delivery to subscribers.
|
||||
type Publisher interface {
|
||||
// Deliver sends an event to all matching subscribers.
|
||||
Deliver(ev *event.E)
|
||||
}
|
||||
|
||||
// IsEphemeral checks if a kind is ephemeral (20000-29999).
|
||||
func IsEphemeral(k uint16) bool {
|
||||
return kind.IsEphemeral(k)
|
||||
}
|
||||
|
||||
// MakeEphemeralHandler creates a handler for ephemeral events.
|
||||
// Ephemeral events (kinds 20000-29999):
|
||||
// - Are NOT persisted to the database
|
||||
// - Are immediately delivered to subscribers
|
||||
func MakeEphemeralHandler(publisher Publisher) Handler {
|
||||
return func(ev *event.E, authedPubkey []byte) Result {
|
||||
// Clone and deliver immediately without persistence
|
||||
cloned := ev.Clone()
|
||||
go publisher.Deliver(cloned)
|
||||
return HandledResult("")
|
||||
}
|
||||
}
|
||||
122
pkg/event/routing/routing.go
Normal file
122
pkg/event/routing/routing.go
Normal file
@@ -0,0 +1,122 @@
|
||||
// Package routing provides event routing services for the ORLY relay.
|
||||
// It dispatches events to specialized handlers based on event kind.
|
||||
package routing
|
||||
|
||||
import (
|
||||
"git.mleku.dev/mleku/nostr/encoders/event"
|
||||
)
|
||||
|
||||
// Action indicates what to do after routing.
|
||||
type Action int
|
||||
|
||||
const (
|
||||
// Continue means continue to normal processing.
|
||||
Continue Action = iota
|
||||
// Handled means event was fully handled, return success.
|
||||
Handled
|
||||
// Error means an error occurred.
|
||||
Error
|
||||
)
|
||||
|
||||
// Result contains the routing decision.
|
||||
type Result struct {
|
||||
Action Action
|
||||
Message string // Success or error message
|
||||
Error error // Error if Action == Error
|
||||
}
|
||||
|
||||
// ContinueResult returns a result indicating normal processing should continue.
|
||||
func ContinueResult() Result {
|
||||
return Result{Action: Continue}
|
||||
}
|
||||
|
||||
// HandledResult returns a result indicating the event was fully handled.
|
||||
func HandledResult(msg string) Result {
|
||||
return Result{Action: Handled, Message: msg}
|
||||
}
|
||||
|
||||
// ErrorResult returns a result indicating an error occurred.
|
||||
func ErrorResult(err error) Result {
|
||||
return Result{Action: Error, Error: err}
|
||||
}
|
||||
|
||||
// Handler processes a specific event kind.
|
||||
// authedPubkey is the authenticated pubkey of the connection (may be nil).
|
||||
type Handler func(ev *event.E, authedPubkey []byte) Result
|
||||
|
||||
// KindCheck tests whether an event kind matches a category (e.g., ephemeral).
|
||||
type KindCheck struct {
|
||||
Name string
|
||||
Check func(kind uint16) bool
|
||||
Handler Handler
|
||||
}
|
||||
|
||||
// Router dispatches events to specialized handlers.
|
||||
type Router interface {
|
||||
// Route checks if event should be handled specially.
|
||||
Route(ev *event.E, authedPubkey []byte) Result
|
||||
|
||||
// Register adds a handler for a specific kind.
|
||||
Register(kind uint16, handler Handler)
|
||||
|
||||
// RegisterKindCheck adds a handler for a kind category.
|
||||
RegisterKindCheck(name string, check func(uint16) bool, handler Handler)
|
||||
}
|
||||
|
||||
// DefaultRouter implements Router with a handler registry.
|
||||
type DefaultRouter struct {
|
||||
handlers map[uint16]Handler
|
||||
kindChecks []KindCheck
|
||||
}
|
||||
|
||||
// New creates a new DefaultRouter.
|
||||
func New() *DefaultRouter {
|
||||
return &DefaultRouter{
|
||||
handlers: make(map[uint16]Handler),
|
||||
kindChecks: make([]KindCheck, 0),
|
||||
}
|
||||
}
|
||||
|
||||
// Register adds a handler for a specific kind.
|
||||
func (r *DefaultRouter) Register(kind uint16, handler Handler) {
|
||||
r.handlers[kind] = handler
|
||||
}
|
||||
|
||||
// RegisterKindCheck adds a handler for a kind category.
|
||||
func (r *DefaultRouter) RegisterKindCheck(name string, check func(uint16) bool, handler Handler) {
|
||||
r.kindChecks = append(r.kindChecks, KindCheck{
|
||||
Name: name,
|
||||
Check: check,
|
||||
Handler: handler,
|
||||
})
|
||||
}
|
||||
|
||||
// Route checks if event should be handled specially.
|
||||
func (r *DefaultRouter) Route(ev *event.E, authedPubkey []byte) Result {
|
||||
// Check exact kind matches first (higher priority)
|
||||
if handler, ok := r.handlers[ev.Kind]; ok {
|
||||
return handler(ev, authedPubkey)
|
||||
}
|
||||
|
||||
// Check kind property handlers (ephemeral, replaceable, etc.)
|
||||
for _, kc := range r.kindChecks {
|
||||
if kc.Check(ev.Kind) {
|
||||
return kc.Handler(ev, authedPubkey)
|
||||
}
|
||||
}
|
||||
|
||||
return ContinueResult()
|
||||
}
|
||||
|
||||
// HasHandler returns true if a handler is registered for the given kind.
|
||||
func (r *DefaultRouter) HasHandler(kind uint16) bool {
|
||||
if _, ok := r.handlers[kind]; ok {
|
||||
return true
|
||||
}
|
||||
for _, kc := range r.kindChecks {
|
||||
if kc.Check(kind) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
240
pkg/event/routing/routing_test.go
Normal file
240
pkg/event/routing/routing_test.go
Normal file
@@ -0,0 +1,240 @@
|
||||
package routing
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"git.mleku.dev/mleku/nostr/encoders/event"
|
||||
)
|
||||
|
||||
func TestNew(t *testing.T) {
|
||||
r := New()
|
||||
if r == nil {
|
||||
t.Fatal("New() returned nil")
|
||||
}
|
||||
if r.handlers == nil {
|
||||
t.Fatal("handlers map is nil")
|
||||
}
|
||||
if r.kindChecks == nil {
|
||||
t.Fatal("kindChecks slice is nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestResultConstructors(t *testing.T) {
|
||||
// ContinueResult
|
||||
r := ContinueResult()
|
||||
if r.Action != Continue {
|
||||
t.Error("ContinueResult should have Action=Continue")
|
||||
}
|
||||
|
||||
// HandledResult
|
||||
r = HandledResult("success")
|
||||
if r.Action != Handled {
|
||||
t.Error("HandledResult should have Action=Handled")
|
||||
}
|
||||
if r.Message != "success" {
|
||||
t.Error("HandledResult should preserve message")
|
||||
}
|
||||
|
||||
// ErrorResult
|
||||
err := errors.New("test error")
|
||||
r = ErrorResult(err)
|
||||
if r.Action != Error {
|
||||
t.Error("ErrorResult should have Action=Error")
|
||||
}
|
||||
if r.Error != err {
|
||||
t.Error("ErrorResult should preserve error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultRouter_Register(t *testing.T) {
|
||||
r := New()
|
||||
|
||||
called := false
|
||||
handler := func(ev *event.E, authedPubkey []byte) Result {
|
||||
called = true
|
||||
return HandledResult("handled")
|
||||
}
|
||||
|
||||
r.Register(1, handler)
|
||||
|
||||
ev := event.New()
|
||||
ev.Kind = 1
|
||||
|
||||
result := r.Route(ev, nil)
|
||||
if !called {
|
||||
t.Error("handler should have been called")
|
||||
}
|
||||
if result.Action != Handled {
|
||||
t.Error("result should be Handled")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultRouter_RegisterKindCheck(t *testing.T) {
|
||||
r := New()
|
||||
|
||||
called := false
|
||||
handler := func(ev *event.E, authedPubkey []byte) Result {
|
||||
called = true
|
||||
return HandledResult("ephemeral")
|
||||
}
|
||||
|
||||
// Register handler for ephemeral events (20000-29999)
|
||||
r.RegisterKindCheck("ephemeral", func(k uint16) bool {
|
||||
return k >= 20000 && k < 30000
|
||||
}, handler)
|
||||
|
||||
ev := event.New()
|
||||
ev.Kind = 20001
|
||||
|
||||
result := r.Route(ev, nil)
|
||||
if !called {
|
||||
t.Error("kind check handler should have been called")
|
||||
}
|
||||
if result.Action != Handled {
|
||||
t.Error("result should be Handled")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultRouter_NoMatch(t *testing.T) {
|
||||
r := New()
|
||||
|
||||
// Register handler for kind 1
|
||||
r.Register(1, func(ev *event.E, authedPubkey []byte) Result {
|
||||
return HandledResult("kind 1")
|
||||
})
|
||||
|
||||
ev := event.New()
|
||||
ev.Kind = 2 // Different kind
|
||||
|
||||
result := r.Route(ev, nil)
|
||||
if result.Action != Continue {
|
||||
t.Error("unmatched kind should return Continue")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultRouter_ExactMatchPriority(t *testing.T) {
|
||||
r := New()
|
||||
|
||||
exactCalled := false
|
||||
checkCalled := false
|
||||
|
||||
// Register exact match for kind 20001
|
||||
r.Register(20001, func(ev *event.E, authedPubkey []byte) Result {
|
||||
exactCalled = true
|
||||
return HandledResult("exact")
|
||||
})
|
||||
|
||||
// Register kind check for ephemeral (also matches 20001)
|
||||
r.RegisterKindCheck("ephemeral", func(k uint16) bool {
|
||||
return k >= 20000 && k < 30000
|
||||
}, func(ev *event.E, authedPubkey []byte) Result {
|
||||
checkCalled = true
|
||||
return HandledResult("check")
|
||||
})
|
||||
|
||||
ev := event.New()
|
||||
ev.Kind = 20001
|
||||
|
||||
result := r.Route(ev, nil)
|
||||
if !exactCalled {
|
||||
t.Error("exact match should be called")
|
||||
}
|
||||
if checkCalled {
|
||||
t.Error("kind check should not be called when exact match exists")
|
||||
}
|
||||
if result.Message != "exact" {
|
||||
t.Errorf("expected 'exact', got '%s'", result.Message)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultRouter_HasHandler(t *testing.T) {
|
||||
r := New()
|
||||
|
||||
// Initially no handlers
|
||||
if r.HasHandler(1) {
|
||||
t.Error("should not have handler for kind 1 yet")
|
||||
}
|
||||
|
||||
// Register exact handler
|
||||
r.Register(1, func(ev *event.E, authedPubkey []byte) Result {
|
||||
return HandledResult("")
|
||||
})
|
||||
|
||||
if !r.HasHandler(1) {
|
||||
t.Error("should have handler for kind 1")
|
||||
}
|
||||
|
||||
// Register kind check for ephemeral
|
||||
r.RegisterKindCheck("ephemeral", func(k uint16) bool {
|
||||
return k >= 20000 && k < 30000
|
||||
}, func(ev *event.E, authedPubkey []byte) Result {
|
||||
return HandledResult("")
|
||||
})
|
||||
|
||||
if !r.HasHandler(20001) {
|
||||
t.Error("should have handler for ephemeral kind 20001")
|
||||
}
|
||||
|
||||
if r.HasHandler(19999) {
|
||||
t.Error("should not have handler for kind 19999")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultRouter_PassesPubkey(t *testing.T) {
|
||||
r := New()
|
||||
|
||||
var receivedPubkey []byte
|
||||
r.Register(1, func(ev *event.E, authedPubkey []byte) Result {
|
||||
receivedPubkey = authedPubkey
|
||||
return HandledResult("")
|
||||
})
|
||||
|
||||
testPubkey := []byte("testpubkey12345")
|
||||
ev := event.New()
|
||||
ev.Kind = 1
|
||||
|
||||
r.Route(ev, testPubkey)
|
||||
|
||||
if string(receivedPubkey) != string(testPubkey) {
|
||||
t.Error("handler should receive the authed pubkey")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultRouter_MultipleKindChecks(t *testing.T) {
|
||||
r := New()
|
||||
|
||||
firstCalled := false
|
||||
secondCalled := false
|
||||
|
||||
// First check matches 10000-19999
|
||||
r.RegisterKindCheck("first", func(k uint16) bool {
|
||||
return k >= 10000 && k < 20000
|
||||
}, func(ev *event.E, authedPubkey []byte) Result {
|
||||
firstCalled = true
|
||||
return HandledResult("first")
|
||||
})
|
||||
|
||||
// Second check matches 15000-25000 (overlaps)
|
||||
r.RegisterKindCheck("second", func(k uint16) bool {
|
||||
return k >= 15000 && k < 25000
|
||||
}, func(ev *event.E, authedPubkey []byte) Result {
|
||||
secondCalled = true
|
||||
return HandledResult("second")
|
||||
})
|
||||
|
||||
// Kind 15000 matches both - first registered wins
|
||||
ev := event.New()
|
||||
ev.Kind = 15000
|
||||
|
||||
result := r.Route(ev, nil)
|
||||
if !firstCalled {
|
||||
t.Error("first check should be called")
|
||||
}
|
||||
if secondCalled {
|
||||
t.Error("second check should not be called")
|
||||
}
|
||||
if result.Message != "first" {
|
||||
t.Errorf("expected 'first', got '%s'", result.Message)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user