package store import ( "sort" "strconv" "strings" "sync" "time" "github.com/tinode/chat/server/store/types" ) const configCacheTTL = time.Minute type configCacheEntry[T any] struct { value T expiresAt time.Time } type configCache[T any] struct { mu sync.RWMutex items map[string]configCacheEntry[T] } func newConfigCache[T any]() *configCache[T] { return &configCache[T]{items: make(map[string]configCacheEntry[T])} } func (c *configCache[T]) get(key string) (T, bool) { var zero T c.mu.RLock() entry, ok := c.items[key] c.mu.RUnlock() if !ok || time.Now().After(entry.expiresAt) { return zero, false } return entry.value, true } func (c *configCache[T]) set(key string, value T) { c.mu.Lock() c.items[key] = configCacheEntry[T]{value: value, expiresAt: time.Now().Add(configCacheTTL)} c.mu.Unlock() } func (c *configCache[T]) delete(key string) { c.mu.Lock() delete(c.items, key) c.mu.Unlock() } func (c *configCache[T]) clear() { c.mu.Lock() c.items = make(map[string]configCacheEntry[T]) c.mu.Unlock() } var ( tenantByCodeCache = newConfigCache[*types.Tenant]() tenantByIDCache = newConfigCache[*types.Tenant]() registrationSMSCache = newConfigCache[bool]() thirdPartyConfigCache = newConfigCache[map[string]string]() appReleaseConfigCache = newConfigCache[*types.AppRelease]() moderationProviderCache = newConfigCache[*types.ModerationProviderConfig]() moderationPolicyCache = newConfigCache[*types.ModerationPolicy]() moderationWordsCache = newConfigCache[*types.SensitiveWordSet]() securityPolicyCache = newConfigCache[*types.SecurityPolicy]() thirdPartyConfigAllKeys = "*" ) func InvalidateSecurityPolicyCache(tenantCode string) { if strings.TrimSpace(tenantCode) == "" { securityPolicyCache.clear() return } securityPolicyCache.delete(strings.TrimSpace(tenantCode)) } func InvalidateModerationConfigCache(tenantCode string, provider, policy, words bool) { if provider { moderationProviderCache.clear() } if policy { if strings.TrimSpace(tenantCode) == "" { moderationPolicyCache.clear() } else { moderationPolicyCache.delete(strings.TrimSpace(tenantCode)) } } if words { if strings.TrimSpace(tenantCode) == "" { moderationWordsCache.clear() } else { moderationWordsCache.delete(strings.TrimSpace(tenantCode)) } } } func cloneTenant(t *types.Tenant) *types.Tenant { if t == nil { return nil } cp := *t return &cp } // InvalidateAppReleaseConfigCache clears global client release policies. func InvalidateAppReleaseConfigCache() { appReleaseConfigCache.clear() } func cloneStringMap(src map[string]string) map[string]string { if src == nil { return nil } dst := make(map[string]string, len(src)) for k, v := range src { dst[k] = v } return dst } func thirdPartyCacheKey(group string, keys ...string) string { group = strings.TrimSpace(group) if len(keys) == 0 { return group + ":" + thirdPartyConfigAllKeys } normalized := make([]string, 0, len(keys)) for _, key := range keys { if key = strings.TrimSpace(key); key != "" { normalized = append(normalized, key) } } sort.Strings(normalized) return group + ":" + strings.Join(normalized, ",") } // InvalidateTenantConfigCache clears cached tenant registry/config records. An // empty selector clears all tenant records. func InvalidateTenantConfigCache(code string, id types.TenantID) { if strings.TrimSpace(code) != "" { tenantByCodeCache.delete(strings.TrimSpace(code)) } else { tenantByCodeCache.clear() } if id.IsValid() { tenantByIDCache.delete(formatTenantIDForCache(id)) } else if strings.TrimSpace(code) == "" { tenantByIDCache.clear() } } // InvalidateRegistrationConfigCache clears cached global registration config. func InvalidateRegistrationConfigCache() { registrationSMSCache.clear() } // InvalidateThirdPartyConfigCache clears cached third-party config. func InvalidateThirdPartyConfigCache(group string) { if strings.TrimSpace(group) == "" { thirdPartyConfigCache.clear() return } prefix := strings.TrimSpace(group) + ":" thirdPartyConfigCache.mu.Lock() for key := range thirdPartyConfigCache.items { if strings.HasPrefix(key, prefix) { delete(thirdPartyConfigCache.items, key) } } thirdPartyConfigCache.mu.Unlock() } func formatTenantIDForCache(id types.TenantID) string { return strconv.FormatInt(int64(id), 10) }