mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-07 08:06:37 +08:00
feat(core): bind request services and implement registered tasks
Wire plugin services through Bind/InjectFrom and AppContext so HTTP and workers resolve dependencies after Apply. Register TaskHandler objects with persisted results, and implement send_email_code, mail:send, cleanup_inactive_users, and dispatch_bot_msg.
This commit is contained in:
@@ -0,0 +1,43 @@
|
||||
// Copyright 2026 Arctel.net
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package core
|
||||
|
||||
import "context"
|
||||
|
||||
type appContextKey struct{}
|
||||
|
||||
// WithAppContext attaches the micro-kernel Context to a standard context.Context
|
||||
// so request and worker handlers can Inject services without package-level setters.
|
||||
func WithAppContext(ctx context.Context, app *Context) context.Context {
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
if app == nil {
|
||||
return ctx
|
||||
}
|
||||
return context.WithValue(ctx, appContextKey{}, app.Root())
|
||||
}
|
||||
|
||||
// AppContext extracts the micro-kernel Context from ctx, if present.
|
||||
func AppContext(ctx context.Context) *Context {
|
||||
if ctx == nil {
|
||||
return nil
|
||||
}
|
||||
if c, ok := ctx.(*Context); ok {
|
||||
return c
|
||||
}
|
||||
app, _ := ctx.Value(appContextKey{}).(*Context)
|
||||
return app
|
||||
}
|
||||
|
||||
// InjectFrom resolves T from ctx when it carries a micro-kernel Context
|
||||
// (*Context itself, or a value attached by WithAppContext).
|
||||
func InjectFrom[T any](ctx context.Context) (T, error) {
|
||||
var zero T
|
||||
app := AppContext(ctx)
|
||||
if app == nil {
|
||||
return zero, ErrNilContext
|
||||
}
|
||||
return Inject[T](app)
|
||||
}
|
||||
@@ -201,13 +201,17 @@ func Using3[T1, T2, T3 any](ctx *Context, fn func(s1 T1, s2 T2, s3 T3)) error {
|
||||
|
||||
// When registers a reactive hook that is called immediately if T is already provided,
|
||||
// or called as soon as T is provided in the future.
|
||||
//
|
||||
// Listeners are stored on the root container so they observe core.Provide, which
|
||||
// always writes to the root. Registering on a Fiber child container would miss
|
||||
// services provided by plugins that load later.
|
||||
func When[T any](ctx *Context, fn func(s T)) {
|
||||
if ctx == nil {
|
||||
panic("core: nil context provided to When")
|
||||
}
|
||||
|
||||
targetType := reflect.TypeFor[T]()
|
||||
c := ctx.Container()
|
||||
c := ctx.Root().Container()
|
||||
|
||||
// If already ready, execute immediately
|
||||
if s, err := Inject[T](ctx); err == nil {
|
||||
@@ -223,3 +227,9 @@ func When[T any](ctx *Context, fn func(s T)) {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Bind is When with a name that matches plugin wiring: fill a dependency as
|
||||
// soon as the root container provides it.
|
||||
func Bind[T any](ctx *Context, fn func(s T)) {
|
||||
When(ctx, fn)
|
||||
}
|
||||
|
||||
@@ -313,6 +313,46 @@ func TestContextReactiveWhen(t *testing.T) {
|
||||
assert.True(t, immediateCalled)
|
||||
}
|
||||
|
||||
func TestWhenObservesProvideFromForkedFiberContext(t *testing.T) {
|
||||
root := core.NewContext(context.Background())
|
||||
adminFiber := root.Fork()
|
||||
lateFiber := root.Fork()
|
||||
|
||||
var got atomic.Bool
|
||||
core.When[SampleService](adminFiber, func(s SampleService) {
|
||||
if s != nil {
|
||||
got.Store(true)
|
||||
}
|
||||
})
|
||||
assert.False(t, got.Load())
|
||||
|
||||
core.Provide[SampleService](lateFiber, &sampleServiceImpl{})
|
||||
assert.True(t, got.Load(), "When on a Fiber child must observe Provide on the root")
|
||||
}
|
||||
|
||||
func TestBindIsWhen(t *testing.T) {
|
||||
ctx := core.NewContext(context.Background())
|
||||
var called atomic.Bool
|
||||
core.Bind[SampleService](ctx, func(s SampleService) {
|
||||
called.Store(true)
|
||||
})
|
||||
core.Provide[SampleService](ctx, &sampleServiceImpl{})
|
||||
assert.True(t, called.Load())
|
||||
}
|
||||
|
||||
func TestInjectFromAppContext(t *testing.T) {
|
||||
app := core.NewContext(context.Background())
|
||||
core.Provide[SampleService](app, &sampleServiceImpl{prefix: "Hi:"})
|
||||
|
||||
req := core.WithAppContext(context.Background(), app)
|
||||
svc, err := core.InjectFrom[SampleService](req)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "Hi: Ada", svc.Greet("Ada"))
|
||||
|
||||
_, err = core.InjectFrom[SampleService](context.Background())
|
||||
assert.ErrorIs(t, err, core.ErrNilContext)
|
||||
}
|
||||
|
||||
func TestContextDisposerLifecycle(t *testing.T) {
|
||||
parent := core.NewContext(context.Background())
|
||||
child := parent.Fork()
|
||||
|
||||
@@ -43,6 +43,12 @@ type TaskResultDTO struct {
|
||||
Detail any `json:"detail,omitempty"`
|
||||
}
|
||||
|
||||
// TaskHandler is the preferred background task handler. Drivers invoke Execute
|
||||
// and persist Message/Detail onto the execution record.
|
||||
type TaskHandler interface {
|
||||
Execute(ctx context.Context, payload []byte) (*TaskResultDTO, error)
|
||||
}
|
||||
|
||||
// TaskExecutionDTO represents a single task execution record.
|
||||
type TaskExecutionDTO struct {
|
||||
ID uint64 `json:"id,string"`
|
||||
|
||||
@@ -229,6 +229,22 @@ func TestTaskExtension(t *testing.T) {
|
||||
assert.False(t, ok)
|
||||
}
|
||||
|
||||
func TestTaskRegisterRejectsNilHandler(t *testing.T) {
|
||||
tr := extpoints.NewTaskRegistry()
|
||||
assert.Panics(t, func() {
|
||||
tr.Register("broken:task", nil)
|
||||
})
|
||||
}
|
||||
|
||||
func TestTaskRegisterRejectsDuplicateType(t *testing.T) {
|
||||
tr := extpoints.NewTaskRegistry()
|
||||
handler := func(ctx context.Context, payload []byte) error { return nil }
|
||||
tr.Register("system:cleanup", handler, extpoints.WithTaskType("system_cleanup"))
|
||||
assert.Panics(t, func() {
|
||||
tr.Register("admin:system_cleanup", handler, extpoints.WithTaskType("system_cleanup"))
|
||||
})
|
||||
}
|
||||
|
||||
func TestScheduleExtension(t *testing.T) {
|
||||
sr := extpoints.NewScheduleRegistry()
|
||||
require.NotNil(t, sr)
|
||||
|
||||
@@ -5,6 +5,8 @@ package extpoints
|
||||
|
||||
import (
|
||||
"Wavelet/core/contracts"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
@@ -230,10 +232,15 @@ func NewTaskRegistry() *TaskRegistry {
|
||||
}
|
||||
|
||||
// Register registers a task pattern and its handler with optional configuration.
|
||||
// A nil handler panics. A non-empty Type that is already used by another pattern panics.
|
||||
func (t *TaskRegistry) Register(pattern string, handler any, opts ...TaskOption) {
|
||||
t.mu.Lock()
|
||||
defer t.mu.Unlock()
|
||||
|
||||
if isNilTaskHandler(handler) {
|
||||
panic(fmt.Sprintf("extpoints: nil handler for task pattern %q", pattern))
|
||||
}
|
||||
|
||||
td := TaskDefinition{
|
||||
Pattern: pattern,
|
||||
Handler: handler,
|
||||
@@ -245,8 +252,23 @@ func (t *TaskRegistry) Register(pattern string, handler any, opts ...TaskOption)
|
||||
opt(&td)
|
||||
}
|
||||
}
|
||||
if td.Type == "" {
|
||||
td.Type = pattern
|
||||
}
|
||||
|
||||
if _, exists := t.lookup[pattern]; exists {
|
||||
for _, item := range t.tasks {
|
||||
if item.Pattern == pattern {
|
||||
continue
|
||||
}
|
||||
if item.Type == td.Type {
|
||||
panic(fmt.Sprintf("extpoints: duplicate task type %q (patterns %q and %q)", td.Type, item.Pattern, pattern))
|
||||
}
|
||||
}
|
||||
|
||||
if existing, exists := t.lookup[pattern]; exists {
|
||||
if existing.Type != "" && existing.Type != pattern {
|
||||
delete(t.lookup, existing.Type)
|
||||
}
|
||||
for i, item := range t.tasks {
|
||||
if item.Pattern == pattern {
|
||||
t.tasks[i] = td
|
||||
@@ -258,13 +280,44 @@ func (t *TaskRegistry) Register(pattern string, handler any, opts ...TaskOption)
|
||||
}
|
||||
|
||||
t.lookup[pattern] = td
|
||||
if td.Type != pattern {
|
||||
t.lookup[td.Type] = td
|
||||
}
|
||||
}
|
||||
|
||||
func isNilTaskHandler(handler any) bool {
|
||||
if handler == nil {
|
||||
return true
|
||||
}
|
||||
v := reflect.ValueOf(handler)
|
||||
switch v.Kind() {
|
||||
case reflect.Chan, reflect.Func, reflect.Map, reflect.Pointer, reflect.UnsafePointer, reflect.Interface, reflect.Slice:
|
||||
return v.IsNil()
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Unregister removes a registered task definition by its pattern.
|
||||
func (t *TaskRegistry) Unregister(pattern string) bool {
|
||||
return unregisterEntry(&t.mu, t.lookup, &t.tasks, pattern, func(item TaskDefinition) bool {
|
||||
return item.Pattern == pattern
|
||||
})
|
||||
t.mu.Lock()
|
||||
defer t.mu.Unlock()
|
||||
td, ok := t.lookup[pattern]
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
delete(t.lookup, td.Pattern)
|
||||
if td.Type != "" && td.Type != td.Pattern {
|
||||
delete(t.lookup, td.Type)
|
||||
}
|
||||
filtered := t.tasks[:0]
|
||||
for _, item := range t.tasks {
|
||||
if item.Pattern != td.Pattern {
|
||||
filtered = append(filtered, item)
|
||||
}
|
||||
}
|
||||
t.tasks = filtered
|
||||
return true
|
||||
}
|
||||
|
||||
// Tasks returns a copy of all registered TaskDefinitions.
|
||||
@@ -280,13 +333,6 @@ func (t *TaskRegistry) Tasks() []TaskDefinition {
|
||||
func (t *TaskRegistry) Get(pattern string) (TaskDefinition, bool) {
|
||||
t.mu.RLock()
|
||||
defer t.mu.RUnlock()
|
||||
if td, ok := t.lookup[pattern]; ok {
|
||||
return td, true
|
||||
}
|
||||
for _, td := range t.tasks {
|
||||
if td.Type == pattern {
|
||||
return td, true
|
||||
}
|
||||
}
|
||||
return TaskDefinition{}, false
|
||||
td, ok := t.lookup[pattern]
|
||||
return td, ok
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user