refactor(core): align architecture with cordis spatiotemporal composability

This commit is contained in:
ryan
2026-08-28 13:28:58 +08:00
parent 9f8890d159
commit e19bf36580
23 changed files with 1469 additions and 374 deletions
+13 -1
View File
@@ -40,6 +40,12 @@ func isNil(i any) bool {
}
}
func (c *Container) remove(targetType reflect.Type) {
c.mu.Lock()
defer c.mu.Unlock()
delete(c.services, targetType)
}
// Provide registers a typed service implementation into the Context's IoC container.
func Provide[T any](ctx *Context, service T) {
if ctx == nil {
@@ -50,7 +56,13 @@ func Provide[T any](ctx *Context, service T) {
}
targetType := reflect.TypeFor[T]()
ctx.Container().provide(targetType, service)
targetContainer := ctx.Container()
targetContainer.provide(targetType, service)
ctx.OnDispose(func() error {
targetContainer.remove(targetType)
return nil
})
}
func (c *Container) provide(targetType reflect.Type, service any) {
+19
View File
@@ -10,6 +10,7 @@ import (
"sync"
"time"
"Wavelet/core/contracts"
"Wavelet/core/extpoints"
)
@@ -214,6 +215,24 @@ func (c *Context) Setting() extpoints.SettingExtension {
return c.settings
}
// DB returns the contracts.DBService registered in the IoC container, or nil if not registered.
func (c *Context) DB() contracts.DBService {
svc, err := Inject[contracts.DBService](c)
if err != nil {
return nil
}
return svc
}
// Cache returns the contracts.CacheService registered in the IoC container, or nil if not registered.
func (c *Context) Cache() contracts.CacheService {
svc, err := Inject[contracts.CacheService](c)
if err != nil {
return nil
}
return svc
}
// OnDispose registers a cleanup callback function to be executed when this Context is disposed.
// It accepts func() error, func(), or Disposer.
func (c *Context) OnDispose(fn any) {
+48
View File
@@ -46,6 +46,33 @@ type OAuthUserInfoDTO struct {
AvatarURL string `json:"avatar_url"`
}
// AuthSourceDTO represents an OAuth / OIDC authentication source.
type AuthSourceDTO struct {
ID uint64 `json:"id,string"`
Name string `json:"name"`
Type string `json:"type"`
DisplayName string `json:"display_name"`
ClientID string `json:"client_id"`
ClientSecret string `json:"client_secret,omitempty"`
OpenIDDiscoveryURL string `json:"openid_discovery_url"`
Scopes string `json:"scopes"`
IconURL string `json:"icon_url"`
IsActive bool `json:"is_active"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// AuthSourceViewDTO is a sanitized view of an AuthSource for admin display.
type AuthSourceViewDTO struct {
ID uint64 `json:"id,string"`
Name string `json:"name"`
Type string `json:"type"`
DisplayName string `json:"display_name"`
IsActive bool `json:"is_active"`
IconURL string `json:"icon_url"`
ClientSecretConfigured bool `json:"client_secret_configured"`
}
// OAuthProvider defines the pluggable OAuth provider contract.
type OAuthProvider interface {
Name() string
@@ -79,6 +106,27 @@ type AuthService interface {
// RevokeUserSessions revokes all active sessions and cached tokens for a user.
RevokeUserSessions(ctx context.Context, userID uint64) error
// InvalidateCachedUser invalidates cached user profile data.
InvalidateCachedUser(ctx context.Context, userID uint64)
// InvalidateCachedToken invalidates cached access token data.
InvalidateCachedToken(ctx context.Context, tokenHash string)
// ListAuthSources lists all configured authentication sources.
ListAuthSources(ctx context.Context) ([]AuthSourceViewDTO, error)
// CreateAuthSource creates a new authentication source.
CreateAuthSource(ctx context.Context, source AuthSourceDTO) (*AuthSourceDTO, error)
// UpdateAuthSource updates an authentication source.
UpdateAuthSource(ctx context.Context, id uint64, source AuthSourceDTO) (*AuthSourceDTO, error)
// DeleteAuthSource removes an authentication source.
DeleteAuthSource(ctx context.Context, id uint64) error
// ToggleAuthSource toggles the active state of an authentication source.
ToggleAuthSource(ctx context.Context, id uint64) (*AuthSourceDTO, error)
// DisallowTokenAuthMiddleware returns a middleware that rejects requests authenticated via access token.
DisallowTokenAuthMiddleware() any
}
+25
View File
@@ -26,6 +26,13 @@ const (
// EventTopicUserDeleted fires when a user account is deleted.
EventTopicUserDeleted = "user:deleted"
// EventTopicUserStatusChanged fires when a user account active status changes.
EventTopicUserStatusChanged = "user:status_changed"
// EventTopicTokenRevoked fires when an access token is revoked.
// #nosec G101
EventTopicTokenRevoked = "auth:token_revoked"
)
// --- Admin & System Events ---
@@ -107,3 +114,21 @@ type NotificationSentEvent struct {
Success bool `json:"success"`
ErrorInfo string `json:"error_info,omitempty"`
}
// UserStatusChangedEvent fires when a user status is enabled/disabled.
type UserStatusChangedEvent struct {
UserID uint64 `json:"user_id,string"`
IsActive bool `json:"is_active"`
}
// TokenRevokedEvent fires when an access token is revoked.
type TokenRevokedEvent struct {
UserID uint64 `json:"user_id,string"`
TokenHash string `json:"token_hash"`
}
// UserDeletedEvent fires when a user account is deleted.
type UserDeletedEvent struct {
CurrentUserID uint64 `json:"current_user_id,string"`
TargetUserID uint64 `json:"target_user_id,string"`
}
+46
View File
@@ -29,6 +29,34 @@ type UpdateUserProfileRequest struct {
Location *string `json:"location,omitempty"`
}
// AdminListUsersFilter contains query parameters for filtering users in admin panel.
type AdminListUsersFilter struct {
Page int
PageSize int
UserID *uint64
Username string
Email string
}
// AdminCreateUserRequest contains fields for admin to create a user.
type AdminCreateUserRequest struct {
Username string `json:"username"`
Password string `json:"password"`
Nickname string `json:"nickname"`
Email string `json:"email"`
IsActive bool `json:"is_active"`
IsAdmin bool `json:"is_admin"`
}
// AdminUpdateUserRequest contains fields for admin to update a user.
type AdminUpdateUserRequest struct {
ID uint64 `json:"id,string"`
Nickname string `json:"nickname"`
Email string `json:"email"`
IsAdmin bool `json:"is_admin"`
Password string `json:"password,omitempty"`
}
// UserService defines the contract for user account management and profile queries.
type UserService interface {
// GetUserByID retrieves a user by ID.
@@ -81,4 +109,22 @@ type UserService interface {
// UniqueUsername generates a unique username candidate based on base.
UniqueUsername(ctx context.Context, base string) (string, error)
// AdminListUsers returns a filtered paginated list of users for admin management.
AdminListUsers(ctx context.Context, filter AdminListUsersFilter) (int64, []*UserDTO, error)
// AdminGetUser retrieves complete user details by ID for admin management.
AdminGetUser(ctx context.Context, id uint64) (*UserDTO, error)
// AdminCreateUser creates a user with admin specified options.
AdminCreateUser(ctx context.Context, req AdminCreateUserRequest) (*UserDTO, error)
// AdminUpdateUser updates user details, email, nickname, admin role, and optional password.
AdminUpdateUser(ctx context.Context, currentUserID uint64, req AdminUpdateUserRequest) error
// AdminUpdateUserStatus updates a user's active status (with admin protection).
AdminUpdateUserStatus(ctx context.Context, id uint64, active bool) error
// AdminDeleteUser deletes a user (with self and admin protection, cascading tokens and accounts).
AdminDeleteUser(ctx context.Context, currentUserID, targetID uint64) error
}
+210 -17
View File
@@ -21,13 +21,16 @@ type eventListener struct {
id uint64
fnVal reflect.Value
numIn int
numOut int
hasCtx bool
hasPayload bool
argType reflect.Type
returnsErr bool
returnsVal bool
}
// EventBus is a thread-safe, strongly-typed in-process domain event bus.
// EventBus is a thread-safe, strongly-typed in-process domain event bus supporting
// Emit, Waterfall, Parallel, and Serial dispatch semantics.
type EventBus struct {
mu sync.RWMutex
nextID atomic.Uint64
@@ -44,6 +47,10 @@ func NewEventBus() *EventBus {
// On registers an event handler for the given topic.
//
// Supported handler signatures:
// - func(ctx context.Context, event T) (T, error)
// - func(ctx context.Context, event T) T
// - func(event T) (T, error)
// - func(event T) T
// - func(ctx context.Context, event T) error
// - func(ctx context.Context, event T)
// - func(event T) error
@@ -71,17 +78,29 @@ func (b *EventBus) On(topic string, handler any) Disposer {
panic(fmt.Sprintf("core/events: handler has %d parameters, maximum 2 supported (ctx, event)", numIn))
}
const maxHandlerReturnValues = 2
numOut := fnType.NumOut()
if numOut > 1 {
panic(fmt.Sprintf("core/events: handler has %d return values, maximum 1 supported (error)", numOut))
if numOut > maxHandlerReturnValues {
panic(fmt.Sprintf("core/events: handler has %d return values, maximum 2 supported (value, error)", numOut))
}
returnsErr := false
if numOut == 1 {
outType := fnType.Out(0)
if !outType.Implements(errInterfaceType) {
panic(fmt.Sprintf("core/events: handler return type must be error, got %v", outType))
returnsVal := false
switch numOut {
case 1:
out0 := fnType.Out(0)
if out0.Implements(errInterfaceType) {
returnsErr = true
} else {
returnsVal = true
}
case 2:
out1 := fnType.Out(1)
if !out1.Implements(errInterfaceType) {
panic(fmt.Sprintf("core/events: second return value must be error, got %v", out1))
}
returnsVal = true
returnsErr = true
}
@@ -89,7 +108,9 @@ func (b *EventBus) On(topic string, handler any) Disposer {
id: b.nextID.Add(1),
fnVal: fnVal,
numIn: numIn,
numOut: numOut,
returnsErr: returnsErr,
returnsVal: returnsVal,
}
switch numIn {
@@ -162,17 +183,11 @@ func (b *EventBus) Emit(ctx context.Context, topic string, payload any) error {
ctx = context.Background()
}
b.mu.RLock()
rawListeners := b.handlers[topic]
if len(rawListeners) == 0 {
b.mu.RUnlock()
listeners := b.getListeners(topic)
if len(listeners) == 0 {
return nil
}
listeners := make([]eventListener, len(rawListeners))
copy(listeners, rawListeners)
b.mu.RUnlock()
var payloadVal reflect.Value
if payload != nil {
payloadVal = reflect.ValueOf(payload)
@@ -190,8 +205,11 @@ func (b *EventBus) Emit(ctx context.Context, topic string, payload any) error {
}()
results := l.fnVal.Call(args)
if l.returnsErr && len(results) > 0 && !results[0].IsNil() {
resErr = results[0].Interface().(error)
if l.returnsErr {
errIdx := l.numOut - 1
if len(results) > errIdx && !results[errIdx].IsNil() {
resErr = results[errIdx].Interface().(error)
}
}
return resErr
}()
@@ -204,6 +222,181 @@ func (b *EventBus) Emit(ctx context.Context, topic string, payload any) error {
return errors.Join(errs...)
}
// Waterfall runs handlers sequentially as a transformation pipeline.
// The returned value of each handler becomes the payload input for the next handler.
// If any handler returns an error or panics, execution aborts immediately.
//
//nolint:contextcheck
func (b *EventBus) Waterfall(ctx context.Context, topic string, initialPayload any) (any, error) {
if ctx == nil {
ctx = context.Background()
}
listeners := b.getListeners(topic)
if len(listeners) == 0 {
return initialPayload, nil
}
currentPayload := initialPayload
for _, l := range listeners {
var payloadVal reflect.Value
if currentPayload != nil {
payloadVal = reflect.ValueOf(currentPayload)
}
args := b.buildArgs(ctx, l, payloadVal)
var stepVal any
var stepErr error
func() {
defer func() {
if r := recover(); r != nil {
stepErr = fmt.Errorf("core/events: panic in waterfall handler for topic %q: %v", topic, r)
}
}()
results := l.fnVal.Call(args)
if l.returnsErr {
errIdx := l.numOut - 1
if len(results) > errIdx && !results[errIdx].IsNil() {
stepErr = results[errIdx].Interface().(error)
}
}
if stepErr == nil && l.returnsVal && len(results) > 0 {
stepVal = results[0].Interface()
}
}()
if stepErr != nil {
return nil, stepErr
}
if l.returnsVal {
currentPayload = stepVal
}
}
return currentPayload, nil
}
// Parallel executes all subscribers of the topic concurrently in separate goroutines.
// It waits for all handlers to complete and collects any errors via errors.Join.
//
//nolint:contextcheck
func (b *EventBus) Parallel(ctx context.Context, topic string, payload any) error {
if ctx == nil {
ctx = context.Background()
}
listeners := b.getListeners(topic)
if len(listeners) == 0 {
return nil
}
var payloadVal reflect.Value
if payload != nil {
payloadVal = reflect.ValueOf(payload)
}
var wg sync.WaitGroup
errCh := make(chan error, len(listeners))
for _, l := range listeners {
wg.Add(1)
go func(listener eventListener) {
defer wg.Done()
defer func() {
if r := recover(); r != nil {
errCh <- fmt.Errorf("core/events: panic in parallel handler for topic %q: %v", topic, r)
}
}()
args := b.buildArgs(ctx, listener, payloadVal)
results := listener.fnVal.Call(args)
if listener.returnsErr {
errIdx := listener.numOut - 1
if len(results) > errIdx && !results[errIdx].IsNil() {
errCh <- results[errIdx].Interface().(error)
}
}
}(l)
}
wg.Wait()
close(errCh)
var errs []error
for err := range errCh {
if err != nil {
errs = append(errs, err)
}
}
return errors.Join(errs...)
}
// Serial executes subscribers strictly in sequence.
// If any subscriber returns an error or panics, execution stops immediately and returns that error.
//
//nolint:contextcheck
func (b *EventBus) Serial(ctx context.Context, topic string, payload any) error {
if ctx == nil {
ctx = context.Background()
}
listeners := b.getListeners(topic)
if len(listeners) == 0 {
return nil
}
var payloadVal reflect.Value
if payload != nil {
payloadVal = reflect.ValueOf(payload)
}
for _, l := range listeners {
args := b.buildArgs(ctx, l, payloadVal)
var stepErr error
func() {
defer func() {
if r := recover(); r != nil {
stepErr = fmt.Errorf("core/events: panic in serial handler for topic %q: %v", topic, r)
}
}()
results := l.fnVal.Call(args)
if l.returnsErr {
errIdx := l.numOut - 1
if len(results) > errIdx && !results[errIdx].IsNil() {
stepErr = results[errIdx].Interface().(error)
}
}
}()
if stepErr != nil {
return stepErr
}
}
return nil
}
func (b *EventBus) getListeners(topic string) []eventListener {
b.mu.RLock()
raw := b.handlers[topic]
if len(raw) == 0 {
b.mu.RUnlock()
return nil
}
listeners := make([]eventListener, len(raw))
copy(listeners, raw)
b.mu.RUnlock()
return listeners
}
func (b *EventBus) buildArgs(ctx context.Context, l eventListener, payloadVal reflect.Value) []reflect.Value {
if l.numIn == 0 {
return nil
+82 -4
View File
@@ -247,13 +247,13 @@ func TestEventBusInvalidHandlerPanics(t *testing.T) {
})
assert.Panics(t, func() {
// More than 1 return value
bus.On("test:invalid", func() (int, error) { return 0, nil })
// More than 2 return values
bus.On("test:invalid", func() (int, string, error) { return 0, "", nil })
})
assert.Panics(t, func() {
// Return value is not error
bus.On("test:invalid", func() int { return 0 })
// 2 return values, but second is not error
bus.On("test:invalid", func() (int, string) { return 0, "" })
})
}
@@ -316,3 +316,81 @@ func TestEventBusConcurrentAccess(t *testing.T) {
wg.Wait()
assert.Greater(t, receivedCount.Load(), int64(0))
}
func TestEventBusWaterfall(t *testing.T) {
bus := core.NewEventBus()
// Handler 1: appends "-first"
bus.On("pipeline:transform", func(ctx context.Context, s string) string {
return s + "-first"
})
// Handler 2: appends "-second" with error return
bus.On("pipeline:transform", func(s string) (string, error) {
return s + "-second", nil
})
res, err := bus.Waterfall(context.Background(), "pipeline:transform", "init")
assert.NoError(t, err)
assert.Equal(t, "init-first-second", res)
// Test short-circuit on error
expectedErr := errors.New("waterfall step failed")
bus.On("pipeline:error", func(s string) (string, error) {
return s, expectedErr
})
bus.On("pipeline:error", func(s string) string {
return s + "-should-not-run"
})
res, err = bus.Waterfall(context.Background(), "pipeline:error", "start")
assert.ErrorIs(t, err, expectedErr)
assert.Nil(t, res)
}
func TestEventBusParallel(t *testing.T) {
bus := core.NewEventBus()
var counter atomic.Int64
err1 := errors.New("parallel err 1")
bus.On("test:parallel", func(ctx context.Context, val int) error {
counter.Add(int64(val))
return nil
})
bus.On("test:parallel", func(val int) error {
counter.Add(int64(val))
return err1
})
err := bus.Parallel(context.Background(), "test:parallel", 10)
assert.ErrorIs(t, err, err1)
assert.Equal(t, int64(20), counter.Load())
}
func TestEventBusSerial(t *testing.T) {
bus := core.NewEventBus()
var executed []int
errStop := errors.New("serial stop")
bus.On("test:serial", func() error {
executed = append(executed, 1)
return nil
})
bus.On("test:serial", func() error {
executed = append(executed, 2)
return errStop
})
bus.On("test:serial", func() error {
executed = append(executed, 3)
return nil
})
err := bus.Serial(context.Background(), "test:serial", nil)
assert.ErrorIs(t, err, errStop)
assert.Equal(t, []int{1, 2}, executed)
}
+41
View File
@@ -278,3 +278,44 @@ func TestContextExtensionPointsIntegration(t *testing.T) {
assert.NoError(t, err)
assert.True(t, eventReceived)
}
func TestExtensionPointsUnregister(t *testing.T) {
ctx := core.NewContext(context.Background())
// 1. Router unregister
rd := ctx.Router().GET("/temp", "temp_handler")
assert.Greater(t, rd.ID, uint64(0))
assert.Len(t, ctx.Router().Routes(), 1)
assert.True(t, ctx.Router().Unregister("GET", "/temp"))
assert.Len(t, ctx.Router().Routes(), 0)
rd2 := ctx.Router().POST("/temp2", "temp2_handler")
assert.Len(t, ctx.Router().Routes(), 1)
assert.True(t, ctx.Router().UnregisterByID(rd2.ID))
assert.Len(t, ctx.Router().Routes(), 0)
// 2. Task unregister
ctx.Task().Register("temp:task", "handler")
assert.Len(t, ctx.Task().Tasks(), 1)
assert.True(t, ctx.Task().Unregister("temp:task"))
assert.Len(t, ctx.Task().Tasks(), 0)
// 3. Schedule unregister
ctx.Schedule().RegisterCron("@hourly", "temp:cron", nil)
assert.Len(t, ctx.Schedule().Schedules(), 1)
assert.True(t, ctx.Schedule().Unregister("temp:cron"))
assert.Len(t, ctx.Schedule().Schedules(), 0)
// 4. Setting unregister
ctx.Settings().Register(extpoints.SettingSchema{Key: "temp.key", Default: 1})
assert.Len(t, ctx.Settings().Schemas(), 1)
assert.True(t, ctx.Settings().Unregister("temp.key"))
assert.Len(t, ctx.Settings().Schemas(), 0)
// 5. Migration unregister
fsys := fstest.MapFS{"001.sql": &fstest.MapFile{Data: []byte("-- migration")}}
ctx.Migrations().Register("temp_plugin", fsys)
assert.Len(t, ctx.Migrations().Entries(), 1)
assert.True(t, ctx.Migrations().Unregister("temp_plugin"))
assert.Len(t, ctx.Migrations().Entries(), 0)
}
+20
View File
@@ -21,6 +21,7 @@ type MigrationExtension interface {
Register(pluginID string, fsys fs.FS, dir ...string)
Entries() []MigrationEntry
Get(pluginID string) (MigrationEntry, bool)
Unregister(pluginID string) bool
}
// MigrationRegistry collects and stores migration entries from plugins.
@@ -69,6 +70,25 @@ func (m *MigrationRegistry) Register(pluginID string, fsys fs.FS, dir ...string)
m.lookup[pluginID] = entry
}
// Unregister removes a registered migration entry by plugin ID.
func (m *MigrationRegistry) Unregister(pluginID string) bool {
m.mu.Lock()
defer m.mu.Unlock()
if _, exists := m.lookup[pluginID]; !exists {
return false
}
delete(m.lookup, pluginID)
for i, e := range m.entries {
if e.PluginID == pluginID {
m.entries = append(m.entries[:i], m.entries[i+1:]...)
break
}
}
return true
}
// Entries returns a copy of all registered migration entries in registration order.
func (m *MigrationRegistry) Entries() []MigrationEntry {
m.mu.RLock()
+50
View File
@@ -10,6 +10,7 @@ import (
// RouteDefinition holds the metadata and handler list for a single HTTP route.
type RouteDefinition struct {
ID uint64
Method string
Path string
Handlers []any
@@ -31,11 +32,14 @@ type RouterExtension interface {
Any(path string, handlers ...any) []RouteDefinition
Routes() []RouteDefinition
Middlewares() []any
Unregister(method, path string) bool
UnregisterByID(id uint64) bool
}
// RouterRegistry implements RouterExtension as the root route and middleware collector.
type RouterRegistry struct {
mu sync.RWMutex
nextID uint64
routes []RouteDefinition
middlewares []any
}
@@ -75,7 +79,9 @@ func (r *RouterRegistry) Handle(method, path string, handlers ...any) RouteDefin
r.mu.Lock()
defer r.mu.Unlock()
r.nextID++
rd := RouteDefinition{
ID: r.nextID,
Method: strings.ToUpper(method),
Path: cleanPath(path),
Handlers: handlers,
@@ -85,6 +91,37 @@ func (r *RouterRegistry) Handle(method, path string, handlers ...any) RouteDefin
return rd
}
// Unregister removes a route matching method and path from the registry.
func (r *RouterRegistry) Unregister(method, path string) bool {
r.mu.Lock()
defer r.mu.Unlock()
targetMethod := strings.ToUpper(method)
targetPath := cleanPath(path)
for i, rd := range r.routes {
if rd.Method == targetMethod && rd.Path == targetPath {
r.routes = append(r.routes[:i], r.routes[i+1:]...)
return true
}
}
return false
}
// UnregisterByID removes a route by its unique ID.
func (r *RouterRegistry) UnregisterByID(id uint64) bool {
r.mu.Lock()
defer r.mu.Unlock()
for i, rd := range r.routes {
if rd.ID == id {
r.routes = append(r.routes[:i], r.routes[i+1:]...)
return true
}
}
return false
}
// GET registers a GET route.
func (r *RouterRegistry) GET(path string, handlers ...any) RouteDefinition {
return r.Handle("GET", path, handlers...)
@@ -176,7 +213,9 @@ func (g *RouterGroup) Handle(method, path string, handlers ...any) RouteDefiniti
allMiddlewares = append(allMiddlewares, g.registry.middlewares...)
allMiddlewares = append(allMiddlewares, g.middlewares...)
g.registry.nextID++
rd := RouteDefinition{
ID: g.registry.nextID,
Method: strings.ToUpper(method),
Path: fullPath,
Handlers: handlers,
@@ -186,6 +225,17 @@ func (g *RouterGroup) Handle(method, path string, handlers ...any) RouteDefiniti
return rd
}
// Unregister removes a route under this group prefix matching method and path.
func (g *RouterGroup) Unregister(method, path string) bool {
fullPath := joinPaths(g.prefix, path)
return g.registry.Unregister(method, fullPath)
}
// UnregisterByID removes a route by its unique ID.
func (g *RouterGroup) UnregisterByID(id uint64) bool {
return g.registry.UnregisterByID(id)
}
// GET registers a GET route in this group.
func (g *RouterGroup) GET(path string, handlers ...any) RouteDefinition {
return g.Handle("GET", path, handlers...)
+20
View File
@@ -32,6 +32,7 @@ type ScheduleExtension interface {
RegisterCron(spec string, taskType string, payload any, opts ...ScheduleOption)
Schedules() []ScheduleDefinition
Get(taskType string) (ScheduleDefinition, bool)
Unregister(taskType string) bool
}
// ScheduleRegistry collects and manages schedule registrations.
@@ -85,6 +86,25 @@ func (s *ScheduleRegistry) RegisterCron(spec string, taskType string, payload an
s.Register(spec, taskType, payload, opts...)
}
// Unregister removes a registered schedule definition by its task type.
func (s *ScheduleRegistry) Unregister(taskType string) bool {
s.mu.Lock()
defer s.mu.Unlock()
if _, exists := s.lookup[taskType]; !exists {
return false
}
delete(s.lookup, taskType)
for i, item := range s.schedules {
if item.TaskType == taskType {
s.schedules = append(s.schedules[:i], s.schedules[i+1:]...)
break
}
}
return true
}
// Schedules returns a copy of all registered ScheduleDefinitions.
func (s *ScheduleRegistry) Schedules() []ScheduleDefinition {
s.mu.RLock()
+20
View File
@@ -22,6 +22,7 @@ type SettingExtension interface {
Register(schema SettingSchema)
Schemas() []SettingSchema
Get(key string) (SettingSchema, bool)
Unregister(key string) bool
}
// SettingRegistry collects and manages setting configuration schemas.
@@ -62,6 +63,25 @@ func (s *SettingRegistry) Register(schema SettingSchema) {
s.lookup[schema.Key] = schema
}
// Unregister removes a registered SettingSchema by its key.
func (s *SettingRegistry) Unregister(key string) bool {
s.mu.Lock()
defer s.mu.Unlock()
if _, exists := s.lookup[key]; !exists {
return false
}
delete(s.lookup, key)
for i, item := range s.schemas {
if item.Key == key {
s.schemas = append(s.schemas[:i], s.schemas[i+1:]...)
break
}
}
return true
}
// Schemas returns a copy of all registered SettingSchemas.
func (s *SettingRegistry) Schemas() []SettingSchema {
s.mu.RLock()
+20
View File
@@ -57,6 +57,7 @@ type TaskExtension interface {
Register(pattern string, handler any, opts ...TaskOption)
Tasks() []TaskDefinition
Get(pattern string) (TaskDefinition, bool)
Unregister(pattern string) bool
}
// TaskRegistry collects and manages task registrations.
@@ -104,6 +105,25 @@ func (t *TaskRegistry) Register(pattern string, handler any, opts ...TaskOption)
t.lookup[pattern] = td
}
// Unregister removes a registered task definition by its pattern.
func (t *TaskRegistry) Unregister(pattern string) bool {
t.mu.Lock()
defer t.mu.Unlock()
if _, exists := t.lookup[pattern]; !exists {
return false
}
delete(t.lookup, pattern)
for i, item := range t.tasks {
if item.Pattern == pattern {
t.tasks = append(t.tasks[:i], t.tasks[i+1:]...)
break
}
}
return true
}
// Tasks returns a copy of all registered TaskDefinitions.
func (t *TaskRegistry) Tasks() []TaskDefinition {
t.mu.RLock()