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 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() }