package global import ( "context" "io" "log/slog" "reflect" "sync" "github.com/redis/go-redis/v9" "go.mongodb.org/mongo-driver/mongo" "gorm.io/gorm" platformmq "kra/pkg/mq" platformws "kra/pkg/websocket" ) // FileStorage is the transport-neutral subset shared by every file storage // implementation. Domain operations that return business models remain in // the biz-owned FileStorage interface instead of making global depend on biz. type FileStorage interface { Open(context.Context, string) (io.ReadCloser, error) Delete(context.Context, string) error DeletePrefix(context.Context, string) error } // Scheduler owns the lifecycle of the process-wide task scheduler. Scheduling // business operations remain behind the task domain interfaces. type Scheduler interface { Start(context.Context) error Stop(context.Context) error } // Resources is an immutable snapshot from the registry's point of view. // Replacing a resource publishes a new snapshot; it does not close the old // resource because lifecycle ownership belongs to the composition root. type Resources struct { Logger *slog.Logger DB *gorm.DB NamedDBs map[string]*gorm.DB Redis redis.UniversalClient NamedRedis map[string]redis.UniversalClient Mongo *mongo.Client Storage FileStorage MQ platformmq.Registry WebSocket platformws.Hub Scheduler Scheduler } // ResourceRegistry provides concurrency-safe access to process-wide runtime // resources. Prefer constructor injection for domain code; the registry is for // framework callbacks and integration points that cannot be wired directly. type ResourceRegistry struct { mu sync.RWMutex resources Resources } func NewResourceRegistry() *ResourceRegistry { return &ResourceRegistry{resources: normalizeResources(Resources{})} } func (r *ResourceRegistry) Snapshot() Resources { if r == nil { return normalizeResources(Resources{}) } r.mu.RLock() resources := cloneResources(r.resources) r.mu.RUnlock() return resources } func (r *ResourceRegistry) Replace(resources Resources) { if r == nil { return } r.mu.Lock() r.resources = normalizeResources(resources) r.mu.Unlock() } func (r *ResourceRegistry) Logger() *slog.Logger { return r.Snapshot().Logger } func (r *ResourceRegistry) SetLogger(logger *slog.Logger) { r.update(func(resources *Resources) { resources.Logger = logger }) } func (r *ResourceRegistry) DB() *gorm.DB { return r.Snapshot().DB } func (r *ResourceRegistry) SetDB(db *gorm.DB) { r.update(func(resources *Resources) { resources.DB = db }) } // NamedDB returns a configured secondary database by alias. An empty alias // resolves to the primary database. func (r *ResourceRegistry) NamedDB(name string) *gorm.DB { if name == "" { return r.DB() } return r.Snapshot().NamedDBs[name] } // SetNamedDBs replaces the complete secondary-database alias set. func (r *ResourceRegistry) SetNamedDBs(databases map[string]*gorm.DB) { r.update(func(resources *Resources) { resources.NamedDBs = cloneDBMap(databases) }) } func (r *ResourceRegistry) Redis() redis.UniversalClient { return r.Snapshot().Redis } func (r *ResourceRegistry) SetRedis(client redis.UniversalClient) { r.update(func(resources *Resources) { resources.Redis = client }) } // NamedRedis returns a configured secondary Redis client by alias. An empty // alias resolves to the primary Redis client. func (r *ResourceRegistry) NamedRedis(name string) redis.UniversalClient { if name == "" { return r.Redis() } return r.Snapshot().NamedRedis[name] } // SetNamedRedis replaces the complete secondary-Redis alias set. func (r *ResourceRegistry) SetNamedRedis(clients map[string]redis.UniversalClient) { r.update(func(resources *Resources) { resources.NamedRedis = cloneRedisMap(clients) }) } func (r *ResourceRegistry) Mongo() *mongo.Client { return r.Snapshot().Mongo } func (r *ResourceRegistry) SetMongo(client *mongo.Client) { r.update(func(resources *Resources) { resources.Mongo = client }) } func (r *ResourceRegistry) Storage() FileStorage { return r.Snapshot().Storage } func (r *ResourceRegistry) SetStorage(storage FileStorage) { r.update(func(resources *Resources) { resources.Storage = storage }) } func (r *ResourceRegistry) MQ() platformmq.Registry { return r.Snapshot().MQ } func (r *ResourceRegistry) SetMQ(registry platformmq.Registry) { r.update(func(resources *Resources) { resources.MQ = registry }) } func (r *ResourceRegistry) WebSocket() platformws.Hub { return r.Snapshot().WebSocket } func (r *ResourceRegistry) SetWebSocket(hub platformws.Hub) { r.update(func(resources *Resources) { resources.WebSocket = hub }) } func (r *ResourceRegistry) Scheduler() Scheduler { return r.Snapshot().Scheduler } func (r *ResourceRegistry) SetScheduler(scheduler Scheduler) { r.update(func(resources *Resources) { resources.Scheduler = scheduler }) } func (r *ResourceRegistry) update(change func(*Resources)) { if r == nil || change == nil { return } r.mu.Lock() resources := cloneResources(r.resources) change(&resources) r.resources = normalizeResources(resources) r.mu.Unlock() } func normalizeResources(resources Resources) Resources { if resources.Logger == nil { resources.Logger = slog.Default() } resources.Redis = normalizeInterface(resources.Redis) resources.Storage = normalizeInterface(resources.Storage) resources.MQ = normalizeInterface(resources.MQ) resources.WebSocket = normalizeInterface(resources.WebSocket) resources.Scheduler = normalizeInterface(resources.Scheduler) resources.NamedDBs = cloneDBMap(resources.NamedDBs) resources.NamedRedis = cloneRedisMap(resources.NamedRedis) return resources } func cloneResources(resources Resources) Resources { resources.NamedDBs = cloneDBMap(resources.NamedDBs) resources.NamedRedis = cloneRedisMap(resources.NamedRedis) return resources } func cloneDBMap(source map[string]*gorm.DB) map[string]*gorm.DB { if len(source) == 0 { return nil } copyOf := make(map[string]*gorm.DB, len(source)) for name, db := range source { if name != "" && db != nil { copyOf[name] = db } } if len(copyOf) == 0 { return nil } return copyOf } func cloneRedisMap(source map[string]redis.UniversalClient) map[string]redis.UniversalClient { if len(source) == 0 { return nil } copyOf := make(map[string]redis.UniversalClient, len(source)) for name, client := range source { if name == "" { continue } client = normalizeInterface(client) if client != nil { copyOf[name] = client } } if len(copyOf) == 0 { return nil } return copyOf } func normalizeInterface[T any](value T) T { reflected := reflect.ValueOf(value) if reflected.IsValid() { switch reflected.Kind() { case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, reflect.Pointer, reflect.Slice: if !reflected.IsNil() { return value } default: return value } } var zero T return zero } var defaultResources = NewResourceRegistry() // Install publishes resources assembled by the composition root and returns // the process-wide registry for lifecycle wiring. func Install(resources Resources) *ResourceRegistry { defaultResources.Replace(resources) return defaultResources } func DefaultRegistry() *ResourceRegistry { return defaultResources } func ResourceSnapshot() Resources { return defaultResources.Snapshot() } func ReplaceResources(resources Resources) { defaultResources.Replace(resources) } func Logger() *slog.Logger { return defaultResources.Logger() } func SetLogger(logger *slog.Logger) { defaultResources.SetLogger(logger) } func DB() *gorm.DB { return defaultResources.DB() } func SetDB(db *gorm.DB) { defaultResources.SetDB(db) } func NamedDB(name string) *gorm.DB { return defaultResources.NamedDB(name) } func SetNamedDBs(databases map[string]*gorm.DB) { defaultResources.SetNamedDBs(databases) } func Redis() redis.UniversalClient { return defaultResources.Redis() } func SetRedis(client redis.UniversalClient) { defaultResources.SetRedis(client) } func NamedRedis(name string) redis.UniversalClient { return defaultResources.NamedRedis(name) } func SetNamedRedis(clients map[string]redis.UniversalClient) { defaultResources.SetNamedRedis(clients) } func Mongo() *mongo.Client { return defaultResources.Mongo() } func SetMongo(client *mongo.Client) { defaultResources.SetMongo(client) } func Storage() FileStorage { return defaultResources.Storage() } func SetStorage(storage FileStorage) { defaultResources.SetStorage(storage) } func MQ() platformmq.Registry { return defaultResources.MQ() } func SetMQ(registry platformmq.Registry) { defaultResources.SetMQ(registry) } func WebSocket() platformws.Hub { return defaultResources.WebSocket() } func SetWebSocket(hub platformws.Hub) { defaultResources.SetWebSocket(hub) } func TaskScheduler() Scheduler { return defaultResources.Scheduler() } func SetTaskScheduler(scheduler Scheduler) { defaultResources.SetScheduler(scheduler) }